fix(render): retain surfaceless particle metadata
Preserve a successfully decoded GfxObj ParticleGfxInfo when SurfaceId is zero and condition only texture acquisition. Missing/no-Gfx descriptors retain Default behavior; material, additive, routing, and untextured drawing remain unchanged. Add a production-path GfxObj-backed synthetic no-surface pin with distinct authored/AABB centers, non-unit size, nonidentity orientation, object/particle merge order, visual center, and unassigned texture-slot assertions. Correct the synthetic-DAT evidence wording and advance only the S4-c3a ledger row. Final F2 source audit found no defect: preparation holds reconstruction inputs only; Append reserves once; false and exception roll back the exact tail once; first-use rejection remains registered; accepted tokens stay stable; independent 3000-entry caps preserve row-2 immediate duplicates; flush/end/abort/next-preparation cleanup converges; retained capacity/bytes remain bounded; warmed production paths remain 0 B. Gates: no-surface target 1/1; unchanged section 19 baseline 95/95; AP boundary/count 1/1; real allocations 2/2 at 0 B; shader/manifest 32/32; Release solution 0 warnings/0 errors; diff-check pass. No graphical client. Mutation: restoring eea5793d2's SurfaceId-zero-to-Default ternary first failed CellTurn_GfxObjBillboardWithoutSurfaceRetainsAuthoredSortCenterAndUntexturedFallback at the authored-distance assertion, expected 40 actual 100. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8bd75ba312
commit
b6b0156045
4 changed files with 202 additions and 8 deletions
|
|
@ -1972,10 +1972,10 @@ public sealed partial class WalkFrameDriverTests
|
|||
}
|
||||
|
||||
/// <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
|
||||
/// S4-c3a fix round 1 F1: production-path GfxObj-backed synthetic DAT
|
||||
/// records keep SortCenter distinct from the vertex-AABB visual center.
|
||||
/// The particle is at (10,0,0), sized 2 and rotated +90 degrees around Z:
|
||||
/// authored SortCenter (1,2,0) 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>
|
||||
|
|
@ -2109,6 +2109,132 @@ public sealed partial class WalkFrameDriverTests
|
|||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S4-c3a final fix round: a successfully decoded GfxObj without a Surface
|
||||
/// still owns its authored SortCenter, bounds-derived visual center, axes,
|
||||
/// and degrade mode. Only texture acquisition is absent. With the same
|
||||
/// scale/orientation geometry as the surfaced sibling above, CYpt remains
|
||||
/// distance² 40, the ordinary object at distance² 49 stays first, and the
|
||||
/// billboard retains its untextured slot plus visual center.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CellTurn_GfxObjBillboardWithoutSurfaceRetainsAuthoredSortCenterAndUntexturedFallback()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
const uint cellId = 0x8C0400A2u;
|
||||
const uint objectGfx = 0x02000CA2u;
|
||||
const uint particleGfx = 0x01000CA2u;
|
||||
const uint degradeId = 0x11000CA2u;
|
||||
InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh(
|
||||
MakeBatch(0x08100CA2u, 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),
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
using var dats = new NoopDatReaderWriter();
|
||||
dats.Add(particleGfx, gfx);
|
||||
dats.Add(degradeId, degrade);
|
||||
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 = 0x32000CA2u,
|
||||
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)!;
|
||||
Assert.Equal(40f, authoredDistanceSq, precision: 4);
|
||||
Assert.Equal(
|
||||
[typeof(WbDrawDispatcher), typeof(ParticleRenderer)],
|
||||
QueueAlphaEntries(fx.AlphaQueue)
|
||||
.Select(static entry => entry.Source.GetType().DeclaringType));
|
||||
|
||||
Vector3 visualCenter = (Vector3)instance.GetType().GetField("Position")!.GetValue(instance)!;
|
||||
GpuTextureSlot textureSlot = (GpuTextureSlot)instance.GetType().GetField("TextureSlot")!
|
||||
.GetValue(instance)!;
|
||||
Assert.True(
|
||||
Vector3.Distance(new Vector3(10, 4, 0), visualCenter) < 1e-4f,
|
||||
$"Visual center was {visualCenter}, expected <10, 4, 0>.");
|
||||
Assert.False(textureSlot.IsAssigned);
|
||||
|
||||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue