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>
149 lines
5.8 KiB
C#
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);
|