refactor(rendering): prepare retained particle routing

Derive attached particle owners from the exact retained PView route stream with a zero-allocation warm path. Correct G4 gate provenance, record the current production-only RDP sample, and keep G5 old-route deletion explicitly blocked until the reapplied retained cutover passes its connected visual gate.
This commit is contained in:
Erik 2026-07-25 08:45:18 +02:00
parent 20f9fadb12
commit c730632075
7 changed files with 374 additions and 20 deletions

View file

@ -1632,10 +1632,13 @@ port in any phase — no separate listing here.
> shadow referee, production cutover, and acceptance matrix. The user
> explicitly approved Slices FL, including the F/G and J gates, on
> 2026-07-24. Slice F and G0G4 are implemented; 46,599 automated G4
> comparisons have zero mismatches. The 2026-07-25 connected visual gate
> accepted the retained scene after independent portal scheduling/private-view/
> ACE-revisit ownership regressions were corrected; G5 old-path deletion and
> production-only profiling are now active. The historical exact cutover
> comparisons have zero mismatches. The first retained-path visual gate exposed
> independent portal scheduling/private-view/ACE-revisit ownership regressions.
> Their correction passed visually while the draw cutover was temporarily
> reverted, which proves the correction but not the retained source.
> `20f9fadb` reapplies the cutover over those fixes; its connected portal/revisit
> re-gate is pending before G5 old-path deletion and production-only profiling.
> The historical exact cutover
> rollback remains
> `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`. Slice H is complete:
> retained UI/frame work, exact bounded light selection, and

View file

@ -108,10 +108,11 @@ publication, retirement, upload, warmup, or resource backlog. Capped/uncapped
CPU p99 is 34.7%/30.4% below Slice A and maximum frame allocation is
84.7%/82.2% lower. The user explicitly approved Slices FL, including both
the F/G ECS and Slice J gameplay-owner gates, on 2026-07-24. Slice F and
G0G4 are implemented; G4's automated gate has zero mismatches and its
2026-07-25 connected visual gate passes after the portal scheduling/private
viewport/ACE revisit regression correction. G5 deletion and production-only
profiling are active. Slice H is
G0G4 are implemented and G4's automated gate has zero mismatches. The portal
scheduling/private-viewport/ACE-revisit correction passed visually while the
retained draw cutover was temporarily reverted, so `20f9fadb` reapplies the
cutover over those fixes for the pending connected retained-path re-gate. G5
deletion and production-only profiling remain gated on that pass. Slice H is
complete: retained UI/frame work, exact bounded light selection, and
pooled/borrowed/direct network I/O pass 8,292 Release tests / 5 skips and the
seven-checkpoint connected lifecycle/reconnect gate. Slice I is active under

View file

@ -2,7 +2,8 @@
**Date:** 2026-07-24
**Status:** approved 2026-07-24; Slice F and G4 complete, G5 active
**Status:** approved 2026-07-24; Slice F complete, G4 retained-path re-gate
active, G5 prepared but not unblocked
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
@ -27,8 +28,8 @@ retail-faithful gameplay
| G1 — scene-query replacement | complete | Exact `e346f8bb` passed capped and uncapped nine-stop routes plus uncapped dense Arwic: 29,392 + 29,025 + 1,522 same-frame candidate comparisons, zero mismatches, equal counts/digests at every checkpoint, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g1-scene-query-candidate-gate.md`. |
| G2 — packed dispatcher input | complete | Exact `f9829d5f` passed capped and uncapped nine-stop routes plus dense Arwic. Candidate order, packed input, classified output, and selection each completed 16,879 + 17,282 + 770 comparisons with zero mismatch; journal/rejection counts were zero and shutdown graceful. Evidence: `docs/research/2026-07-25-slice-g2-packed-dispatcher-gate.md`. |
| G3 — retained classification/storage | complete | Exact `6a026c5a` passed capped and uncapped nine-stop routes plus dense Arwic with 17,069 + 16,827 + 757 exact comparisons in every channel, zero mismatch, warm cross-frame reuse, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g3-retained-classification-gate.md`. |
| G4 — production cutover | complete | Exact cutover `ef1d263337997bb030eadb7b8e71d73dc659907a` makes the retained frame product the production entity source at the existing five retail PView stages. Exact `03b10183` passed capped/uncapped nine-stop and uncapped dense-Arwic automation with 46,599 total product comparisons and zero mismatch. The 2026-07-25 connected visual pass accepted the retained scene after its independent portal scheduling/ownership regressions were corrected. Evidence: `docs/research/2026-07-25-slice-g4-production-cutover-automated-gate.md` and `docs/research/2026-07-25-portal-regression-closeout.md`. |
| G5 | active | The accepted visual gate unblocks production-only performance/lifecycle closeout and old-route deletion. |
| G4 — production cutover | re-gate active | Exact cutover `ef1d263337997bb030eadb7b8e71d73dc659907a` makes the retained frame product the production entity source at the existing five retail PView stages. Exact `03b10183` passed capped/uncapped nine-stop and uncapped dense-Arwic automation with 46,599 total product comparisons and zero mismatch. The portal correction was visually accepted while the cutover was temporarily reverted by `2c848d41`; that proves the correction but cannot accept the retained draw source. Commit `20f9fadb` reapplies the cutover over the correction and is the current connected visual re-gate. Evidence: `docs/research/2026-07-25-slice-g4-production-cutover-automated-gate.md` and `docs/research/2026-07-25-portal-regression-closeout.md`. |
| G5 | prepared | Old-route deletion and the production-only reference matrix remain blocked only on the connected `20f9fadb` retained-path re-gate. An allocation-free retained-route particle-owner selector and steady RDP sample are prepared without changing the running gate binary. |
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
non-drawing diagnostic infrastructure and therefore is not a visual-cutover
@ -646,12 +647,15 @@ the scene journal was empty, source and binary hashes matched, and every
process closed gracefully. The gate corrected whole-retained-world composite
warmup discovery by sourcing the exact published 3×3 destination neighborhood;
that correction is draw-path independent. Fixed-camera artifacts are captured.
The user completed the connected visual gate on 2026-07-25. The retained scene
itself remained exact; the pass exposed independent portal scheduling, private
viewport ownership, and ACE revisit-liveness defects. Their root-cause
The first connected retained-path gate exposed independent portal scheduling,
private-viewport ownership, and ACE revisit-liveness defects. Their root-cause
correction is recorded in
`docs/research/2026-07-25-portal-regression-closeout.md`. G4 is accepted and
old-route deletion plus G5 production-only profiling are unblocked.
`docs/research/2026-07-25-portal-regression-closeout.md`. The user's subsequent
pass occurred while exact cutover `ef1d2633` was temporarily reverted by
`2c848d41`, so it proves those corrections but not the retained draw source.
`20f9fadb` reapplies the cutover over the accepted corrections. G4 acceptance,
old-route deletion, and the G5 production-only matrix remain gated on the
connected retained-path recheck.
### G5 — Physical performance and lifecycle gate

View file

@ -10,9 +10,10 @@ The first connected retained-scene visual pass reported:
4. doors, signs, portals, NPCs, and other server-spawned objects disappeared
after repeated portal travel.
This note records the root causes and the accepted correction. It does not
reduce world radius, texture quality, particles, readiness, or retail
presentation.
This note records the root causes and the correction. It does not reduce world
radius, texture quality, particles, readiness, or retail presentation. The
correction's visual pass occurred on the temporary old-draw rollback; the
retained-source re-gate is tracked separately below.
## Retail and protocol anchors
@ -99,7 +100,11 @@ This ACE adaptation remains explicit in divergence AP-69.
- Complete App Release suite: 3,813 passed / 3 skipped.
- Release solution build: green with zero warnings.
- Complete Release solution suite: 8,322 passed / 5 skipped.
- User connected visual gate:
- User connected correction gate on rollback route (`2c848d41`):
- paperdoll remains visible;
- portal transit is materially faster;
- server-spawned objects remain after repeated travel.
- Retained production source:
- `20f9fadb` reapplies exact cutover `ef1d2633` over the correction;
- complete Release suite: 8,326 passed / 5 skipped;
- connected portal/revisit visual re-gate pending before G4 acceptance.

View file

@ -0,0 +1,43 @@
# Slice G5 production profile — 2026-07-25
## Status
Work in progress. The retained-scene cutover is running at `20f9fadb`, but G5
old-route deletion remains blocked on its connected portal/revisit visual
re-gate. This note records production-only evidence gathered without enabling
the referee or lifecycle automation.
## First steady RDP sample
The client was idle in-world at 60 Hz with the normal capped Release
presentation, High quality, a 4/12 near/far radius, and no developer or
automation UI. The title reported 173/625 landblocks, 10,383 entities, and 51
active animations. Five seconds of process CPU plus three one-second Windows
GPU counter samples measured:
| Metric | Value |
|---|---:|
| CPU | 2.68% of 16 logical processors, about 42.9% of one logical core |
| Working set | 255.8 MiB |
| Private bytes | 1,193.9 MiB |
| Threads / handles | 26 / 663 |
| GPU 3-D engine | 55.74% average, 56.26% maximum |
| Dedicated GPU process memory | 422 MiB |
| Shared GPU process memory | 141 MiB |
This is an RDP presentation sample, not the required physical-local reference
matrix and not evidence of a plateau across travel. It establishes that the
post-cutover retained path is active and gives G5 a pre-deletion production
point. The complete capped/uncapped nine-stop, dense-Arwic, lifecycle,
allocation, GC, stage-timing, and screenshot/instance-set matrix remains
mandatory after the visual re-gate.
## Prepared old-route dependency removal
`RenderFrameRouteOwnerSelector` derives attached particle owner IDs from the
exact retained PView route ranges. It supports exact-route replace/union and
all-range exclusion without allocation after warmup. This removes the final
particle-routing reason for normal production to build
`InteriorEntityPartition`; it is not wired into the running visual-gate binary.
Four focused tests and the 3,821-pass / 3-skip App Release suite are green
through an isolated output path.

View file

@ -0,0 +1,120 @@
namespace AcDream.App.Rendering.Scene;
/// <summary>
/// Projects the retained PView route stream into particle-owner identities.
/// This keeps attached-effect routing on the same visibility decision as mesh
/// submission without walking spatial/gameplay entities a second time.
/// </summary>
internal static class RenderFrameRouteOwnerSelector
{
public static void Replace(
HashSet<uint> destination,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId)
{
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
Union(
destination,
in view,
route,
routeIndex,
cellId);
}
public static void Union(
HashSet<uint> destination,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId)
{
ArgumentNullException.ThrowIfNull(destination);
VisitExact(
destination,
in view,
route,
routeIndex,
cellId,
remove: false);
}
public static void ExceptRoute(
HashSet<uint> destination,
in RenderFrameView view,
RenderFrameCandidateRoute route)
{
ArgumentNullException.ThrowIfNull(destination);
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> candidates =
view.RouteCandidates;
for (int index = 0; index < ranges.Length; index++)
{
RenderFrameCandidateRange range = ranges[index];
if (range.Route != route)
continue;
ApplyRange(
destination,
candidates,
in range,
remove: true);
}
}
private static void VisitExact(
HashSet<uint> destination,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
bool remove)
{
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> candidates =
view.RouteCandidates;
for (int index = 0; index < ranges.Length; index++)
{
RenderFrameCandidateRange range = ranges[index];
if (range.Route != route
|| range.RouteIndex != routeIndex
|| range.CellId != cellId)
{
continue;
}
ApplyRange(
destination,
candidates,
in range,
remove);
return;
}
}
private static void ApplyRange(
HashSet<uint> destination,
ReadOnlySpan<RenderProjectionRecord> candidates,
in RenderFrameCandidateRange range,
bool remove)
{
ReadOnlySpan<RenderProjectionRecord> owners =
candidates.Slice(range.Offset, range.Count);
for (int index = 0; index < owners.Length; index++)
{
uint localEntityId = owners[index].Source.LocalEntityId;
if (localEntityId == 0)
continue;
if (remove)
destination.Remove(localEntityId);
else
destination.Add(localEntityId);
}
}
}

View file

@ -0,0 +1,178 @@
using AcDream.App.Rendering.Scene;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderFrameRouteOwnerSelectorTests
{
private static readonly RenderSceneGeneration Generation =
RenderSceneGeneration.FromRaw(1);
[Fact]
public void ReplaceAndUnionUseExactRouteIdentityAndDeduplicateOwners()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint> { 999 };
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.Union(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
routeIndex: 0,
cellId: 0);
Assert.Equal([10u, 20u, 30u], owners.Order());
exchange.Release(in view);
}
[Fact]
public void ExceptRouteRemovesOwnersAcrossEveryRangeOfThatRoute()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint>();
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.DynamicLast,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.ExceptRoute(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
Assert.Equal([50u], owners);
exchange.Release(in view);
}
[Fact]
public void MissingRouteClearsOnReplaceAndDoesNotMutateOnUnion()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint> { 10, 20 };
RenderFrameRouteOwnerSelector.Union(
owners,
in view,
RenderFrameCandidateRoute.LookInObject,
routeIndex: 9,
cellId: 0x01010009);
Assert.Equal([10u, 20u], owners.Order());
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.LookInObject,
routeIndex: 9,
cellId: 0x01010009);
Assert.Empty(owners);
exchange.Release(in view);
}
[Fact]
public void WarmSelectionAllocatesNothing()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint>();
owners.EnsureCapacity(16);
SelectOwners(owners, in view);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 1_000; iteration++)
SelectOwners(owners, in view);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Equal([50u], owners);
exchange.Release(in view);
}
private static void SelectOwners(
HashSet<uint> owners,
in RenderFrameView view)
{
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.DynamicLast,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.ExceptRoute(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
}
private static void PublishRoutes(
RenderFrameExchange exchange,
ulong sequence)
{
RenderProjectionRecord owner10 = Projection(1, 10);
RenderProjectionRecord owner20 = Projection(2, 20);
RenderProjectionRecord owner30 = Projection(3, 30);
RenderProjectionRecord owner40 = Projection(4, 40);
RenderProjectionRecord owner50 = Projection(5, 50);
RenderProjectionRecord anonymous = Projection(6, 0);
RenderFrameWriter writer =
exchange.BeginBuild(Generation, sequence);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
[owner10, owner20, anonymous]);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
0,
0,
[owner20, owner30]);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
1,
0,
[owner40]);
writer.AddRouteRange(
RenderFrameCandidateRoute.DynamicLast,
0,
0,
[owner20, owner40, owner50]);
writer.SetSourceDigest(
new RenderSceneDigest(
Generation,
default,
default));
writer.Publish();
}
private static RenderProjectionRecord Projection(
ulong projectionId,
uint localEntityId) =>
new()
{
Id = RenderProjectionId.FromRaw(projectionId),
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Source = new RenderSourceMetadata(
LocalEntityId: localEntityId,
ServerGuid: 0,
SourceId: 0,
ParentCellId: 0,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default),
};
}