checkpoint: preserve cathedral look-in investigation state

This commit is contained in:
Erik 2026-08-30 23:29:28 +02:00
parent c287db8651
commit 572de1ec30
11 changed files with 757 additions and 197 deletions

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
@ -118,6 +119,17 @@ internal interface IWalkFrameLeafRenderer
/// positional invariant this carries (the #132 falls containment).</summary>
void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
/// <summary>Draws the packed dynamic occupants of one building look-in
/// cell at that cell's OWN <c>PView::DrawCells</c> turn, then submits the
/// cell's static + dynamic particle owners at the same turn. The route
/// index is assigned in the exact order Collect encountered look-in cell
/// turns and therefore matches the frame product's walk-keyed
/// <c>LookInObject</c> ranges.</summary>
void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds);
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
/// (pc:432731-432732) between the outside stage and the interior root's
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
@ -259,8 +271,37 @@ internal enum WalkFrameEventKind : byte
/// cathedral bleed; the old pipeline's user-verified #132 fix
/// `e102fb36` encoded the same invariant).</summary>
StaticParticles,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawLookInDynamics"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the look-in cell and
/// <see cref="WalkFrameEvent.IntArg"/> is its walk-ordered packed route
/// index.</summary>
LookInDynamics,
}
/// <summary>
/// The exact portal-view cones installed at the walk's building look-in
/// <c>DrawCells</c> turns. Retail <c>RenderDeviceD3D::DrawMesh</c>
/// @0x005A0860 tests each object's drawing sphere against these views before
/// drawing the mesh whole; cell membership alone is not an admission rule.
/// </summary>
internal interface IWalkLookInViewSource
{
IReadOnlyList<uint> LookInCellTurns { get; }
bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius);
}
internal readonly record struct WalkLookInSlice(int PlaneStart, int PlaneCount);
internal readonly record struct WalkLookInTurn(
uint CellId,
int SliceStart,
int SliceCount);
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's
/// per-turn list a flat, allocation-cheap <c>List&lt;WalkFrameEvent&gt;</c> —
@ -320,6 +361,9 @@ internal readonly struct WalkFrameEvent
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
internal static WalkFrameEvent LookInDynamics(uint cellId, int routeIndex) =>
new(WalkFrameEventKind.LookInDynamics, routeIndex, cellId, 0f, null);
internal static WalkFrameEvent ClearInteriorDepth() =>
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
@ -404,7 +448,7 @@ internal readonly struct WalkFrameEvent
/// @0x0059f2a0 (the <c>part-&gt;gfxobj[deg_level]!=0</c> gate @0x0059f2d3
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b0x0059f345).</para>
/// </summary>
internal sealed class WalkFrameDriver : IWalkEventSink
internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
{
private readonly WbDrawDispatcher _dispatcher;
private readonly WalkStaticStreamPopulator _populator;
@ -422,6 +466,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink
// walk pass dedicated to nothing but set-gathering.
internal HashSet<uint> VisitedCells { get; } = new();
/// <summary>Building look-in cells in the exact order the walk encountered
/// their <c>DrawCells</c> turns. Duplicates are intentional: two authored
/// portal views can independently visit the same cell and therefore own
/// distinct packed route indices.</summary>
internal List<uint> LookInCellTurns { get; } = new();
private readonly List<WalkLookInTurn> _lookInTurns = new();
private readonly List<WalkLookInSlice> _lookInSlices = new();
private readonly List<WalkPlane> _lookInPlanes = new();
private WalkPlane _lookInCyPlane;
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
/// exclusion and root-flood particle bookkeeping.</summary>
internal HashSet<uint> LookInCells { get; } = new();
/// <summary>FW4 slice 2: the interior root's ORDERED flood cell list,
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the
/// exit-portal seals (pc:432785-432786) — captured at
@ -459,6 +520,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
private bool _skyDrawnThisFrame;
private WalkDrawStage? _currentDcStage;
private bool _readyToReplay;
private int _lookInRouteIndex;
internal WalkFrameDriver(
WbDrawDispatcher dispatcher,
@ -534,7 +596,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
/// GPU work — see this type's own doc comment.
/// </summary>
internal void BeginFrame(
IWalkBuildingFrameContext ctx,
IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
@ -557,9 +619,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_events.Clear();
_markPositions.Clear();
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
_lookInSlices.Clear();
_lookInPlanes.Clear();
_lookInCyPlane = ctx.CyPlane;
LookInCells.Clear();
VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear();
_lookInRouteIndex = 0;
}
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
@ -655,6 +724,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
if (_staticParticleOwnerScratch.Count > 0)
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
break;
case WalkFrameEventKind.LookInDynamics:
_staticParticleOwnerScratch.Clear();
UnionOwners(
_worldData.GetCellStatics(e.CellId),
_staticParticleOwnerScratch);
_leafRenderer.DrawLookInDynamics(
e.CellId,
e.IntArg,
_staticParticleOwnerScratch);
break;
}
}
@ -883,6 +962,76 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection);
if (stage == WalkDrawStage.LookInStatic)
{
// Retail draws a look-in cell's complete object list at this
// re-entrant DrawCells turn. The packed dynamic route used to run
// much later at the pre-clear boundary, after nearer building
// shells, which let the cathedral's 0x112 remote player overpaint
// opaque walls. Keep animation/fade in the packed route, but replay
// it here between this cell's content and the building shell.
MarkIfGrown();
int routeIndex = _lookInRouteIndex++;
LookInCellTurns.Add(cellId);
LookInCells.Add(cellId);
CaptureLookInViews(cellId);
_events.Add(WalkFrameEvent.LookInDynamics(cellId, routeIndex));
}
}
private void CaptureLookInViews(uint cellId)
{
IWalkBuildingFrameContext ctx = RequireOpenFrame();
WalkCell? cell = ctx.GetVisible(cellId);
if (cell is null || cell.NumView <= 0)
{
_lookInTurns.Add(new WalkLookInTurn(
cellId, _lookInSlices.Count, 0));
return;
}
WalkPortalView portalView = cell.TopView;
int sliceStart = _lookInSlices.Count;
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
{
WalkViewPoly poly = portalView.View.Polys[sliceIndex];
int planeStart = _lookInPlanes.Count;
for (int edge = 0; edge < poly.VertexCount; edge++)
{
_lookInPlanes.Add(
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
}
_lookInSlices.Add(new WalkLookInSlice(
planeStart, poly.VertexCount));
}
_lookInTurns.Add(new WalkLookInTurn(
cellId, sliceStart, _lookInSlices.Count - sliceStart));
}
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return false;
WalkLookInTurn turn = _lookInTurns[routeIndex];
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (WalkVisibilityMath.ViewconeCheck(
center,
radius,
_lookInCyPlane,
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
slice.PlaneStart,
slice.PlaneCount)) != WalkBoundingType.Outside)
{
return true;
}
}
return false;
}
/// <summary>Campaign FW3.4a: the collect-time analogue of the old