fix(render): draw look-in dynamics only at retail walk turns
This commit is contained in:
parent
572de1ec30
commit
966836895f
5 changed files with 84 additions and 37 deletions
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 @0x005a4a00–0x005a4ade
|
||||
/// before the complete reverse <c>DrawObjCellForDummies</c> loop
|
||||
/// @0x005a4ade–0x005a4b2d; <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 (005A4A00–005A4ADE):
|
||||
// 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 (005A4ADE–005A4B2D): 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);
|
||||
|
|
|
|||
|
|
@ -193,10 +193,14 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
0x0100_0000_0000_0022,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: lookInCell);
|
||||
RenderProjectionRecord outdoorDynamic = Record(
|
||||
0x0100_0000_0000_0023,
|
||||
RenderProjectionClass.LiveDynamicRoot);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 3, outdoorDynamic),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
|
|
@ -235,6 +239,18 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
|
||||
RenderFrameCandidateRange outside = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route
|
||||
== RenderFrameCandidateRoute.LandscapeOutsideDynamic);
|
||||
RenderProjectionId[] outsideIds = frame.RouteCandidates
|
||||
.Slice(outside.Offset, outside.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id)
|
||||
.ToArray();
|
||||
Assert.Contains(outdoorDynamic.Id, outsideIds);
|
||||
Assert.DoesNotContain(lookInDynamic.Id, outsideIds);
|
||||
|
||||
RenderFrameCandidateRange dynamicLast = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
/// Campaign FW stage FW3.2b-1: <see cref="WalkFrameDriver"/>'s headless
|
||||
/// referee suite — proves that driving <see cref="RetailFrameWalk"/> with the
|
||||
/// driver as its <see cref="IWalkEventSink"/> produces retail's own turn
|
||||
/// order (shell-then-contents per cell, flush-before-every-leaf-action,
|
||||
/// order (all shells then all contents in reverse flood order,
|
||||
/// flush-before-every-leaf-action,
|
||||
/// content-before-punch) through the REAL <see cref="WbDrawDispatcher.SubmitOrderedStream"/>
|
||||
/// onto a <see cref="RecordingGpuDevice"/> — never a mock of the submission
|
||||
/// path itself. No production wiring is exercised (<c>WorldSceneRenderer</c>
|
||||
|
|
@ -172,10 +173,9 @@ public sealed class WalkFrameDriverTests
|
|||
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
|
||||
};
|
||||
|
||||
// ── Deliverable: RunFrame drives an interior two-cell flood; shell
|
||||
// precedes contents per cell, and a flush happens exactly at the point
|
||||
// the NEXT cell's shell needs the stream clear (never before, never
|
||||
// batched across cells within this stage's turn-by-turn discipline). ──
|
||||
// ── Deliverable: RunFrame drives an interior two-cell flood using
|
||||
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
|
||||
// then ALL object cells far-to-near. ─────────────────────────────────
|
||||
|
||||
// ── Deliverable (2026-08-30 decomp correction): PView::DrawCells
|
||||
// @0x005a4840's actual DRAW order for an interior root's OWN flood is
|
||||
|
|
@ -186,7 +186,7 @@ public sealed class WalkFrameDriverTests
|
|||
// (pc:432785-432786), and ONLY THEN the flood's own cells far-to-near.
|
||||
// This case has a surviving exit view (ov=1): DC records the flood list
|
||||
// (no draw), the landscape turn runs (flush no-op, sky, terrain), THEN
|
||||
// clear, seals, and the two flood cells shell-then-contents. ──────────
|
||||
// clear, seals, then all shells and all contents in reverse order. ───
|
||||
|
||||
[Fact]
|
||||
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
|
||||
|
|
@ -258,7 +258,8 @@ public sealed class WalkFrameDriverTests
|
|||
new[]
|
||||
{
|
||||
"SKY", "TERRAIN:0", "CLEAR", "SEALS",
|
||||
"SHELL:00000100", "FLUSH:1:CellStatic", "SHELL:00000101", "FLUSH:1:CellStatic",
|
||||
"SHELL:00000101", "SHELL:00000100",
|
||||
"FLUSH:1:CellStatic", "FLUSH:1:CellStatic",
|
||||
},
|
||||
log);
|
||||
|
||||
|
|
@ -329,7 +330,11 @@ public sealed class WalkFrameDriverTests
|
|||
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
|
||||
// at all — but CLEAR/SEALS still fire unconditionally.
|
||||
Assert.Equal(
|
||||
new[] { "CLEAR", "SEALS", "SHELL:00000100", "FLUSH:1:CellStatic", "SHELL:00000101", "FLUSH:1:CellStatic" },
|
||||
new[]
|
||||
{
|
||||
"CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100",
|
||||
"FLUSH:1:CellStatic", "FLUSH:1:CellStatic",
|
||||
},
|
||||
log);
|
||||
|
||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue