acdream/src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs

204 lines
8.3 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW stage FW3.2a: the walk→draw population layer. Turns one
/// cell's already-classified static content into <see cref="OrderedDrawCommand"/>
/// appends (opaque, walk order), per-instance deferred-alpha submissions
/// (translucent), and selection-scene publications (picking) — WITHOUT any
/// production frame wiring. <c>WorldSceneRenderer</c> does not call this
/// class yet; FW3.2b roots it into the real frame.
///
/// <para><b>Why per-entity append instead of the classic material grouping</b>:
/// under <see cref="WorldDepthContract"/>'s depth-compare Less, opaque draw
/// order is pixel-relevant ONLY for coplanar surfaces (a rug on a floor, a
/// shell over terrain) — and retail resolves those by first-drawn-wins in
/// ITS walk/cell-content order, not by any texture/material grouping. So
/// this populator appends one <see cref="OrderedDrawCommand"/> per (entity,
/// opaque batch) in the SAME order the caller's <c>records</c> span presents
/// them — never material-grouped, never re-sorted. Translucent batches go to
/// the SAME <see cref="RetailAlphaQueue"/> the classic/packed paths already
/// use (global far→near re-sort still applies; walk-order submission only
/// improves retail's submission-order tie-break fidelity for coincident
/// distances — <c>WbDrawDispatcher.DeferTransparentGroups</c>'s own doc
/// comment cites the same <c>CShadowPart::insertion_sort</c> stability this
/// preserves).</para>
///
/// <para>Reads no retained scene state itself: every method takes the target
/// <see cref="OrderedDrawStream"/> and a caller-supplied span of already
/// -queried <see cref="RenderProjectionRecord"/>s (from
/// <c>RenderSceneQuery.CopyCellStaticsTo</c> / <c>CopyIndexTo</c>). Owns only
/// two small per-call scratch lists — the classify seam's per-entity output
/// — reused across records to keep this hot path allocation-free after
/// warmup.</para>
/// </summary>
internal sealed class WalkStaticStreamPopulator
{
private readonly WbDrawDispatcher _dispatcher;
private readonly List<WbDrawDispatcher.WalkClassifiedBatch> _batchScratch = new();
private readonly List<WbDrawDispatcher.WalkClassifiedSelectionPart> _selectionScratch = new();
internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Populates <paramref name="stream"/> from one indoor cell's static
/// content (<c>RenderProjectionClass.IndoorCellStatic</c> —
/// <c>RenderSceneQuery.CopyCellStaticsTo</c>). <paramref name="stage"/>
/// is caller-selected per the walk turn this cell's content belongs to
/// — <see cref="WalkDrawStage.CellStatic"/> for an ordinary
/// <c>PView::DrawCells</c> flood, <see cref="WalkDrawStage.LookInStatic"/>
/// for a <c>ConstructView(CBldPortal)</c> look-in, or
/// <see cref="WalkDrawStage.BuildingShell"/> for a building's own
/// exterior shell content.
/// </summary>
internal void PopulateCell(
OrderedDrawStream stream,
WalkDrawStage stage,
uint cellId,
ReadOnlySpan<RenderProjectionRecord> records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
{
ClassifyAndAppend(
stream, stage, cellId, in records[i], tupleLandblockId,
cameraWorldPosition, viewProjection,
liveDynamic: false, views, viewRouteIndex, alphaSubmissions);
}
}
/// <summary>
/// The landscape entry point: outdoor static content
/// (<c>RenderProjectionClass.OutdoorStatic</c> —
/// <c>RenderSceneQuery.CopyIndexTo(RenderSceneIndex.OutdoorStatic, ...)</c>)
/// at <see cref="WalkDrawStage.OutdoorStatic"/>, the terrain-adjacent
/// stage <c>LScape::draw</c> visits a landblock's static objects at
/// alongside its ground mesh (see that stage value's own doc comment).
/// <paramref name="cellId"/> is the OUTDOOR landblock id — outdoor
/// statics have no EnvCell of their own, so this is walk-order
/// provenance only, not a clip-slot key.
/// </summary>
internal void PopulateOutdoorStatics(
OrderedDrawStream stream,
uint cellId,
ReadOnlySpan<RenderProjectionRecord> records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1,
ISet<RenderProjectionId>? drawnOnce = null,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
{
if (drawnOnce is not null && !drawnOnce.Add(records[i].Id))
continue;
ClassifyAndAppend(
stream, WalkDrawStage.OutdoorStatic, cellId, in records[i],
tupleLandblockId, cameraWorldPosition, viewProjection,
liveDynamic: false, views, viewRouteIndex, alphaSubmissions);
}
}
internal void PopulateCellDynamics(
OrderedDrawStream stream,
uint cellId,
ReadOnlySpan<RenderProjectionRecord> records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
ISet<RenderProjectionId>? drawnOnce = null,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
ArgumentNullException.ThrowIfNull(stream);
for (int i = 0; i < records.Length; i++)
{
if (drawnOnce is not null && !drawnOnce.Add(records[i].Id))
continue;
ClassifyAndAppend(
stream,
WalkDrawStage.Dynamic,
cellId,
in records[i],
tupleLandblockId,
cameraWorldPosition,
viewProjection,
liveDynamic: true,
lookInViews,
lookInRouteIndex,
alphaSubmissions);
}
}
private void ClassifyAndAppend(
OrderedDrawStream stream,
WalkDrawStage stage,
uint cellId,
in RenderProjectionRecord record,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
bool liveDynamic = false,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
{
_batchScratch.Clear();
_selectionScratch.Clear();
_dispatcher.ClassifyEntityForWalk(
in record,
tupleLandblockId,
_batchScratch,
_selectionScratch,
liveDynamic,
lookInViews,
lookInRouteIndex,
cellId);
for (int i = 0; i < _batchScratch.Count; i++)
{
WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[i];
if (batch.IsOpaque)
{
stream.Append(new OrderedDrawCommand(
batch.Key, batch.Transform, stage, cellId, batch.ClipSlot,
batch.Lights, batch.IndoorFlag, batch.Alpha,
batch.SelectionLighting, batch.DetailCategory));
}
else
{
if (alphaSubmissions is null)
{
_dispatcher.SubmitWalkAlphaInstance(
in batch, cameraWorldPosition, viewProjection);
}
else
{
alphaSubmissions.Add(batch);
}
}
}
for (int i = 0; i < _selectionScratch.Count; i++)
{
WbDrawDispatcher.WalkClassifiedSelectionPart part = _selectionScratch[i];
_dispatcher.PublishWalkSelectionPart(in part);
}
}
}