Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
207 lines
7.9 KiB
C#
207 lines
7.9 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.App.Rendering;
|
||
|
||
/// <summary>
|
||
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
|
||
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
|
||
/// view by a bounding-sphere test against the view's edge planes, never
|
||
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
|
||
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
|
||
/// the object's drawing sphere against the installed view's plane set;
|
||
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
|
||
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
|
||
///
|
||
/// <para>Our views are clip-space half-planes (≤8 per slice,
|
||
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
|
||
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
|
||
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
|
||
/// is one matrix fold: with row-vector convention (System.Numerics),
|
||
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
|
||
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
|
||
/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).</para>
|
||
///
|
||
/// <para>A sphere is visible through a SLICE when it is not entirely outside
|
||
/// any of the slice's planes (convex region); visible for a CELL when any of
|
||
/// the cell's slices passes. A slice with zero planes is pass-all (the
|
||
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
|
||
/// retail an object whose cell is not in the draw list is simply never
|
||
/// reached.</para>
|
||
/// </summary>
|
||
public sealed class ViewconeCuller
|
||
{
|
||
private const int MaxRetainedCellPlaneSets = 512;
|
||
private const int MaxRetainedPlanesPerCell = 256;
|
||
private const int MaxRetainedSlicesPerCell = 64;
|
||
|
||
private readonly Dictionary<uint, PlaneSet> _cellPlanes = new();
|
||
private readonly Stack<PlaneSet> _planeSetPool = new();
|
||
private PlaneSet _outsidePlanes = new();
|
||
|
||
private readonly record struct SliceRange(int Start, int Count);
|
||
private readonly record struct LiftedPlane(Vector4 Equation, float NormalLength);
|
||
|
||
/// <summary>
|
||
/// Contiguous per-cell plane storage. Reusing two Lists per visible cell
|
||
/// avoids rebuilding a jagged array graph on every render frame while
|
||
/// retaining the exact slice boundaries used by retail's any-view test.
|
||
/// </summary>
|
||
private sealed class PlaneSet
|
||
{
|
||
public List<LiftedPlane> Planes { get; } = new();
|
||
public List<SliceRange> Slices { get; } = new();
|
||
|
||
public bool IsRetainable =>
|
||
Planes.Capacity <= MaxRetainedPlanesPerCell
|
||
&& Slices.Capacity <= MaxRetainedSlicesPerCell;
|
||
|
||
public void Reset()
|
||
{
|
||
Planes.Clear();
|
||
Slices.Clear();
|
||
}
|
||
}
|
||
|
||
/// <summary>True when the outside view is a full-screen pass-all (the
|
||
/// synthetic outdoor root) — every outside-test passes.</summary>
|
||
public bool OutsideIsFullScreen { get; private set; }
|
||
|
||
public static ViewconeCuller Build(
|
||
ClipFrameAssembly assembly,
|
||
in Matrix4x4 viewProjection,
|
||
ViewconeCuller? reuse = null)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(assembly);
|
||
var culler = reuse ?? new ViewconeCuller();
|
||
culler.Reset();
|
||
|
||
foreach (var (cellId, slices) in assembly.CellIdToViewSlices)
|
||
{
|
||
PlaneSet lifted = culler.RentPlaneSet();
|
||
for (int s = 0; s < slices.Length; s++)
|
||
AppendLiftedSlice(lifted, slices[s].Planes, viewProjection);
|
||
culler._cellPlanes[cellId] = lifted;
|
||
}
|
||
|
||
var outside = assembly.OutsideViewSlices;
|
||
bool fullScreen = false;
|
||
for (int s = 0; s < outside.Length; s++)
|
||
{
|
||
AppendLiftedSlice(culler._outsidePlanes, outside[s].Planes, viewProjection);
|
||
if (outside[s].Planes.Length == 0)
|
||
fullScreen = true;
|
||
}
|
||
culler.OutsideIsFullScreen = fullScreen;
|
||
return culler;
|
||
}
|
||
|
||
private void Reset()
|
||
{
|
||
foreach (PlaneSet set in _cellPlanes.Values)
|
||
{
|
||
set.Reset();
|
||
if (set.IsRetainable && _planeSetPool.Count < MaxRetainedCellPlaneSets)
|
||
_planeSetPool.Push(set);
|
||
}
|
||
_cellPlanes.Clear();
|
||
if (_outsidePlanes.IsRetainable)
|
||
_outsidePlanes.Reset();
|
||
else
|
||
_outsidePlanes = new PlaneSet();
|
||
OutsideIsFullScreen = false;
|
||
}
|
||
|
||
private PlaneSet RentPlaneSet()
|
||
{
|
||
PlaneSet result = _planeSetPool.Count != 0
|
||
? _planeSetPool.Pop()
|
||
: new PlaneSet();
|
||
result.Reset();
|
||
return result;
|
||
}
|
||
|
||
private static void AppendLiftedSlice(
|
||
PlaneSet destination,
|
||
Vector4[] clipPlanes,
|
||
in Matrix4x4 m)
|
||
{
|
||
int start = destination.Planes.Count;
|
||
for (int i = 0; i < clipPlanes.Length; i++)
|
||
{
|
||
var p = clipPlanes[i];
|
||
var equation = new Vector4(
|
||
m.M11 * p.X + m.M12 * p.Y + m.M13 * p.Z + m.M14 * p.W,
|
||
m.M21 * p.X + m.M22 * p.Y + m.M23 * p.Z + m.M24 * p.W,
|
||
m.M31 * p.X + m.M32 * p.Y + m.M33 * p.Z + m.M34 * p.W,
|
||
m.M41 * p.X + m.M42 * p.Y + m.M43 * p.Z + m.M44 * p.W);
|
||
float normalLength = MathF.Sqrt(
|
||
equation.X * equation.X
|
||
+ equation.Y * equation.Y
|
||
+ equation.Z * equation.Z);
|
||
destination.Planes.Add(new LiftedPlane(equation, normalLength));
|
||
}
|
||
destination.Slices.Add(new SliceRange(start, clipPlanes.Length));
|
||
}
|
||
|
||
private static bool SphereInsidePlanes(
|
||
PlaneSet set,
|
||
SliceRange slice,
|
||
in Vector3 center,
|
||
float radius)
|
||
{
|
||
int end = slice.Start + slice.Count;
|
||
for (int i = slice.Start; i < end; i++)
|
||
{
|
||
LiftedPlane plane = set.Planes[i];
|
||
Vector4 l = plane.Equation;
|
||
float nLen = plane.NormalLength;
|
||
if (nLen < 1e-12f)
|
||
continue; // degenerate plane — no constraint
|
||
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
|
||
if (dist < -radius * nLen)
|
||
return false; // entirely outside this edge plane
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
|
||
/// A cell with no views culls (not in the draw list ⇒ never reached in
|
||
/// retail). A zero-plane slice is pass-all.</summary>
|
||
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
|
||
{
|
||
if (!_cellPlanes.TryGetValue(cellId, out PlaneSet? set))
|
||
return false;
|
||
for (int s = 0; s < set.Slices.Count; s++)
|
||
if (SphereInsidePlanes(set, set.Slices[s], center, radius))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
/// <summary>Sphere-vs-the-outside-views (objects in outdoor space seen
|
||
/// from an interior root through doorways; pass-all under the outdoor
|
||
/// root's full-screen outside view).</summary>
|
||
public bool SphereVisibleOutside(in Vector3 center, float radius)
|
||
{
|
||
if (OutsideIsFullScreen)
|
||
return true;
|
||
for (int s = 0; s < _outsidePlanes.Slices.Count; s++)
|
||
if (SphereInsidePlanes(_outsidePlanes, _outsidePlanes.Slices[s], center, radius))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
/// <summary>Sphere vs ONE outside slice (the landscape pass draws per
|
||
/// slice; its statics pre-filter tests against exactly that slice).</summary>
|
||
public bool SphereVisibleInOutsideSlice(int sliceIndex, in Vector3 center, float radius)
|
||
{
|
||
if ((uint)sliceIndex >= (uint)_outsidePlanes.Slices.Count)
|
||
return false;
|
||
return SphereInsidePlanes(
|
||
_outsidePlanes,
|
||
_outsidePlanes.Slices[sliceIndex],
|
||
center,
|
||
radius);
|
||
}
|
||
}
|