using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
/// App-layer port of the retail indoor render orchestration:
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
/// PView::DrawInside -> ConstructView -> DrawCells.
///
public sealed class RetailPViewRenderer
{
private readonly GL _gl;
private readonly ClipFrame _clipFrame;
private readonly EnvCellRenderer _envCells;
private readonly WbDrawDispatcher _entities;
private static readonly ClipViewSlice NoClipSlice =
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty());
private readonly HashSet _oneCell = new(1);
private readonly Dictionary _oneCellSlot = new(1);
public RetailPViewRenderer(
GL gl,
ClipFrame clipFrame,
EnvCellRenderer envCells,
WbDrawDispatcher entities)
{
_gl = gl;
_clipFrame = clipFrame;
_envCells = envCells;
_entities = entities;
}
public RetailPViewFrameResult DrawInside(RetailPViewDrawContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
var pvFrame = PortalVisibilityBuilder.Build(
ctx.RootCell,
ctx.ViewerEyePos,
ctx.CellLookup,
ctx.ViewProjection);
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
UploadClipFrame(ctx.SetTerrainClipUbo);
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
// so every visible cell's shell has a prepared batch and seals — killing the grey
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
var drawableCells = new HashSet(pvFrame.OrderedVisibleCells);
UseIndoorMembershipOnlyRouting();
_envCells.PrepareRenderBatches(
ctx.ViewProjection,
ctx.CameraWorldPosition,
filter: drawableCells,
centerLbX: ctx.RenderCenterLbX,
centerLbY: ctx.RenderCenterLbY,
renderRadius: ctx.RenderRadius);
var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries);
var result = new RetailPViewFrameResult
{
PortalFrame = pvFrame,
ClipAssembly = clipAssembly,
DrawableCells = drawableCells,
Partition = partition,
};
ctx.EmitDiagnostics?.Invoke(result);
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition);
UseIndoorMembershipOnlyRouting();
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells);
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
return result;
}
public RetailPViewFrameResult? DrawPortal(RetailPViewPortalDrawContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
var pvFrame = PortalVisibilityBuilder.BuildFromExterior(
ctx.CandidateCells,
ctx.ViewerEyePos,
ctx.CellLookup,
ctx.ViewProjection,
ctx.MaxSeedDistance);
if (pvFrame.OrderedVisibleCells.Count == 0)
{
RestoreNoClip(ctx.SetTerrainClipUbo);
return null;
}
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
UploadClipFrame(ctx.SetTerrainClipUbo);
var drawableCells = new HashSet(clipAssembly.CellIdToSlot.Keys);
UseIndoorMembershipOnlyRouting();
_envCells.PrepareRenderBatches(
ctx.ViewProjection,
ctx.CameraWorldPosition,
filter: drawableCells,
centerLbX: ctx.RenderCenterLbX,
centerLbY: ctx.RenderCenterLbY,
renderRadius: ctx.RenderRadius);
var partition = InteriorEntityPartition.Partition(drawableCells, ctx.LandblockEntries);
var result = new RetailPViewFrameResult
{
PortalFrame = pvFrame,
ClipAssembly = clipAssembly,
DrawableCells = drawableCells,
Partition = partition,
};
ctx.EmitDiagnostics?.Invoke(result);
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(ctx, pvFrame, clipAssembly, drawableCells);
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
RestoreNoClip(ctx.SetTerrainClipUbo);
return result;
}
private void DrawLandscapeThroughOutsideView(
RetailPViewDrawContext ctx,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition)
{
if (clipAssembly.OutsideViewSlices.Length == 0)
return;
foreach (var slice in clipAssembly.OutsideViewSlices)
{
_clipFrame.SetTerrainClip(slice.Planes);
UploadClipFrame(ctx.SetTerrainClipUbo);
_entities.SetClipRouting(clipAssembly.CellIdToSlot, slice.Slot, outdoorVisible: true);
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
}
foreach (var slice in clipAssembly.OutsideViewSlices)
ctx.ClearDepthSlice?.Invoke(slice);
UseIndoorMembershipOnlyRouting();
}
private void DrawExitPortalMasks(
IRetailPViewCellDrawCallbacks ctx,
PortalVisibilityFrame pvFrame,
ClipFrameAssembly clipAssembly,
HashSet drawableCells)
{
if (ctx.DrawExitPortalMasks is null)
return;
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = pvFrame.OrderedVisibleCells[i];
if (!drawableCells.Contains(cellId))
continue;
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
ctx.DrawExitPortalMasks(new RetailPViewCellSliceContext(cellId, slice, Array.Empty()));
}
}
private void DrawEnvCellShells(
IRetailPViewCellDrawCallbacks ctx,
PortalVisibilityFrame pvFrame,
ClipFrameAssembly clipAssembly,
HashSet drawableCells) // param kept this task; removed in Task 4
{
// Retail DrawCells Loop 2: every visible cell's shell, reverse cell_draw_list
// (far→near), per portal_view slice. No drawableCells filter — a cell without a
// clip-slot falls through GetCellSlicesOrNoClip to NoClipSlice and draws unclipped
// (sealed; per-slice trim returns in Task 4).
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
{
uint cellId = entry.CellId;
_oneCell.Clear();
_oneCell.Add(cellId);
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
{
UseShellClipRouting(cellId, slice);
_envCells.Render(WbRenderPass.Opaque, _oneCell);
_envCells.Render(WbRenderPass.Transparent, _oneCell);
}
}
}
private void DrawCellObjectLists(
IRetailPViewCellDrawContext ctx,
PortalVisibilityFrame pvFrame,
ClipFrameAssembly clipAssembly,
HashSet drawableCells,
InteriorEntityPartition.Result partition)
{
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = pvFrame.OrderedVisibleCells[i];
if (!drawableCells.Contains(cellId))
continue;
if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0)
continue;
_oneCell.Clear();
_oneCell.Add(cellId);
UseIndoorMembershipOnlyRouting();
DrawEntityBucket(ctx, bucket, _oneCell);
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, bucket));
}
}
private static ClipViewSlice[] GetCellSlicesOrNoClip(
ClipFrameAssembly clipAssembly,
uint cellId)
{
if (clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices)
&& slices.Length > 0)
return slices;
return new[] { NoClipSlice };
}
private void UseIndoorMembershipOnlyRouting()
{
// Retail's PView portal views decide which cells/objects are eligible,
// but DrawMesh only performs portal-view visibility checks before drawing
// the mesh. Feeding those 2D views into gl_ClipDistance slices characters
// and cell shells at stair/door boundaries, which retail does not do.
_envCells.SetClipRouting(null);
_entities.ClearClipRouting();
}
private void UseShellClipRouting(uint cellId, ClipViewSlice slice)
{
_oneCellSlot.Clear();
_oneCellSlot[cellId] = slice.Slot;
_envCells.SetClipRouting(_oneCellSlot);
_entities.ClearClipRouting();
}
private void DrawEntityBucket(
IRetailPViewCellDrawContext ctx,
IReadOnlyList bucket,
HashSet? visibleCellIds)
{
uint lbId = ctx.PlayerLandblockId ?? 0u;
var entry = (lbId, Vector3.Zero, Vector3.Zero,
(IReadOnlyList)bucket,
(IReadOnlyDictionary?)null);
_entities.Draw(
ctx.Camera,
new[] { entry },
ctx.Frustum,
neverCullLandblockId: ctx.PlayerLandblockId,
visibleCellIds: visibleCellIds,
animatedEntityIds: ctx.AnimatedEntityIds);
}
private void RestoreNoClip(Action setTerrainClipUbo)
{
_clipFrame.Reset();
UploadClipFrame(setTerrainClipUbo);
UseIndoorMembershipOnlyRouting();
}
private void UploadClipFrame(Action setTerrainClipUbo)
{
_clipFrame.UploadShared(_gl);
_entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
_envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
setTerrainClipUbo(_clipFrame.TerrainUbo);
}
}
public interface IRetailPViewCellDrawCallbacks
{
public Action? DrawExitPortalMasks { get; }
public Action? DrawCellParticles { get; }
}
public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
{
public ICamera Camera { get; }
public FrustumPlanes? Frustum { get; }
public uint? PlayerLandblockId { get; }
public HashSet? AnimatedEntityIds { get; }
}
public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
{
public required LoadedCell RootCell { get; init; }
public required Vector3 ViewerEyePos { get; init; }
public required Matrix4x4 ViewProjection { get; init; }
public required Func CellLookup { get; init; }
public required ICamera Camera { get; init; }
public required Vector3 CameraWorldPosition { get; init; }
public required FrustumPlanes? Frustum { get; init; }
public required uint? PlayerLandblockId { get; init; }
public required HashSet? AnimatedEntityIds { get; init; }
public required int RenderCenterLbX { get; init; }
public required int RenderCenterLbY { get; init; }
public required int RenderRadius { get; init; }
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList Entities,
IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; }
public required Action SetTerrainClipUbo { get; init; }
public required Action DrawLandscapeSlice { get; init; }
public Action? ClearDepthSlice { get; init; }
public Action? DrawExitPortalMasks { get; init; }
public Action? DrawCellParticles { get; init; }
public Action? EmitDiagnostics { get; init; }
}
public sealed class RetailPViewPortalDrawContext : IRetailPViewCellDrawContext
{
public required IEnumerable CandidateCells { get; init; }
public required Vector3 ViewerEyePos { get; init; }
public required Matrix4x4 ViewProjection { get; init; }
public required Func CellLookup { get; init; }
public required ICamera Camera { get; init; }
public required Vector3 CameraWorldPosition { get; init; }
public required FrustumPlanes? Frustum { get; init; }
public required uint? PlayerLandblockId { get; init; }
public required HashSet? AnimatedEntityIds { get; init; }
public required int RenderCenterLbX { get; init; }
public required int RenderCenterLbY { get; init; }
public required int RenderRadius { get; init; }
public required float MaxSeedDistance { get; init; }
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList Entities,
IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; }
public required Action SetTerrainClipUbo { get; init; }
public Action? DrawExitPortalMasks { get; init; }
public Action? DrawCellParticles { get; init; }
public Action? EmitDiagnostics { get; init; }
}
public sealed class RetailPViewFrameResult
{
public required PortalVisibilityFrame PortalFrame { get; init; }
public required ClipFrameAssembly ClipAssembly { get; init; }
public required HashSet DrawableCells { get; init; }
public required InteriorEntityPartition.Result Partition { get; init; }
}
public readonly record struct RetailPViewLandscapeSliceContext(
ClipViewSlice Slice,
IReadOnlyList OutdoorEntities);
public readonly record struct RetailPViewCellSliceContext(
uint CellId,
ClipViewSlice Slice,
IReadOnlyList CellEntities);