From 26e97ba4128a35d83abadcc40300a211db70aa6e Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 Sep 2026 15:02:23 +0200 Subject: [PATCH] refactor(render): delete superseded alpha sidecars Remove the dead classic-group CachedBatch/InstanceGroup sort-center sidecar and its cache, append, reset, and diagnostic-digest plumbing. Remove the discarded camera parameter chain from grouped alpha deferral and dispatcher submission diagnostics while preserving walk/particle CYpt keys, FIFO submission ordinals, and opaque camera distance. Add production-facing reflection/source pins for the deleted shapes and camera chain, retained per-cell and opaque owners, the exact three AlphaFlushCounts reasons, and deleted alpha-order symbols. Update directly affected cache/digest/group lifecycle tests and record the complete section 22 result. Gates: Release 0W/0E; focused App 132/132 plus Core 29/29; new pins 4/4; real allocation pins 2/2 at 0 B; shaders 32/32; hermetic 16755/16755; InstalledDat exactly 385 pass, 10 documented fail, 1 skip; diff-check clean. Mutations: InstanceGroup and CachedBatch sidecars fail their Assert.Null pins; Defer and digest camera parameters fail method-shape pins; false count prose fails the exact-reason pin; RetailAlphaOrdering resurrection fails the deletion pin. Each was restored independently before the final matrix. Co-Authored-By: Claude Sonnet 4.5 --- .../s4-depth-alpha-packet.md | 90 +++++++++++++++++++ src/AcDream.App/Rendering/Wb/CachedBatch.cs | 3 - .../Wb/WbDrawDispatcher.WalkClassify.cs | 13 ++- .../Rendering/Wb/WbDrawDispatcher.cs | 57 +++--------- .../Walk/WalkStaticStreamPopulatorTests.cs | 51 ++++++++++- .../Rendering/Wb/InstanceGroupClearTests.cs | 74 ++++++++++++--- .../Wb/WbDrawDispatcherBucketingTests.cs | 41 +-------- 7 files changed, 224 insertions(+), 105 deletions(-) diff --git a/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md b/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md index fe073b6e..22785c9b 100644 --- a/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md +++ b/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md @@ -1990,3 +1990,93 @@ call reachability, diagnostic-digest consequences, cache/reset/storage lifecycle, allocation, allowed scope, every mutation claim, and all gate counts. A failed lens gets one bounded fix contract; a third fix round stops and is written up. Dual PASS lands c3b and unlocks G3. + +### 22.7 S4-c3b implementation result (implementer, 2026-09-04) + +PASS for the bounded implementation return. The classic grouped dispatcher no +longer carries `CachedBatch.LocalSortCenter` or +`InstanceGroup.LocalSortCenters`; their constructor/cache-hit/append/reset/ +release and diagnostic-hash plumbing is gone. The discarded alpha camera +parameter is also absent from `DeferTransparentGroups`, +`BuildTransparentSubmissionDigest`, `CreateDispatcherSubmission`, +`ObserveCurrentDispatcherSubmission`, and +`ObserveClassifiedDispatcherSubmission`, with no camera tombstone remaining. +The diagnostic transparent digest therefore hashes only rendered state plus the +live submission ordinal, as intended. + +Production-facing reflection/source pins prove the deletion and retain the +separate live owners: `WalkClassifiedBatch.LocalSortCenter`/ +`SortDistanceSq`, the transformed authored-center per-cell stable descending +sort, and `PartitionInstanceGroups(cameraWorldPosition)` feeding opaque +`InstanceGroup.SortDistance`. Existing cache-hit matrix composition, +transparent submission-order digest, group lifecycle, walk/particle merge, +queue/router, allocation, and shader/state tests remain green. The +`AlphaFlushCounts_*` source-truth pin keeps exactly the content-empty replay, +AP-238 `(cell,list)` coalescing, and AP-239/AP-240 list-membership reasons and +explicitly preserves the fact that the former broad “per instance versus per +subset” explanation was false. Static production truth rejects resurrection of +`RetailAlphaOrdering`, `FlushFartherThan`, alpha-submit `viewerDistance`, or a +discarded camera tombstone. + +Automated return: + +- The first `dotnet build AcDream.slnx -c Release --no-restore` invocation did + not reach compilation because the fresh worktree had no + `obj/project.assets.json` files (`NETSDK1004`, 57 projects). One + `dotnet restore AcDream.slnx` populated those assets; the Release rerun then + passed with 0 warnings / 0 errors. This was dependency preparation, not a + product retry. +- focused App + `InstanceGroupClearTests|WalkStaticStreamPopulatorTests|ParticleRendererRouteTests|WalkFrameDriverTests|RetailAlphaQueueTests|RetailParticleGeometryClassifierTests` + — PASS, 132/132; focused Core + `WbDrawDispatcherBucketingTests|EntityClassificationCacheTests` — PASS, + 29/29 (161/161 aggregate); +- the four new deletion/truth pins — PASS, 4/4 after every mutation restore; +- real warmed + `ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes|ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate` + — PASS, 2/2, both observing 0 managed bytes; +- `VulkanShaderDescriptorContractTests|VulkanShaderManifestTests|RenderPackSpirvValidatorTests` + — PASS, 32/32; no shader, SPIR-V, manifest, RHI, or pipeline file changed; +- official no-retry 14-assembly hermetic gate — PASS, 16,755 passed / 0 + skipped / 0 failed; evidence + `artifacts/s4-c3b-hermetic-20260904`; +- InstalledDat with explicit `C:\Turbine\Asheron's Call`, inclusive of the + retained KnownFailures — 385 passed / 10 expected failed / 1 skipped. The + failures were exactly TowerAscent, the two #383 layout sweeps, + `Oh_doorway_still_first_frame_diff` (#458), and the six + `AlphaFlushCounts_*`; the skip remained `Sweep_the_lookin_gate_decodes_against_the_street_fixture`. + No new failure appeared; evidence + `artifacts/s4-c3b-installed-inclusive-20260904`. + +Mutation ledger (each mutation was applied alone and restored by patch before +the next; the restored pin set passed 4/4): + +1. Restoring `InstanceGroup.LocalSortCenters` first failed + `ClassicGroupedShapes_DeleteDeadSortCenterSidecarsButRetainWalkCyptKey` at + its second `Assert.Null`: expected null; actual + `List LocalSortCenters`. +2. Restoring `CachedBatch.LocalSortCenter` first failed the same pin at its + first `Assert.Null`: expected null; actual `Vector3 LocalSortCenter`. +3. Restoring the `DeferTransparentGroups(Vector3, Matrix4x4)` parameter and + tombstone first failed + `AlphaSubmissionShapes_DeleteDeadCameraChainButRetainOpaqueCameraDistance`: + `Assert.Equal` expected `[Matrix4x4]`, actual `[Vector3, Matrix4x4]`. +4. Restoring `cameraWorldPosition` on + `BuildTransparentSubmissionDigest` first failed the same pin's next + `Assert.DoesNotContain`: the parameter collection contained + `Vector3 cameraWorldPosition` at position 1. +5. Replacing the exact three-reason prose with a false broad fourth reason + first failed + `AlphaFlushCountKnownFailure_StatesOnlyTheThreeExactReasons` at + `Assert.Contains`: the required `former broad "per instance versus per + subset" explanation was false.` substring was absent. +6. Restoring an executable `RetailAlphaOrdering` symbol first failed + `ClassicGroupedAlphaSourceTruth_DeletesDeadOrderingChainAndPreservesLiveOwners` + at `Assert.DoesNotContain`: substring `RetailAlphaOrdering` was found. + +The implementation changes only the three allowed production files, the two +named App test files, the one directly affected Core rendering test file, and +this result. AD-119 and AP-238 through AP-243, queue/router/list membership, +capacities, barriers, particles, per-cell ordering, building/private/portal +distances, shaders/RHI, package/content, and graphical behavior are unchanged. +No graphical client was launched. diff --git a/src/AcDream.App/Rendering/Wb/CachedBatch.cs b/src/AcDream.App/Rendering/Wb/CachedBatch.cs index 3cadb4b1..5c31fccf 100644 --- a/src/AcDream.App/Rendering/Wb/CachedBatch.cs +++ b/src/AcDream.App/Rendering/Wb/CachedBatch.cs @@ -9,8 +9,6 @@ namespace AcDream.App.Rendering.Wb; /// subPart contributes its own entries, with /// already containing the /// subPart.PartTransform * meshRef.PartTransform product. -/// preserves the authored GfxObj key used by -/// retail's delayed-alpha viewer-distance ordering on cache hits. /// is a registration-checked fast handle into the /// dispatcher's bounded group table; a retired or synthetic entry falls back /// to key lookup and refreshes this array entry. @@ -25,7 +23,6 @@ internal readonly record struct CachedBatch( GroupKey Key, GpuTextureSlot TextureSlot, Matrix4x4 RestPose, - Vector3 LocalSortCenter = default, WbDrawDispatcher.InstanceGroup? Group = null, long GroupRegistration = 0); diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs index 995dd83f..8062b8b0 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs @@ -143,13 +143,12 @@ public sealed partial class WbDrawDispatcher /// one needs to reach the alpha queue: (so the /// populator can route without re-deriving it from /// ) and - /// (the authored GfxObj sort center — the same value - /// InstanceGroup.LocalSortCenters carries per instance today; S4-c2 - /// deleted its one alpha-ordering consumer, RetailAlphaOrdering.ComputeViewerDistance, - /// but S4-c3a restores its retail use: WalkStaticStreamPopulator - /// transforms it through the part model and retains the resulting - /// squared CYpt key in before the per-cell - /// stable sort. The key feeds two FIFO alpha lists; neither queue sorts. + /// (the authored GfxObj sort center consumed by the live per-cell CYpt + /// sort; the classic grouped path has no corresponding sidecar). + /// WalkStaticStreamPopulator transforms it through the part model + /// and retains the resulting squared CYpt key in + /// before the per-cell stable sort. The key + /// feeds two FIFO alpha lists; neither queue sorts. /// internal readonly record struct WalkClassifiedBatch( GroupKey Key, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 3dcfbb04..8fb6b34d 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -1818,8 +1818,7 @@ public sealed partial class WbDrawDispatcher : IDisposable ObserveClassifiedDispatcherSubmission(observeCurrentPath, visibleInstanceCount: 0, immediateInstanceCount: 0, - deferTransparent: false, - camPos); + deferTransparent: false); _cpuStopwatch.Stop(); if (diag) MaybeFlushDiag(); return; @@ -1841,8 +1840,7 @@ public sealed partial class WbDrawDispatcher : IDisposable ObserveClassifiedDispatcherSubmission(observeCurrentPath, visibleInstanceCount: 0, immediateInstanceCount: 0, - deferTransparent, - camPos); + deferTransparent); _cpuStopwatch.Stop(); if (diag) MaybeFlushDiag(); return; @@ -1850,7 +1848,7 @@ public sealed partial class WbDrawDispatcher : IDisposable _opaqueDraws.Sort(CompareOpaqueSubmissionOrder); if (deferTransparent) - DeferTransparentGroups(camPos, vp); + DeferTransparentGroups(vp); else _translucentDraws.Sort(CompareTransparentSubmissionOrder); @@ -1969,8 +1967,7 @@ public sealed partial class WbDrawDispatcher : IDisposable ObserveClassifiedDispatcherSubmission(observeCurrentPath, totalInstances, immediateInstances, - deferTransparent, - camPos); + deferTransparent); // Campaign V slice V11: every per-frame upload is a frame ring slice // bound through the borrowed world pass, which retires the buffer-set @@ -2153,8 +2150,7 @@ public sealed partial class WbDrawDispatcher : IDisposable private void ObserveCurrentDispatcherSubmission( int visibleInstanceCount, int immediateInstanceCount, - bool deferTransparent, - Vector3 cameraWorldPosition) + bool deferTransparent) { ICurrentRenderDispatcherObserver? observer = _currentRenderSceneObserver; @@ -2168,7 +2164,6 @@ public sealed partial class WbDrawDispatcher : IDisposable deferTransparent, _opaqueDraws, _translucentDraws, - cameraWorldPosition, _alphaFingerprintScratch); observer.ObserveDispatcherSubmission(in submission); } @@ -2177,16 +2172,14 @@ public sealed partial class WbDrawDispatcher : IDisposable bool observeCurrentPath, int visibleInstanceCount, int immediateInstanceCount, - bool deferTransparent, - Vector3 cameraWorldPosition) + bool deferTransparent) { if (observeCurrentPath) { ObserveCurrentDispatcherSubmission( visibleInstanceCount, immediateInstanceCount, - deferTransparent, - cameraWorldPosition); + deferTransparent); } } @@ -2197,7 +2190,6 @@ public sealed partial class WbDrawDispatcher : IDisposable bool deferTransparent, IReadOnlyList opaque, IReadOnlyList transparent, - Vector3 cameraWorldPosition, List alphaScratch) { // The no-VAO production early-out deliberately skips group @@ -2226,7 +2218,6 @@ public sealed partial class WbDrawDispatcher : IDisposable RenderSceneHash128 transparentDigest = BuildTransparentSubmissionDigest( acceptedTransparent, - cameraWorldPosition, alphaScratch); RenderSceneHash128 transparentSetDigest = BuildOpaqueSubmissionDigest(acceptedTransparent); @@ -2285,11 +2276,8 @@ public sealed partial class WbDrawDispatcher : IDisposable private static RenderSceneHash128 BuildTransparentSubmissionDigest( IReadOnlyList groups, - Vector3 cameraWorldPosition, List scratch) { - _ = cameraWorldPosition; // S4-c2: the digest reflects retail's FIFO - // submission order, not a distance sort. scratch.Clear(); for (int groupIndex = 0; groupIndex < groups.Count; @@ -2395,7 +2383,6 @@ public sealed partial class WbDrawDispatcher : IDisposable int index) { hash.Add(group.Matrices[index]); - hash.Add(group.LocalSortCenters[index]); hash.Add(group.Slots[index]); InstanceLightSet lights = group.LightSets[index]; for (int lightIndex = 0; @@ -2410,11 +2397,8 @@ public sealed partial class WbDrawDispatcher : IDisposable hash.Add(group.SelectionLighting[index]); } - private void DeferTransparentGroups(Vector3 cameraWorldPosition, Matrix4x4 viewProjection) + private void DeferTransparentGroups(Matrix4x4 viewProjection) { - _ = cameraWorldPosition; // S4-c2: retail's queues are FIFO, not distance-sorted; kept as a - // parameter so callers need not change (WalkClassify's sibling - // call site still needs it for nothing else either). RetailAlphaQueue queue = _alphaQueue!; if (_deferredAlpha.Count == 0) _deferredAlphaViewProjection = viewProjection; @@ -2858,7 +2842,7 @@ public sealed partial class WbDrawDispatcher : IDisposable internal static void ApplyCacheHit( EntityCacheEntry entry, Matrix4x4 entityWorld, - Action appendInstance) + Action appendInstance) { foreach (var cached in entry.Batches) { @@ -2869,8 +2853,7 @@ public sealed partial class WbDrawDispatcher : IDisposable // out of sync with it. appendInstance( cached.Key, - cached.RestPose * entityWorld, - cached.LocalSortCenter); + cached.RestPose * entityWorld); } } @@ -2899,7 +2882,7 @@ public sealed partial class WbDrawDispatcher : IDisposable GroupRegistration = group.Registration, }; } - AppendInstanceToGroup(group!, model, cached.LocalSortCenter); + AppendInstanceToGroup(group!, model); } } @@ -3012,11 +2995,10 @@ public sealed partial class WbDrawDispatcher : IDisposable /// private void AppendInstanceToGroup( GroupKey key, - Matrix4x4 model, - Vector3 localSortCenter) + Matrix4x4 model) { InstanceGroup grp = GetOrCreateInstanceGroup(key); - AppendInstanceToGroup(grp, model, localSortCenter); + AppendInstanceToGroup(grp, model); } private InstanceGroup GetOrCreateInstanceGroup(GroupKey key) @@ -3047,12 +3029,10 @@ public sealed partial class WbDrawDispatcher : IDisposable private void AppendInstanceToGroup( InstanceGroup grp, - Matrix4x4 model, - Vector3 localSortCenter) + Matrix4x4 model) { grp.LastUsedFrame = _groupFrame; grp.Matrices.Add(model); - grp.LocalSortCenters.Add(localSortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices @@ -3231,7 +3211,6 @@ public sealed partial class WbDrawDispatcher : IDisposable InstanceGroup grp = GetOrCreateInstanceGroup(key); grp.Matrices.Add(model); - grp.LocalSortCenters.Add(renderData.SortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices @@ -3241,7 +3220,6 @@ public sealed partial class WbDrawDispatcher : IDisposable key, texSlot, restPose, - renderData.SortCenter, grp, grp.Registration)); } @@ -3712,11 +3690,6 @@ public sealed partial class WbDrawDispatcher : IDisposable public float SortDistance; // squared distance from camera to first instance, for opaque sort public readonly List Matrices = new(); - // Retail CPhysicsPart::CYpt uses the transformed GfxObj sort center, - // not the entity origin. Parallel to Matrices so delayed-alpha - // submissions retain the exact per-part key after material grouping. - public readonly List LocalSortCenters = new(); - // Retail CShadowPart::insertion_sort (0x006B5130) is stable for equal // CYpt. Material groups erase authored entity/part/batch traversal // unless that order is retained explicitly. SubmissionOrders[i] is @@ -3771,7 +3744,6 @@ public sealed partial class WbDrawDispatcher : IDisposable public void ClearPerInstanceData() { Matrices.Clear(); - LocalSortCenters.Clear(); SubmissionOrders.Clear(); Slots.Clear(); LightSets.Clear(); @@ -3785,7 +3757,6 @@ public sealed partial class WbDrawDispatcher : IDisposable { ClearPerInstanceData(); Matrices.TrimExcess(); - LocalSortCenters.TrimExcess(); SubmissionOrders.TrimExcess(); Slots.TrimExcess(); LightSets.TrimExcess(); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs index c0b03a0a..d6ab130e 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs @@ -345,6 +345,54 @@ public sealed class WalkStaticStreamPopulatorTests Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal); } + [Fact] + public void ClassicGroupedAlphaSourceTruth_DeletesDeadOrderingChainAndPreservesLiveOwners() + { + string root = FindRepoRoot(); + string dispatcher = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "Wb", "WbDrawDispatcher.cs")); + string cachedBatch = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "Wb", "CachedBatch.cs")); + string walkClassifier = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "Wb", "WbDrawDispatcher.WalkClassify.cs")); + string alphaProduction = dispatcher + cachedBatch + walkClassifier; + + Assert.Contains( + "Vector3.DistanceSquared(cameraWorldPosition, groupPosition)", + dispatcher, + StringComparison.Ordinal); + Assert.Contains("group.SortDistance =", dispatcher, StringComparison.Ordinal); + Assert.Contains("Vector3 LocalSortCenter", walkClassifier, StringComparison.Ordinal); + Assert.Contains("float SortDistanceSq", walkClassifier, StringComparison.Ordinal); + Assert.DoesNotContain("RetailAlphaOrdering", alphaProduction, StringComparison.Ordinal); + Assert.DoesNotContain("FlushFartherThan(", alphaProduction, StringComparison.Ordinal); + Assert.DoesNotContain("viewerDistance", alphaProduction, StringComparison.Ordinal); + Assert.DoesNotContain("_ = cameraWorldPosition", dispatcher, StringComparison.Ordinal); + } + + [Fact] + public void AlphaFlushCountKnownFailure_StatesOnlyTheThreeExactReasons() + { + string root = FindRepoRoot(); + string alphaTranscript = File.ReadAllText(Path.Combine( + root, + "tests", + "AcDream.App.Tests", + "Rendering", + "Walk", + "WalkTraceConformanceTests.AlphaDepthTranscript.cs")); + + Assert.Contains("THREE independent reasons", alphaTranscript, StringComparison.Ordinal); + Assert.Contains("GfxObj/particle mesh content at all", alphaTranscript, StringComparison.Ordinal); + Assert.Contains("AP-238 coalesces transparent", alphaTranscript, StringComparison.Ordinal); + Assert.Contains("one token per (cell,list)", alphaTranscript, StringComparison.Ordinal); + Assert.Contains("AP-239/AP-240 change", alphaTranscript, StringComparison.Ordinal); + Assert.Contains( + "former broad \"per instance versus per subset\" explanation was false.", + alphaTranscript, + StringComparison.Ordinal); + } + private static string FindRepoRoot() { string? directory = AppContext.BaseDirectory; @@ -907,7 +955,7 @@ public sealed class WalkStaticStreamPopulatorTests "DeferTransparentGroups", BindingFlags.Instance | BindingFlags.NonPublic)!; - defer.Invoke(fx.Dispatcher, [Vector3.Zero, Matrix4x4.Identity]); + defer.Invoke(fx.Dispatcher, [Matrix4x4.Identity]); Assert.Equal(0, fx.AlphaQueue!.PendingCount); Assert.Contains( @@ -947,7 +995,6 @@ public sealed class WalkStaticStreamPopulatorTests Translucency = TranslucencyKind.AlphaBlend, }; group.Matrices.Add(Matrix4x4.Identity); - group.LocalSortCenters.Add(Vector3.Zero); group.SubmissionOrders.Add(0); group.Slots.Add(0); group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); diff --git a/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs index e64b5a25..7925bdaf 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Reflection; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Wb; using AcDream.Core.Meshing; @@ -104,7 +105,6 @@ public class InstanceGroupClearTests deferTransparent: true, opaque: [], transparent: [first, second], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); var reversedMaterialGroups = WbDrawDispatcher.CreateDispatcherSubmission( @@ -113,7 +113,6 @@ public class InstanceGroupClearTests deferTransparent: true, opaque: [], transparent: [second, first], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); Assert.Equal( @@ -128,7 +127,6 @@ public class InstanceGroupClearTests deferTransparent: true, opaque: [], transparent: [first, second], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); Assert.NotEqual( @@ -154,7 +152,6 @@ public class InstanceGroupClearTests deferTransparent: false, opaque: [], transparent: [], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); CurrentRenderDispatcherSubmission actual = WbDrawDispatcher.CreateDispatcherSubmission( @@ -163,7 +160,6 @@ public class InstanceGroupClearTests deferTransparent: false, opaque: [stale], transparent: [stale], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); Assert.Equal(expected, actual); @@ -188,7 +184,6 @@ public class InstanceGroupClearTests deferTransparent: true, opaque: [], transparent: [ordinary], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); CurrentRenderDispatcherSubmission buildingSubmission = WbDrawDispatcher.CreateDispatcherSubmission( @@ -197,7 +192,6 @@ public class InstanceGroupClearTests deferTransparent: true, opaque: [], transparent: [building], - cameraWorldPosition: Vector3.Zero, alphaScratch: scratch); Assert.NotEqual( @@ -208,6 +202,63 @@ public class InstanceGroupClearTests buildingSubmission.TransparentSetDigest); } + [Fact] + public void ClassicGroupedShapes_DeleteDeadSortCenterSidecarsButRetainWalkCyptKey() + { + Assert.Null(typeof(CachedBatch).GetProperty("LocalSortCenter")); + Assert.Null(typeof(WbDrawDispatcher.InstanceGroup).GetField("LocalSortCenters")); + + Type walkBatch = typeof(WbDrawDispatcher).GetNestedType( + "WalkClassifiedBatch", + BindingFlags.NonPublic) + ?? throw new InvalidOperationException("WalkClassifiedBatch was not found."); + Assert.Equal(typeof(Vector3), walkBatch.GetProperty("LocalSortCenter")?.PropertyType); + Assert.Equal(typeof(float), walkBatch.GetProperty("SortDistanceSq")?.PropertyType); + } + + [Fact] + public void AlphaSubmissionShapes_DeleteDeadCameraChainButRetainOpaqueCameraDistance() + { + Type dispatcher = typeof(WbDrawDispatcher); + MethodInfo defer = dispatcher.GetMethod( + "DeferTransparentGroups", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("DeferTransparentGroups was not found."); + MethodInfo transparentDigest = dispatcher.GetMethod( + "BuildTransparentSubmissionDigest", + BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("BuildTransparentSubmissionDigest was not found."); + MethodInfo createSubmission = dispatcher.GetMethod( + "CreateDispatcherSubmission", + BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CreateDispatcherSubmission was not found."); + MethodInfo observeCurrent = dispatcher.GetMethod( + "ObserveCurrentDispatcherSubmission", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("ObserveCurrentDispatcherSubmission was not found."); + MethodInfo observeClassified = dispatcher.GetMethod( + "ObserveClassifiedDispatcherSubmission", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("ObserveClassifiedDispatcherSubmission was not found."); + + Assert.Equal([typeof(Matrix4x4)], defer.GetParameters().Select(static p => p.ParameterType)); + Assert.DoesNotContain(transparentDigest.GetParameters(), static p => p.ParameterType == typeof(Vector3)); + Assert.DoesNotContain(createSubmission.GetParameters(), static p => p.ParameterType == typeof(Vector3)); + Assert.DoesNotContain(observeCurrent.GetParameters(), static p => p.ParameterType == typeof(Vector3)); + Assert.DoesNotContain(observeClassified.GetParameters(), static p => p.ParameterType == typeof(Vector3)); + + MethodInfo partition = dispatcher.GetMethod( + "PartitionInstanceGroups", + BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("PartitionInstanceGroups was not found."); + Assert.Contains( + partition.GetParameters(), + static p => p.Name == "cameraWorldPosition" && p.ParameterType == typeof(Vector3)); + Assert.Equal( + typeof(float), + typeof(WbDrawDispatcher.InstanceGroup).GetField("SortDistance")?.FieldType); + } + [Fact] public void CachedGroupHandle_RequiresLiveMatchingRegistration() { @@ -392,7 +443,6 @@ public class InstanceGroupClearTests Translucency = TranslucencyKind.AlphaBlend, }; group.Matrices.Add(Matrix4x4.CreateTranslation(10f, 0f, 0f)); - group.LocalSortCenters.Add(Vector3.Zero); group.SubmissionOrders.Add(submissionOrder); group.Slots.Add(0u); group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); @@ -404,9 +454,9 @@ public class InstanceGroupClearTests } // #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds - // nine per-instance parallel lists — Matrices, LocalSortCenters, - // SubmissionOrders, Slots, LightSets, IndoorFlags, Opacities, and - // DetailCategories, SelectionLighting — appended in lockstep + // eight per-instance parallel lists — Matrices, SubmissionOrders, Slots, + // LightSets, IndoorFlags, Opacities, DetailCategories, and + // SelectionLighting — appended in lockstep // (one entry per drawn instance) every frame. The // per-frame reset must clear ALL of them. #188 added Opacities but left it out of // the inline clear loop, so it grew one float per instance per frame forever; as @@ -418,7 +468,6 @@ public class InstanceGroupClearTests { var grp = new WbDrawDispatcher.InstanceGroup(); grp.Matrices.Add(Matrix4x4.Identity); - grp.LocalSortCenters.Add(Vector3.Zero); grp.SubmissionOrders.Add(0); grp.Slots.Add(1u); grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); @@ -430,7 +479,6 @@ public class InstanceGroupClearTests grp.ClearPerInstanceData(); Assert.Empty(grp.Matrices); - Assert.Empty(grp.LocalSortCenters); Assert.Empty(grp.SubmissionOrders); Assert.Empty(grp.Slots); Assert.Empty(grp.LightSets); diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs index f9c0c111..446b56d4 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs @@ -417,8 +417,7 @@ public sealed class WbDrawDispatcherBucketingTests uint firstIndex, int indexCount, uint texSlot, - Matrix4x4? restPose = null, - Vector3? localSortCenter = null) + Matrix4x4? restPose = null) { var key = new GroupKey( FirstIndex: firstIndex, @@ -431,8 +430,7 @@ public sealed class WbDrawDispatcherBucketingTests return new CachedBatch( key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), - restPose ?? Matrix4x4.Identity, - localSortCenter ?? Vector3.Zero); + restPose ?? Matrix4x4.Identity); } [Fact] @@ -488,8 +486,7 @@ public sealed class WbDrawDispatcherBucketingTests // Production code: this is the !isAnimated && _cache.TryGet branch // at the top of the per-entity loop body in Draw. var groups = new Dictionary>(); - var sortCenters = new List(); - void AppendInstance(GroupKey k, Matrix4x4 m, Vector3 localSortCenter) + void AppendInstance(GroupKey k, Matrix4x4 m) { if (!groups.TryGetValue(k, out var list)) { @@ -497,7 +494,6 @@ public sealed class WbDrawDispatcherBucketingTests groups[k] = list; } list.Add(m); - sortCenters.Add(localSortCenter); } Assert.True(cache.TryGet(EntityId, LandblockId, out var entryHit)); @@ -519,35 +515,6 @@ public sealed class WbDrawDispatcherBucketingTests // appended matrix must equal entityWorld. foreach (var (_, list) in groups) Assert.Equal(entityWorld, list[0]); - Assert.All(sortCenters, center => Assert.Equal(Vector3.Zero, center)); - } - - [Fact] - public void ApplyCacheHit_PreservesAuthoredSortCenter() - { - Vector3 authoredCenter = new(1.25f, -2.5f, 7.75f); - var entry = new EntityCacheEntry - { - EntityId = 100, - LandblockHint = 0xA9B40000u, - Batches = - [ - MakeCachedBatch( - ibo: 1, - firstIndex: 0, - indexCount: 6, - texSlot: 0xAA, - localSortCenter: authoredCenter), - ], - }; - Vector3 observedCenter = default; - - WbDrawDispatcher.ApplyCacheHit( - entry, - Matrix4x4.Identity, - (_, _, center) => observedCenter = center); - - Assert.Equal(authoredCenter, observedCenter); } [Fact] @@ -788,7 +755,7 @@ public sealed class WbDrawDispatcherBucketingTests const uint EntityId = 100; const int MeshRefCount = 3; - void AppendInstance(GroupKey k, Matrix4x4 m, Vector3 localSortCenter) + void AppendInstance(GroupKey k, Matrix4x4 m) { if (!groups.TryGetValue(k, out var list)) {