feat(render) Campaign FW3.2a: the walk-to-draw population layer

The piece that turns walk-visited static content into draws, with no
production frame wiring (FW3.2b roots the frame):

- TryClassifyBatch: ONE shared per-batch classify core (the #426
  untextured gate, #188 opacity promotion, texture resolve, foliage
  classification, in the exact original order) extracted from
  ClassifyBatches; the classic and packed classifiers now call it -
  behavior-identical, proven by the full hermetic + InstalledDat +
  Core Wb suites.
- ClassifyEntityForWalk / WalkClassifiedBatch: the per-entity seam
  yielding per-batch keys + instance data WITHOUT InstanceGroup
  bucketing, plus the per-part selection data (picking stays alive on
  the walk path - the survey's unlisted-consumer fix).
- WalkStaticStreamPopulator: per-entity walk-ordered opaque appends
  (under depth Less, opaque order is pixel-relevant only for coplanar
  surfaces, which retail resolves first-drawn-wins in ITS order -
  never material-grouped), translucent instances to the SAME
  RetailAlphaQueue via SubmitWalkAlphaInstance (identical viewer
  distances; walk-order submission improves retail's tie fidelity),
  selection parts published per entity.
- SubmitOrderedStream now owns _orderedDrawCullModes, retiring the
  FW2-recorded alpha-scope interleaving constraint;
  DrawIndirectRangeRhi takes an optional cull array (all existing
  call sites unchanged). The referee test was verified to FAIL
  against the old shared-scratch behavior.
- WalkDrawStage.OutdoorStatic added for the landscape turn.

Suites: full Release build 0 warnings; Walk lane 195/1 skip;
hermetic 6,747/0 (the two failures the implementation round reported
were transient - both pass in isolation and in the full run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 13:52:23 +02:00
parent b10ad662b0
commit 81c6531727
9 changed files with 1264 additions and 116 deletions

View file

@ -253,14 +253,14 @@ public sealed unsafe partial class WbDrawDispatcher
// Per-instance-first emission — the PrepareDeferredAlphaDraws shape,
// into the SAME per-frame scratch arrays PrepareDeferredAlphaDraws/
// SubmitRhi write. Most are consumed immediately by the ring uploads
// below, but _drawCullModes is NOT write-then-consume: the deferred-
// alpha path reads it at FLUSH time (DrawIndirectRangeRhi's internal
// cull split), so an ordered submission may never interleave between
// RetailAlphaQueue prepare and flush. FW2 has no production caller;
// the FW3 wiring must either sequence around the alpha scope or give
// this path its own cull scratch.
// 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.
EnsureDeferredAlphaCapacity(count);
EnsureOrderedCullModeCapacity(count);
for (int i = 0; i < count; i++)
{
GroupKey key = stream.Keys[i];
@ -286,7 +286,7 @@ public sealed unsafe partial class WbDrawDispatcher
BaseVertex = key.BaseVertex,
BaseInstance = (uint)i,
};
_drawCullModes[i] = key.CullMode;
_orderedDrawCullModes[i] = key.CullMode;
}
// Write every section ONCE — the PrepareRhiAlphaSections shape, but
@ -350,10 +350,10 @@ public sealed unsafe partial class WbDrawDispatcher
// One in-order pass over the pre-built merge runs: bind the run's
// pipeline, set RenderPass, draw. DrawIndirectRangeRhi still splits
// internally on _drawCullModes (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.
// 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)
{
ValidateMergeRun(stream, run);
@ -365,7 +365,20 @@ public sealed unsafe partial class WbDrawDispatcher
BindPipelineWithMesh(encoder, pipeline, global);
DrawIndirectRangeRhi(
encoder, ref pushConstants, commandBuffer, commandBase,
run.FirstCommand, run.CommandCount);
run.FirstCommand, run.CommandCount, _orderedDrawCullModes);
}
}
/// <summary>
/// Grows <see cref="_orderedDrawCullModes"/> to at least
/// <paramref name="count"/> — the same growth shape
/// <c>EnsureDeferredAlphaCapacity</c> uses for <see cref="_drawCullModes"/>,
/// kept as its own method because this scratch array is not part of that
/// method's shared per-instance group (see this file's type doc comment).
/// </summary>
private void EnsureOrderedCullModeCapacity(int count)
{
if (_orderedDrawCullModes.Length < count)
_orderedDrawCullModes = new CullMode[count + 64];
}
}