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

@ -70,6 +70,47 @@ internal readonly record struct RenderInstanceCandidate(
TupleLandblockId: tupleLandblockId);
}
/// <summary>
/// Campaign FW stage FW3.2a: builds a candidate straight from a
/// <see cref="RenderProjectionRecord"/>, without the packed route's
/// <see cref="RenderFrameEntityCandidate"/>/frame-arena mesh-part
/// flattening. The walk populator reads <see cref="RenderSceneQuery"/>
/// records directly (<c>CopyCellStaticsTo</c> / <c>CopyIndexTo</c>), so it
/// has no frame arena to look the source candidate up in — every field
/// this needs already lives on the record's own <see cref="RenderSourceMetadata"/>
/// and <see cref="RenderEntityPayload"/>.
///
/// <para><paramref name="animated"/> defaults to false: the walk's static
/// routes (<c>RenderProjectionClass.OutdoorStatic</c> /
/// <c>IndoorCellStatic</c>) never carry retail's per-part
/// <c>TransparentPartHook</c> animation — that mechanic keys off a live
/// entity's ServerGuid, not a world static's. A future dynamic walk route
/// passes true explicitly.</para>
/// </summary>
internal static RenderInstanceCandidate FromProjection(
in RenderProjectionRecord projection,
uint tupleLandblockId,
bool animated = false)
{
RenderEntityPayload payload = projection.EntityPayload;
return new RenderInstanceCandidate(
ProjectionId: projection.Id,
LocalEntityId: projection.Source.LocalEntityId,
ServerGuid: projection.Source.ServerGuid,
SourceId: projection.Source.SourceId,
ParentCellId: projection.Source.ParentCellId,
RootWorld: projection.Transform.LocalToWorld,
Position: projection.Transform.Position,
Rotation: projection.Transform.Rotation,
Scale: projection.Transform.UniformScale,
Bounds: projection.Bounds,
PaletteOverride: payload.PaletteOverride,
IsBuildingShell: payload.IsBuildingShell,
Animated: animated,
MeshPartCount: payload.MeshRefs?.Count ?? 0,
TupleLandblockId: tupleLandblockId);
}
internal static RenderInstanceCandidate FromFrame(
in RenderFrameEntityCandidate source,
uint tupleLandblockId)