acdream/src/AcDream.App/Rendering/Scene/RenderInstanceCandidate.cs
Erik 81c6531727 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>
2026-08-30 13:52:23 +02:00

149 lines
5.8 KiB
C#

using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Scene;
/// <summary>
/// The complete acdream-owned value boundary consumed by the object
/// dispatcher. It contains presentation facts only; the dispatcher must not
/// call back into <see cref="WorldEntity"/> or any gameplay owner after this
/// value has been produced.
/// </summary>
internal readonly record struct RenderInstanceCandidate(
RenderProjectionId ProjectionId,
uint LocalEntityId,
uint ServerGuid,
uint SourceId,
uint ParentCellId,
Matrix4x4 RootWorld,
Vector3 Position,
Quaternion Rotation,
float Scale,
RenderWorldBounds Bounds,
PaletteOverride? PaletteOverride,
bool IsBuildingShell,
bool Animated,
int MeshPartCount,
uint TupleLandblockId)
{
internal uint Id => LocalEntityId;
internal uint? ParentCell =>
ParentCellId == 0 ? null : ParentCellId;
internal uint CacheLandblockId =>
ParentCellId != 0
? (ParentCellId & 0xFFFF0000u) | 0xFFFFu
: TupleLandblockId;
internal static RenderInstanceCandidate FromWorldEntity(
WorldEntity entity,
bool animated,
int meshPartCount,
uint tupleLandblockId)
{
ArgumentNullException.ThrowIfNull(entity);
if (meshPartCount < 0)
throw new ArgumentOutOfRangeException(nameof(meshPartCount));
if (entity.AabbDirty)
entity.RefreshAabb();
return new RenderInstanceCandidate(
ProjectionId: default,
LocalEntityId: entity.Id,
ServerGuid: entity.ServerGuid,
SourceId: entity.SourceGfxObjOrSetupId,
ParentCellId: entity.ParentCellId ?? 0u,
RootWorld:
Matrix4x4.CreateFromQuaternion(entity.Rotation)
* Matrix4x4.CreateTranslation(entity.Position),
Position: entity.Position,
Rotation: entity.Rotation,
Scale: entity.Scale,
Bounds: new RenderWorldBounds(
entity.AabbMin,
entity.AabbMax),
PaletteOverride: entity.PaletteOverride,
IsBuildingShell: entity.IsBuildingShell,
Animated: animated,
MeshPartCount: meshPartCount,
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)
{
RenderProjectionRecord projection = source.Projection;
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:
projection.EntityPayload.PaletteOverride,
IsBuildingShell:
projection.EntityPayload.IsBuildingShell,
Animated: source.Animated,
MeshPartCount: source.MeshPartCount,
TupleLandblockId: tupleLandblockId);
}
}
/// <summary>
/// One ordered mesh reference belonging to a dispatcher candidate. Candidate
/// values repeat across the entity's tuples only in the transitional current
/// source; the scene frame product supplies the same facts from retained arena
/// storage.
/// </summary>
internal readonly record struct RenderInstanceTuple(
RenderInstanceCandidate Candidate,
int MeshRefIndex,
MeshRef MeshRef);