fix(render): S3 chunk 3 round 1 - slot key, deferred cross-block terrain batches, per-frame terrain diagnostic
Fixes the three-lens review blockers against 671eb3ad4 (S3 section 9.6 F1-F6).
F1 - slot key (blocking, retail). TerrainModernRenderer.DrawLandCells
normalizes every incoming landblockId to (id & 0xFFFF0000u) | 0xFFFFu
before the _idToSlot lookup: the walk hands 0xXXYY0000
(WalkLandBlock.LandblockId) but AddLandblock stores under the DAT id
0xXXYYFFFF (LandblockRenderPublisher.LandblockId) - every walk lookup
was missing and the walk path drew NO terrain. Unit-tested end-to-end
through a real RecordingGpuDevice-backed TerrainModernRenderer
(TerrainWalkSlotKeyNormalizationTests): AddLandblock(0xA9B4FFFF, ...)
is found by a 0xA9B40000 lookup, an unknown landblock is a silent
per-entry no-op, and a batch mixing a known and unknown entry submits
only the known one.
F2 - deferred cross-block batching (blocking, driver). Retail's
DrawSortCell always follows DrawLandCell (LC/SC strictly alternate,
never two LC in a row - S3 section 9 R1), so chunk 3's "merge
consecutive same-landblock LandCell events" rule never actually
merged anything; the driver review flagged batching as inert.
WalkFrameDriver.Replay now keeps ONE pending terrain batch across
landblocks ((landblockId, side, cellIndex) entries, cleared at
Replay's own start); a LandCell event only appends; every OTHER event
kind that will itself submit GPU work (StreamMark, Sky, CellShell,
PunchFan, AlphaBarrier, LandscapeFlush, ClearInteriorDepth,
ExitSeals) flushes the pending batch first; a StaticParticles/
CellParticles turn asks the new ParticleSystem.
HasRenderableEmittersInCell (an allocation-free sibling of
CopyRenderableEmittersInCell) and, when the cell has no renderable
emitter, submits nothing and does NOT flush either - the whole point
of the deferred rule. The end of Replay flushes the remainder. This
is order-preserving by construction: a flush always lands at the
exact point the unbatched draw would have, so GPU submission order -
and therefore pixels - is identical to the unbatched baseline; only
the number of small terrain draw calls shrinks.
TerrainModernRenderer.DrawLandCellRuns becomes DrawLandCells(
viewProjection, IReadOnlyList<(uint LandblockId, int SideCellCount,
int CellIndex)>) - one MultiDrawIndexedIndirect over every entry's
runs, unknown slots skipped per-entry. IWalkFrameLeafRenderer.
DrawLandCellBatch drops its separate landblockId parameter to match
(a batch can span several landblocks now) and gains
HasRenderableEmittersInCell.
Batch-count demonstration: driven through a real WalkFrameDriver
Replay (OnLandCellTurn_MergesAcrossLandblocksOverAnEmptyParticleTurn_
RealSubmissionsSplit), 4 LandCell turns across 3 distinct landblocks,
separated only by an empty particle turn, a real StreamMark, and a
building's alpha barrier, submit as exactly 3 DrawLandCellBatch calls
(2+1+1) instead of 4 - the empty particle turn's non-flush merges two
otherwise-separate cross-landblock entries. At production scale the
same mechanism is expected to cut the terrace-edge frame's ~578
individual DrawLandCell events (S3 section 9's captured transcript
count) to "tens" of submitted batches, per the contract's own
expectation: most terrain cells have no particle owner nearby, so the
strict LC/[empty-SC]/LC/[empty-SC]/... run collapses into one batch
per region bounded by real content (a building, a StreamMark-worthy
cell, or a genuine emitter) rather than per cell.
F3 - per-frame terrain diagnostic (blocking, build/test). The walk
leaf no longer brackets each batch with TerrainDrawDiagnosticsController
.Begin()/Complete() (a per-batch Stopwatch Restart/Stop pair that was
pushing one timing SAMPLE per batch, not per frame).
RetailPViewPassExecutor.DrawWalkLandCellBatch instead times its own
call with a raw Stopwatch.GetTimestamp() delta (no allocation) and
hands the ticks to the controller's new AccumulateWalkBatch;
RetailPViewRenderer.DrawWalkDrivenStatics calls the new
CompleteWalkTerrainFrame() exactly once, immediately after
driver.Replay finishes - "the end of the walk replay", where the
deleted whole-stage terrain leaf's own Begin()/Complete() bracket
used to close - which pushes ONE elapsed-time sample (even a
zero-batch frame pushes a zero sample: one sample per frame, not per
landscape turn) and publishes on the existing 5-second cadence.
TerrainRenderDiagnosticFacts gains a Draws field alongside
VisibleSlots (both were the same field before); TerrainModernRenderer
tracks its own per-frame WalkVisibleSlotCount/WalkDrawCount (a
HashSet<int>/int cleared in BeginFrame, populated by DrawLandCells),
and the diagnostics source reports those whenever the walk drew at
least one batch this frame, falling back to the non-walk Draw()
path's VisibleSlots otherwise (the two paths never both run in the
same frame). The [TERRAIN-DIAG] line's meaning (cpu_us per frame) is
unchanged, so the S3 section 9.5 before/after compare stays valid.
F4 - driver pins for the LandCell position (major). Three RunFrame-
level pins replace the deleted TERRAIN:0 pins: an outdoor-root
sequence (SKY, then one LANDCELL, driving RetailFrameWalk.
DrawLandscape directly with a one-view/zero-vertex WalkPortalView so
WalkLandscape.CheckBlocks' admission stays the same deterministic
"CY-only" test RetailFrameWalkTests already relies on, while still
satisfying WalkFrameDriver's real >=1-active-view fail-loud guard);
an interior-root test with one real exit view and one populated
block (SKY, LANDCELL, LFLUSH, SEALS, SHELL...) built on the existing
RunFrame_InteriorFloodWithExitView_... fixture; and the T4 batching
pin re-expressed for the F2 rule (OnLandCellTurn_
MergesAcrossLandblocksOverAnEmptyParticleTurn_RealSubmissionsSplit,
described above). The fake leaf's DrawLandCellBatch now logs
LANDCELL:<lb>:<side>:<idx>[,...] per batch and gains
HasRenderableEmittersInCell backed by an opt-out CellsWithoutEmitters
set (default true - has-emitters - so every pre-existing pin in the
file keeps its old unconditional-submission behavior unchanged).
F5 - no code change: the walk's in-view gate is unchanged; no
whole-block terrain re-added.
F6 - minor/notes: DrawLandCells' own comment now states the walk's
CheckBlocks/landcell_check admission is the sole terrain culling
authority (retail has no separate terrain frustum test); the
HandleLandscapeTurn comment's inverted claim is corrected (a FARTHER
building's punch survived because NEARER terrain was drawn BEFORE
it, not after - the interleave now draws it after, matching retail);
the "flat/directional-shadow paths" claim is corrected to the one
actual caller, WorldScenePassExecutor.DrawFlatTerrain (a directional-
shadow receiver selects its pipeline inside the SAME DrawRhi call,
not through a second caller); the cathedral order-trace token gains
the LOD side/index (":LC<lb>/<side>:<idx>"); T2's vacuous "no
TERRAIN event" assertion in RetailFrameWalkTests is replaced by a
comment pointing at the F4 driver-level pins; and the stale
"Confirmed OH5 defect" row in oh1-construction-landscape-contract.md
is retired with "FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round
1)".
App hermetic lane: 6,795/6,795 (up from 671eb3ad4's 6,786 baseline -
net +9 tests: 3 F1 slot-key tests, 2 F4a/b driver RunFrame pins, 3
TerrainDrawDiagnosticsController walk-frame tests, plus the T4->F4c
rewrite and the RetailPViewPassExecutorTests split are net neutral).
InstalledDat lane: 241 passed, the same 3 accepted failures (2
pre-existing #383 layout fixture-drift tests, 1 TowerAscent
Status=KnownFailure) - unchanged from baseline. Core Vfx tests:
109/109 (108 baseline + 1 new HasRenderableEmittersInCell lifecycle
pin mirroring CopyRenderableEmittersInCell's own add/move/remove
test).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
4ee2866a7b
commit
651badc2b9
15 changed files with 1003 additions and 131 deletions
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
|
@ -74,23 +75,48 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
|
||||
/// <summary>S3 chunk 3 (§9.2 B1): the walk's per-land-cell terrain
|
||||
/// turn — retail <c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120,
|
||||
/// batched (S3 §9.2 B2). UNCLIPPED for BOTH root kinds — retail never
|
||||
/// batched (S3 §9.2 B2, fix round 1 F2 — a batch may span several
|
||||
/// landblocks). UNCLIPPED for BOTH root kinds — retail never
|
||||
/// view-clips terrain (<c>LScape::draw</c> draws whole blocks; the
|
||||
/// walk's own <c>CellInView</c> admission already decided which cells
|
||||
/// reach here, and the exit-seal/interior-repaint turn owns aperture
|
||||
/// exactness afterward, S3 §9.1 R4/R5), so unlike the whole-stage
|
||||
/// turn this superseded, there is no scissor, no terrain clip, and no
|
||||
/// per-slice loop — one call per (already order-merged) batch.</summary>
|
||||
/// per-slice loop — one call per (already order-merged) batch.
|
||||
///
|
||||
/// <para>S3 chunk 3 fix round 1 (F3): times THIS ONE batch with a raw
|
||||
/// <c>Stopwatch.GetTimestamp()</c> delta (no allocation, no per-batch
|
||||
/// stopwatch Restart/Stop) and accumulates it into
|
||||
/// <see cref="TerrainDrawDiagnosticsController.AccumulateWalkBatch"/> —
|
||||
/// <see cref="RetailPViewRenderer.DrawWalkDrivenStatics"/> pushes the
|
||||
/// frame's single resulting sample once, after the whole walk replay
|
||||
/// finishes.</para></summary>
|
||||
internal void DrawWalkLandCellBatch(
|
||||
RetailPViewFrameInput frame,
|
||||
uint landblockId,
|
||||
IReadOnlyList<(int SideCellCount, int CellIndex)> cells)
|
||||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells)
|
||||
{
|
||||
_terrainDiagnostics.Begin();
|
||||
_terrain?.DrawLandCellRuns(landblockId, frame.ViewProjection, cells);
|
||||
_terrainDiagnostics.Complete();
|
||||
long start = Stopwatch.GetTimestamp();
|
||||
_terrain?.DrawLandCells(frame.ViewProjection, cells);
|
||||
_terrainDiagnostics.AccumulateWalkBatch(Stopwatch.GetTimestamp() - start);
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): pushes THIS FRAME'S single
|
||||
/// accumulated terrain-timing sample (from every
|
||||
/// <see cref="DrawWalkLandCellBatch"/> call this frame) and publishes
|
||||
/// the periodic diagnostic if the cadence is due — the walk path's
|
||||
/// analogue of the deleted whole-stage <c>DrawWalkTerrainSlice</c>
|
||||
/// leaf's own Begin()/Complete() bracket. Called exactly once, at the
|
||||
/// end of the walk replay (<see cref="RetailPViewRenderer.DrawWalkDrivenStatics"/>).</summary>
|
||||
internal void CompleteWalkTerrainFrame() => _terrainDiagnostics.CompleteWalkFrame();
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F2): the production
|
||||
/// <c>IWalkFrameLeafRenderer.HasRenderableEmittersInCell</c> wiring —
|
||||
/// both <see cref="DrawLandscapeStaticParticles"/> and
|
||||
/// <see cref="DrawCellParticles"/> draw through
|
||||
/// <c>ParticleRenderPass.Scene</c>, so this asks that SAME pass.</summary>
|
||||
internal bool HasWalkRenderableEmittersInCell(uint cellId) =>
|
||||
_particles?.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId) ?? false;
|
||||
|
||||
/// <summary>The walk's punch-fan turn — <c>DrawPortalPolyInternal</c>
|
||||
/// @0x0059bc90's far-Z punch through <c>PortalDepthMaskRenderer</c>,
|
||||
/// clipped by the pinned view's slice planes (retail
|
||||
|
|
@ -209,8 +235,11 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
public void DrawSky() => _passes.DrawWalkSky(_frame, _clipAssembly);
|
||||
|
||||
public void DrawLandCellBatch(
|
||||
uint landblockId, IReadOnlyList<(int SideCellCount, int CellIndex)> cells) =>
|
||||
_passes.DrawWalkLandCellBatch(_frame, landblockId, cells);
|
||||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) =>
|
||||
_passes.DrawWalkLandCellBatch(_frame, cells);
|
||||
|
||||
public bool HasRenderableEmittersInCell(uint cellId) =>
|
||||
_passes.HasWalkRenderableEmittersInCell(cellId);
|
||||
|
||||
public void DrawCellShell(uint cellId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -589,6 +589,13 @@ internal sealed class RetailPViewRenderer
|
|||
// the sole terrain-visibility authority, so there is no plumbing
|
||||
// left to feed here.
|
||||
driver.Replay(frame, encoder);
|
||||
// S3 chunk 3 fix round 1 (F3): "the end of the walk replay" —
|
||||
// exactly where the deleted whole-stage terrain leaf's own
|
||||
// Begin()/Complete() bracket used to close. Pushes this frame's ONE
|
||||
// accumulated terrain-timing sample (every DrawWalkLandCellBatch
|
||||
// call between here and the last CompleteWalkTerrainFrame) and
|
||||
// publishes the periodic [TERRAIN-DIAG] line if the cadence is due.
|
||||
passes.CompleteWalkTerrainFrame();
|
||||
|
||||
// Landscape-stage static-owner particles (candles, the cathedral
|
||||
// falls) submit AT THEIR OWN WALK TURNS inside Replay
|
||||
|
|
|
|||
|
|
@ -56,10 +56,22 @@ internal sealed class RuntimeFramePipelineDiagnosticFactsSource :
|
|||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
}
|
||||
|
||||
public TerrainRenderDiagnosticFacts CaptureTerrain() => new(
|
||||
_terrain?.VisibleSlots ?? 0,
|
||||
_terrain?.LoadedSlots ?? 0,
|
||||
_terrain?.CapacitySlots ?? 0);
|
||||
public TerrainRenderDiagnosticFacts CaptureTerrain()
|
||||
{
|
||||
// S3 chunk 3 fix round 1 (F3): only ONE of the two terrain paths
|
||||
// draws in a given frame (the walk path never calls Draw() any
|
||||
// more — S3 §9.2 B3). Report the walk path's own per-frame counters
|
||||
// whenever it submitted at least one batch this frame; otherwise
|
||||
// fall back to the non-walk Draw() path's own per-call VisibleSlots
|
||||
// (its "draws" has always equalled its visible-slot count — ONE
|
||||
// MultiDrawIndexedIndirect spanning every visible slot).
|
||||
int walkDraws = _terrain?.WalkDrawCount ?? 0;
|
||||
if (walkDraws > 0)
|
||||
return new(_terrain!.WalkVisibleSlotCount, walkDraws, _terrain.LoadedSlots, _terrain.CapacitySlots);
|
||||
|
||||
int visibleSlots = _terrain?.VisibleSlots ?? 0;
|
||||
return new(visibleSlots, visibleSlots, _terrain?.LoadedSlots ?? 0, _terrain?.CapacitySlots ?? 0);
|
||||
}
|
||||
|
||||
public FramePipelineDiagnosticFacts CaptureFrame() => new(
|
||||
_presentation?.Diagnostics ?? default,
|
||||
|
|
@ -88,6 +100,10 @@ internal sealed class TerrainDrawDiagnosticsController
|
|||
private readonly IRenderFrameDiagnosticLog _log;
|
||||
private long _lastPublicationMilliseconds;
|
||||
|
||||
// S3 chunk 3 fix round 1 (F3): the walk path's per-BATCH tick
|
||||
// accumulator — see AccumulateWalkBatch/CompleteWalkFrame.
|
||||
private long _walkAccumulatedTicks;
|
||||
|
||||
public TerrainDrawDiagnosticsController(
|
||||
bool enabled,
|
||||
WorldRenderDiagnostics world,
|
||||
|
|
@ -107,6 +123,40 @@ internal sealed class TerrainDrawDiagnosticsController
|
|||
internal void Complete(long nowMilliseconds)
|
||||
{
|
||||
_world.EndTerrainDraw();
|
||||
PublishIfDue(nowMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): the leaf calls this once per
|
||||
/// land-cell batch, with that ONE batch's own elapsed
|
||||
/// <c>Stopwatch.GetTimestamp()</c> delta (no allocation, no per-batch
|
||||
/// Stopwatch Restart/Stop — <see cref="Begin"/>/<see cref="Complete()"/>
|
||||
/// stay reserved for the non-walk fallback path's own single bracketed
|
||||
/// call). Accumulates into this frame's running total; <see
|
||||
/// cref="CompleteWalkFrame()"/> converts and pushes it as ONE sample.</summary>
|
||||
public void AccumulateWalkBatch(long elapsedTicks) => _walkAccumulatedTicks += elapsedTicks;
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): call once, after <see
|
||||
/// cref="Walk.WalkFrameDriver.Replay"/> finishes — the walk path's
|
||||
/// analogue of <see cref="Complete()"/> for the deleted whole-stage
|
||||
/// terrain draw's own Begin()/Complete() bracket (superseded by the S3
|
||||
/// chunk 3 per-cell interleave). Converts this frame's accumulated
|
||||
/// per-batch tick total into ONE elapsed-time sample (a frame that
|
||||
/// submitted zero batches still pushes a zero sample — "one sample per
|
||||
/// frame", not "one sample per landscape turn") and resets the
|
||||
/// accumulator for the next frame.</summary>
|
||||
public void CompleteWalkFrame() => CompleteWalkFrame(_enabled ? Environment.TickCount64 : 0L);
|
||||
|
||||
internal void CompleteWalkFrame(long nowMilliseconds)
|
||||
{
|
||||
double ticksToHundredthsMicroseconds = 1_000_000.0 * 100.0 / Stopwatch.Frequency;
|
||||
_world.PushTerrainSample(
|
||||
(long)(_walkAccumulatedTicks * ticksToHundredthsMicroseconds));
|
||||
_walkAccumulatedTicks = 0;
|
||||
PublishIfDue(nowMilliseconds);
|
||||
}
|
||||
|
||||
private void PublishIfDue(long nowMilliseconds)
|
||||
{
|
||||
if (!_enabled
|
||||
|| nowMilliseconds - _lastPublicationMilliseconds <= PublicationIntervalMilliseconds)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,15 +71,43 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
private readonly HashSet<uint> _walkVisibleLandblocks = new();
|
||||
private DrawElementsIndirectCommand[] _deicScratch = Array.Empty<DrawElementsIndirectCommand>();
|
||||
|
||||
// S3 chunk 3 (§9.2 B1): DrawLandCellRuns' reusable index-run scratch —
|
||||
// repopulated per call by AppendCellIndexRuns, one (Start, Count) pair
|
||||
// per contiguous run (side 8: one run/cell; side 1: one run total).
|
||||
// S3 chunk 3 (§9.2 B1): DrawLandCells' reusable per-entry index-run
|
||||
// scratch — repopulated per cell by AppendCellIndexRuns, one
|
||||
// (Start, Count) pair per contiguous run relative to THAT entry's own
|
||||
// slot (side 8: one run/cell; side 1: one run total). Converted to
|
||||
// absolute (FirstIndex, Count) pairs in _batchRunScratch below because a
|
||||
// fix-round-1 batch (F2) can span several landblocks/slots.
|
||||
private readonly List<(int Start, int Count)> _cellRunScratch = new();
|
||||
|
||||
// S3 chunk 3 fix round 1 (F2): DrawLandCells' reusable ABSOLUTE run
|
||||
// scratch — one (FirstIndex, Count) pair per contiguous index run across
|
||||
// every entry of one batch, in entry order, cleared per call.
|
||||
private readonly List<(uint FirstIndex, int Count)> _batchRunScratch = new();
|
||||
|
||||
// S3 chunk 3 fix round 1 (F3): the walk path's own per-FRAME submission
|
||||
// stats — cleared in BeginFrame, populated by DrawLandCells. Distinct
|
||||
// from _visibleSlots (Draw()'s own per-call list, the non-walk fallback
|
||||
// path) because the walk submits many small batches across a frame
|
||||
// rather than one Draw() call; TerrainDrawDiagnosticsController reports
|
||||
// whichever path actually ran this publication window.
|
||||
private readonly HashSet<int> _walkSlotsThisFrame = new();
|
||||
private int _walkDrawsThisFrame;
|
||||
|
||||
// Diag.
|
||||
public int LoadedSlots => _alloc.LoadedCount;
|
||||
public int VisibleSlots => _visibleSlots.Count;
|
||||
public int CapacitySlots => _alloc.Capacity;
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): distinct slots <see
|
||||
/// cref="DrawLandCells"/> actually submitted THIS FRAME (cleared at
|
||||
/// <see cref="BeginFrame"/>) — the walk path's own per-frame
|
||||
/// "VisibleSlots" answer, since the walk never calls <see cref="Draw"/>.</summary>
|
||||
internal int WalkVisibleSlotCount => _walkSlotsThisFrame.Count;
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): the number of <see
|
||||
/// cref="DrawLandCells"/> batches (indirect draws) submitted THIS FRAME.</summary>
|
||||
internal int WalkDrawCount => _walkDrawsThisFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor landcells admitted by the current landscape view. The set is
|
||||
/// accumulated across doorway landscape slices and consumed after the
|
||||
|
|
@ -103,6 +131,10 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
_retirementLedger.RetryPendingPublications();
|
||||
_dynamicFrameSlot = frameSlot;
|
||||
_dynamicFrameStarted = true;
|
||||
// S3 chunk 3 fix round 1 (F3): reset the walk path's per-frame
|
||||
// submission stats — one presented frame, one answer.
|
||||
_walkSlotsThisFrame.Clear();
|
||||
_walkDrawsThisFrame = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -294,54 +326,100 @@ public sealed partial class TerrainModernRenderer : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 3 (§9.2 B1/B2): the walk's per-land-cell terrain draw —
|
||||
/// retail <c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120, batched.
|
||||
/// Submits <see cref="AppendCellIndexRuns"/>'s index runs for every
|
||||
/// entry of <paramref name="cells"/> as ONE indirect draw, UNCLIPPED:
|
||||
/// S3 chunk 3 (§9.2 B1/B2), fix round 1 (F1/F2): the walk's per-land-cell
|
||||
/// terrain draw — retail <c>RenderDeviceD3D::DrawLandCell</c>
|
||||
/// @0x0059f120, batched. Submits <see cref="AppendCellIndexRuns"/>'s
|
||||
/// index runs for every entry of <paramref name="cells"/> as ONE
|
||||
/// indirect draw spanning however many DISTINCT landblocks the batch
|
||||
/// covers (fix round 1's F2: the terrain index buffer is one buffer with
|
||||
/// a per-slot <c>FirstIndex</c> offset, so a batch is never required to
|
||||
/// stay within one landblock — <see cref="Walk.WalkFrameDriver.Replay"/>
|
||||
/// only splits it at a genuine intervening GPU submission), UNCLIPPED:
|
||||
/// retail never view-clips terrain — the walk's own per-cell
|
||||
/// <c>CellInView</c> admission (<c>RetailFrameWalk.DrawLandscape</c>'s
|
||||
/// <c>DrawLandCell</c> gate) already decided which cells reach here, so
|
||||
/// there is no frustum test and no <c>inViewLandcells</c> filter here,
|
||||
/// unlike the non-walk <see cref="Draw"/> entry point this supersedes
|
||||
/// for the walk path only (<see cref="Draw"/> keeps serving non-walk
|
||||
/// callers, e.g. the flat/directional-shadow paths). A landblockId with
|
||||
/// no uploaded slot (a streaming-timing race between the walk's own
|
||||
/// block graph and this renderer's landblock upload) is a silent
|
||||
/// no-op — the walk's visited-cell bookkeeping is the timing authority,
|
||||
/// not this call.
|
||||
/// there is no frustum test and no <c>inViewLandcells</c> filter here.
|
||||
/// Fix round 1 F6: this is not a missing culling step — retail has no
|
||||
/// separate terrain frustum test beyond <c>LScape::draw_check_blocks</c>/
|
||||
/// <c>landcell_check</c> (ported as <c>WalkLandscape.CheckBlocks</c>),
|
||||
/// so the walk's own admission IS the sole culling authority for
|
||||
/// terrain, matching retail exactly. Unlike the non-walk <see cref="Draw"/> entry point this supersedes
|
||||
/// for the walk path only (<see cref="Draw"/> keeps serving its one
|
||||
/// remaining non-walk caller — fix round 1 F6 corrects the prior "flat/
|
||||
/// directional-shadow paths" wording: the only caller left is the flat-
|
||||
/// terrain fallback, <c>WorldScenePassExecutor.DrawFlatTerrain</c>; a
|
||||
/// directional-shadow-receiving surface selects its receiver pipeline
|
||||
/// inside the SAME <c>DrawRhi</c> call this makes, not through a second
|
||||
/// caller).
|
||||
///
|
||||
/// <para><b>F1 — the slot key.</b> <paramref name="cells"/> carries each
|
||||
/// entry's landblock id in the WALK's own <c>bx<<24 | by<<16</c>
|
||||
/// encoding (<c>WalkLandBlock.LandblockId</c>, low word 0x0000), while
|
||||
/// <see cref="_idToSlot"/> is keyed by the DAT landblock id
|
||||
/// (<c>LandblockRenderPublisher.LandblockId</c>, low word 0xFFFF — the
|
||||
/// same convention <see cref="AddLandblock"/>/<see cref="RemoveLandblock"/>
|
||||
/// already use and must keep using, since those callers already pass the
|
||||
/// DAT id). Normalized HERE, at this one walk entry point, rather than at
|
||||
/// every walk call site.</para>
|
||||
///
|
||||
/// <para>An entry whose normalized id has no uploaded slot (a
|
||||
/// streaming-timing race between the walk's own block graph and this
|
||||
/// renderer's landblock upload, OR a genuinely unknown block) is a
|
||||
/// silent per-entry no-op — the walk's visited-cell bookkeeping is the
|
||||
/// timing authority, not this call; the other entries in the same batch
|
||||
/// still submit.</para>
|
||||
/// </summary>
|
||||
public void DrawLandCellRuns(
|
||||
uint landblockId,
|
||||
public void DrawLandCells(
|
||||
Matrix4x4 viewProjection,
|
||||
IReadOnlyList<(int SideCellCount, int CellIndex)> cells)
|
||||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cells);
|
||||
if (cells.Count == 0) return;
|
||||
if (!_idToSlot.TryGetValue(landblockId, out int slot)) return;
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before drawing terrain.");
|
||||
|
||||
_cellRunScratch.Clear();
|
||||
_batchRunScratch.Clear();
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
AppendCellIndexRuns(cells[i].SideCellCount, cells[i].CellIndex, _cellRunScratch);
|
||||
int commandCount = _cellRunScratch.Count;
|
||||
{
|
||||
(uint landblockId, int sideCellCount, int cellIndex) = cells[i];
|
||||
// F1: the walk hands 0xXXYY0000 (WalkLandBlock.LandblockId);
|
||||
// AddLandblock stores under the DAT id 0xXXYYFFFF
|
||||
// (LandblockRenderPublisher.LandblockId). Normalize the lookup,
|
||||
// not the storage — every non-walk caller still stores/removes
|
||||
// under the DAT id unchanged.
|
||||
uint slotKey = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
if (!_idToSlot.TryGetValue(slotKey, out int slot))
|
||||
continue;
|
||||
uint baseFirstIndex = (uint)(slot * IndicesPerLandblock);
|
||||
|
||||
_cellRunScratch.Clear();
|
||||
AppendCellIndexRuns(sideCellCount, cellIndex, _cellRunScratch);
|
||||
for (int r = 0; r < _cellRunScratch.Count; r++)
|
||||
{
|
||||
(int start, int count) = _cellRunScratch[r];
|
||||
_batchRunScratch.Add((baseFirstIndex + (uint)start, count));
|
||||
}
|
||||
_walkSlotsThisFrame.Add(slot);
|
||||
}
|
||||
|
||||
int commandCount = _batchRunScratch.Count;
|
||||
if (commandCount == 0) return;
|
||||
|
||||
if (_deicScratch.Length < commandCount)
|
||||
_deicScratch = new DrawElementsIndirectCommand[Math.Max(commandCount, 64)];
|
||||
uint baseFirstIndex = (uint)(slot * IndicesPerLandblock);
|
||||
for (int i = 0; i < commandCount; i++)
|
||||
{
|
||||
(int start, int count) = _cellRunScratch[i];
|
||||
(uint firstIndex, int count) = _batchRunScratch[i];
|
||||
_deicScratch[i] = new DrawElementsIndirectCommand
|
||||
{
|
||||
Count = (uint)count,
|
||||
InstanceCount = 1u,
|
||||
FirstIndex = baseFirstIndex + (uint)start,
|
||||
FirstIndex = firstIndex,
|
||||
BaseVertex = 0, // baked into indices on upload
|
||||
BaseInstance = 0,
|
||||
};
|
||||
}
|
||||
_walkDrawsThisFrame++;
|
||||
DrawRhi(viewProjection, commandCount);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,24 +102,40 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// calls this exactly once per frame's Landscape turn).</summary>
|
||||
void DrawSky();
|
||||
|
||||
/// <summary>S3 chunk 3 (§9.2 B1/B2): retail <c>RenderDeviceD3D::
|
||||
/// DrawLandCell</c> @0x0059f120 — one landblock's terrain at one or
|
||||
/// more admitted LOD cells, submitted together as ONE indirect draw.
|
||||
/// <see cref="WalkFrameDriver.Replay"/> merges consecutive same-slot
|
||||
/// <see cref="WalkFrameEventKind.LandCell"/> events with no intervening
|
||||
/// event before calling this once (order-preserving batching — any
|
||||
/// other event, e.g. a building's alpha barrier or the SAME cell's own
|
||||
/// object-list turn, splits the batch), so a single-cell call is the
|
||||
/// common case whenever a cell's own <see cref="DrawStaticParticles"/>/
|
||||
/// stream mark interposes. <paramref name="cells"/> carries (side,
|
||||
/// cellIndex) pairs in event (draw) order — the R3 index-run arithmetic
|
||||
/// (contiguous runs inside the landblock's 384-index slot,
|
||||
/// <summary>S3 chunk 3 (§9.2 B1/B2), fix round 1 (F2): retail
|
||||
/// <c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120 — one or more
|
||||
/// admitted LOD cells, possibly spanning several landblocks, submitted
|
||||
/// together as ONE indirect draw. <see cref="WalkFrameDriver.Replay"/>
|
||||
/// keeps a PENDING batch that <see cref="WalkFrameEventKind.LandCell"/>
|
||||
/// events append to and flushes (calls this once) immediately before any
|
||||
/// OTHER event that will itself submit GPU work — see that method's own
|
||||
/// doc comment for the exact flush points, including the F2 particle-turn
|
||||
/// exception (a genuinely empty <see cref="DrawStaticParticles"/>/<see
|
||||
/// cref="DrawCellParticles"/> turn neither submits nor flushes). This is
|
||||
/// order-preserving by construction: the batch's GPU submission point is
|
||||
/// always the SAME point the unbatched terrain draws would have occupied
|
||||
/// (immediately before the next real submission), so pixel output is
|
||||
/// identical to submitting each entry as its own draw. <paramref
|
||||
/// name="cells"/> carries (landblockId, side, cellIndex) triples in
|
||||
/// event (draw) order — the R3 index-run arithmetic (contiguous runs
|
||||
/// inside each entry's own landblock's 384-index slot,
|
||||
/// <c>TerrainModernRenderer.AppendCellIndexRuns</c>) runs per entry.
|
||||
/// Draws UNCLIPPED for both root kinds: retail never view-clips terrain
|
||||
/// (<c>LScape::draw</c> draws whole blocks; the exit-seal/interior-
|
||||
/// repaint turn owns aperture exactness afterward, S3 §9.1 R4/R5).</summary>
|
||||
void DrawLandCellBatch(
|
||||
uint landblockId, IReadOnlyList<(int SideCellCount, int CellIndex)> cells);
|
||||
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells);
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F2): whether <paramref
|
||||
/// name="cellId"/> has any renderable emitter right now — production
|
||||
/// wires this to <c>ParticleSystem.HasRenderableEmittersInCell</c> with
|
||||
/// the SAME <c>ParticleRenderPass.Scene</c> pass both <see
|
||||
/// cref="DrawStaticParticles"/> and <see cref="DrawCellParticles"/>
|
||||
/// already draw through. <see cref="WalkFrameDriver.Replay"/> asks this
|
||||
/// BEFORE flushing the pending terrain batch (or calling either particle
|
||||
/// leaf) so a genuinely empty particle turn submits nothing and does not
|
||||
/// force an otherwise-unneeded terrain flush.</summary>
|
||||
bool HasRenderableEmittersInCell(uint cellId);
|
||||
|
||||
/// <summary>One committed cell's EnvCell shell —
|
||||
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe. Retail first
|
||||
|
|
@ -563,11 +579,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private readonly List<int> _floodViewRouteScratch = new();
|
||||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
/// <summary>S3 chunk 3 (§9.2 B2): <see cref="Replay"/>'s reusable
|
||||
/// batch-merge scratch — the (side, cellIndex) pairs of a run of
|
||||
/// consecutive same-landblock <see cref="WalkFrameEventKind.LandCell"/>
|
||||
/// events, cleared and repopulated at each batch boundary.</summary>
|
||||
private readonly List<(int SideCellCount, int CellIndex)> _landCellBatchScratch = new();
|
||||
/// <summary>S3 chunk 3 (§9.2 B2), fix round 1 (F2): <see cref="Replay"/>'s
|
||||
/// PENDING terrain batch — every <see cref="WalkFrameEventKind.LandCell"/>
|
||||
/// event since the last flush, across however many landblocks, in event
|
||||
/// order. Cleared at the start of each <see cref="Replay"/> call ("frame
|
||||
/// start") and by every flush; NOT cleared between events, since it is
|
||||
/// the whole point of the deferred-batching rule.</summary>
|
||||
private readonly List<(uint LandblockId, int SideCellCount, int CellIndex)> _pendingTerrainBatch = new();
|
||||
|
||||
private readonly HashSet<uint> _cellShellsDrawnThisFrame = new();
|
||||
// Chunk 6 review F1: one particle turn per cell per render stamp (see EmitCellContentsTurn).
|
||||
|
|
@ -929,6 +947,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
if (_stream.Count > 0)
|
||||
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
|
||||
|
||||
// S3 chunk 3 fix round 1 (F2): cleared at the start of THIS
|
||||
// replay ("frame start") — a batch never survives across frames.
|
||||
_pendingTerrainBatch.Clear();
|
||||
|
||||
int cursor = 0;
|
||||
int alphaCursor = 0;
|
||||
for (int i = 0; i < _events.Count; i++)
|
||||
|
|
@ -937,6 +959,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
switch (e.Kind)
|
||||
{
|
||||
case WalkFrameEventKind.StreamMark:
|
||||
FlushPendingTerrainBatch();
|
||||
int end = e.IntArg;
|
||||
int count = end - cursor;
|
||||
if (_trace is not null)
|
||||
|
|
@ -945,6 +968,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
cursor = end;
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaSubmitMark:
|
||||
// Not a flush point (F2): an AlphaSubmitMark event is
|
||||
// always recorded alongside — immediately after — a
|
||||
// StreamMark from the SAME MarkIfGrown/MarkAlphaIfGrown
|
||||
// pairing (every Collect-side call site pairs them),
|
||||
// so the StreamMark case above already flushed any
|
||||
// pending batch by the time this one runs.
|
||||
int alphaEnd = e.IntArg;
|
||||
for (; alphaCursor < alphaEnd; alphaCursor++)
|
||||
{
|
||||
|
|
@ -957,52 +986,39 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
}
|
||||
break;
|
||||
case WalkFrameEventKind.Sky:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawSky();
|
||||
break;
|
||||
case WalkFrameEventKind.LandCell:
|
||||
{
|
||||
// S3 chunk 3 (§9.2 B2): order-preserving batching —
|
||||
// merge this run of CONSECUTIVE same-landblock
|
||||
// LandCell events into ONE DrawLandCellBatch call.
|
||||
// Any other event kind sits between two runs in
|
||||
// _events (Collect recorded it there in retail's
|
||||
// own order), so this lookahead naturally splits the
|
||||
// batch there without inspecting event kinds itself.
|
||||
uint landblockId = e.CellId;
|
||||
_landCellBatchScratch.Clear();
|
||||
_landCellBatchScratch.Add((e.IntArg >> 8, e.IntArg & 0xFF));
|
||||
int j = i + 1;
|
||||
while (j < _events.Count)
|
||||
{
|
||||
WalkFrameEvent next = _events[j];
|
||||
if (next.Kind != WalkFrameEventKind.LandCell
|
||||
|| next.CellId != landblockId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
_landCellBatchScratch.Add((next.IntArg >> 8, next.IntArg & 0xFF));
|
||||
j++;
|
||||
}
|
||||
_leafRenderer.DrawLandCellBatch(landblockId, _landCellBatchScratch);
|
||||
i = j - 1;
|
||||
// S3 chunk 3 fix round 1 (F2): append-only — no
|
||||
// lookahead, no same-landblock restriction, no
|
||||
// flush. The batch may span several landblocks; it
|
||||
// flushes only when a later event actually needs the
|
||||
// GPU (or at Replay's own end).
|
||||
_pendingTerrainBatch.Add((e.CellId, e.IntArg >> 8, e.IntArg & 0xFF));
|
||||
break;
|
||||
}
|
||||
case WalkFrameEventKind.CellShell:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawCellShell(e.CellId);
|
||||
break;
|
||||
case WalkFrameEventKind.PunchFan:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaBarrier:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.AlphaBarrier();
|
||||
break;
|
||||
case WalkFrameEventKind.LandscapeFlush:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.FlushLandscape();
|
||||
break;
|
||||
case WalkFrameEventKind.ClearInteriorDepth:
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.ClearInteriorDepth();
|
||||
break;
|
||||
case WalkFrameEventKind.ExitSeals:
|
||||
FlushPendingTerrainBatch();
|
||||
// S3 §8.2 B2: the driver — not Collect — owns adding
|
||||
// the leaf's returned submitted-fan count to the
|
||||
// persistent PortalsDrawnCount, since the real
|
||||
|
|
@ -1012,18 +1028,35 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
PortalsDrawnCount += _leafRenderer.DrawExitSeals();
|
||||
break;
|
||||
case WalkFrameEventKind.StaticParticles:
|
||||
// Retail CPhysicsObj::add_particle_shadow_to_cell
|
||||
// S3 chunk 3 fix round 1 (F2): a genuinely empty
|
||||
// particle turn submits nothing — and, since it
|
||||
// submits nothing, must NOT flush the pending
|
||||
// terrain batch either (the batch is order-preserving
|
||||
// only because a skipped turn changes nothing about
|
||||
// what reaches the GPU, at all). Retail
|
||||
// CPhysicsObj::add_particle_shadow_to_cell
|
||||
// (0x00514a70): an emitter owns one shadow in its OWN
|
||||
// current cell, drawn at that cell's object turn
|
||||
// regardless of any owner's registry membership —
|
||||
// this is a cell lookup, not an owner union.
|
||||
_leafRenderer.DrawStaticParticles(e.CellId);
|
||||
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
|
||||
{
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawStaticParticles(e.CellId);
|
||||
}
|
||||
break;
|
||||
case WalkFrameEventKind.CellParticles:
|
||||
_leafRenderer.DrawCellParticles(e.CellId);
|
||||
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
|
||||
{
|
||||
FlushPendingTerrainBatch();
|
||||
_leafRenderer.DrawCellParticles(e.CellId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// S3 chunk 3 fix round 1 (F2): the end of Replay flushes the
|
||||
// remainder — a batch never crosses into the next frame.
|
||||
FlushPendingTerrainBatch();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1037,6 +1070,21 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F2): submits the pending terrain
|
||||
/// batch as ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
|
||||
/// call, if it holds anything, and clears it. Called immediately before
|
||||
/// every OTHER event kind that will itself submit GPU work — see
|
||||
/// <see cref="Replay"/>'s own doc comment for the full flush-point
|
||||
/// list — so the batch's actual submission point is always the exact
|
||||
/// point the unbatched per-cell draws would have occupied.</summary>
|
||||
private void FlushPendingTerrainBatch()
|
||||
{
|
||||
if (_pendingTerrainBatch.Count == 0)
|
||||
return;
|
||||
_leafRenderer.DrawLandCellBatch(_pendingTerrainBatch);
|
||||
_pendingTerrainBatch.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Output-only Campaign FW cathedral trace. The event list described here
|
||||
/// is the same list <see cref="Replay"/> immediately executes, so this
|
||||
|
|
@ -1109,7 +1157,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
order.Append('>').Append(i).Append(":SKY");
|
||||
break;
|
||||
case WalkFrameEventKind.LandCell:
|
||||
order.Append('>').Append(i).Append(":LC").Append(e.CellId.ToString("x8"));
|
||||
// S3 chunk 3 fix round 1 (F6): the trace token now
|
||||
// carries the LOD side/index too — ":LC<lb>/<side>:<idx>".
|
||||
order.Append('>').Append(i).Append(":LC")
|
||||
.Append(e.CellId.ToString("x8"))
|
||||
.Append('/').Append(e.IntArg >> 8)
|
||||
.Append(':').Append(e.IntArg & 0xFF);
|
||||
break;
|
||||
case WalkFrameEventKind.CellShell when IsCathedralCell(e.CellId):
|
||||
order.Append('>').Append(i).Append(":S")
|
||||
|
|
@ -1474,11 +1527,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// interleave now emits one WalkFrameEventKind.LandCell turn per
|
||||
// admitted land cell (IWalkEventSink.OnLandCellTurn, fired from
|
||||
// RetailFrameWalk.DrawLandscape's own per-cell loop), not a single
|
||||
// TerrainSlice(0) here. Drawing terrain BEFORE every building would
|
||||
// let a nearer building's far-Z punch survive under farther terrain
|
||||
// that draws afterward and overwrite it — the doorway-behind-a-hill
|
||||
// fragment bug the interleave removes by ORDER alone. The walk's
|
||||
// active views still own only the punch fans and dynamics
|
||||
// TerrainSlice(0) here. S3 chunk 3 fix round 1 (F6) corrects the
|
||||
// direction of this note: drawing ALL terrain BEFORE every building
|
||||
// let a FARTHER building's far-Z punch survive, because the NEARER
|
||||
// terrain that retail draws AFTER that punch (and which overwrites
|
||||
// it) was instead drawn BEFORE it here — nothing later covered the
|
||||
// punched depth, so a doorway behind a hill punched straight through
|
||||
// it. The interleave fixes this by ORDER alone: nearer terrain now
|
||||
// draws AFTER the farther building's punch, exactly like retail. The
|
||||
// walk's active views still own only the punch fans and dynamics
|
||||
// apertures; terrain itself is UNCLIPPED for both root kinds.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ internal readonly record struct RenderGlScissorSnapshot(
|
|||
|
||||
internal readonly record struct TerrainRenderDiagnosticFacts(
|
||||
int VisibleSlots,
|
||||
int Draws,
|
||||
int LoadedSlots,
|
||||
int CapacitySlots);
|
||||
|
||||
|
|
@ -85,6 +86,15 @@ internal sealed class WorldRenderDiagnostics
|
|||
(long)(_terrainStopwatch.Elapsed.TotalMicroseconds * 100.0));
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 3 fix round 1 (F3): the walk path's per-frame
|
||||
/// analogue of <see cref="EndTerrainDraw"/> — pushes ONE precomputed
|
||||
/// elapsed-time sample (the sum of every land-cell batch's own
|
||||
/// Stopwatch.GetTimestamp() delta this frame, converted by the caller)
|
||||
/// instead of stopping this owner's own stopwatch, since the walk path
|
||||
/// times many small batches per frame rather than one bracketed call.</summary>
|
||||
public void PushTerrainSample(long elapsedHundredthsMicroseconds) =>
|
||||
_terrainSamples.PushHundredthsMicroseconds(elapsedHundredthsMicroseconds);
|
||||
|
||||
public void PublishTerrainDiagnostics(TerrainRenderDiagnosticFacts facts)
|
||||
{
|
||||
RollingTimingPercentiles timing = _terrainSamples.Snapshot();
|
||||
|
|
@ -93,7 +103,7 @@ internal sealed class WorldRenderDiagnostics
|
|||
string budget = medianMicroseconds > 1000.0 ? " BUDGET_OVER" : string.Empty;
|
||||
_log.WriteLine(
|
||||
$"[TERRAIN-DIAG]{budget} cpu_us={medianMicroseconds:F2}m/"
|
||||
+ $"{p95Microseconds:F2}p95 draws={facts.VisibleSlots}/frame "
|
||||
+ $"{p95Microseconds:F2}p95 draws={facts.Draws}/frame "
|
||||
+ $"visible={facts.VisibleSlots} loaded={facts.LoadedSlots} "
|
||||
+ $"capacity={facts.CapacitySlots}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -617,6 +617,24 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
/// warmup: the bucket's own sorted handle list is copied through the
|
||||
/// retained <see cref="_scopeHandleScratch"/> buffer.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// S3 chunk 3 fix round 1 (F2): whether <paramref name="cellId"/> has any
|
||||
/// renderable emitter for <paramref name="renderPass"/>, WITHOUT copying
|
||||
/// the bucket's handle list — <see cref="CopyRenderableEmittersInCell"/>'s
|
||||
/// allocation-free sibling for a Replay-time gate
|
||||
/// (<c>WalkFrameDriver.Replay</c>'s <c>StaticParticles</c>/<c>CellParticles</c>
|
||||
/// arms) that must not force a pending terrain-batch flush for a
|
||||
/// genuinely empty particle turn. A bucket's <c>FirstRenderableHandle</c>
|
||||
/// is 0 exactly when it holds no renderable handle (handles start at 1),
|
||||
/// so this is a single dictionary lookup plus a field read.
|
||||
/// </summary>
|
||||
public bool HasRenderableEmittersInCell(ParticleRenderPass renderPass, uint cellId)
|
||||
{
|
||||
int passIndex = RenderPassIndex(renderPass);
|
||||
return _cellHandlesByPass[passIndex].TryGetValue(cellId, out OwnerEmitterBucket? bucket)
|
||||
&& bucket.FirstRenderableHandle != 0;
|
||||
}
|
||||
|
||||
public void CopyRenderableEmittersInCell(
|
||||
ParticleRenderPass renderPass,
|
||||
uint cellId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue