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

@ -22,6 +22,20 @@ internal enum WalkDrawStage : byte
/// <c>LScape::grab_visible_cells</c> @0x00504EC0 — outdoor terrain.</summary>
Terrain,
/// <summary>
/// Campaign FW stage FW3.2a: the outdoor static objects (scenery,
/// buildings-as-decor, landblock statics —
/// <c>RenderProjectionClass.OutdoorStatic</c>) <c>LScape::draw</c> visits
/// for each landblock alongside its terrain mesh — retail's landscape
/// walk draws a landblock's ground and its static content in the same
/// pass, distinct from the ground mesh itself (<see cref="Terrain"/>) and
/// from a building's own exterior shell (<see cref="BuildingShell"/>).
/// The packed route's analogue is
/// <c>RenderFrameCandidateRoute.LandscapeOutdoorStatic</c>
/// (<c>WbDrawDispatcher.PackedOracle.cs</c>).
/// </summary>
OutdoorStatic,
/// <summary>An indoor <c>PView::DrawCells</c> @0x005A4840 flood's static
/// geometry: EnvCell shells plus the static meshes they contain.</summary>
CellStatic,

View file

@ -0,0 +1,137 @@
using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW stage FW3.2a: the walk→draw population layer. Turns one
/// cell's already-classified static content into <see cref="OrderedDrawCommand"/>
/// appends (opaque, walk order), per-instance deferred-alpha submissions
/// (translucent), and selection-scene publications (picking) — WITHOUT any
/// production frame wiring. <c>WorldSceneRenderer</c> does not call this
/// class yet; FW3.2b roots it into the real frame.
///
/// <para><b>Why per-entity append instead of the classic material grouping</b>:
/// under <see cref="WorldDepthContract"/>'s depth-compare Less, opaque draw
/// order is pixel-relevant ONLY for coplanar surfaces (a rug on a floor, a
/// shell over terrain) — and retail resolves those by first-drawn-wins in
/// ITS walk/cell-content order, not by any texture/material grouping. So
/// this populator appends one <see cref="OrderedDrawCommand"/> per (entity,
/// opaque batch) in the SAME order the caller's <c>records</c> span presents
/// them — never material-grouped, never re-sorted. Translucent batches go to
/// the SAME <see cref="RetailAlphaQueue"/> the classic/packed paths already
/// use (global far→near re-sort still applies; walk-order submission only
/// improves retail's submission-order tie-break fidelity for coincident
/// distances — <c>WbDrawDispatcher.DeferTransparentGroups</c>'s own doc
/// comment cites the same <c>CShadowPart::insertion_sort</c> stability this
/// preserves).</para>
///
/// <para>Reads no retained scene state itself: every method takes the target
/// <see cref="OrderedDrawStream"/> and a caller-supplied span of already
/// -queried <see cref="RenderProjectionRecord"/>s (from
/// <c>RenderSceneQuery.CopyCellStaticsTo</c> / <c>CopyIndexTo</c>). Owns only
/// two small per-call scratch lists — the classify seam's per-entity output
/// — reused across records to keep this hot path allocation-free after
/// warmup.</para>
/// </summary>
internal sealed class WalkStaticStreamPopulator
{
private readonly WbDrawDispatcher _dispatcher;
private readonly List<WbDrawDispatcher.WalkClassifiedBatch> _batchScratch = new();
private readonly List<WbDrawDispatcher.WalkClassifiedSelectionPart> _selectionScratch = new();
internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Populates <paramref name="stream"/> from one indoor cell's static
/// content (<c>RenderProjectionClass.IndoorCellStatic</c> —
/// <c>RenderSceneQuery.CopyCellStaticsTo</c>). <paramref name="stage"/>
/// is caller-selected per the walk turn this cell's content belongs to
/// — <see cref="WalkDrawStage.CellStatic"/> for an ordinary
/// <c>PView::DrawCells</c> flood, <see cref="WalkDrawStage.LookInStatic"/>
/// for a <c>ConstructView(CBldPortal)</c> look-in, or
/// <see cref="WalkDrawStage.BuildingShell"/> for a building's own
/// exterior shell content.
/// </summary>
internal void PopulateCell(
OrderedDrawStream stream,
WalkDrawStage stage,
uint cellId,
ReadOnlySpan<RenderProjectionRecord> records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
{
ClassifyAndAppend(
stream, stage, cellId, in records[i], tupleLandblockId,
cameraWorldPosition, viewProjection);
}
}
/// <summary>
/// The landscape entry point: outdoor static content
/// (<c>RenderProjectionClass.OutdoorStatic</c> —
/// <c>RenderSceneQuery.CopyIndexTo(RenderSceneIndex.OutdoorStatic, ...)</c>)
/// at <see cref="WalkDrawStage.OutdoorStatic"/>, the terrain-adjacent
/// stage <c>LScape::draw</c> visits a landblock's static objects at
/// alongside its ground mesh (see that stage value's own doc comment).
/// <paramref name="cellId"/> is the OUTDOOR landblock id — outdoor
/// statics have no EnvCell of their own, so this is walk-order
/// provenance only, not a clip-slot key.
/// </summary>
internal void PopulateOutdoorStatics(
OrderedDrawStream stream,
uint cellId,
ReadOnlySpan<RenderProjectionRecord> records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection) =>
PopulateCell(
stream, WalkDrawStage.OutdoorStatic, cellId, records,
tupleLandblockId, cameraWorldPosition, viewProjection);
private void ClassifyAndAppend(
OrderedDrawStream stream,
WalkDrawStage stage,
uint cellId,
in RenderProjectionRecord record,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection)
{
_batchScratch.Clear();
_selectionScratch.Clear();
_dispatcher.ClassifyEntityForWalk(
in record, tupleLandblockId, _batchScratch, _selectionScratch);
for (int i = 0; i < _batchScratch.Count; i++)
{
WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[i];
if (batch.IsOpaque)
{
stream.Append(new OrderedDrawCommand(
batch.Key, batch.Transform, stage, cellId, batch.ClipSlot,
batch.Lights, batch.IndoorFlag, batch.Alpha,
batch.SelectionLighting, batch.DetailCategory));
}
else
{
_dispatcher.SubmitWalkAlphaInstance(
in batch, cameraWorldPosition, viewProjection);
}
}
for (int i = 0; i < _selectionScratch.Count; i++)
{
WbDrawDispatcher.WalkClassifiedSelectionPart part = _selectionScratch[i];
_dispatcher.PublishWalkSelectionPart(in part);
}
}
}