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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-04 15:02:23 +02:00
parent a0523e6057
commit 26e97ba412
7 changed files with 224 additions and 105 deletions

View file

@ -1990,3 +1990,93 @@ call reachability, diagnostic-digest consequences, cache/reset/storage
lifecycle, allocation, allowed scope, every mutation claim, and all gate lifecycle, allocation, allowed scope, every mutation claim, and all gate
counts. A failed lens gets one bounded fix contract; a third fix round stops 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. 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<Vector3> 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.

View file

@ -9,8 +9,6 @@ namespace AcDream.App.Rendering.Wb;
/// subPart contributes its own <see cref="CachedBatch"/> entries, with /// subPart contributes its own <see cref="CachedBatch"/> entries, with
/// <see cref="RestPose"/> already containing the /// <see cref="RestPose"/> already containing the
/// <c>subPart.PartTransform * meshRef.PartTransform</c> product. /// <c>subPart.PartTransform * meshRef.PartTransform</c> product.
/// <see cref="LocalSortCenter"/> preserves the authored GfxObj key used by
/// retail's delayed-alpha viewer-distance ordering on cache hits.
/// <see cref="Group"/> is a registration-checked fast handle into the /// <see cref="Group"/> is a registration-checked fast handle into the
/// dispatcher's bounded group table; a retired or synthetic entry falls back /// dispatcher's bounded group table; a retired or synthetic entry falls back
/// to key lookup and refreshes this array entry. /// to key lookup and refreshes this array entry.
@ -25,7 +23,6 @@ internal readonly record struct CachedBatch(
GroupKey Key, GroupKey Key,
GpuTextureSlot TextureSlot, GpuTextureSlot TextureSlot,
Matrix4x4 RestPose, Matrix4x4 RestPose,
Vector3 LocalSortCenter = default,
WbDrawDispatcher.InstanceGroup? Group = null, WbDrawDispatcher.InstanceGroup? Group = null,
long GroupRegistration = 0); long GroupRegistration = 0);

View file

@ -143,13 +143,12 @@ public sealed partial class WbDrawDispatcher
/// one needs to reach the alpha queue: <see cref="IsOpaque"/> (so the /// one needs to reach the alpha queue: <see cref="IsOpaque"/> (so the
/// populator can route without re-deriving it from /// populator can route without re-deriving it from
/// <see cref="GroupKey.Translucency"/>) and <see cref="LocalSortCenter"/> /// <see cref="GroupKey.Translucency"/>) and <see cref="LocalSortCenter"/>
/// (the authored GfxObj sort center — the same value /// (the authored GfxObj sort center consumed by the live per-cell CYpt
/// <c>InstanceGroup.LocalSortCenters</c> carries per instance today; S4-c2 /// sort; the classic grouped path has no corresponding sidecar).
/// deleted its one alpha-ordering consumer, <c>RetailAlphaOrdering.ComputeViewerDistance</c>, /// <c>WalkStaticStreamPopulator</c> transforms it through the part model
/// but S4-c3a restores its retail use: <c>WalkStaticStreamPopulator</c> /// and retains the resulting squared CYpt key in
/// transforms it through the part model and retains the resulting /// <see cref="SortDistanceSq"/> before the per-cell stable sort. The key
/// squared CYpt key in <see cref="SortDistanceSq"/> before the per-cell /// feeds two FIFO alpha lists; neither queue sorts.
/// stable sort. The key feeds two FIFO alpha lists; neither queue sorts.
/// </summary> /// </summary>
internal readonly record struct WalkClassifiedBatch( internal readonly record struct WalkClassifiedBatch(
GroupKey Key, GroupKey Key,

View file

@ -1818,8 +1818,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveClassifiedDispatcherSubmission(observeCurrentPath,
visibleInstanceCount: 0, visibleInstanceCount: 0,
immediateInstanceCount: 0, immediateInstanceCount: 0,
deferTransparent: false, deferTransparent: false);
camPos);
_cpuStopwatch.Stop(); _cpuStopwatch.Stop();
if (diag) MaybeFlushDiag(); if (diag) MaybeFlushDiag();
return; return;
@ -1841,8 +1840,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveClassifiedDispatcherSubmission(observeCurrentPath,
visibleInstanceCount: 0, visibleInstanceCount: 0,
immediateInstanceCount: 0, immediateInstanceCount: 0,
deferTransparent, deferTransparent);
camPos);
_cpuStopwatch.Stop(); _cpuStopwatch.Stop();
if (diag) MaybeFlushDiag(); if (diag) MaybeFlushDiag();
return; return;
@ -1850,7 +1848,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
_opaqueDraws.Sort(CompareOpaqueSubmissionOrder); _opaqueDraws.Sort(CompareOpaqueSubmissionOrder);
if (deferTransparent) if (deferTransparent)
DeferTransparentGroups(camPos, vp); DeferTransparentGroups(vp);
else else
_translucentDraws.Sort(CompareTransparentSubmissionOrder); _translucentDraws.Sort(CompareTransparentSubmissionOrder);
@ -1969,8 +1967,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
ObserveClassifiedDispatcherSubmission(observeCurrentPath, ObserveClassifiedDispatcherSubmission(observeCurrentPath,
totalInstances, totalInstances,
immediateInstances, immediateInstances,
deferTransparent, deferTransparent);
camPos);
// Campaign V slice V11: every per-frame upload is a frame ring slice // Campaign V slice V11: every per-frame upload is a frame ring slice
// bound through the borrowed world pass, which retires the buffer-set // bound through the borrowed world pass, which retires the buffer-set
@ -2153,8 +2150,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
private void ObserveCurrentDispatcherSubmission( private void ObserveCurrentDispatcherSubmission(
int visibleInstanceCount, int visibleInstanceCount,
int immediateInstanceCount, int immediateInstanceCount,
bool deferTransparent, bool deferTransparent)
Vector3 cameraWorldPosition)
{ {
ICurrentRenderDispatcherObserver? observer = ICurrentRenderDispatcherObserver? observer =
_currentRenderSceneObserver; _currentRenderSceneObserver;
@ -2168,7 +2164,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
deferTransparent, deferTransparent,
_opaqueDraws, _opaqueDraws,
_translucentDraws, _translucentDraws,
cameraWorldPosition,
_alphaFingerprintScratch); _alphaFingerprintScratch);
observer.ObserveDispatcherSubmission(in submission); observer.ObserveDispatcherSubmission(in submission);
} }
@ -2177,16 +2172,14 @@ public sealed partial class WbDrawDispatcher : IDisposable
bool observeCurrentPath, bool observeCurrentPath,
int visibleInstanceCount, int visibleInstanceCount,
int immediateInstanceCount, int immediateInstanceCount,
bool deferTransparent, bool deferTransparent)
Vector3 cameraWorldPosition)
{ {
if (observeCurrentPath) if (observeCurrentPath)
{ {
ObserveCurrentDispatcherSubmission( ObserveCurrentDispatcherSubmission(
visibleInstanceCount, visibleInstanceCount,
immediateInstanceCount, immediateInstanceCount,
deferTransparent, deferTransparent);
cameraWorldPosition);
} }
} }
@ -2197,7 +2190,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
bool deferTransparent, bool deferTransparent,
IReadOnlyList<InstanceGroup> opaque, IReadOnlyList<InstanceGroup> opaque,
IReadOnlyList<InstanceGroup> transparent, IReadOnlyList<InstanceGroup> transparent,
Vector3 cameraWorldPosition,
List<AlphaFingerprint> alphaScratch) List<AlphaFingerprint> alphaScratch)
{ {
// The no-VAO production early-out deliberately skips group // The no-VAO production early-out deliberately skips group
@ -2226,7 +2218,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
RenderSceneHash128 transparentDigest = RenderSceneHash128 transparentDigest =
BuildTransparentSubmissionDigest( BuildTransparentSubmissionDigest(
acceptedTransparent, acceptedTransparent,
cameraWorldPosition,
alphaScratch); alphaScratch);
RenderSceneHash128 transparentSetDigest = RenderSceneHash128 transparentSetDigest =
BuildOpaqueSubmissionDigest(acceptedTransparent); BuildOpaqueSubmissionDigest(acceptedTransparent);
@ -2285,11 +2276,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
private static RenderSceneHash128 BuildTransparentSubmissionDigest( private static RenderSceneHash128 BuildTransparentSubmissionDigest(
IReadOnlyList<InstanceGroup> groups, IReadOnlyList<InstanceGroup> groups,
Vector3 cameraWorldPosition,
List<AlphaFingerprint> scratch) List<AlphaFingerprint> scratch)
{ {
_ = cameraWorldPosition; // S4-c2: the digest reflects retail's FIFO
// submission order, not a distance sort.
scratch.Clear(); scratch.Clear();
for (int groupIndex = 0; for (int groupIndex = 0;
groupIndex < groups.Count; groupIndex < groups.Count;
@ -2395,7 +2383,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
int index) int index)
{ {
hash.Add(group.Matrices[index]); hash.Add(group.Matrices[index]);
hash.Add(group.LocalSortCenters[index]);
hash.Add(group.Slots[index]); hash.Add(group.Slots[index]);
InstanceLightSet lights = group.LightSets[index]; InstanceLightSet lights = group.LightSets[index];
for (int lightIndex = 0; for (int lightIndex = 0;
@ -2410,11 +2397,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
hash.Add(group.SelectionLighting[index]); 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!; RetailAlphaQueue queue = _alphaQueue!;
if (_deferredAlpha.Count == 0) if (_deferredAlpha.Count == 0)
_deferredAlphaViewProjection = viewProjection; _deferredAlphaViewProjection = viewProjection;
@ -2858,7 +2842,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
internal static void ApplyCacheHit( internal static void ApplyCacheHit(
EntityCacheEntry entry, EntityCacheEntry entry,
Matrix4x4 entityWorld, Matrix4x4 entityWorld,
Action<GroupKey, Matrix4x4, Vector3> appendInstance) Action<GroupKey, Matrix4x4> appendInstance)
{ {
foreach (var cached in entry.Batches) foreach (var cached in entry.Batches)
{ {
@ -2869,8 +2853,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
// out of sync with it. // out of sync with it.
appendInstance( appendInstance(
cached.Key, cached.Key,
cached.RestPose * entityWorld, cached.RestPose * entityWorld);
cached.LocalSortCenter);
} }
} }
@ -2899,7 +2882,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
GroupRegistration = group.Registration, GroupRegistration = group.Registration,
}; };
} }
AppendInstanceToGroup(group!, model, cached.LocalSortCenter); AppendInstanceToGroup(group!, model);
} }
} }
@ -3012,11 +2995,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
/// </summary> /// </summary>
private void AppendInstanceToGroup( private void AppendInstanceToGroup(
GroupKey key, GroupKey key,
Matrix4x4 model, Matrix4x4 model)
Vector3 localSortCenter)
{ {
InstanceGroup grp = GetOrCreateInstanceGroup(key); InstanceGroup grp = GetOrCreateInstanceGroup(key);
AppendInstanceToGroup(grp, model, localSortCenter); AppendInstanceToGroup(grp, model);
} }
private InstanceGroup GetOrCreateInstanceGroup(GroupKey key) private InstanceGroup GetOrCreateInstanceGroup(GroupKey key)
@ -3047,12 +3029,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
private void AppendInstanceToGroup( private void AppendInstanceToGroup(
InstanceGroup grp, InstanceGroup grp,
Matrix4x4 model, Matrix4x4 model)
Vector3 localSortCenter)
{ {
grp.LastUsedFrame = _groupFrame; grp.LastUsedFrame = _groupFrame;
grp.Matrices.Add(model); grp.Matrices.Add(model);
grp.LocalSortCenters.Add(localSortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, 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); InstanceGroup grp = GetOrCreateInstanceGroup(key);
grp.Matrices.Add(model); grp.Matrices.Add(model);
grp.LocalSortCenters.Add(renderData.SortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, 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, key,
texSlot, texSlot,
restPose, restPose,
renderData.SortCenter,
grp, grp,
grp.Registration)); 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 float SortDistance; // squared distance from camera to first instance, for opaque sort
public readonly List<Matrix4x4> Matrices = new(); public readonly List<Matrix4x4> 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<Vector3> LocalSortCenters = new();
// Retail CShadowPart::insertion_sort (0x006B5130) is stable for equal // Retail CShadowPart::insertion_sort (0x006B5130) is stable for equal
// CYpt. Material groups erase authored entity/part/batch traversal // CYpt. Material groups erase authored entity/part/batch traversal
// unless that order is retained explicitly. SubmissionOrders[i] is // unless that order is retained explicitly. SubmissionOrders[i] is
@ -3771,7 +3744,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
public void ClearPerInstanceData() public void ClearPerInstanceData()
{ {
Matrices.Clear(); Matrices.Clear();
LocalSortCenters.Clear();
SubmissionOrders.Clear(); SubmissionOrders.Clear();
Slots.Clear(); Slots.Clear();
LightSets.Clear(); LightSets.Clear();
@ -3785,7 +3757,6 @@ public sealed partial class WbDrawDispatcher : IDisposable
{ {
ClearPerInstanceData(); ClearPerInstanceData();
Matrices.TrimExcess(); Matrices.TrimExcess();
LocalSortCenters.TrimExcess();
SubmissionOrders.TrimExcess(); SubmissionOrders.TrimExcess();
Slots.TrimExcess(); Slots.TrimExcess();
LightSets.TrimExcess(); LightSets.TrimExcess();

View file

@ -345,6 +345,54 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal); 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 <c>(cell,list)</c>", 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() private static string FindRepoRoot()
{ {
string? directory = AppContext.BaseDirectory; string? directory = AppContext.BaseDirectory;
@ -907,7 +955,7 @@ public sealed class WalkStaticStreamPopulatorTests
"DeferTransparentGroups", "DeferTransparentGroups",
BindingFlags.Instance | BindingFlags.NonPublic)!; 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.Equal(0, fx.AlphaQueue!.PendingCount);
Assert.Contains( Assert.Contains(
@ -947,7 +995,6 @@ public sealed class WalkStaticStreamPopulatorTests
Translucency = TranslucencyKind.AlphaBlend, Translucency = TranslucencyKind.AlphaBlend,
}; };
group.Matrices.Add(Matrix4x4.Identity); group.Matrices.Add(Matrix4x4.Identity);
group.LocalSortCenters.Add(Vector3.Zero);
group.SubmissionOrders.Add(0); group.SubmissionOrders.Add(0);
group.Slots.Add(0); group.Slots.Add(0);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.Core.Meshing; using AcDream.Core.Meshing;
@ -104,7 +105,6 @@ public class InstanceGroupClearTests
deferTransparent: true, deferTransparent: true,
opaque: [], opaque: [],
transparent: [first, second], transparent: [first, second],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
var reversedMaterialGroups = var reversedMaterialGroups =
WbDrawDispatcher.CreateDispatcherSubmission( WbDrawDispatcher.CreateDispatcherSubmission(
@ -113,7 +113,6 @@ public class InstanceGroupClearTests
deferTransparent: true, deferTransparent: true,
opaque: [], opaque: [],
transparent: [second, first], transparent: [second, first],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
Assert.Equal( Assert.Equal(
@ -128,7 +127,6 @@ public class InstanceGroupClearTests
deferTransparent: true, deferTransparent: true,
opaque: [], opaque: [],
transparent: [first, second], transparent: [first, second],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
Assert.NotEqual( Assert.NotEqual(
@ -154,7 +152,6 @@ public class InstanceGroupClearTests
deferTransparent: false, deferTransparent: false,
opaque: [], opaque: [],
transparent: [], transparent: [],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
CurrentRenderDispatcherSubmission actual = CurrentRenderDispatcherSubmission actual =
WbDrawDispatcher.CreateDispatcherSubmission( WbDrawDispatcher.CreateDispatcherSubmission(
@ -163,7 +160,6 @@ public class InstanceGroupClearTests
deferTransparent: false, deferTransparent: false,
opaque: [stale], opaque: [stale],
transparent: [stale], transparent: [stale],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
@ -188,7 +184,6 @@ public class InstanceGroupClearTests
deferTransparent: true, deferTransparent: true,
opaque: [], opaque: [],
transparent: [ordinary], transparent: [ordinary],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
CurrentRenderDispatcherSubmission buildingSubmission = CurrentRenderDispatcherSubmission buildingSubmission =
WbDrawDispatcher.CreateDispatcherSubmission( WbDrawDispatcher.CreateDispatcherSubmission(
@ -197,7 +192,6 @@ public class InstanceGroupClearTests
deferTransparent: true, deferTransparent: true,
opaque: [], opaque: [],
transparent: [building], transparent: [building],
cameraWorldPosition: Vector3.Zero,
alphaScratch: scratch); alphaScratch: scratch);
Assert.NotEqual( Assert.NotEqual(
@ -208,6 +202,63 @@ public class InstanceGroupClearTests
buildingSubmission.TransparentSetDigest); 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] [Fact]
public void CachedGroupHandle_RequiresLiveMatchingRegistration() public void CachedGroupHandle_RequiresLiveMatchingRegistration()
{ {
@ -392,7 +443,6 @@ public class InstanceGroupClearTests
Translucency = TranslucencyKind.AlphaBlend, Translucency = TranslucencyKind.AlphaBlend,
}; };
group.Matrices.Add(Matrix4x4.CreateTranslation(10f, 0f, 0f)); group.Matrices.Add(Matrix4x4.CreateTranslation(10f, 0f, 0f));
group.LocalSortCenters.Add(Vector3.Zero);
group.SubmissionOrders.Add(submissionOrder); group.SubmissionOrders.Add(submissionOrder);
group.Slots.Add(0u); group.Slots.Add(0u);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
@ -404,9 +454,9 @@ public class InstanceGroupClearTests
} }
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds // #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
// nine per-instance parallel lists — Matrices, LocalSortCenters, // eight per-instance parallel lists — Matrices, SubmissionOrders, Slots,
// SubmissionOrders, Slots, LightSets, IndoorFlags, Opacities, and // LightSets, IndoorFlags, Opacities, DetailCategories, and
// DetailCategories, SelectionLighting — appended in lockstep // SelectionLighting — appended in lockstep
// (one entry per drawn instance) every frame. The // (one entry per drawn instance) every frame. The
// per-frame reset must clear ALL of them. #188 added Opacities but left it out of // 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 // 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(); var grp = new WbDrawDispatcher.InstanceGroup();
grp.Matrices.Add(Matrix4x4.Identity); grp.Matrices.Add(Matrix4x4.Identity);
grp.LocalSortCenters.Add(Vector3.Zero);
grp.SubmissionOrders.Add(0); grp.SubmissionOrders.Add(0);
grp.Slots.Add(1u); grp.Slots.Add(1u);
grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
@ -430,7 +479,6 @@ public class InstanceGroupClearTests
grp.ClearPerInstanceData(); grp.ClearPerInstanceData();
Assert.Empty(grp.Matrices); Assert.Empty(grp.Matrices);
Assert.Empty(grp.LocalSortCenters);
Assert.Empty(grp.SubmissionOrders); Assert.Empty(grp.SubmissionOrders);
Assert.Empty(grp.Slots); Assert.Empty(grp.Slots);
Assert.Empty(grp.LightSets); Assert.Empty(grp.LightSets);

View file

@ -417,8 +417,7 @@ public sealed class WbDrawDispatcherBucketingTests
uint firstIndex, uint firstIndex,
int indexCount, int indexCount,
uint texSlot, uint texSlot,
Matrix4x4? restPose = null, Matrix4x4? restPose = null)
Vector3? localSortCenter = null)
{ {
var key = new GroupKey( var key = new GroupKey(
FirstIndex: firstIndex, FirstIndex: firstIndex,
@ -431,8 +430,7 @@ public sealed class WbDrawDispatcherBucketingTests
return new CachedBatch( return new CachedBatch(
key, key,
new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
restPose ?? Matrix4x4.Identity, restPose ?? Matrix4x4.Identity);
localSortCenter ?? Vector3.Zero);
} }
[Fact] [Fact]
@ -488,8 +486,7 @@ public sealed class WbDrawDispatcherBucketingTests
// Production code: this is the !isAnimated && _cache.TryGet branch // Production code: this is the !isAnimated && _cache.TryGet branch
// at the top of the per-entity loop body in Draw. // at the top of the per-entity loop body in Draw.
var groups = new Dictionary<GroupKey, List<Matrix4x4>>(); var groups = new Dictionary<GroupKey, List<Matrix4x4>>();
var sortCenters = new List<Vector3>(); void AppendInstance(GroupKey k, Matrix4x4 m)
void AppendInstance(GroupKey k, Matrix4x4 m, Vector3 localSortCenter)
{ {
if (!groups.TryGetValue(k, out var list)) if (!groups.TryGetValue(k, out var list))
{ {
@ -497,7 +494,6 @@ public sealed class WbDrawDispatcherBucketingTests
groups[k] = list; groups[k] = list;
} }
list.Add(m); list.Add(m);
sortCenters.Add(localSortCenter);
} }
Assert.True(cache.TryGet(EntityId, LandblockId, out var entryHit)); Assert.True(cache.TryGet(EntityId, LandblockId, out var entryHit));
@ -519,35 +515,6 @@ public sealed class WbDrawDispatcherBucketingTests
// appended matrix must equal entityWorld. // appended matrix must equal entityWorld.
foreach (var (_, list) in groups) foreach (var (_, list) in groups)
Assert.Equal(entityWorld, list[0]); 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] [Fact]
@ -788,7 +755,7 @@ public sealed class WbDrawDispatcherBucketingTests
const uint EntityId = 100; const uint EntityId = 100;
const int MeshRefCount = 3; 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)) if (!groups.TryGetValue(k, out var list))
{ {