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:
parent
06b986622b
commit
a86ec73ece
17 changed files with 1067 additions and 197 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>< 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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue