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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue