The decisive probe between the two surviving suspects from the 2026-06-09 building-flood-merge handoff (docs/research/2026-06-09-flap-outdoor-fullworld- building-flood-merge-handoff.md section 1), gated by ACDREAM_PROBE_CLIPROUTE=1, all print-on-change: - [clip-route] (RetailPViewRenderer.DrawLandscapeThroughOutsideView): the outside slice slot + NDC AABB + planes, the CellIdToSlot routing table, the region-SSBO bytes DECODED at the routed slot, and the terrain-UBO head — captured after SetTerrainClip + UploadClipFrame + SetClipRouting, i.e. exactly what the landscape draws consume. Pins/refutes suspect (b) and the slot-repack half of suspect (a). - [clip-route-disp] (WbDrawDispatcher.Draw, routed draws only): per-slot instance histogram exactly as staged for binding=3 plus the count of entities dropped by ResolveSlotForFrame CULL. Pins/refutes the instance-routing half of suspect (a). - [clip-route-scis] (GameWindow.DrawRetailPViewLandscapeSlice): the ACTUAL GL scissor enable + box read back right after BeginDoorwayScissor — the whole landscape pass (sky + terrain + outdoor entities + player) draws inside this box, so a doorway-sized box here IS the full-world kill by construction. Code-reading findings recorded while building the probe: the landscape pass is scissored to slice.NdcAabb end-to-end (GameWindow.cs DrawRetailPViewLandscapeSlice), and ResolveEntitySlot CULLs server entities with null ParentCellId while routing is active — both now directly observable under the probe. Throwaway apparatus — strip once §4 ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
533 lines
23 KiB
C#
533 lines
23 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// App-layer port of the retail indoor render orchestration:
|
|
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
|
/// PView::DrawInside -> ConstructView -> DrawCells.
|
|
/// </summary>
|
|
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<Vector4>());
|
|
|
|
private readonly HashSet<uint> _oneCell = new(1);
|
|
private readonly Dictionary<uint, int> _oneCellSlot = new(1);
|
|
|
|
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
|
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
|
private const float OutdoorBuildingSeedDistance = 48f;
|
|
|
|
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);
|
|
|
|
// R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
|
|
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
|
|
// the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the
|
|
// land root itself has no portals (it floods nothing into buildings). Per-building seeding is
|
|
// robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
|
|
// oscillated as the chase eye grazed a doorway (the indoor flap).
|
|
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
|
MergeNearbyBuildingFloods(ctx, pvFrame);
|
|
|
|
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<uint>(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;
|
|
}
|
|
|
|
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
|
|
// (retail's per-building ConstructView(CBldPortal)), merging each small view into the frame. The
|
|
// grouping dict is reused across frames; inner lists are cleared each frame so a building that left
|
|
// the near set simply contributes an empty (skipped) group.
|
|
private void MergeNearbyBuildingFloods(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
|
{
|
|
foreach (var group in _buildingGroups.Values)
|
|
group.Clear();
|
|
|
|
foreach (var cell in ctx.NearbyBuildingCells!)
|
|
{
|
|
// R-A2 seam fix: a cell without a BuildingId (unstamped, or outdoor-adjacent with an exit
|
|
// portal) must STILL flood — the pre-R-A2 node flood reached it via a reverse portal, so
|
|
// dropping it (the original `continue`) left holes at building/terrain seams. Key it by its
|
|
// own CellId → a singleton per-entrance flood: a cell with an exit portal seeds from it, a
|
|
// cell with none contributes nothing (same as before). BuildingId/CellId key collisions are
|
|
// harmless — BuildFromExterior seeds each exit-portal cell in a group independently.
|
|
uint groupKey = cell.BuildingId ?? cell.CellId;
|
|
if (!_buildingGroups.TryGetValue(groupKey, out var group))
|
|
{
|
|
group = new List<LoadedCell>();
|
|
_buildingGroups[groupKey] = group;
|
|
}
|
|
group.Add(cell);
|
|
}
|
|
|
|
foreach (var group in _buildingGroups.Values)
|
|
{
|
|
if (group.Count == 0)
|
|
continue;
|
|
var buildingFrame = PortalVisibilityBuilder.ConstructViewBuilding(
|
|
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection, OutdoorBuildingSeedDistance);
|
|
MergeBuildingFrame(pvFrame, buildingFrame);
|
|
}
|
|
}
|
|
|
|
// Append a per-building flood's cells + views into the frame. Each building cell belongs to exactly
|
|
// one building, so there is no cross-building overlap; ContainsKey is a safety dedup. OutsideView is
|
|
// NOT merged — the outdoor root already seeds full-screen terrain, and ConstructViewBuilding
|
|
// (BuildFromExterior) leaves OutsideView empty (it stops at exit portals once inside the building).
|
|
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
|
{
|
|
foreach (uint cellId in src.OrderedVisibleCells)
|
|
{
|
|
if (target.CellViews.ContainsKey(cellId))
|
|
continue;
|
|
target.CellViews[cellId] = src.CellViews[cellId];
|
|
target.OrderedVisibleCells.Add(cellId);
|
|
}
|
|
}
|
|
|
|
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<uint>(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;
|
|
|
|
int probeSliceIndex = 0;
|
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
|
{
|
|
_clipFrame.SetTerrainClip(slice.Planes);
|
|
UploadClipFrame(ctx.SetTerrainClipUbo);
|
|
_entities.SetClipRouting(clipAssembly.CellIdToSlot, slice.Slot, outdoorVisible: true);
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled)
|
|
EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
|
|
probeSliceIndex++;
|
|
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
|
|
}
|
|
|
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
|
ctx.ClearDepthSlice?.Invoke(slice);
|
|
|
|
UseIndoorMembershipOnlyRouting();
|
|
}
|
|
|
|
// §4 flap [clip-route] probe state (2026-06-10, throwaway): print-on-change signature +
|
|
// monotonic sequence so held-flap vs healthy frames diff cleanly in one capture.
|
|
private string? _lastClipRouteSig;
|
|
private long _clipRouteSeq;
|
|
private readonly List<uint> _clipRouteCellKeys = new();
|
|
|
|
// §4 flap apparatus (2026-06-10): the decisive probe between the surviving suspects
|
|
// (handoff 2026-06-09 §1). Emits the EXACT clip inputs the landscape pass draws under:
|
|
// the outside slice's slot + NDC AABB + planes (CPU side), the region-SSBO bytes decoded
|
|
// at that slot (what mesh_modern.vert reads for routed instances), the terrain-UBO head
|
|
// (what terrain/sky gate against), and the CellIdToSlot routing table. Fires AFTER
|
|
// SetTerrainClip + UploadClipFrame + SetClipRouting, BEFORE DrawLandscapeSlice — so the
|
|
// printed bytes are exactly what this slice's draws consume.
|
|
private void EmitClipRouteProbe(ClipFrameAssembly clipAssembly, ClipViewSlice slice, int sliceIndex)
|
|
{
|
|
var sb = new System.Text.StringBuilder(256);
|
|
sb.Append(System.FormattableString.Invariant(
|
|
$"slice={sliceIndex}/{clipAssembly.OutsideViewSlices.Length} slot={slice.Slot}"));
|
|
sb.Append(System.FormattableString.Invariant(
|
|
$" ndc=({slice.NdcAabb.X:F3},{slice.NdcAabb.Y:F3},{slice.NdcAabb.Z:F3},{slice.NdcAabb.W:F3})"));
|
|
sb.Append(System.FormattableString.Invariant($" planes={slice.Planes.Length}["));
|
|
for (int i = 0; i < slice.Planes.Length; i++)
|
|
{
|
|
var p = slice.Planes[i];
|
|
if (i > 0) sb.Append(' ');
|
|
sb.Append(System.FormattableString.Invariant($"({p.X:F3},{p.Y:F3},{p.Z:F3},{p.W:F3})"));
|
|
}
|
|
|
|
// CellIdToSlot sorted by cell id so dictionary enumeration order can't fake a change.
|
|
sb.Append("] cells={");
|
|
_clipRouteCellKeys.Clear();
|
|
foreach (uint key in clipAssembly.CellIdToSlot.Keys)
|
|
_clipRouteCellKeys.Add(key);
|
|
_clipRouteCellKeys.Sort();
|
|
for (int i = 0; i < _clipRouteCellKeys.Count; i++)
|
|
{
|
|
if (i > 0) sb.Append(',');
|
|
sb.Append(System.FormattableString.Invariant(
|
|
$"0x{_clipRouteCellKeys[i]:X8}:{clipAssembly.CellIdToSlot[_clipRouteCellKeys[i]]}"));
|
|
}
|
|
sb.Append('}');
|
|
|
|
// Region-SSBO content decoded at the routed slot, from the packed bytes UploadClipFrame
|
|
// just uploaded — slot stride 144: count uint at +0, planes[8] at +16.
|
|
var rb = _clipFrame.RegionBytesForTest;
|
|
int off = slice.Slot * ClipFrame.CellClipStrideBytes;
|
|
if (off >= 0 && off + ClipFrame.CellClipStrideBytes <= rb.Length)
|
|
{
|
|
uint ssboCount = System.BitConverter.ToUInt32(rb.Slice(off, 4));
|
|
sb.Append(System.FormattableString.Invariant($" ssbo[{slice.Slot}]: n={ssboCount}"));
|
|
int planeN = (int)System.Math.Min(ssboCount, (uint)ClipFrame.MaxPlanes);
|
|
for (int i = 0; i < planeN; i++)
|
|
{
|
|
int po = off + ClipFrame.CellClipPlanesOffset + i * 16;
|
|
float px = System.BitConverter.ToSingle(rb.Slice(po, 4));
|
|
float py = System.BitConverter.ToSingle(rb.Slice(po + 4, 4));
|
|
float pz = System.BitConverter.ToSingle(rb.Slice(po + 8, 4));
|
|
float pw = System.BitConverter.ToSingle(rb.Slice(po + 12, 4));
|
|
sb.Append(System.FormattableString.Invariant($" ({px:F3},{py:F3},{pz:F3},{pw:F3})"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.Append(System.FormattableString.Invariant(
|
|
$" ssbo[{slice.Slot}]: OUT-OF-RANGE len={rb.Length}"));
|
|
}
|
|
|
|
// Terrain-UBO head as uploaded (std140: int count at +0, planes[8] at +16).
|
|
var tb = _clipFrame.TerrainBytesForTest;
|
|
int uboCount = System.BitConverter.ToInt32(tb.Slice(0, 4));
|
|
float u0 = System.BitConverter.ToSingle(tb.Slice(16, 4));
|
|
float u1 = System.BitConverter.ToSingle(tb.Slice(20, 4));
|
|
float u2 = System.BitConverter.ToSingle(tb.Slice(24, 4));
|
|
float u3 = System.BitConverter.ToSingle(tb.Slice(28, 4));
|
|
sb.Append(System.FormattableString.Invariant(
|
|
$" ubo: n={uboCount} p0=({u0:F3},{u1:F3},{u2:F3},{u3:F3})"));
|
|
|
|
string sig = sb.ToString();
|
|
_clipRouteSeq++;
|
|
if (sig == _lastClipRouteSig)
|
|
return;
|
|
_lastClipRouteSig = sig;
|
|
Console.WriteLine($"[clip-route] n={_clipRouteSeq} {sig}");
|
|
}
|
|
|
|
private void DrawExitPortalMasks(
|
|
IRetailPViewCellDrawCallbacks ctx,
|
|
PortalVisibilityFrame pvFrame,
|
|
ClipFrameAssembly clipAssembly,
|
|
HashSet<uint> 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<WorldEntity>()));
|
|
}
|
|
}
|
|
|
|
private void DrawEnvCellShells(
|
|
IRetailPViewCellDrawCallbacks ctx,
|
|
PortalVisibilityFrame pvFrame,
|
|
ClipFrameAssembly clipAssembly,
|
|
HashSet<uint> 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<uint> 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<WorldEntity> bucket,
|
|
HashSet<uint>? visibleCellIds)
|
|
{
|
|
uint lbId = ctx.PlayerLandblockId ?? 0u;
|
|
var entry = (lbId, Vector3.Zero, Vector3.Zero,
|
|
(IReadOnlyList<WorldEntity>)bucket,
|
|
(IReadOnlyDictionary<uint, WorldEntity>?)null);
|
|
|
|
_entities.Draw(
|
|
ctx.Camera,
|
|
new[] { entry },
|
|
ctx.Frustum,
|
|
neverCullLandblockId: ctx.PlayerLandblockId,
|
|
visibleCellIds: visibleCellIds,
|
|
animatedEntityIds: ctx.AnimatedEntityIds);
|
|
}
|
|
|
|
private void RestoreNoClip(Action<uint> setTerrainClipUbo)
|
|
{
|
|
_clipFrame.Reset();
|
|
UploadClipFrame(setTerrainClipUbo);
|
|
UseIndoorMembershipOnlyRouting();
|
|
}
|
|
|
|
private void UploadClipFrame(Action<uint> setTerrainClipUbo)
|
|
{
|
|
_clipFrame.UploadShared(_gl);
|
|
_entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
|
_envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
|
setTerrainClipUbo(_clipFrame.TerrainUbo);
|
|
}
|
|
}
|
|
|
|
public interface IRetailPViewCellDrawCallbacks
|
|
{
|
|
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; }
|
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; }
|
|
}
|
|
|
|
public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
|
{
|
|
public ICamera Camera { get; }
|
|
public FrustumPlanes? Frustum { get; }
|
|
public uint? PlayerLandblockId { get; }
|
|
public HashSet<uint>? AnimatedEntityIds { get; }
|
|
}
|
|
|
|
public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|
{
|
|
public required LoadedCell RootCell { get; init; }
|
|
|
|
/// <summary>R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the
|
|
/// outdoor node. Null for interior roots. Grouped by BuildingId inside <see cref="DrawInside"/>.</summary>
|
|
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; init; }
|
|
|
|
public required Vector3 ViewerEyePos { get; init; }
|
|
public required Matrix4x4 ViewProjection { get; init; }
|
|
public required Func<uint, LoadedCell?> 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<uint>? 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<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
|
public required Action<uint> SetTerrainClipUbo { get; init; }
|
|
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
|
|
public Action<ClipViewSlice>? ClearDepthSlice { get; init; }
|
|
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
|
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
|
}
|
|
|
|
public sealed class RetailPViewPortalDrawContext : IRetailPViewCellDrawContext
|
|
{
|
|
public required IEnumerable<LoadedCell> CandidateCells { get; init; }
|
|
public required Vector3 ViewerEyePos { get; init; }
|
|
public required Matrix4x4 ViewProjection { get; init; }
|
|
public required Func<uint, LoadedCell?> 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<uint>? 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<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
|
public required Action<uint> SetTerrainClipUbo { get; init; }
|
|
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
|
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
|
}
|
|
|
|
public sealed class RetailPViewFrameResult
|
|
{
|
|
public required PortalVisibilityFrame PortalFrame { get; init; }
|
|
public required ClipFrameAssembly ClipAssembly { get; init; }
|
|
public required HashSet<uint> DrawableCells { get; init; }
|
|
public required InteriorEntityPartition.Result Partition { get; init; }
|
|
}
|
|
|
|
public readonly record struct RetailPViewLandscapeSliceContext(
|
|
ClipViewSlice Slice,
|
|
IReadOnlyList<WorldEntity> OutdoorEntities);
|
|
|
|
public readonly record struct RetailPViewCellSliceContext(
|
|
uint CellId,
|
|
ClipViewSlice Slice,
|
|
IReadOnlyList<WorldEntity> CellEntities);
|