perf(render) Campaign FW3.4a: one walk pass; prepare-once/draw-ranges; arena records
The FW3.4 dense-Arwic pair triggered the +/-20% stop rule (+33.5% CPU p50, 14x frame allocation). This slice removes the three measured costs without changing GPU command order (the referee suites assert identical recorded call sequences): - WalkFrameDriver: Collect (ONE walk per frame - no GPU work; leaf calls and flush points become a recorded event list; the driver absorbed the renderer collection pass and exposes the visited sets) + Replay (prepare the whole stream once, then replay events, interleaving DrawOrderedRange with leaf calls in the exact recorded order). RunFrame = Collect+Replay for existing callers. - WbDrawDispatcher: SubmitOrderedStream split into PrepareOrderedStream (all sections + commands + merge runs uploaded once per frame) and DrawOrderedRange (bind-once latch; per-run pipeline + DrawIdOffset + DrawIndirectRangeRhi). Load-bearing correctness catch from the implementation round: merge runs take FORCED BREAKS at the recorded event marks - whole-stream merging must not fuse two segments that retail separates with a leaf GPU call (shell, punch); the straddle assert stays as a dead-code safety net. - WalkProductionWorldData: WalkFrameStaticRecords carries an ArraySegment into a per-frame grow-only arena; the per-cell fresh-array copies (the 1.9 MB/frame alloc p50) are gone - zero steady-state allocation after warmup. Suites (lead-verified): full Release build 0 warnings; hermetic 6,758/0; Walk lane 209/1; InstalledDat Walk conformance 40/1 untouched. Next: the dense-Arwic re-measure against the same-session baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6301e4dbea
commit
212f5a12e5
7 changed files with 1051 additions and 359 deletions
|
|
@ -20,18 +20,36 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// (command <c>i</c> owns exactly one instance, <c>BaseInstance = i</c>, so
|
||||
/// walk order — never material bucketing — survives into the indirect array)
|
||||
/// and <see cref="PrepareRhiAlphaSections"/>'s "write every section once"
|
||||
/// shape (locals here, not the persisted <c>_alpha*</c> fields: those belong
|
||||
/// to <see cref="RetailAlphaQueue"/>, which can still be mid-flight in the
|
||||
/// same frame, and overwriting them would corrupt that replay).</para>
|
||||
/// shape (<c>_ordered*</c> fields here, not the persisted <c>_alpha*</c>
|
||||
/// fields: those belong to <see cref="RetailAlphaQueue"/>, which can still be
|
||||
/// mid-flight in the same frame, and overwriting them would corrupt that
|
||||
/// replay).</para>
|
||||
///
|
||||
/// <para>Campaign FW stage FW3.4a (2026-08-30): the FW2 submitter was ONE
|
||||
/// method, <c>SubmitOrderedStream</c>, that wrote every per-instance section
|
||||
/// AND drew every merge run in one call — fine for FW2/FW3.2's proof, but the
|
||||
/// FW3.4 perf checkpoint measured ~40 of these per frame at a town (one per
|
||||
/// walk segment — every cell shell, every building's alpha barrier, every
|
||||
/// punch fan flushes the accumulated stream so far), each one re-writing all
|
||||
/// nine per-instance ring sections and rebinding everything, for that
|
||||
/// segment's handful of instances. <see cref="PrepareOrderedStream"/> +
|
||||
/// <see cref="DrawOrderedRange"/> replace it: the WHOLE frame's stream is
|
||||
/// written and bound ONCE, and each walk segment becomes a cheap
|
||||
/// <see cref="DrawOrderedRange"/> call over the ALREADY-uploaded payload —
|
||||
/// the same split <see cref="PrepareRhiAlphaSections"/>/
|
||||
/// <see cref="DrawPreparedAlphaBatchRhi"/> already prove for the alpha path.
|
||||
/// <c>SubmitOrderedStream</c> itself is deleted; every FW2 caller/test now
|
||||
/// calls the pair (prepare once, draw the whole stream as one range, or as
|
||||
/// several — see <c>OrderPreservingSubmitterTests</c>).</para>
|
||||
///
|
||||
/// <para>Scope: this stage proves walk-order submission through the existing
|
||||
/// RHI on static content (plan §FW2). It does NOT wire the retail
|
||||
/// building-detail overlay replay (<c>DrawBuildingDetailRangeRhi</c>'s second
|
||||
/// pass through <c>RetailDetail</c>/<c>RetailDetailTransparent</c>) — a
|
||||
/// detail-category command still forces a solo merge run (mirroring the
|
||||
/// deferred-alpha detail break), but <see cref="SubmitOrderedStream"/> issues
|
||||
/// only the base-pipeline draw for it. The overlay replay is production
|
||||
/// wiring, deferred to whichever stage cuts the walk over for real content.</para>
|
||||
/// deferred-alpha detail break), but this submitter issues only the
|
||||
/// base-pipeline draw for it. The overlay replay is production wiring,
|
||||
/// deferred to whichever stage cuts the walk over for real content.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe partial class WbDrawDispatcher
|
||||
{
|
||||
|
|
@ -99,12 +117,34 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// reorders or drops anything: every command in <paramref name="stream"/>
|
||||
/// belongs to exactly one returned run, in stream order.</para>
|
||||
///
|
||||
/// <para><paramref name="forcedBreaksAscending"/> (FW3.4a addition,
|
||||
/// default none): extra command indices, sorted ascending, at which a run
|
||||
/// must end even when the state comparison above would otherwise extend
|
||||
/// it. <see cref="WalkFrameDriver"/>'s Replay phase draws the frame's ONE
|
||||
/// prepared stream as several <see cref="DrawOrderedRange"/> calls — one
|
||||
/// per walk segment, each separated by a leaf GPU call (a cell shell, a
|
||||
/// punch fan, an alpha barrier) that MUST execute between them — and nothing
|
||||
/// about <see cref="WalkDrawStage"/>/bucket/cull/detail forbids two
|
||||
/// DIFFERENT segments from sharing all four (two consecutive indoor cells
|
||||
/// drawn Opaque/CounterClockwise, the overwhelmingly common case). Without
|
||||
/// this parameter, a whole-stream merge pass would happily fuse such
|
||||
/// segments into one run spanning the leaf call that must run BETWEEN
|
||||
/// them, silently reordering GPU commands relative to retail's walk — the
|
||||
/// one invariant this campaign may never trade away. Passing each
|
||||
/// segment's start index here makes <see cref="DrawOrderedRange"/>'s
|
||||
/// "a range boundary always coincides with a run boundary" assumption
|
||||
/// true BY CONSTRUCTION instead of by hope; that method's own assert
|
||||
/// stays as insurance against a future bug in how boundaries are
|
||||
/// supplied. Every FW2 call site keeps passing none, so existing
|
||||
/// single-segment behavior (and its tests) is unchanged.</para>
|
||||
///
|
||||
/// <para>Fails loud before building any run: <see cref="WalkDrawStage.PortalPunch"/>
|
||||
/// has no FW2 submission path (see that value's own documentation), so a
|
||||
/// has no submission path (see that value's own documentation), so a
|
||||
/// stream carrying one throws immediately rather than silently degrading
|
||||
/// to some other stage's handling.</para>
|
||||
/// </summary>
|
||||
internal static List<OrderedMergeRun> BuildOrderedMergeRuns(OrderedDrawStream stream)
|
||||
internal static List<OrderedMergeRun> BuildOrderedMergeRuns(
|
||||
OrderedDrawStream stream, IReadOnlyList<int>? forcedBreaksAscending = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
int count = stream.Count;
|
||||
|
|
@ -123,10 +163,19 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<int> breaks = forcedBreaksAscending ?? Array.Empty<int>();
|
||||
int breakCursor = 0;
|
||||
|
||||
var runs = new List<OrderedMergeRun>();
|
||||
int cursor = 0;
|
||||
while (cursor < count)
|
||||
{
|
||||
// A break AT OR BEFORE cursor already ended the previous run (or
|
||||
// predates the stream entirely) — only a break STRICTLY AFTER
|
||||
// cursor can stop the one starting here.
|
||||
while (breakCursor < breaks.Count && breaks[breakCursor] <= cursor)
|
||||
breakCursor++;
|
||||
|
||||
WalkDrawStage stage = stream.Stages[cursor];
|
||||
PipelineBucket bucket = BucketFor(stream.Keys[cursor].Translucency);
|
||||
CullMode cull = stream.Keys[cursor].CullMode;
|
||||
|
|
@ -136,6 +185,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
if (!detail)
|
||||
{
|
||||
while (end < count
|
||||
&& !(breakCursor < breaks.Count && breaks[breakCursor] == end)
|
||||
&& stream.Stages[end] == stage
|
||||
&& stream.DetailCategories[end] == 0
|
||||
&& BucketFor(stream.Keys[end].Translucency) == bucket
|
||||
|
|
@ -212,11 +262,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
|
||||
/// <summary>
|
||||
/// Campaign FW3.2b-2: the production frame/encoder pair for
|
||||
/// <see cref="WalkFrameDriver"/>'s own <see cref="SubmitOrderedStream"/>
|
||||
/// <see cref="WalkFrameDriver"/>'s own <see cref="DrawOrderedRange"/>
|
||||
/// calls (contrast this stage's diagnostic-target callers, which supply
|
||||
/// their own frame/encoder — see <see cref="SubmitOrderedStream"/>'s own
|
||||
/// doc comment). Reads the SAME world-pass scope <see cref="SubmitRhi"/>
|
||||
/// already requires (<see cref="RequireRhiFrame"/> /
|
||||
/// their own frame/encoder). Reads the SAME world-pass scope
|
||||
/// <see cref="SubmitRhi"/> already requires (<see cref="RequireRhiFrame"/> /
|
||||
/// <c>_scope.RequireEncoder()</c>) — fails loud rather than handing the
|
||||
/// driver a null pair when the world phase is not bracketing.
|
||||
/// </summary>
|
||||
|
|
@ -236,55 +285,101 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
internal (int Width, int Height)? WalkAttachmentExtent =>
|
||||
_scope is null ? null : (_scope.AttachmentWidth, _scope.AttachmentHeight);
|
||||
|
||||
// ── Campaign FW3.4a: the prepared-once, drawn-in-ranges pair ───────────
|
||||
//
|
||||
// Persisted state a PrepareOrderedStream call fills and every later
|
||||
// DrawOrderedRange call in the SAME frame reads. Deliberately its own set
|
||||
// — never _alpha* — for the same reason SubmitOrderedStream's per-frame
|
||||
// locals were never _alpha* (see this file's type doc comment):
|
||||
// RetailAlphaQueue can still be mid-flight when a walk segment flushes,
|
||||
// and sharing storage would corrupt whichever path writes second.
|
||||
|
||||
private OrderedDrawStream? _orderedStream;
|
||||
private List<OrderedMergeRun> _orderedRuns = new();
|
||||
private int _orderedPreparedCount;
|
||||
private bool _orderedSectionsBound;
|
||||
// Caller-supplied, exactly like SubmitOrderedStream's own frame/encoder
|
||||
// parameters were (see this file's type doc comment: the walk submitter
|
||||
// draws into whatever pass its caller has open, never pulled from
|
||||
// _frames/_scope) — DrawOrderedRange's bind-once step needs the SAME
|
||||
// frame Prepare wrote sections into, for WorldFrameSectionBinding's
|
||||
// clip-region/scene-lighting binds; RequireRhiFrame() is the wrong tool
|
||||
// here since it demands the dispatcher's OWN BeginFrame/_dynamicFrameStarted
|
||||
// bookkeeping, which the walk path never participates in.
|
||||
private IGpuFrame? _orderedFrame;
|
||||
private Matrix4x4 _orderedViewProjection;
|
||||
private uint _orderedTransformBaseInstance;
|
||||
private RhiSection _orderedInstances;
|
||||
private RhiSection _orderedBatches;
|
||||
private RhiSection _orderedClipSlots;
|
||||
private RhiSection _orderedGlobalLights;
|
||||
private RhiSection _orderedLightSets;
|
||||
private RhiSection _orderedIndoor;
|
||||
private RhiSection _orderedAlpha;
|
||||
private RhiSection _orderedSelectionLighting;
|
||||
private RhiSection _orderedDetailCategory;
|
||||
private RhiSection _orderedCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Submits <paramref name="stream"/> in walk order through the existing
|
||||
/// RHI: per-instance-first emission (see the type doc comment), one
|
||||
/// section write per per-instance array, then one
|
||||
/// <see cref="DrawIndirectRangeRhi"/> call per maximal merge run from
|
||||
/// <see cref="BuildOrderedMergeRuns"/>. N commands in yield indirect
|
||||
/// commands <c>[0, N)</c> in stream order, each covered by exactly one
|
||||
/// emitted run — nothing is reordered, sorted, or dropped.
|
||||
/// Writes <paramref name="stream"/>'s ENTIRE walk-order payload into the
|
||||
/// frame ring exactly ONCE — the per-instance-first emission
|
||||
/// (<see cref="PrepareDeferredAlphaDraws"/>'s shape) followed by one
|
||||
/// section write per per-instance array (<see cref="PrepareRhiAlphaSections"/>'s
|
||||
/// shape, into the <c>_ordered*</c> fields above). No draw happens here;
|
||||
/// <see cref="DrawOrderedRange"/> issues the actual
|
||||
/// <see cref="DrawIndirectRangeRhi"/> calls against this prepared payload,
|
||||
/// as many times as the caller needs (<see cref="WalkFrameDriver"/> calls
|
||||
/// it once per walk segment, interleaved with the leaf GPU calls that
|
||||
/// must run between segments).
|
||||
///
|
||||
/// <para><paramref name="frame"/> and <paramref name="encoder"/> are
|
||||
/// caller-supplied rather than pulled from <c>_frames</c>/<c>_scope</c>
|
||||
/// (contrast <see cref="SubmitRhi"/>'s <c>RequireRhiFrame</c>/
|
||||
/// <c>scope.RequireEncoder()</c>): the walk submitter draws into whatever
|
||||
/// pass its caller has open, including an offscreen diagnostic target
|
||||
/// that never touches the dispatcher's own world-pass scope. The frame's
|
||||
/// shared clip-region/scene-lighting sections still come from
|
||||
/// <c>_scope.Sections</c> — those are canonical per-frame published
|
||||
/// state, not something this submitter owns.</para>
|
||||
/// <para><paramref name="forcedBreaksAscending"/> forwards to
|
||||
/// <see cref="BuildOrderedMergeRuns"/> — see that parameter's own doc
|
||||
/// comment. Pass the walk segment boundaries here so a later
|
||||
/// <see cref="DrawOrderedRange"/> call's range always aligns with a merge
|
||||
/// run by construction.</para>
|
||||
///
|
||||
/// <para>A no-op (leaves <see cref="_orderedPreparedCount"/> at 0) when
|
||||
/// the stream is empty or the mesh source is not yet ready — mirrors
|
||||
/// <see cref="PrepareDeferredAlphaDraws"/>'s own early-outs. Fails loud
|
||||
/// BEFORE any GPU work if the stream carries an unsupported stage (see
|
||||
/// <see cref="BuildOrderedMergeRuns"/>).</para>
|
||||
/// </summary>
|
||||
internal void SubmitOrderedStream(
|
||||
internal void PrepareOrderedStream(
|
||||
IGpuFrame frame,
|
||||
IGpuPassEncoder encoder,
|
||||
OrderedDrawStream stream,
|
||||
in Matrix4x4 viewProjection)
|
||||
in Matrix4x4 viewProjection,
|
||||
IReadOnlyList<int>? forcedBreaksAscending = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
_orderedStream = stream;
|
||||
_orderedFrame = frame;
|
||||
_orderedSectionsBound = false;
|
||||
_orderedPreparedCount = 0;
|
||||
|
||||
// Fail loud before any GPU work: a PortalPunch command has no
|
||||
// submission path.
|
||||
_orderedRuns = BuildOrderedMergeRuns(stream, forcedBreaksAscending);
|
||||
|
||||
int count = stream.Count;
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
// Fail loud before any GPU work: a PortalPunch command has no FW2
|
||||
// submission path.
|
||||
List<OrderedMergeRun> runs = BuildOrderedMergeRuns(stream);
|
||||
|
||||
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
|
||||
if (global is null || !MeshSourceReady())
|
||||
return;
|
||||
|
||||
// Per-instance-first emission — the PrepareDeferredAlphaDraws shape,
|
||||
// into the SAME per-frame scratch arrays PrepareDeferredAlphaDraws/
|
||||
// SubmitRhi write, EXCEPT cull modes: this stage (FW3.2a) gives the
|
||||
// ordered path its own _orderedDrawCullModes scratch (see
|
||||
// DrawIndirectRangeRhi's doc comment) precisely so this loop and its
|
||||
// draws below can freely interleave with a mid-flight
|
||||
// RetailAlphaQueue scope without corrupting — or being corrupted by
|
||||
// — the alpha path's _drawCullModes.
|
||||
// Per-instance-first emission — see PrepareDeferredAlphaDraws, into
|
||||
// the SAME shared per-instance scratch arrays that method writes
|
||||
// (safe: this is a single-threaded render frame, and the writer here
|
||||
// runs to completion — including the section uploads below — before
|
||||
// any other per-instance producer touches the scratch again). Cull
|
||||
// modes are the one exception: this stage keeps its own
|
||||
// _orderedDrawCullModes scratch (see DrawIndirectRangeRhi's doc
|
||||
// comment) precisely so a walk-ordered draw can freely interleave
|
||||
// with a mid-flight RetailAlphaQueue scope without corrupting — or
|
||||
// being corrupted by — the alpha path's _drawCullModes.
|
||||
EnsureDeferredAlphaCapacity(count);
|
||||
EnsureOrderedCullModeCapacity(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
|
|
@ -315,76 +410,159 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_orderedDrawCullModes[i] = key.CullMode;
|
||||
}
|
||||
|
||||
// Write every section ONCE — the PrepareRhiAlphaSections shape, but
|
||||
// into locals rather than the persisted _alpha* fields (see the type
|
||||
// doc comment for why those must stay untouched here).
|
||||
RhiSection instances = WriteWorldTransformSection(
|
||||
// Write every section ONCE — the PrepareRhiAlphaSections shape, into
|
||||
// _ordered* rather than _alpha* (see this file's type doc comment).
|
||||
_orderedViewProjection = viewProjection;
|
||||
_orderedInstances = WriteWorldTransformSection(
|
||||
frame, _instanceData.AsSpan(0, count * 16), out uint transformBaseInstance);
|
||||
RhiSection batches = WriteRingSection<BatchData>(frame, _batchData.AsSpan(0, count));
|
||||
RhiSection clipSlots = WriteRingSection<uint>(frame, _clipSlotData.AsSpan(0, count));
|
||||
_orderedTransformBaseInstance = transformBaseInstance;
|
||||
_orderedBatches = WriteRingSection<BatchData>(frame, _batchData.AsSpan(0, count));
|
||||
_orderedClipSlots = WriteRingSection<uint>(frame, _clipSlotData.AsSpan(0, count));
|
||||
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
int uploadCount = lightCount > 0 ? lightCount : 1;
|
||||
RhiSection globalLights = WriteRingSection<float>(
|
||||
_orderedGlobalLights = WriteRingSection<float>(
|
||||
frame,
|
||||
_globalLightData.AsSpan(0, uploadCount * GlobalLightPacker.FloatsPerLight));
|
||||
RhiSection lightSets = WriteRingSection<int>(
|
||||
_orderedLightSets = WriteRingSection<int>(
|
||||
frame, _lightSetData.AsSpan(0, count * LightManager.MaxLightsPerObject));
|
||||
RhiSection indoor = WriteRingSection<uint>(frame, _indoorData.AsSpan(0, count));
|
||||
RhiSection alpha = WriteRingSection<float>(frame, _alphaData.AsSpan(0, count));
|
||||
RhiSection selectionLighting = WriteRingSection<Vector2>(
|
||||
_orderedIndoor = WriteRingSection<uint>(frame, _indoorData.AsSpan(0, count));
|
||||
_orderedAlpha = WriteRingSection<float>(frame, _alphaData.AsSpan(0, count));
|
||||
_orderedSelectionLighting = WriteRingSection<Vector2>(
|
||||
frame, _selectionLightingData.AsSpan(0, count));
|
||||
RhiSection detailCategory = WriteRingSection<uint>(frame, _detailCategoryData.AsSpan(0, count));
|
||||
_orderedDetailCategory = WriteRingSection<uint>(frame, _detailCategoryData.AsSpan(0, count));
|
||||
GpuRingAllocation commandsAllocation = WriteIndirectCommands(
|
||||
frame, _indirectCommands.AsSpan(0, count), transformBaseInstance);
|
||||
IGpuBuffer commandBuffer = commandsAllocation.Buffer;
|
||||
uint commandBase = commandsAllocation.OffsetBytes;
|
||||
_orderedCommands = new RhiSection(
|
||||
commandsAllocation.Buffer,
|
||||
commandsAllocation.OffsetBytes,
|
||||
checked((uint)(count * DrawCommandStride)));
|
||||
|
||||
_orderedPreparedCount = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws commands <c>[firstCommand, firstCommand + commandCount)</c> of
|
||||
/// the payload the most recent <see cref="PrepareOrderedStream"/> call
|
||||
/// uploaded. The FIRST call in a frame also binds the pipeline, push
|
||||
/// constants, and all nine per-instance sections — <see cref="_orderedSectionsBound"/>
|
||||
/// gates that so every later call in the same frame is just the merge-run
|
||||
/// walk-and-draw loop, never a rebind (the whole point of the FW3.4a
|
||||
/// split: what used to be ~40 full <c>SubmitOrderedStream</c> rebinds per
|
||||
/// frame at a town becomes one bind plus ~40 cheap
|
||||
/// <see cref="DrawIndirectRangeRhi"/> calls). Section binds survive
|
||||
/// pipeline switches (every mesh pipeline shares one layout — the same
|
||||
/// reasoning <c>SubmitRhi</c>/the old <c>SubmitOrderedStream</c> already
|
||||
/// relied on), so binding once per frame rather than once per pipeline
|
||||
/// switch is safe.
|
||||
///
|
||||
/// <para>Fail-loud range check mirrors <see cref="DrawPreparedAlphaBatchRhi"/>'s:
|
||||
/// a range outside <c>[0, _orderedPreparedCount]</c> throws
|
||||
/// <see cref="ArgumentOutOfRangeException"/> rather than silently
|
||||
/// clamping or drawing garbage — including when nothing was ever
|
||||
/// prepared this frame (a caller drawing without preparing is a real
|
||||
/// bug, not a valid empty draw). A zero-length range is a legal no-op
|
||||
/// (mirrors an empty walk segment).</para>
|
||||
///
|
||||
/// <para>Walks the runs <see cref="PrepareOrderedStream"/> built that
|
||||
/// intersect this range. By construction (the boundaries the caller fed
|
||||
/// <see cref="PrepareOrderedStream"/> as <c>forcedBreaksAscending</c>) a
|
||||
/// run never starts before the range and never ends after it — this is
|
||||
/// asserted, not assumed: a run that straddles the range edge throws
|
||||
/// rather than being silently sliced, per plan §FW3.4a.</para>
|
||||
/// </summary>
|
||||
internal void DrawOrderedRange(IGpuPassEncoder encoder, int firstCommand, int commandCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
if (firstCommand < 0
|
||||
|| commandCount < 0
|
||||
|| firstCommand > _orderedPreparedCount - commandCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(firstCommand),
|
||||
"The ordered draw range exceeds the payload the most recent "
|
||||
+ "PrepareOrderedStream call uploaded.");
|
||||
}
|
||||
if (commandCount == 0)
|
||||
return;
|
||||
if (_orderedCommands.Buffer is null || _orderedStream is null)
|
||||
return;
|
||||
|
||||
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
|
||||
if (global is null)
|
||||
return;
|
||||
|
||||
MeshPipelineSet pipelines = PipelinesFor(encoder);
|
||||
|
||||
var pushConstants = new GpuPushConstants
|
||||
{
|
||||
ViewProjection = viewProjection,
|
||||
ViewProjection = _orderedViewProjection,
|
||||
DrawIdOffset = 0,
|
||||
LightingMode = 0,
|
||||
RenderPass = 0,
|
||||
LightDebug = RenderingDiagnostics.LightDebugMode,
|
||||
TextureIndexA = 0,
|
||||
TextureIndexB = transformBaseInstance,
|
||||
TextureIndexB = _orderedTransformBaseInstance,
|
||||
ParamA = 0f,
|
||||
ParamB = 0f,
|
||||
};
|
||||
|
||||
// Bind the opaque variant first so the storage/uniform binds below
|
||||
// land on a live program (SubmitRhi's own rationale) — every mesh
|
||||
// pipeline shares one layout, so these bindings survive the per-run
|
||||
// pipeline switches in the loop below.
|
||||
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstances, instances);
|
||||
BindSection(encoder, GpuBindingModel.StorageBatches, batches);
|
||||
BindSection(encoder, GpuBindingModel.StorageClipSlots, clipSlots);
|
||||
BindSection(encoder, GpuBindingModel.StorageGlobalLights, globalLights);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceLightSets, lightSets);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceIndoor, indoor);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceAlpha, alpha);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceSelectionLighting, selectionLighting);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceDetailCategory, detailCategory);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
||||
encoder, _scope!.Sections, frame);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||
encoder, _scope!.Sections, frame);
|
||||
|
||||
// One in-order pass over the pre-built merge runs: bind the run's
|
||||
// pipeline, set RenderPass, draw. DrawIndirectRangeRhi still splits
|
||||
// internally on _orderedDrawCullModes (issue #52's absolute
|
||||
// DrawIdOffset per sub-call) — every run here already shares one
|
||||
// cull mode by construction, so that inner split is a no-op here,
|
||||
// never a second boundary this loop failed to expect.
|
||||
foreach (OrderedMergeRun run in runs)
|
||||
if (!_orderedSectionsBound)
|
||||
{
|
||||
ValidateMergeRun(stream, run);
|
||||
IGpuFrame frame = _orderedFrame
|
||||
?? throw new InvalidOperationException(
|
||||
"DrawOrderedRange's first call this frame has no frame to bind clip-region/"
|
||||
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
|
||||
|
||||
PipelineBucket bucket = BucketFor(stream.Keys[run.FirstCommand].Translucency);
|
||||
// Bind the opaque variant first so the storage/uniform binds
|
||||
// below land on a live program (SubmitRhi's own rationale) —
|
||||
// every mesh pipeline shares one layout, so these bindings
|
||||
// survive every per-run pipeline switch below, across every
|
||||
// DrawOrderedRange call this frame.
|
||||
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
|
||||
BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches);
|
||||
BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots);
|
||||
BindSection(encoder, GpuBindingModel.StorageGlobalLights, _orderedGlobalLights);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceLightSets, _orderedLightSets);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceIndoor, _orderedIndoor);
|
||||
BindSection(encoder, GpuBindingModel.StorageInstanceAlpha, _orderedAlpha);
|
||||
BindSection(
|
||||
encoder, GpuBindingModel.StorageInstanceSelectionLighting, _orderedSelectionLighting);
|
||||
BindSection(
|
||||
encoder, GpuBindingModel.StorageInstanceDetailCategory, _orderedDetailCategory);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
|
||||
encoder, _scope!.Sections, frame);
|
||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||
encoder, _scope!.Sections, frame);
|
||||
|
||||
_orderedSectionsBound = true;
|
||||
}
|
||||
|
||||
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
||||
uint commandBase = _orderedCommands.OffsetBytes;
|
||||
int rangeEnd = firstCommand + commandCount;
|
||||
|
||||
foreach (OrderedMergeRun run in _orderedRuns)
|
||||
{
|
||||
int runEnd = run.FirstCommand + run.CommandCount;
|
||||
if (runEnd <= firstCommand)
|
||||
continue;
|
||||
if (run.FirstCommand >= rangeEnd)
|
||||
break;
|
||||
|
||||
if (run.FirstCommand < firstCommand || runEnd > rangeEnd)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"DrawOrderedRange [{firstCommand}, {rangeEnd}) straddles merge run "
|
||||
+ $"[{run.FirstCommand}, {runEnd}) — a range boundary must coincide with "
|
||||
+ "a run boundary by construction (PrepareOrderedStream's "
|
||||
+ "forcedBreaksAscending should have forced a break here; Campaign FW "
|
||||
+ "§FW3.4a).");
|
||||
}
|
||||
|
||||
ValidateMergeRun(_orderedStream, run);
|
||||
|
||||
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
||||
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket);
|
||||
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue