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

@ -598,6 +598,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
private BatchData[] _batchData = new BatchData[256];
private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256];
private CullMode[] _drawCullModes = new CullMode[256];
// Campaign FW stage FW3.2a: SubmitOrderedStream's OWN cull-mode scratch,
// separate from _drawCullModes above. See DrawIndirectRangeRhi's doc
// comment for why sharing the array made an ordered submission unsafe to
// interleave with a mid-flight RetailAlphaQueue scope.
private CullMode[] _orderedDrawCullModes = new CullMode[256];
private BatchDataPublic[] _batchPublicScratch = new BatchDataPublic[256];
private readonly List<IndirectGroupInput> _groupInputScratch = new(256);
private readonly List<GroupKey> _retiredGroupKeys = new();
@ -3312,62 +3318,25 @@ public sealed partial class WbDrawDispatcher : IDisposable
bool allTexturesReady = true;
for (int batchIdx = 0; batchIdx < renderData.Batches.Count; batchIdx++)
{
var batch = renderData.Batches[batchIdx];
// #426: retail's D3DPolyRender::DrawMesh skips an UNTEXTURED
// (solid-colour) subset only on a BUILDING SHELL
// (RenderDeviceD3D::DrawBuilding sets ObjBuildingOrBuildingPart);
// ordinary statics/scenery/creatures/items draw it same as any
// textured subset. ONE shared predicate with ClassifyPackedBatches
// and AddDirectionalShadowBatches — see RetailUntexturedSubsetPolicy.
if (!RetailUntexturedSubsetPolicy.Draws(entity.IsBuildingShell, batch.Key.IsSolid))
continue;
TranslucencyKind translucency = batch.Translucency;
// #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap
// must route through the alpha-blend pass so mesh_modern.frag's
// (blend-enabled) shader actually composites the reduced alpha —
// the no-blend opaque pass would ignore it.
if (opacityMultiplier < 1.0f && IsOpaque(translucency))
translucency = TranslucencyKind.AlphaBlend;
ResolvedTexture texture = ResolveTexture(
in entity,
meshRef,
batch,
paletteIdentity,
out bool compositePending);
// Campaign FW stage FW3.2a: the untextured-subset gate, the #188
// opacity promotion, texture resolution, and Campaign VM foliage
// classification now live in the one shared core
// (WbDrawDispatcher.WalkClassify.cs) also used by
// ClassifyPackedBatches and the walk classifier — see
// TryClassifyBatch's doc comment. `survives=false` still applies
// compositePending exactly as before this extraction (a batch
// that fails the untextured-subset gate never touched
// ResolveTexture, so compositePending is always false there; a
// batch with an unresolved texture slot still reports it).
bool survives = TryClassifyBatch(
renderData, batchIdx, in entity, meshRef, paletteIdentity,
opacityMultiplier, entityHasCutoutSubset,
out GroupKey key, out bool compositePending);
if (compositePending)
allTexturesReady = false;
// Campaign V slice V4t: an unassigned slot is the "no texture yet"
// case a zero handle used to signal. It is a real sentinel
// (GpuTextureSlot.Unassigned == ACDREAM_TEXTURE_NONE), not the
// default value, so nothing here can silently resolve to slot 0.
if (!texture.Slot.IsAssigned) continue;
GpuTextureSlot texSlot = texture.Slot;
uint texLayer = texture.Layer;
// Campaign VM VM6 review fix round: classify BEFORE constructing
// the key and fold the result INTO the key (rather than
// stamping it onto whatever group the key already resolves to).
// Classification is from the RAW (pre-#188-promotion)
// batch.Translucency — a mid-fade trunk is still a trunk, it
// just landed in the alpha-blend group instead of opaque. This
// is what keeps a scenery instance and a non-scenery instance
// of the identical mesh subset in two SEPARATE groups instead of
// coalescing into one group whose classification depends on
// whichever entity classified it last.
uint foliageFlags = FoliageWindClassification.Classify(
entity.LocalEntityId,
FoliageWindExclusions.Contains(meshRef.GfxObjId),
batch.Translucency,
entityHasCutoutSubset);
var key = new GroupKey(
batch.FirstIndex, (int)batch.BaseVertex,
batch.IndexCount, texSlot, texLayer, translucency,
FoliageFlags: foliageFlags,
CullMode: batch.CullMode);
if (!survives)
continue;
GpuTextureSlot texSlot = key.TextureSlot;
InstanceGroup grp = GetOrCreateInstanceGroup(key);
grp.Matrices.Add(model);