fix(render): FW4 - static-owner particles submit at their walk turns
Ports the user-verified #132 invariant (e102fb36 on the old pipeline)
walk-natively, on the owner''s direction: retail''s falls containment
is POSITIONAL - an outdoor emitter''s polys join the ONE alpha list
during its owner cell''s DrawObjCell in the far-to-near landscape walk
(DrawSortCell @0x005a17c0), so every nearer building''s pre-punch
barrier (DrawBuilding @0x0059f2a0''s FlushAlphaList @0x0059f30b)
drains the already-queued FARTHER content against still-true depth
BEFORE its punch stamps far-Z into the aperture. The walk path had
kept the per-building AlphaBarrier events but batched ALL landscape
static-owner submission into one lump at the pre-clear closure - after
every punch had run: the barriers fired over an empty queue and the
falls drained against punched-far aperture pixels (phase=pre in every
probe line, which is why six rounds of phase-staging repairs could
never see it - the phase was right, the position within the phase was
wrong).
New WalkFrameEventKind.StaticParticles: each landscape cell''s and
each building shell''s emitter owners now submit AT THAT TURN during
Replay (marked so the owner''s meshes flush first - retail''s
per-object order); the batched SubmitWalkLandscapeStaticParticles and
its closure/post-replay call sites are deleted for both root kinds.
Two driver sequence pins adjudicated to the new turn order.
Hermetic 6,762/0; InstalledDat walk conformance 40/1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f4522297cc
commit
280c054943
4 changed files with 123 additions and 47 deletions
|
|
@ -238,6 +238,10 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
|
|
||||||
public void ClearInteriorDepth() => _clearInteriorDepth();
|
public void ClearInteriorDepth() => _clearInteriorDepth();
|
||||||
|
|
||||||
|
public void DrawStaticParticles(IReadOnlySet<uint> ownerIds) =>
|
||||||
|
_passes.DrawLandscapeStaticParticles(
|
||||||
|
_frame, new RetailPViewLandscapeStaticParticleContext(ownerIds));
|
||||||
|
|
||||||
public void DrawExitSeals() => _drawExitSeals();
|
public void DrawExitSeals() => _drawExitSeals();
|
||||||
|
|
||||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||||
|
|
|
||||||
|
|
@ -622,11 +622,10 @@ public sealed class RetailPViewRenderer
|
||||||
Walk.WalkFrameDriver capturedDriver = walkDriver!;
|
Walk.WalkFrameDriver capturedDriver = walkDriver!;
|
||||||
_walkPreClearDynamics = () =>
|
_walkPreClearDynamics = () =>
|
||||||
{
|
{
|
||||||
// Retail inserts static-owner emitters (candle flames)
|
// Static-owner emitters now submit AT THEIR OWN WALK
|
||||||
// during their cells' landscape walk turns — submit
|
// TURNS during Replay (WalkFrameEventKind.StaticParticles
|
||||||
// BEFORE the pre-clear drain so they composite against
|
// — the #132 positional invariant), so the pre-clear
|
||||||
// still-true landscape depth, never the seals.
|
// closure carries only the dynamics phase.
|
||||||
SubmitWalkLandscapeStaticParticles(ctx, walkExecutor!, capturedDriver);
|
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
DrawLandscapeDynamicsPhase(
|
DrawLandscapeDynamicsPhase(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -1358,33 +1357,6 @@ public sealed class RetailPViewRenderer
|
||||||
/// routes it replaced used to ride, from the driver's own visited
|
/// routes it replaced used to ride, from the driver's own visited
|
||||||
/// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and
|
/// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and
|
||||||
/// 4).</summary>
|
/// 4).</summary>
|
||||||
/// <summary>The landscape-stage static-owner particle submission: the
|
|
||||||
/// union of every outdoor-static record from a landscape cell the walk
|
|
||||||
/// visited this frame, plus every shell record from a building the walk
|
|
||||||
/// visited (plan §FW3 item 4 — retail gates particles per cell turn,
|
|
||||||
/// <c>ShouldDrawParticles</c> @0x0050FE60, so this is MORE
|
|
||||||
/// retail-faithful than the old per-slice sphere filter it replaced).
|
|
||||||
/// Interior roots invoke this at the walk's pre-clear boundary; outdoor
|
|
||||||
/// roots post-replay.</summary>
|
|
||||||
private void SubmitWalkLandscapeStaticParticles(
|
|
||||||
RetailPViewFrameInput ctx,
|
|
||||||
RetailPViewPassExecutor passes,
|
|
||||||
Walk.WalkFrameDriver driver)
|
|
||||||
{
|
|
||||||
_staticParticleUnionScratch.Clear();
|
|
||||||
foreach (uint cellId in driver.VisitedLandscapeCellIds)
|
|
||||||
UnionRecordOwners(_walkWorldData!.GetOutdoorStatics(cellId), _staticParticleUnionScratch);
|
|
||||||
foreach (Walk.WalkBuilding building in driver.VisitedBuildings)
|
|
||||||
UnionRecordOwners(_walkWorldData!.GetBuildingShellStatics(building), _staticParticleUnionScratch);
|
|
||||||
if (_staticParticleUnionScratch.Count > 0)
|
|
||||||
{
|
|
||||||
passes.DrawLandscapeStaticParticles(
|
|
||||||
ctx,
|
|
||||||
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
|
|
||||||
_staticParticleUnionScratch.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawWalkDrivenStatics(
|
private void DrawWalkDrivenStatics(
|
||||||
RetailPViewFrameInput ctx,
|
RetailPViewFrameInput ctx,
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
|
|
@ -1393,18 +1365,14 @@ public sealed class RetailPViewRenderer
|
||||||
var (frame, encoder) = passes.RequireWalkSubmission();
|
var (frame, encoder) = passes.RequireWalkSubmission();
|
||||||
driver.Replay(frame, encoder);
|
driver.Replay(frame, encoder);
|
||||||
|
|
||||||
// Landscape-stage static-owner particles (candle flames on lamp and
|
// Landscape-stage static-owner particles (candles, the cathedral
|
||||||
// candle statics): for an OUTDOOR root they submit here, post-replay
|
// falls) submit AT THEIR OWN WALK TURNS inside Replay
|
||||||
// (drained at the frame-end flush over finished depth — the #132
|
// (WalkFrameEventKind.StaticParticles) for BOTH root kinds — the
|
||||||
// placement). For an INTERIOR root they must submit at the walk's
|
// #132 positional invariant (e102fb36's user-verified rule ported
|
||||||
// PRE-CLEAR boundary instead — retail inserts each one during its
|
// walk-natively): each owner cell's emitters enter the one alpha
|
||||||
// cell's landscape walk turn, before the clear+seals; a post-seal
|
// queue at that cell's far-to-near position, so every nearer
|
||||||
// drain z-fails them at the aperture (the visual-gate "no candle
|
// building's pre-punch AlphaBarrier drains the farther content
|
||||||
// flames looking out" report) — so SubmitWalkLandscapeStaticParticles
|
// against still-true depth before its punch stamps far-Z.
|
||||||
// is invoked from _walkPreClearDynamics in that case, and skipped
|
|
||||||
// here.
|
|
||||||
if (ctx.RootCell.IsOutdoorNode)
|
|
||||||
SubmitWalkLandscapeStaticParticles(ctx, passes, driver);
|
|
||||||
|
|
||||||
// Cell-stage particle owners for the interior root's OWN flood cells
|
// Cell-stage particle owners for the interior root's OWN flood cells
|
||||||
// (non-look-in — the walk draws their statics too, via
|
// (non-look-in — the walk draws their statics too, via
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,12 @@ internal interface IWalkFrameLeafRenderer
|
||||||
/// this same shell-then-contents order).</summary>
|
/// this same shell-then-contents order).</summary>
|
||||||
void DrawCellShell(uint cellId);
|
void DrawCellShell(uint cellId);
|
||||||
|
|
||||||
|
/// <summary>One landscape cell's or building shell's static-owner
|
||||||
|
/// particle submission, at its own walk turn — see
|
||||||
|
/// <see cref="WalkFrameEventKind.StaticParticles"/> for the retail
|
||||||
|
/// positional invariant this carries (the #132 falls containment).</summary>
|
||||||
|
void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
|
||||||
|
|
||||||
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
|
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
|
||||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||||
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
|
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
|
||||||
|
|
@ -236,6 +242,23 @@ internal enum WalkFrameEventKind : byte
|
||||||
|
|
||||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
|
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
|
||||||
ExitSeals,
|
ExitSeals,
|
||||||
|
|
||||||
|
/// <summary><see cref="IWalkFrameLeafRenderer.DrawStaticParticles"/> —
|
||||||
|
/// ONE landscape cell's (<see cref="WalkFrameEvent.CellId"/>) or ONE
|
||||||
|
/// building shell's (<see cref="WalkFrameEvent.Building"/>) static-owner
|
||||||
|
/// particle submission, emitted AT ITS OWN WALK TURN. Retail's falls
|
||||||
|
/// containment is positional: an outdoor emitter's polys join the one
|
||||||
|
/// alpha list during its owner cell's <c>DrawObjCell</c> in the
|
||||||
|
/// far-to-near landscape walk, so every nearer building's pre-punch
|
||||||
|
/// alpha barrier (<c>DrawBuilding</c> @0x0059f2a0's
|
||||||
|
/// <c>FlushAlphaList</c> @0x0059f30b) drains the already-queued FARTHER
|
||||||
|
/// content against still-true depth BEFORE the punch stamps far-Z into
|
||||||
|
/// the aperture. The former single batched submission at the pre-clear
|
||||||
|
/// closure ran AFTER every punch — the barriers fired over an empty
|
||||||
|
/// queue and the falls drained against punched-far aperture pixels (the
|
||||||
|
/// cathedral bleed; the old pipeline's user-verified #132 fix
|
||||||
|
/// `e102fb36` encoded the same invariant).</summary>
|
||||||
|
StaticParticles,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
|
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
|
||||||
|
|
@ -247,13 +270,15 @@ internal enum WalkFrameEventKind : byte
|
||||||
internal readonly struct WalkFrameEvent
|
internal readonly struct WalkFrameEvent
|
||||||
{
|
{
|
||||||
private WalkFrameEvent(
|
private WalkFrameEvent(
|
||||||
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon)
|
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon,
|
||||||
|
WalkBuilding? building = null)
|
||||||
{
|
{
|
||||||
Kind = kind;
|
Kind = kind;
|
||||||
IntArg = intArg;
|
IntArg = intArg;
|
||||||
CellId = cellId;
|
CellId = cellId;
|
||||||
FloatArg = floatArg;
|
FloatArg = floatArg;
|
||||||
Polygon = polygon;
|
Polygon = polygon;
|
||||||
|
Building = building;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal WalkFrameEventKind Kind { get; }
|
internal WalkFrameEventKind Kind { get; }
|
||||||
|
|
@ -266,6 +291,11 @@ internal readonly struct WalkFrameEvent
|
||||||
|
|
||||||
internal WalkPolygon? Polygon { get; }
|
internal WalkPolygon? Polygon { get; }
|
||||||
|
|
||||||
|
/// <summary><see cref="WalkFrameEventKind.StaticParticles"/> only: the
|
||||||
|
/// building whose shell statics' owners submit at this turn; null for a
|
||||||
|
/// landscape cell's turn (then <see cref="CellId"/> names the cell).</summary>
|
||||||
|
internal WalkBuilding? Building { get; }
|
||||||
|
|
||||||
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
|
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
|
||||||
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
|
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
|
||||||
|
|
||||||
|
|
@ -284,6 +314,12 @@ internal readonly struct WalkFrameEvent
|
||||||
internal static WalkFrameEvent AlphaBarrier(float viewerDistance) =>
|
internal static WalkFrameEvent AlphaBarrier(float viewerDistance) =>
|
||||||
new(WalkFrameEventKind.AlphaBarrier, 0, 0, viewerDistance, null);
|
new(WalkFrameEventKind.AlphaBarrier, 0, 0, viewerDistance, null);
|
||||||
|
|
||||||
|
internal static WalkFrameEvent LandscapeCellParticles(uint cellId) =>
|
||||||
|
new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null);
|
||||||
|
|
||||||
|
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
|
||||||
|
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
|
||||||
|
|
||||||
internal static WalkFrameEvent ClearInteriorDepth() =>
|
internal static WalkFrameEvent ClearInteriorDepth() =>
|
||||||
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
||||||
|
|
||||||
|
|
@ -397,6 +433,20 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
||||||
/// alpha drain splats through (the cathedral falls shine-through).</summary>
|
/// alpha drain splats through (the cathedral falls shine-through).</summary>
|
||||||
internal List<uint> InteriorFloodCells { get; } = new();
|
internal List<uint> InteriorFloodCells { get; } = new();
|
||||||
|
|
||||||
|
// Replay scratch for StaticParticles events (sequential replay — one
|
||||||
|
// reused set is safe).
|
||||||
|
private readonly HashSet<uint> _staticParticleOwnerScratch = new();
|
||||||
|
|
||||||
|
private static void UnionOwners(
|
||||||
|
in WalkFrameStaticRecords records, HashSet<uint> destination)
|
||||||
|
{
|
||||||
|
foreach (RenderProjectionRecord record in records.Records)
|
||||||
|
{
|
||||||
|
if (record.Source.LocalEntityId != 0)
|
||||||
|
destination.Add(record.Source.LocalEntityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal List<WalkBuilding> VisitedBuildings { get; } = new();
|
internal List<WalkBuilding> VisitedBuildings { get; } = new();
|
||||||
|
|
||||||
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
|
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
|
||||||
|
|
@ -595,6 +645,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
||||||
case WalkFrameEventKind.ExitSeals:
|
case WalkFrameEventKind.ExitSeals:
|
||||||
_leafRenderer.DrawExitSeals();
|
_leafRenderer.DrawExitSeals();
|
||||||
break;
|
break;
|
||||||
|
case WalkFrameEventKind.StaticParticles:
|
||||||
|
_staticParticleOwnerScratch.Clear();
|
||||||
|
UnionOwners(
|
||||||
|
e.Building is WalkBuilding shellOwner
|
||||||
|
? _worldData.GetBuildingShellStatics(shellOwner)
|
||||||
|
: _worldData.GetOutdoorStatics(e.CellId),
|
||||||
|
_staticParticleOwnerScratch);
|
||||||
|
if (_staticParticleOwnerScratch.Count > 0)
|
||||||
|
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -639,6 +699,27 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
||||||
_populator.PopulateOutdoorStatics(
|
_populator.PopulateOutdoorStatics(
|
||||||
_stream, cellId, records.Records, records.TupleLandblockId,
|
_stream, cellId, records.Records, records.TupleLandblockId,
|
||||||
_cameraWorldPosition, _viewProjection);
|
_cameraWorldPosition, _viewProjection);
|
||||||
|
// FW4 (the #132 positional invariant): this cell's emitter owners
|
||||||
|
// submit AT THIS TURN, so nearer buildings' pre-punch barriers
|
||||||
|
// drain them against still-true depth — see
|
||||||
|
// WalkFrameEventKind.StaticParticles. Mark first so the cell's own
|
||||||
|
// meshes flush ahead of its particle submission (retail's
|
||||||
|
// per-object DrawObjCell order).
|
||||||
|
if (HasAnyOwner(records))
|
||||||
|
{
|
||||||
|
MarkIfGrown();
|
||||||
|
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasAnyOwner(in WalkFrameStaticRecords records)
|
||||||
|
{
|
||||||
|
foreach (RenderProjectionRecord record in records.Records)
|
||||||
|
{
|
||||||
|
if (record.Source.LocalEntityId != 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWalkEventSink.OnBuildingTurn(WalkBuilding building)
|
void IWalkEventSink.OnBuildingTurn(WalkBuilding building)
|
||||||
|
|
@ -672,6 +753,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
||||||
_populator.PopulateCell(
|
_populator.PopulateCell(
|
||||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
||||||
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection);
|
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection);
|
||||||
|
// FW4 (the #132 positional invariant): the building's own shell
|
||||||
|
// emitters submit at the shell turn, after the shell content
|
||||||
|
// flushes — see WalkFrameEventKind.StaticParticles.
|
||||||
|
if (HasAnyOwner(shell))
|
||||||
|
{
|
||||||
|
MarkIfGrown();
|
||||||
|
_events.Add(WalkFrameEvent.BuildingShellParticles(building));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWalkEventSink.OnPunchGeometry(
|
void IWalkEventSink.OnPunchGeometry(
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,13 @@ public sealed class WalkFrameDriverTests
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AlphaBarrier(float viewerDistance) => log.Add($"ALPHA:{viewerDistance:F2}");
|
public void AlphaBarrier(float viewerDistance) => log.Add($"ALPHA:{viewerDistance:F2}");
|
||||||
|
|
||||||
|
public void DrawStaticParticles(IReadOnlySet<uint> ownerIds)
|
||||||
|
{
|
||||||
|
var sorted = new List<uint>(ownerIds);
|
||||||
|
sorted.Sort();
|
||||||
|
log.Add($"PARTICLES:{string.Join(",", sorted.ConvertAll(o => o.ToString("x")))}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
|
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
|
||||||
|
|
@ -405,7 +412,13 @@ public sealed class WalkFrameDriverTests
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new[] { "ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104", "FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell" },
|
// FW4 #132 positional invariant: the building's shell emitters
|
||||||
|
// submit at the shell turn, after the shell content flushes.
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104",
|
||||||
|
"FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell", "PARTICLES:c9",
|
||||||
|
},
|
||||||
log);
|
log);
|
||||||
|
|
||||||
// The punch polygon reached the leaf renderer in WORLD space: the
|
// The punch polygon reached the leaf renderer in WORLD space: the
|
||||||
|
|
@ -554,7 +567,9 @@ public sealed class WalkFrameDriverTests
|
||||||
Assert.Empty(log); // still nothing — EndFrame closes Collect, it does not Replay
|
Assert.Empty(log); // still nothing — EndFrame closes Collect, it does not Replay
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
Assert.Equal(new[] { "FLUSH:1:OutdoorStatic" }, log);
|
// FW4 #132 positional invariant: the cell's emitters submit at its
|
||||||
|
// own landscape turn, after its meshes flush.
|
||||||
|
Assert.Equal(new[] { "FLUSH:1:OutdoorStatic", "PARTICLES:12d" }, log);
|
||||||
GpuRecordedMultiDrawIndirect mdi = Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
GpuRecordedMultiDrawIndirect mdi = Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
Assert.Equal(1u, mdi.DrawCount);
|
Assert.Equal(1u, mdi.DrawCount);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue