diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md
index 2b496e86..2b454517 100644
--- a/docs/plans/2026-04-11-roadmap.md
+++ b/docs/plans/2026-04-11-roadmap.md
@@ -1632,10 +1632,13 @@ port in any phase — no separate listing here.
> shadow referee, production cutover, and acceptance matrix. The user
> explicitly approved Slices F–L, including the F/G and J gates, on
> 2026-07-24. Slice F and G0–G4 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
diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md
index c65f6251..f2daa1eb 100644
--- a/docs/plans/2026-05-12-milestones.md
+++ b/docs/plans/2026-05-12-milestones.md
@@ -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 F–L, including both
the F/G ECS and Slice J gameplay-owner gates, on 2026-07-24. Slice F and
-G0–G4 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
+G0–G4 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
diff --git a/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md b/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md
index 48f6aa2f..01e33365 100644
--- a/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md
+++ b/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md
@@ -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
diff --git a/docs/research/2026-07-25-portal-regression-closeout.md b/docs/research/2026-07-25-portal-regression-closeout.md
index e8384efc..ee54c690 100644
--- a/docs/research/2026-07-25-portal-regression-closeout.md
+++ b/docs/research/2026-07-25-portal-regression-closeout.md
@@ -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.
diff --git a/docs/research/2026-07-25-slice-g5-production-profile.md b/docs/research/2026-07-25-slice-g5-production-profile.md
new file mode 100644
index 00000000..c3f1c1f6
--- /dev/null
+++ b/docs/research/2026-07-25-slice-g5-production-profile.md
@@ -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.
diff --git a/src/AcDream.App/Rendering/Scene/RenderFrameRouteOwnerSelector.cs b/src/AcDream.App/Rendering/Scene/RenderFrameRouteOwnerSelector.cs
new file mode 100644
index 00000000..2419e0ae
--- /dev/null
+++ b/src/AcDream.App/Rendering/Scene/RenderFrameRouteOwnerSelector.cs
@@ -0,0 +1,120 @@
+namespace AcDream.App.Rendering.Scene;
+
+///
+/// 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.
+///
+internal static class RenderFrameRouteOwnerSelector
+{
+ public static void Replace(
+ HashSet 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 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 destination,
+ in RenderFrameView view,
+ RenderFrameCandidateRoute route)
+ {
+ ArgumentNullException.ThrowIfNull(destination);
+
+ ReadOnlySpan ranges =
+ view.RouteRanges;
+ ReadOnlySpan 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 destination,
+ in RenderFrameView view,
+ RenderFrameCandidateRoute route,
+ int routeIndex,
+ uint cellId,
+ bool remove)
+ {
+ ReadOnlySpan ranges =
+ view.RouteRanges;
+ ReadOnlySpan 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 destination,
+ ReadOnlySpan candidates,
+ in RenderFrameCandidateRange range,
+ bool remove)
+ {
+ ReadOnlySpan 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);
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs b/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs
new file mode 100644
index 00000000..2f79c133
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs
@@ -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 { 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();
+
+ 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 { 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();
+ 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 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),
+ };
+}