using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Walk;
///
/// Campaign FW stage FW3.2a: the walk→draw population layer. Turns one
/// cell's already-classified static content into
/// appends (opaque, walk order), per-instance deferred-alpha submissions
/// (translucent), and selection-scene publications (picking) — WITHOUT any
/// production frame wiring. WorldSceneRenderer does not call this
/// class yet; FW3.2b roots it into the real frame.
///
/// Why per-entity append instead of the classic material grouping:
/// under '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 per (entity,
/// opaque batch) in the SAME order the caller's records span presents
/// them — never material-grouped, never re-sorted. Translucent batches go to
/// the SAME 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 — WbDrawDispatcher.DeferTransparentGroups's own doc
/// comment cites the same CShadowPart::insertion_sort stability this
/// preserves).
///
/// Reads no retained scene state itself: every method takes the target
/// and a caller-supplied span of already
/// -queried s (from
/// RenderSceneQuery.CopyCellStaticsTo / CopyIndexTo). 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.
///
internal sealed class WalkStaticStreamPopulator
{
private readonly WbDrawDispatcher _dispatcher;
private readonly List _batchScratch = new();
private readonly List _selectionScratch = new();
internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
///
/// Populates from one indoor cell's static
/// content (RenderProjectionClass.IndoorCellStatic —
/// RenderSceneQuery.CopyCellStaticsTo).
/// is caller-selected per the walk turn this cell's content belongs to
/// — for an ordinary
/// PView::DrawCells flood,
/// for a ConstructView(CBldPortal) look-in, or
/// for a building's own
/// exterior shell content.
///
internal void PopulateCell(
OrderedDrawStream stream,
WalkDrawStage stage,
uint cellId,
ReadOnlySpan records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1,
List? 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);
}
}
///
/// The landscape entry point: outdoor static content
/// (RenderProjectionClass.OutdoorStatic —
/// RenderSceneQuery.CopyIndexTo(RenderSceneIndex.OutdoorStatic, ...))
/// at , the terrain-adjacent
/// stage LScape::draw visits a landblock's static objects at
/// alongside its ground mesh (see that stage value's own doc comment).
/// 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.
///
internal void PopulateOutdoorStatics(
OrderedDrawStream stream,
uint cellId,
ReadOnlySpan records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? views = null,
int viewRouteIndex = -1,
ISet? drawnOnce = null,
List? 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 records,
uint tupleLandblockId,
Vector3 cameraWorldPosition,
Matrix4x4 viewProjection,
IWalkLookInViewSource? lookInViews = null,
int lookInRouteIndex = -1,
ISet? drawnOnce = null,
List? 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? 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);
}
}
}