fix(render): draw look-in dynamics only at retail walk turns

This commit is contained in:
Erik 2026-08-31 03:22:01 +02:00
parent 572de1ec30
commit 966836895f
5 changed files with 84 additions and 37 deletions

View file

@ -1196,6 +1196,17 @@ internal sealed class RenderScenePViewFrameBuilder
_projectionIds.Clear();
LoadSceneIndices(input.Scene);
// The walk owns every look-in cell's complete object-list turn.
// Keep one set for both downstream dynamic routes: those objects
// belong to LookInObject exactly once, never to the generic
// landscape-outside route and never to DynamicLast. The former
// outside-route duplicate was especially damaging across an
// interior root's depth clear: its color survived the clear even
// when the later look-in draw was correctly occluded by a wall.
_lookInCellScratch.Clear();
for (int index = 0; index < input.LookInCellTurns.Count; index++)
_lookInCellScratch.Add(input.LookInCellTurns[index]);
// Campaign FW3.2b-2: LandscapeOutdoorStatic, LandscapeBuildingShell,
// and CellStatic no longer emit here — WalkFrameDriver draws every
// outdoor static, building shell, and cell static (including
@ -1347,6 +1358,10 @@ internal sealed class RenderScenePViewFrameBuilder
for (int i = 0; i < _dynamicCount; i++)
{
RenderProjectionRecord record = _dynamics[i];
uint? parentCellId = ParentCell(in record);
if (parentCellId is uint parentCell
&& _lookInCellScratch.Contains(parentCell))
continue;
Sphere(in record, out Vector3 center, out float radius);
bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage(
ParentCell(in record),
@ -1386,10 +1401,6 @@ internal sealed class RenderScenePViewFrameBuilder
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
_lookInCellScratch.Clear();
for (int index = 0; index < input.LookInCellTurns.Count; index++)
_lookInCellScratch.Add(input.LookInCellTurns[index]);
int count = 0;
EnsureCapacity(ref _survivors, _dynamicCount);
for (int i = 0; i < _dynamicCount; i++)

View file

@ -156,9 +156,9 @@ public interface IWalkEventSink
/// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) —
/// BOTH unconditional for an interior root's
/// own flood, whether or not a landscape turn just ran — and ONLY THEN
/// the flood's cells far-to-near (shell then contents per cell, same
/// discipline as <see cref="OnLandscapeCellTurn"/>'s per-cell contents
/// and a building's look-in). This hook fires at that later point, so
/// the flood's cells in two reverse passes: every EnvCell shell first,
/// then every cell object list (the same <c>PView::DrawCells</c>
/// discipline used by a building's look-in). This hook fires at that later point, so
/// this is where a driver should actually draw <paramref name="cells"/>.
/// Building look-in floods are UNAFFECTED — retail calls
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no

View file

@ -104,13 +104,11 @@ internal interface IWalkFrameLeafRenderer
void DrawTerrainSlice(int sliceIndex);
/// <summary>One committed cell's EnvCell shell —
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe, which
/// precedes <c>DrawObjCellForDummies</c> @0x005a4b0d (the cell's static
/// contents, appended to the stream instead — see
/// <see cref="WalkFrameDriver"/>'s type doc comment) for every cell of
/// EVERY flood this stage drives (the ordinary interior root's own
/// <c>DrawCells</c> AND a building's look-in <c>DrawCells</c> both walk
/// this same shell-then-contents order).</summary>
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe. Retail first
/// draws ALL shells in reverse <c>cell_draw_list</c> order, then starts a
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
/// The ordinary interior-root flood and every building look-in flood use
/// this same two-pass discipline.</summary>
void DrawCellShell(uint cellId);
/// <summary>One landscape cell's or building shell's static-owner
@ -421,16 +419,17 @@ internal readonly struct WalkFrameEvent
/// <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only gets a
/// mark ahead of whatever non-stream event comes next (the next building's
/// alpha barrier, or the final mark at <see cref="Replay"/>'s prepare step).
/// This single rule, combined with "shell before contents" per cell,
/// This single rule, combined with retail's two reverse flood passes (ALL
/// shells, then ALL contents),
/// retail's own building order (alpha barrier → portal pass → shell — see
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
/// comment; this is NOT the order the walk's EVENTS fire in, which is
/// breakpoint-entry order matching the FW0 oracle traces), is what produces
/// every ordering constraint the plan's frame script names: [cell1 shell]
/// [cell1 contents mark] [cell2 shell] …, [alpha barrier] [punch fan(s) +
/// look-in flood(s), each following the SAME shell-then-contents per-cell
/// every ordering constraint the plan's frame script names: [far shell] …
/// [near shell] [far contents] … [near contents], [alpha barrier] [punch
/// fan(s) + look-in flood(s), each following the SAME reverse two-pass
/// discipline] [building shell content mark], [landscape (if exit views
/// survived)] [interior depth clear] [exit-portal seals] [the interior
/// root's own flood cells], and a final mark at Replay's prepare step. No
@ -440,8 +439,9 @@ internal readonly struct WalkFrameEvent
/// root <see cref="RetailFrameWalk.WalkFrame"/> already ports),
/// <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 (building-before-
/// DrawObjCell per landscape cell), <c>PView::DrawCells</c> @0x005a4840
/// (<c>DrawEnvCell</c> @0x005a4abe before <c>DrawObjCellForDummies</c>
/// @0x005a4b0d per flooded cell; <c>LScape::draw</c> pc:432719, the depth
/// (the complete reverse <c>DrawEnvCell</c> loop @0x005a4a000x005a4ade
/// before the complete reverse <c>DrawObjCellForDummies</c> loop
/// @0x005a4ade0x005a4b2d; <c>LScape::draw</c> pc:432719, the depth
/// clear pc:432731-432732, and the exit-portal seals pc:432785-432786 —
/// ALL strictly before the flood's own cells, though the event marking
/// <c>DrawCells</c> entry fires before all three), <c>RenderDeviceD3D::DrawBuilding</c>
@ -878,8 +878,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
for (int i = 0; i < cells.Count; i++)
InteriorFloodCells.Add(cells[i]);
for (int i = 0; i < cells.Count; i++)
EmitCellTurn(WalkDrawStage.CellStatic, cells[i]);
EmitFloodTurns(WalkDrawStage.CellStatic, cells);
}
// ------------------------------------------------------------------
@ -949,19 +948,36 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// Any other stage (LookInStatic) is a building's look-in flood:
// retail calls DrawCells re-entrantly there with no landscape/clear/
// seal step, so its DC event already fires at the real draw point —
// record immediately, unchanged from before this correction.
for (int i = 0; i < cells.Count; i++)
EmitCellTurn(stage, cells[i]);
// record immediately. PView::DrawCells uses two complete reverse
// loops: every EnvCell shell first, then every cell object list.
EmitFloodTurns(stage, cells);
}
private void EmitCellTurn(WalkDrawStage stage, uint cellId)
private void EmitFloodTurns(WalkDrawStage stage, IReadOnlyList<uint> cells)
{
// PView::DrawCells @0x005A4840, loop 2 (005A4A00005A4ADE):
// cell_draw_list[count - 1] down to zero, DrawEnvCell only.
for (int i = cells.Count - 1; i >= 0; i--)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.CellShell(cells[i]));
}
// Loop 3 (005A4ADE005A4B2D): restart at count - 1 and draw each
// complete object cell. Keep a stream mark per cell so the packed
// static records and its dynamic/particle tail stay at that exact
// retail turn.
for (int i = cells.Count - 1; i >= 0; i--)
EmitCellContentsTurn(stage, cells[i]);
}
private void EmitCellContentsTurn(WalkDrawStage stage, uint cellId)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.CellShell(cellId));
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
_populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection);
MarkIfGrown();
if (stage == WalkDrawStage.LookInStatic)
{
// Retail draws a look-in cell's complete object list at this
@ -970,7 +986,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// 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);