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

@ -635,56 +635,22 @@ public sealed unsafe partial class WbDrawDispatcher
batchIndex < renderData.Batches.Count;
batchIndex++)
{
ObjectRenderBatch batch =
renderData.Batches[batchIndex];
// #426: mirrors the classic ClassifyBatches gate exactly — see
// RetailUntexturedSubsetPolicy for the retail citation. ONE
// shared predicate so the classic and packed classifiers cannot
// drift (Campaign VM VM6).
if (!RetailUntexturedSubsetPolicy.Draws(entity.IsBuildingShell, batch.Key.IsSolid))
continue;
TranslucencyKind translucency = batch.Translucency;
if (opacity < 1f && IsOpaque(translucency))
translucency = TranslucencyKind.AlphaBlend;
ResolvedTexture texture = ResolveTexture(
in entity,
meshRef,
batch,
paletteIdentity,
out bool compositePending);
// Campaign FW stage FW3.2a: mirrors the classic ClassifyBatches
// gate/promotion/resolve/foliage-classify sequence exactly — see
// the one shared core (WbDrawDispatcher.WalkClassify.cs's
// TryClassifyBatch) also used by ClassifyBatches and the walk
// classifier, so the classic and packed classifiers cannot drift
// (Campaign VM VM6). `survives=false` still applies
// compositePending exactly as before this extraction.
bool survives = TryClassifyBatch(
renderData, batchIndex, in entity, meshRef, paletteIdentity,
opacity, entityHasCutoutSubset,
out GroupKey key, out bool compositePending);
if (compositePending)
reusableAcrossFrames = false;
if (!texture.Slot.IsAssigned)
if (!survives)
continue;
// Campaign VM VM6 review fix round 2 (F1 BLOCKER): the packed
// production classifier never computed FoliageFlags, so the
// production BatchData.flags word was always 0 for every
// scenery entity — the world geometry never swayed even though
// the independently-classified shadow caster did. Classify
// BEFORE constructing the key, from the RAW (pre-#188-
// promotion) batch.Translucency, exactly as the classic
// ClassifyBatches does — see that method's own comment for why
// raw translucency is used for classification but the (possibly
// promoted) local `translucency` is still what the key/group
// partitions draws by.
uint foliageFlags = FoliageWindClassification.Classify(
entity.LocalEntityId,
FoliageWindExclusions.Contains(meshRef.GfxObjId),
batch.Translucency,
entityHasCutoutSubset);
var key = new GroupKey(
batch.FirstIndex,
(int)batch.BaseVertex,
batch.IndexCount,
texture.Slot,
texture.Layer,
translucency,
FoliageFlags: foliageFlags,
CullMode: batch.CullMode);
var classified = new PackedClassifiedBatch(
key,
restPose,