fix(render): repair particle CYpt preparation
Retain the GfxObj-authored SortCenter independently from the rendered AABB center and use its scaled, oriented world point for billboard/mode-2-5 CYpt. Keep the visual center unchanged. Reserve prepared source payload only at Append, roll back the exact tail once on false or exception, cap retained preparation at 3000 CLIP plus 3000 ALPHA, preserve immediate duplicates, account the bounded scratch, and correct the static-record ordering comment. The lead-authorized plan edit records candidate 44e2bc227b's retail FAIL and this section 19 result. Gates: Release solution build 0 warnings/0 errors; focused particle/driver/queue/order/bounds 95/95; register boundary/count 1/1; real allocations 2/2 at 0 B; shader/manifest 32/32; one-shot hermetic 16751/16751; InstalledDat 385 pass/10 documented fail/1 skip; diff-check pass. No graphical client. Mutations: rendered pos first failed distance expected 40 actual 116; AABB center expected 40 actual 116; dropped size expected 40 actual 65; dropped orientation expected 40 actual 160; eager reservation first failed Assert.Empty with one payload; omitted false rollback first failed Assert.Single with two payloads; omitted exception rollback first failed Assert.Empty with one payload; removed caps first failed prepared count expected 6000 actual 6002. Evidence note: the first ordinary InstalledDat filter produced 385 pass/2 expected fail/1 skip but excluded the eight required KnownFailure rows; the separate inclusive no-retry artifact produced the exact documented 10-failure set. Initial test fixture compilation/analyzer setup failures are recorded in packet section 19.5. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8e0c6fb145
commit
01674bcc78
7 changed files with 727 additions and 32 deletions
|
|
@ -190,6 +190,28 @@ public sealed class ParticleRendererRouteTests
|
|||
Assert.Equal(RetailAlphaList.Alpha, decision.List);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellCap_DropsOnlyRetentionAndPreservesRow2ImmediateDuplicate()
|
||||
{
|
||||
int retainedClip = RetailAlphaQueue.ListCapacity;
|
||||
int retainedAlpha = 0;
|
||||
var decision = new RetailAlphaMeshDecision(
|
||||
RetailAlphaMeshAction.AppendClipAndImmediate,
|
||||
RetailAlphaList.Clip,
|
||||
OverrideClipmap: true);
|
||||
|
||||
ParticleRenderer.PreparedCellAlphaActions actions =
|
||||
ParticleRenderer.ResolvePreparedCellAlphaActions(
|
||||
decision,
|
||||
ref retainedClip,
|
||||
ref retainedAlpha);
|
||||
|
||||
Assert.False(actions.Retain);
|
||||
Assert.True(actions.DrawImmediate);
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, retainedClip);
|
||||
Assert.Equal(0, retainedAlpha);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ using AcDream.Core.World;
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Lib;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
using DatReaderWriter.Types;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
|
@ -37,6 +39,22 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
/// </summary>
|
||||
public sealed partial class WalkFrameDriverTests
|
||||
{
|
||||
[Fact]
|
||||
public void WalkFrameStaticRecordsComment_StatesAuthoredInputOrderAndDownstreamCellSort()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
WalkOracleTraceRepoRoot.Find(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"Walk",
|
||||
"WalkFrameDriver.cs"));
|
||||
|
||||
Assert.Contains("retaining authored traversal/registration", source, StringComparison.Ordinal);
|
||||
Assert.Contains("not final stream order", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("never re-sorted downstream", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ── Shared ordered log: BOTH the fake leaf renderer and the fake trace
|
||||
// write into ONE list, so a single sequence assertion proves the FULL
|
||||
// interleave (stream flushes interleaved with sky/terrain/shell/punch/
|
||||
|
|
@ -1953,6 +1971,144 @@ public sealed partial class WalkFrameDriverTests
|
|||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4-c3a fix round 1 F1: the real DAT-backed mode-2 path keeps the GfxObj
|
||||
/// SortCenter distinct from its vertex-AABB visual center. The particle is
|
||||
/// at (10,0,0), sized 2 and rotated +90 degrees around Z: authored
|
||||
/// SortCenter (1,2,0) therefore lands at (6,2,0), distance² 40. The AABB
|
||||
/// center (2,0,0) remains the viewer-facing draw center at (10,4,0).
|
||||
/// The ordinary object at distance² 49 must win the cross-source merge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CellTurn_GfxObjBillboardUsesScaledOrientedAuthoredSortCenterButKeepsVisualCenter()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400A1u;
|
||||
const uint objectGfx = 0x02000CA1u;
|
||||
const uint particleGfx = 0x01000CA1u;
|
||||
const uint particleSurface = 0x08000CA1u;
|
||||
const uint degradeId = 0x11000CA1u;
|
||||
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08100CA1u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1)));
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords(
|
||||
new[] { MakeRecord(1, 0, new Vector3(7, 0, 0),
|
||||
[new MeshRef(objectGfx, Matrix4x4.Identity)]) },
|
||||
0x8C04u);
|
||||
|
||||
var gfx = new GfxObj
|
||||
{
|
||||
Id = particleGfx,
|
||||
Flags = GfxObjFlags.HasDIDDegrade,
|
||||
DIDDegrade = degradeId,
|
||||
SortCenter = new Vector3(1, 2, 0),
|
||||
Surfaces = { particleSurface },
|
||||
VertexArray = new VertexArray
|
||||
{
|
||||
Vertices =
|
||||
{
|
||||
[0] = new SWVertex { Origin = new Vector3(1, 0, -1) },
|
||||
[1] = new SWVertex { Origin = new Vector3(3, 0, 1) },
|
||||
},
|
||||
},
|
||||
};
|
||||
var degrade = new GfxObjDegradeInfo
|
||||
{
|
||||
Id = degradeId,
|
||||
Degrades =
|
||||
{
|
||||
new GfxObjInfo
|
||||
{
|
||||
Id = particleGfx,
|
||||
DegradeMode = 2u,
|
||||
MaxDist = float.MaxValue,
|
||||
},
|
||||
},
|
||||
};
|
||||
var surface = new Surface
|
||||
{
|
||||
Id = particleSurface,
|
||||
Type = SurfaceType.Base1Solid,
|
||||
ColorValue = new DatReaderWriter.Types.ColorARGB
|
||||
{
|
||||
Alpha = 255,
|
||||
Red = 255,
|
||||
Green = 255,
|
||||
Blue = 255,
|
||||
},
|
||||
};
|
||||
using var dats = new NoopDatReaderWriter();
|
||||
dats.Add(particleGfx, gfx);
|
||||
dats.Add(degradeId, degrade);
|
||||
dats.Add(particleSurface, surface);
|
||||
using var textures = new TextureCache(fx.Device, dats);
|
||||
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
Quaternion orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
|
||||
int handle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CA1u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
GfxObjId = particleGfx,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
StartSize = 2f,
|
||||
EndSize = 2f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
new Vector3(10, 0, 0),
|
||||
orientation);
|
||||
particles.UpdateEmitterOwnerCell(handle, cellId);
|
||||
using var renderer = new ParticleRenderer(
|
||||
fx.Device,
|
||||
fx.FrameLifetime,
|
||||
fx.Scope,
|
||||
particles,
|
||||
textures,
|
||||
dats,
|
||||
fx.MeshAdapter,
|
||||
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);
|
||||
|
||||
IList payload = DeferredParticlePayload(renderer);
|
||||
object deferred = Assert.Single(payload.Cast<object>());
|
||||
object billboard = deferred.GetType().GetProperty("Billboard")!.GetValue(deferred)!;
|
||||
object instance = billboard.GetType().GetProperty("Instance")!.GetValue(billboard)!;
|
||||
float authoredDistanceSq = (float)instance.GetType().GetField("DistanceSq")!.GetValue(instance)!;
|
||||
Vector3 visualCenter = (Vector3)instance.GetType().GetField("Position")!.GetValue(instance)!;
|
||||
Assert.Equal(40f, authoredDistanceSq, precision: 4);
|
||||
Assert.True(
|
||||
Vector3.Distance(new Vector3(10, 4, 0), visualCenter) < 1e-4f,
|
||||
$"Visual center was {visualCenter}, expected <10, 4, 0>.");
|
||||
Assert.NotEqual(new Vector3(6, 2, 0), visualCenter);
|
||||
Assert.Equal(
|
||||
[typeof(WbDrawDispatcher), typeof(ParticleRenderer)],
|
||||
QueueAlphaEntries(fx.AlphaQueue)
|
||||
.Select(static entry => entry.Source.GetType().DeclaringType));
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther()
|
||||
{
|
||||
|
|
@ -2073,11 +2229,316 @@ public sealed partial class WalkFrameDriverTests
|
|||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellAlpha_AcceptsStableTokenAndRollsBackRejectedTailOnAbort()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400B1u;
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int handle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB1u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 2,
|
||||
InitialParticles = 2,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
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 dummy = new DummyAlphaSource();
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
for (int i = 0; i < RetailAlphaQueue.ListCapacity - 1; i++)
|
||||
{
|
||||
Assert.True(fx.AlphaQueue.TryAppend(
|
||||
RetailAlphaList.Alpha, dummy, i, overrideClipmap: false));
|
||||
}
|
||||
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission> prepared =
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(),
|
||||
Vector3.Zero,
|
||||
ParticleRenderPass.Scene,
|
||||
cellId);
|
||||
Assert.Equal(2, prepared.Length);
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
|
||||
prepared[0].Append();
|
||||
prepared[1].Append();
|
||||
|
||||
Assert.Single(DeferredParticlePayload(renderer));
|
||||
RetailAlphaEntry accepted = Assert.Single(
|
||||
QueueAlphaEntries(fx.AlphaQueue),
|
||||
entry => entry.Source.GetType().DeclaringType == typeof(ParticleRenderer));
|
||||
Assert.Equal(0, accepted.Token);
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, fx.AlphaQueue.AlphaCount);
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellAlpha_AppendExceptionRollsBackExactTailToken()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400B5u;
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int handle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB7u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
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);
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
PreparedParticleAlphaSubmission prepared = renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, cellId)[0];
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => prepared.Append());
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, 0xDEAD0002u);
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellAlpha_FlushAndEndResetAcceptedPayloadAndPreparedScratch()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400B2u;
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int handle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB2u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
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);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
renderer.BeginFrame(frameSlot: 0);
|
||||
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, cellId)[0].Append();
|
||||
Assert.Single(DeferredParticlePayload(renderer));
|
||||
fx.AlphaQueue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, cellId)[0].Append();
|
||||
Assert.Single(DeferredParticlePayload(renderer));
|
||||
fx.AlphaQueue.EndFrame();
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellAlpha_CapsBothListsAndRejectStormRetainsNoPayloadOrUnboundedScratch()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400B3u;
|
||||
const uint clipGfx = 0x01000CB3u;
|
||||
InjectRenderData(fx.Manager, clipGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08000CB3u, TranslucencyKind.ClipMap, 0, 0, 3, 1)));
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int alphaHandle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB3u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = RetailAlphaQueue.ListCapacity + 1,
|
||||
InitialParticles = RetailAlphaQueue.ListCapacity + 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
new Vector3(30, 0, 0));
|
||||
int clipHandle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB4u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
GfxObjId = clipGfx,
|
||||
MaxParticles = RetailAlphaQueue.ListCapacity + 1,
|
||||
InitialParticles = RetailAlphaQueue.ListCapacity + 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
new Vector3(20, 0, 0));
|
||||
particles.UpdateEmitterOwnerCell(alphaHandle, cellId);
|
||||
particles.UpdateEmitterOwnerCell(clipHandle, cellId);
|
||||
using var renderer = new ParticleRenderer(
|
||||
fx.Device,
|
||||
fx.FrameLifetime,
|
||||
fx.Scope,
|
||||
particles,
|
||||
meshAdapter: fx.MeshAdapter,
|
||||
alphaQueue: fx.AlphaQueue);
|
||||
var dummy = new DummyAlphaSource();
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
|
||||
{
|
||||
Assert.True(fx.AlphaQueue.TryAppend(
|
||||
RetailAlphaList.Alpha, dummy, i, overrideClipmap: false));
|
||||
Assert.True(fx.AlphaQueue.TryAppend(
|
||||
RetailAlphaList.Clip, dummy, i, overrideClipmap: false));
|
||||
}
|
||||
|
||||
ReadOnlySpan<PreparedParticleAlphaSubmission> prepared =
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, cellId);
|
||||
int alphaCount = 0;
|
||||
int clipCount = 0;
|
||||
for (int i = 0; i < prepared.Length; i++)
|
||||
{
|
||||
if (prepared[i].List == RetailAlphaList.Clip)
|
||||
clipCount++;
|
||||
else
|
||||
alphaCount++;
|
||||
}
|
||||
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity * 2, prepared.Length);
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, clipCount);
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, alphaCount);
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.InRange(renderer.PreparedCellAlphaScratchDiagnostics.Capacity, 6000, 8192);
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.RetainedBytes > 0);
|
||||
Assert.True(
|
||||
renderer.PreparedCellAlphaScratchDiagnostics.RetainedBytes
|
||||
<= renderer.RetainedAlphaScratchBytes);
|
||||
|
||||
for (int i = 0; i < prepared.Length; i++)
|
||||
prepared[i].Append();
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity * 2, fx.AlphaQueue.PendingCount);
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
Assert.InRange(renderer.PreparedCellAlphaScratchDiagnostics.Capacity, 6000, 8192);
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedCellAlpha_ImmediateExceptionAfterDelayedCandidateDoesNotReservePayload()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400B4u;
|
||||
const uint immediateGfx = 0x01000CB4u;
|
||||
InjectRenderData(fx.Manager, immediateGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08000CB4u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||
var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42));
|
||||
int delayedHandle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB5u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
new Vector3(100, 0, 0));
|
||||
int immediateHandle = particles.SpawnEmitter(
|
||||
new EmitterDesc
|
||||
{
|
||||
DatId = 0x32000CB6u,
|
||||
Type = AcDream.Core.Vfx.ParticleType.Still,
|
||||
GfxObjId = immediateGfx,
|
||||
MaxParticles = 1,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 100f,
|
||||
LifetimeMax = 100f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Gravity = Vector3.Zero,
|
||||
},
|
||||
new Vector3(10, 0, 0));
|
||||
particles.UpdateEmitterOwnerCell(delayedHandle, cellId);
|
||||
particles.UpdateEmitterOwnerCell(immediateHandle, cellId);
|
||||
using var renderer = new ParticleRenderer(
|
||||
fx.Device,
|
||||
fx.FrameLifetime,
|
||||
fx.Scope,
|
||||
particles,
|
||||
meshAdapter: fx.MeshAdapter,
|
||||
alphaQueue: fx.AlphaQueue);
|
||||
fx.AlphaQueue.BeginFrame();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, cellId));
|
||||
Assert.Empty(DeferredParticlePayload(renderer));
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 1);
|
||||
|
||||
renderer.PrepareForCellAlpha(
|
||||
new IdentityCamera(), Vector3.Zero, ParticleRenderPass.Scene, 0xDEAD0001u);
|
||||
Assert.True(renderer.PreparedCellAlphaScratchDiagnostics.Count == 0);
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
private static List<RetailAlphaEntry> QueueAlphaEntries(RetailAlphaQueue queue) =>
|
||||
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue).GetField(
|
||||
"_alpha", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(queue)!;
|
||||
|
||||
private static IList DeferredParticlePayload(ParticleRenderer renderer) =>
|
||||
(IList)typeof(ParticleRenderer).GetField(
|
||||
"_deferredAlpha", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(renderer)!;
|
||||
|
||||
// ── 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
|
||||
|
|
@ -2547,6 +3008,7 @@ public sealed partial class WalkFrameDriverTests
|
|||
|
||||
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
||||
{
|
||||
private readonly Dictionary<(Type Type, uint Id), IDBObj> _objects = new();
|
||||
private readonly StubDatabase _portal = new();
|
||||
private readonly StubDatabase _highRes = new();
|
||||
private readonly StubDatabase _language = new();
|
||||
|
|
@ -2578,6 +3040,9 @@ public sealed partial class WalkFrameDriverTests
|
|||
|
||||
public int LanguageIteration => 0;
|
||||
|
||||
public void Add<T>(uint id, T value) where T : IDBObj =>
|
||||
_objects[(typeof(T), id)] = value;
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint regionId,
|
||||
uint fileId,
|
||||
|
|
@ -2604,12 +3069,21 @@ public sealed partial class WalkFrameDriverTests
|
|||
throw new NotSupportedException();
|
||||
|
||||
[return: MaybeNull]
|
||||
public T Get<T>(uint fileId) where T : IDBObj => default;
|
||||
public T Get<T>(uint fileId) where T : IDBObj =>
|
||||
_objects.TryGetValue((typeof(T), fileId), out IDBObj? value)
|
||||
? (T)value
|
||||
: default;
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
if (_objects.TryGetValue((typeof(T), fileId), out IDBObj? found))
|
||||
{
|
||||
value = (T)found;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue