using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
/// Per-frame inputs for one flood.
public sealed class InteriorRenderContext
{
/// Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame.
public required IReadOnlyList OrderedVisibleCells { get; init; }
/// The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys =
/// the GameWindow envCellShellFilter). A cell may appear in but
/// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the
/// membership filter; supplies the draw ORDER.
public required IReadOnlySet DrawableCells { get; init; }
/// Per-cell portal_view slots, in the same order retail setup_view(cell, i)
/// selects them inside PView::DrawCells.
public required IReadOnlyDictionary CellClipSlots { get; init; }
public required int OutdoorSlot { get; init; }
public required bool OutdoorVisible { get; init; }
/// The 3-bucket entity split (). Only ByCell +
/// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door
/// step (clipped to OutsideView).
public required InteriorEntityPartition.Result Partition { get; init; }
public required ICamera Camera { get; init; }
public required FrustumPlanes? Frustum { get; init; }
/// The full FFFF-suffixed landblock id of the player. Used as BOTH the synthetic
/// per-cell entry id AND neverCullLandblockId so the degenerate (zero) synthetic AABB is never
/// landblock-culled — per-entity frustum culling inside Draw still applies.
public required uint? PlayerLandblockId { get; init; }
public required HashSet? AnimatedEntityIds { get; init; }
}
///
/// The interior render flood, matching retail PView::DrawCells @ 0x005a4840:
/// after the caller handles outside_view terrain + the depth-only clear, DrawCells
/// walks cell_draw_list from the end back to zero in separate stages: cell shells,
/// then each cell's object_list. The transparent shell pass is split out because
/// the modern renderer batches opaque/transparent surfaces separately.
///
public sealed class InteriorRenderer
{
private readonly EnvCellRenderer _envCells;
private readonly WbDrawDispatcher _entities;
// Reused single-cell filter set — cleared + repopulated per cell to avoid per-frame allocs.
private readonly HashSet _oneCell = new(1);
public InteriorRenderer(EnvCellRenderer envCells, WbDrawDispatcher entities)
{
_envCells = envCells;
_entities = entities;
}
public void DrawInside(InteriorRenderContext ctx)
{
// Retail Loop 2: DrawEnvCell for each drawable cell, farthest-to-nearest
// (cell_draw_list[cell_draw_num - 1] down to 0).
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
ApplyMembershipOnlyRouting();
_envCells.Render(WbRenderPass.Opaque, _oneCell);
}
// Retail Loop 3: Render::PortalList = cell->portal_view; DrawObjCellForDummies(cell).
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
if (ctx.Partition.ByCell.TryGetValue(cellId, out var cellEntities) && cellEntities.Count > 0)
{
ApplyMembershipOnlyRouting();
DrawEntityBucket(ctx, cellEntities, visibleCellIds: _oneCell);
}
}
// Modern split of DrawEnvCell's transparent/additive batches, same reverse cell order.
for (int i = ctx.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = ctx.OrderedVisibleCells[i];
if (!TryBeginCell(ctx, cellId, out _)) continue;
_oneCell.Clear();
_oneCell.Add(cellId);
ApplyMembershipOnlyRouting();
_envCells.Render(WbRenderPass.Transparent, _oneCell);
}
}
private bool TryBeginCell(InteriorRenderContext ctx, uint cellId, out int[] slots)
{
if (ctx.DrawableCells.Contains(cellId))
{
ctx.CellClipSlots.TryGetValue(cellId, out slots!);
slots ??= System.Array.Empty();
return true;
}
slots = System.Array.Empty();
return false;
}
private void ApplyMembershipOnlyRouting()
{
// PView membership controls which cell shell/object bucket is visited.
// Do not turn the 2D portal view into gl_ClipDistance for indoor meshes:
// that slices avatars and shell triangles at stairs/doorways instead of
// matching retail's DrawMesh view-check-then-draw behavior.
_envCells.SetClipRouting(null);
_entities.ClearClipRouting();
}
// Draws one bucket of entities via the existing dispatcher, scoped to a synthetic single-entry
// landblock list. visibleCellIds gates which entities pass the cell-membership walk (a single-cell
// set for per-cell objects; null only for fallback/outdoor buckets where clip-slot routing owns cull).
// The clip slot per entity comes from the SetClipRouting the caller installed (cellIdToSlot +
// outdoorSlot + outdoorVisible) via ResolveEntitySlot.
private void DrawEntityBucket(
InteriorRenderContext ctx, IReadOnlyList bucket, HashSet? visibleCellIds)
=> DrawEntityBucket(
ctx.Camera,
ctx.Frustum,
ctx.PlayerLandblockId,
ctx.AnimatedEntityIds,
bucket,
visibleCellIds);
public void DrawEntityBucket(
ICamera camera,
FrustumPlanes? frustum,
uint? playerLandblockId,
HashSet? animatedEntityIds,
IReadOnlyList bucket,
HashSet? visibleCellIds)
{
// LandblockId == neverCullLandblockId (PlayerLandblockId) ⇒ the degenerate (zero) AABB is
// never landblock-frustum-culled; per-entity AABB culling inside Draw still applies.
uint lbId = playerLandblockId ?? 0u;
var entry = (lbId, Vector3.Zero, Vector3.Zero,
(IReadOnlyList)bucket,
(IReadOnlyDictionary?)null);
_entities.Draw(
camera,
new[] { entry },
frustum,
neverCullLandblockId: playerLandblockId,
visibleCellIds: visibleCellIds,
animatedEntityIds: animatedEntityIds);
}
}