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), + }; +}