fix(render): restore retail per-cell alpha order

Reconstruct one combined static/dynamic object-part stream for each ordinary outdoor or interior cell, compute authored SortCenter CYpt keys, and stable-sort far to near before projecting opaque and delayed subsets. Prepare real cell-particle records at the leaf, preserve every S4-c2 router outcome, and merge object and particle delayed records by retained key before either source appends to the unchanged CLIP/ALPHA FIFO lists. Cell turns remain cell-major; equal cross-source ties are deterministically object-first.

File AP-241 and AP-242 for the remaining separate opaque/row-5 channels and unrepresented equal-key common ordinal. File AP-243 for the paired-binary correction: retail shares the cell CYpt/heading beyond 50 m while this bounded port always uses the more exact authored per-part center. Pin 162 active AP rows and correct world-alpha and AlphaFlushCounts prose.

Lead-approved scope clarification: RetailPViewPassExecutor.WalkLeaf.cs and RetailPViewPassExecutor.cs are the minimum existing production leaf adapter and thin particle-prepare forwarder omitted by the literal Walk/Wb file list. They contain no router, queue, mask, state, depth, or flush behavior; relocating them would create an artificial seam.

Gates: Release solution build 0W/0E; shader/manifest 32/32; focused production 210/210; real allocation 3/3 at 0 B; one-shot hermetic 16743/0/0 across 14 assemblies; InstalledDat 385 pass/10 documented fail/1 skip with all six AlphaFlushSites passing; git diff --check PASS. Initial no-restore solution build failed NETSDK1004 for 42 missing scratch assets; one solution restore preceded the official build.

Mutation proof, each restored before final gates:

1. Reverse comparator: authored-center order expected [202,101], actual [101,202].

2. Move ties left: multipart/subset order expected [11,12,21,22], actual [22,21,12,11].

3. Restore static/dynamic blocks: expected [2,3,1], actual [3,1,2].

4. Use entity origin: authored-center order expected [202,101], actual [101,202].

5. Restore particle tail: expected [Wb,Particle,Wb,Particle], actual [Wb,Wb,Particle,Particle].

6. Scope-global sort: first cell model X expected 5, actual 50.

7. Restore dead camera parameter: SubmitWalkAlphaInstance parameter count expected 2, actual 3.

8. Restore stale global-queue prose: exact Assert.DoesNotContain failure on distance-sorts one shared queue.

9. Remove AP-241 identity: Assert.Single found no matching row.

10. Allocate in real merge: expected 0 B, actual 3072 B.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-04 13:18:54 +02:00
parent 06b986622b
commit a86ec73ece
17 changed files with 1067 additions and 197 deletions

View file

@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
@ -12,6 +13,7 @@ using AcDream.App.Rendering.Walk;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
@ -130,9 +132,17 @@ public sealed partial class WalkFrameDriverTests
alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f);
}
public void DrawStaticParticles(uint cellId) => log.Add($"PARTICLES:{cellId:x8}");
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId)
{
log.Add($"PARTICLES:{cellId:x8}");
return ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
}
public void DrawCellParticles(uint cellId) => log.Add($"CELL-PARTICLES:{cellId:x8}");
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId)
{
log.Add($"CELL-PARTICLES:{cellId:x8}");
return ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
}
}
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
@ -141,6 +151,39 @@ public sealed partial class WalkFrameDriverTests
log.Add($"FLUSH:{commandCount}:{string.Join(',', stages.Distinct())}");
}
private sealed class ProductionParticleLeaf(
ParticleSystem particles,
ParticleRenderer renderer,
ICamera camera,
Vector3 cameraWorldPosition) : IWalkFrameLeafRenderer
{
public void DrawSky() { }
public void DrawLandCellBatch(
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
public bool HasRenderableEmittersInCell(uint cellId) =>
particles.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId);
public void DrawCellShell(uint cellId) { }
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId) =>
renderer.PrepareForCellAlpha(
camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId);
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId) =>
renderer.PrepareForCellAlpha(
camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId);
public void ClearInteriorDepth() { }
public void FlushLandscape() { }
public int DrawExitSeals() => 0;
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
public void AlphaBarrier() { }
public void FlushSortCellExit() { }
}
private sealed class IdentityCamera : ICamera
{
public Matrix4x4 View => Matrix4x4.Identity;
public Matrix4x4 Projection => Matrix4x4.Identity;
public float Aspect { get; set; } = 1f;
}
private sealed class FakeWorldData : IWalkFrameWorldData
{
public readonly Dictionary<uint, WalkFrameStaticRecords> CellStaticsByCell = new();
@ -153,12 +196,22 @@ public sealed partial class WalkFrameDriverTests
public WalkFrameStaticRecords GetCellStatics(uint cellId) =>
CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetCellObjects(uint cellId) =>
Combine(
CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty),
CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty));
public WalkFrameStaticRecords GetCellDynamics(uint cellId) =>
CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) =>
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) =>
Combine(
OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty),
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty));
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) =>
OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty);
@ -167,6 +220,25 @@ public sealed partial class WalkFrameDriverTests
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) =>
WorldTransformByBuilding.GetValueOrDefault(building, Matrix4x4.Identity);
private static WalkFrameStaticRecords Combine(
WalkFrameStaticRecords first,
WalkFrameStaticRecords second)
{
if (second.Records.Count == 0)
return first;
RenderProjectionRecord[] dynamicRecords = second.Records
.Select(record => record with
{
ProjectionClass = RenderProjectionClass.LiveDynamicRoot,
})
.ToArray();
if (first.Records.Count == 0)
return new WalkFrameStaticRecords(dynamicRecords, second.TupleLandblockId);
return new WalkFrameStaticRecords(
first.Records.Concat(dynamicRecords).ToArray(),
first.TupleLandblockId);
}
}
// ── The walk-level test context (interior flood + building portal pass) ─
@ -1001,7 +1073,9 @@ public sealed partial class WalkFrameDriverTests
new[]
{
"ALPHA", "PUNCH:4@v0", "SHELL:00000104",
"FLUSH:1:LookInStatic", "FLUSH:1:Dynamic",
// S4-c3a: one cell shadow list, so static + dynamic are one
// sorted stream segment rather than two artificial blocks.
"FLUSH:2:LookInStatic,Dynamic",
"CELL-PARTICLES:00000104",
"FLUSH:1:BuildingShell",
},
@ -1805,6 +1879,205 @@ public sealed partial class WalkFrameDriverTests
}
}
[Fact]
public void CellTurn_RealParticlePreparationMergesWithObjectAlphaByCypt()
{
using var fx = new DispatcherFixture();
const uint cellId = 0x8C040005u;
const ulong objectGfx = 0x0200_0C31UL;
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
MakeBatch(0x08100C31u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords(
new[]
{
MakeRecord(1, 0, new Vector3(20, 0, 0),
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]),
MakeRecord(2, 0, new Vector3(40, 0, 0),
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]),
},
0x8C04u);
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
EmitterDesc desc = new()
{
DatId = 0x32000C31u,
Type = AcDream.Core.Vfx.ParticleType.Still,
MaxParticles = 1,
InitialParticles = 1,
LifetimeMin = 100f,
LifetimeMax = 100f,
StartAlpha = 1f,
EndAlpha = 1f,
};
int particle30 = particles.SpawnEmitter(desc, new Vector3(30, 0, 0));
// Equal to object 1: AP-242's deterministic source tie is object-first.
int particle20 = particles.SpawnEmitter(desc, new Vector3(20, 0, 0));
particles.UpdateEmitterOwnerCell(particle30, cellId);
particles.UpdateEmitterOwnerCell(particle20, cellId);
using var renderer = new ParticleRenderer(
fx.Device,
fx.FrameLifetime,
fx.Scope,
particles,
meshAdapter: fx.MeshAdapter,
alphaQueue: fx.AlphaQueue);
var leaf = new ProductionParticleLeaf(
particles, renderer, new IdentityCamera(), Vector3.Zero);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
var ctx = new TestContext();
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
renderer.BeginFrame(frameSlot: 0);
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(activeViews);
sink.OnLandscapeCellTurn(cellId);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
List<RetailAlphaEntry> entries = QueueAlphaEntries(fx.AlphaQueue);
Assert.Equal(4, entries.Count);
Assert.Equal(
[
typeof(WbDrawDispatcher),
typeof(ParticleRenderer),
typeof(WbDrawDispatcher),
typeof(ParticleRenderer),
],
entries.Select(static entry => entry.Source.GetType().DeclaringType));
fx.AlphaQueue.AbortFrame();
}
[Fact]
public void SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther()
{
using var fx = new DispatcherFixture();
const uint firstCell = 0x8C040005u;
const uint secondCell = 0x8C040006u;
const ulong objectGfx = 0x0200_0C32UL;
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
MakeBatch(0x08100C32u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[firstCell] = new WalkFrameStaticRecords(
new[] { MakeRecord(1, 0, new Vector3(5, 0, 0),
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
0x8C04u);
worldData.OutdoorStaticsByCell[secondCell] = new WalkFrameStaticRecords(
new[] { MakeRecord(2, 0, new Vector3(50, 0, 0),
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
0x8C04u);
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
leaf.CellsWithoutEmitters.UnionWith([firstCell, secondCell]);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
var ctx = new TestContext();
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(activeViews);
sink.OnLandscapeCellTurn(firstCell);
sink.OnLandscapeCellTurn(secondCell);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(2, QueueAlphaEntries(fx.AlphaQueue).Count);
IList payload = (IList)typeof(WbDrawDispatcher).GetField(
"_deferredAlpha", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(fx.Dispatcher)!;
Matrix4x4 firstModel = (Matrix4x4)payload[0]!.GetType().GetProperty("Model")!
.GetValue(payload[0])!;
Matrix4x4 secondModel = (Matrix4x4)payload[1]!.GetType().GetProperty("Model")!
.GetValue(payload[1])!;
Assert.Equal(5f, firstModel.M41);
Assert.Equal(50f, secondModel.M41);
fx.AlphaQueue.AbortFrame();
}
[Fact]
public void ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes()
{
using var fx = new DispatcherFixture();
const uint cellId = 0x8C040007u;
const ulong objectGfx = 0x0200_0C33UL;
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
MakeBatch(0x08100C33u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
var worldData = new FakeWorldData();
worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords(
new[] { MakeRecord(1, 0, new Vector3(40, 0, 0),
[new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) },
0x8C04u);
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
int handle = particles.SpawnEmitter(
new EmitterDesc
{
DatId = 0x32000C33u,
Type = AcDream.Core.Vfx.ParticleType.Still,
MaxParticles = 1,
InitialParticles = 1,
LifetimeMin = 100f,
LifetimeMax = 100f,
StartAlpha = 1f,
EndAlpha = 1f,
},
new Vector3(20, 0, 0));
particles.UpdateEmitterOwnerCell(handle, cellId);
using var renderer = new ParticleRenderer(
fx.Device,
fx.FrameLifetime,
fx.Scope,
particles,
meshAdapter: fx.MeshAdapter,
alphaQueue: fx.AlphaQueue);
var leaf = new ProductionParticleLeaf(
particles, renderer, new IdentityCamera(), Vector3.Zero);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
var ctx = new TestContext();
IWalkEventSink sink = driver;
var activeViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
using DrawScope draw = fx.BeginDraw();
fx.Dispatcher.BeginFrame(frameSlot: 0);
renderer.BeginFrame(frameSlot: 0);
fx.Device.RecordingEnabled = false;
void RunCellTurn()
{
fx.AlphaQueue.BeginFrame();
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnLandscapeViews(activeViews);
sink.OnLandscapeCellTurn(cellId);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
fx.AlphaQueue.AbortFrame();
}
long allocated = ZeroAllocationProbe.MeasureWarmed(
RunCellTurn,
batchSize: 128,
warmupBatches: 2,
samples: 4);
Assert.Equal(0, allocated);
}
private static List<RetailAlphaEntry> QueueAlphaEntries(RetailAlphaQueue queue) =>
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue).GetField(
"_alpha", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(queue)!;
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
// surviving exit view — the SAME fixture as
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
@ -2217,6 +2490,8 @@ public sealed partial class WalkFrameDriverTests
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
public WbMeshAdapter MeshAdapter => _meshAdapter;
public DrawScope BeginDraw(bool beginAlpha = false)
{
if (beginAlpha)

View file

@ -120,6 +120,11 @@ public sealed class WalkStaticStreamPopulatorTests
private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) =>
new() { Batches = new List<ObjectRenderBatch>(batches) };
private static ObjectRenderData MakeSortedMesh(
Vector3 sortCenter,
params ObjectRenderBatch[] batches) =>
new() { SortCenter = sortCenter, Batches = new List<ObjectRenderBatch>(batches) };
// ── Reflection seam: ObjectMeshManager owns no test-injection API, and
// driving real GPU/GfxObj upload for a unit test is out of this stage's
// scope — ObjectRenderData/ObjectRenderBatch are plain settable classes,
@ -135,6 +140,224 @@ public sealed class WalkStaticStreamPopulatorTests
dict[id] = data;
}
[Fact]
public void PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder()
{
using var fx = new DispatcherFixture();
const ulong nearOrigin = 0x0100_0C01UL;
const ulong farAuthoredCenter = 0x0100_0C02UL;
InjectRenderData(fx.Manager, nearOrigin, MakeSortedMesh(
Vector3.Zero,
MakeBatch(101, TranslucencyKind.AlphaBlend, 101, 0, 3, 1)));
InjectRenderData(fx.Manager, farAuthoredCenter, MakeSortedMesh(
new Vector3(10, 0, 0),
MakeBatch(202, TranslucencyKind.AlphaBlend, 202, 0, 3, 2)));
RenderProjectionRecord[] records =
[
MakeRecord(1, 0, new Vector3(5, 0, 0), [new MeshRef((uint)nearOrigin, Matrix4x4.Identity)]),
MakeRecord(2, 0, Vector3.Zero, [new MeshRef((uint)farAuthoredCenter, Matrix4x4.Identity)]),
];
var alpha = new List<WbDrawDispatcher.WalkClassifiedBatch>();
var stream = new OrderedDrawStream();
var populator = new WalkStaticStreamPopulator(fx.Dispatcher);
populator.PopulateCellObjects(
stream,
WalkDrawStage.CellStatic,
0x8C040112u,
records,
0x8C04u,
Vector3.Zero,
Matrix4x4.Identity,
views: null,
viewRouteIndex: -1,
alpha);
Assert.Empty(stream.Keys);
Assert.Equal([202u, 101u], alpha.Select(static batch => batch.Key.FirstIndex));
Assert.Equal([100f, 25f], alpha.Select(static batch => batch.SortDistanceSq));
}
[Fact]
public void PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder()
{
using var fx = new DispatcherFixture();
const ulong setup = 0x1000_0C10UL;
const ulong firstPart = 0x0100_0C11UL;
const ulong secondPart = 0x0100_0C12UL;
InjectRenderData(fx.Manager, firstPart, MakeSortedMesh(
Vector3.Zero,
MakeBatch(11, TranslucencyKind.AlphaBlend, 11, 0, 3, 1),
MakeBatch(12, TranslucencyKind.AlphaBlend, 12, 0, 3, 2)));
InjectRenderData(fx.Manager, secondPart, MakeSortedMesh(
Vector3.Zero,
MakeBatch(21, TranslucencyKind.AlphaBlend, 21, 0, 3, 3),
MakeBatch(22, TranslucencyKind.AlphaBlend, 22, 0, 3, 4)));
InjectRenderData(fx.Manager, setup, new ObjectRenderData
{
IsSetup = true,
SetupParts =
[
(firstPart, Matrix4x4.CreateTranslation(10, 0, 0)),
(secondPart, Matrix4x4.CreateTranslation(0, 10, 0)),
],
});
RenderProjectionRecord record = MakeRecord(
10, 0, Vector3.Zero, [new MeshRef((uint)setup, Matrix4x4.Identity)]);
var alpha = new List<WbDrawDispatcher.WalkClassifiedBatch>();
new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects(
new OrderedDrawStream(),
WalkDrawStage.CellStatic,
0x8C040112u,
[record],
0x8C04u,
Vector3.Zero,
Matrix4x4.Identity,
views: null,
viewRouteIndex: -1,
alpha);
Assert.Equal([11u, 12u, 21u, 22u], alpha.Select(static batch => batch.Key.FirstIndex));
Assert.All(alpha, static batch => Assert.Equal(100f, batch.SortDistanceSq));
}
[Fact]
public void PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt()
{
using var fx = new DispatcherFixture();
const ulong nearStatic = 0x0100_0C21UL;
const ulong farDynamic = 0x0100_0C22UL;
const ulong middleStatic = 0x0100_0C23UL;
InjectRenderData(fx.Manager, nearStatic, MakeFlatMesh(
MakeBatch(1, TranslucencyKind.Opaque, 1, 0, 3, 1)));
InjectRenderData(fx.Manager, farDynamic, MakeFlatMesh(
MakeBatch(2, TranslucencyKind.Opaque, 2, 0, 3, 2)));
InjectRenderData(fx.Manager, middleStatic, MakeFlatMesh(
MakeBatch(3, TranslucencyKind.Opaque, 3, 0, 3, 3)));
RenderProjectionRecord dynamic = MakeRecord(
2, 0, new Vector3(30, 0, 0), [new MeshRef((uint)farDynamic, Matrix4x4.Identity)])
with { ProjectionClass = RenderProjectionClass.LiveDynamicRoot };
RenderProjectionRecord[] records =
[
MakeRecord(1, 0, new Vector3(10, 0, 0), [new MeshRef((uint)nearStatic, Matrix4x4.Identity)]),
dynamic,
MakeRecord(3, 0, new Vector3(20, 0, 0), [new MeshRef((uint)middleStatic, Matrix4x4.Identity)]),
];
var stream = new OrderedDrawStream();
new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects(
stream,
WalkDrawStage.CellStatic,
0x8C040112u,
records,
0x8C04u,
Vector3.Zero,
Matrix4x4.Identity,
views: null,
viewRouteIndex: -1,
[]);
Assert.Equal([2u, 3u, 1u], stream.Keys.Select(static key => key.FirstIndex));
Assert.Equal(
[WalkDrawStage.Dynamic, WalkDrawStage.CellStatic, WalkDrawStage.CellStatic],
stream.Stages);
}
[Fact]
public void WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading()
{
Type batchType = typeof(WbDrawDispatcher).GetNestedType(
"WalkClassifiedBatch", BindingFlags.NonPublic)
?? throw new InvalidOperationException("WalkClassifiedBatch was not found.");
PropertyInfo sortDistance = batchType.GetProperty("SortDistanceSq")
?? throw new InvalidOperationException("SortDistanceSq was not retained.");
Assert.Equal(typeof(float), sortDistance.PropertyType);
MethodInfo submit = typeof(WbDrawDispatcher).GetMethod(
"SubmitWalkAlphaInstance", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("SubmitWalkAlphaInstance was not found.");
ParameterInfo[] parameters = submit.GetParameters();
Assert.Equal(2, parameters.Length);
Assert.Equal("batch", parameters[0].Name);
Assert.Equal(typeof(Matrix4x4), parameters[1].ParameterType);
Assert.DoesNotContain(parameters, static parameter => parameter.ParameterType == typeof(Vector3));
string root = FindRepoRoot();
string populator = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "Walk", "WalkStaticStreamPopulator.cs"));
Assert.Contains("batch.LocalSortCenter, batch.Transform", populator);
Assert.Contains("SortDistanceSq = Vector3.DistanceSquared", populator);
Assert.Contains("value.Batch.SortDistanceSq >", populator);
Assert.Contains("AP-243", populator);
Assert.Contains("greater-than-50 m", populator);
Assert.Contains("0x005A06B7..0x005A0720", populator);
}
[Fact]
public void WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals()
{
string root = FindRepoRoot();
string inventory = File.ReadAllText(Path.Combine(
root, "docs", "architecture", "worldbuilder-inventory.md"));
string architecture = File.ReadAllText(Path.Combine(
root, "docs", "architecture", "acdream-architecture.md"));
string registerPath = Path.Combine(
root, "docs", "architecture", "retail-divergence-register.md");
string register = File.ReadAllText(registerPath);
Assert.Contains("per-cell", inventory, StringComparison.OrdinalIgnoreCase);
Assert.Contains("two FIFO", inventory, StringComparison.Ordinal);
Assert.DoesNotContain(
"distance-sorts one shared queue", inventory, StringComparison.OrdinalIgnoreCase);
Assert.Contains("per-cell", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("two FIFO", architecture, StringComparison.Ordinal);
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("162 active rows", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" })
{
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
Assert.True(
rowIndex > apSectionStart && rowIndex < apSectionEnd,
$"{id} row must be inside the AP table before the TS section; "
+ $"rowIndex={rowIndex}, AP=[{apSectionStart},{apSectionEnd}).");
}
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-241 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Equal(
162,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal);
}
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
}
// ── Deliverable 1: ClassifyEntityForWalk data equivalence ─────────────
[Fact]
@ -573,13 +796,12 @@ public sealed class WalkStaticStreamPopulatorTests
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
Vector3 localSortCenter = new(1, 2, 3);
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
var cameraWorldPosition = Vector3.Zero;
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1,
Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1,
IsOpaque: false, LocalSortCenter: localSortCenter);
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, cameraWorldPosition, Matrix4x4.Identity);
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity);
Assert.Equal(1, fx.AlphaQueue.PendingCount);
Assert.Equal(1, fx.AlphaQueue.AlphaCount);
@ -614,7 +836,6 @@ public sealed class WalkStaticStreamPopulatorTests
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
on.Dispatcher.SubmitWalkAlphaInstance(
in building,
Vector3.Zero,
Matrix4x4.Identity);
Assert.Equal(0, on.AlphaQueue!.PendingCount);
@ -636,7 +857,6 @@ public sealed class WalkStaticStreamPopulatorTests
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u);
off.Dispatcher.SubmitWalkAlphaInstance(
in building,
Vector3.Zero,
Matrix4x4.Identity);
Assert.Equal(1, off.AlphaQueue!.PendingCount);
@ -656,7 +876,6 @@ public sealed class WalkStaticStreamPopulatorTests
WbDrawDispatcher.WalkClassifiedBatch batch = AlphaWalkBatch(detailCategory: 0u);
ordinary.Dispatcher.SubmitWalkAlphaInstance(
in batch,
Vector3.Zero,
Matrix4x4.Identity);
Assert.Equal(1, ordinary.AlphaQueue!.PendingCount);
@ -750,11 +969,11 @@ public sealed class WalkStaticStreamPopulatorTests
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Vector3.Zero, Matrix4x4.Identity);
fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity);
Assert.Throws<InvalidOperationException>(() =>
fx.Dispatcher.SubmitWalkAlphaInstance(
in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0)));
in batch, Matrix4x4.CreateTranslation(1, 0, 0)));
fx.AlphaQueue.AbortFrame();
}

View file

@ -218,8 +218,10 @@ public sealed partial class WalkTraceConformanceTests
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
public bool HasRenderableEmittersInCell(uint cellId) => false;
public void DrawCellShell(uint cellId) { }
public void DrawStaticParticles(uint cellId) { }
public void DrawCellParticles(uint cellId) { }
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareStaticParticles(uint cellId) =>
ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
public ReadOnlySpan<PreparedParticleAlphaSubmission> PrepareCellParticles(uint cellId) =>
ReadOnlySpan<PreparedParticleAlphaSubmission>.Empty;
public void ClearInteriorDepth() { }
public void FlushLandscape() => RecordFlush(RetailAlphaFlushSite.LandscapeFlush, 0f);
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
@ -275,6 +277,8 @@ public sealed partial class WalkTraceConformanceTests
/// query returns the shared empty record.</summary>
private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData
{
public WalkFrameStaticRecords GetCellObjects(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty;
@ -475,16 +479,14 @@ public sealed partial class WalkTraceConformanceTests
/// so every 0f-threshold flush observes count (0, 0) — always a real
/// drain (0 is never <c>&lt; 0</c>), matching retail's own always-drains
/// behavior at threshold 0f, but with a different count than the
/// capture's real content volume; (2) even WITH content, acdream appends
/// one queue entry per INSTANCE, where retail's <c>AddMeshToAlphaList</c>
/// appends one entry per SUBSET per <c>DrawMesh</c> call — a single
/// multi-subset instance inflates retail's count relative to acdream's;
/// (3) acdream's CLIP list is structurally empty for ordinary content
/// (M2's new register row) — <c>WbDrawDispatcher.IsOpaque</c> filters
/// clip-mapped subsets out before they ever reach a submit call, so
/// acdream's CLIP count reads 0 wherever retail's capture shows nonzero
/// CLIP entries. See the S4 packet's §9/§10/§11 subsections for the
/// full per-pose sequences.
/// capture's real content volume; (2) AP-238 coalesces transparent
/// EnvCell work to one token per <c>(cell,list)</c>, whereas retail
/// appends each contributing subset; (3) AP-239/AP-240 change
/// CLIP-vs-ALPHA membership for the named ordinary-Wb surface cases.
/// Ordinary Wb and particle paths otherwise append per real subset; the
/// former broad "per instance versus per subset" explanation was false.
/// See the S4 packet's §9/§10/§11 subsections for the full per-pose
/// sequences.
/// </summary>
private void RunAlphaFlushCountsGate(string fixtureName)
{