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();
|
_projectionIds.Clear();
|
||||||
LoadSceneIndices(input.Scene);
|
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,
|
// Campaign FW3.2b-2: LandscapeOutdoorStatic, LandscapeBuildingShell,
|
||||||
// and CellStatic no longer emit here — WalkFrameDriver draws every
|
// and CellStatic no longer emit here — WalkFrameDriver draws every
|
||||||
// outdoor static, building shell, and cell static (including
|
// outdoor static, building shell, and cell static (including
|
||||||
|
|
@ -1347,6 +1358,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
for (int i = 0; i < _dynamicCount; i++)
|
for (int i = 0; i < _dynamicCount; i++)
|
||||||
{
|
{
|
||||||
RenderProjectionRecord record = _dynamics[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);
|
Sphere(in record, out Vector3 center, out float radius);
|
||||||
bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage(
|
bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage(
|
||||||
ParentCell(in record),
|
ParentCell(in record),
|
||||||
|
|
@ -1386,10 +1401,6 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
RenderFrameWriter writer,
|
RenderFrameWriter writer,
|
||||||
in RenderScenePViewBuildInput input)
|
in RenderScenePViewBuildInput input)
|
||||||
{
|
{
|
||||||
_lookInCellScratch.Clear();
|
|
||||||
for (int index = 0; index < input.LookInCellTurns.Count; index++)
|
|
||||||
_lookInCellScratch.Add(input.LookInCellTurns[index]);
|
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
EnsureCapacity(ref _survivors, _dynamicCount);
|
EnsureCapacity(ref _survivors, _dynamicCount);
|
||||||
for (int i = 0; i < _dynamicCount; i++)
|
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) —
|
/// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) —
|
||||||
/// BOTH unconditional for an interior root's
|
/// BOTH unconditional for an interior root's
|
||||||
/// own flood, whether or not a landscape turn just ran — and ONLY THEN
|
/// 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
|
/// the flood's cells in two reverse passes: every EnvCell shell first,
|
||||||
/// discipline as <see cref="OnLandscapeCellTurn"/>'s per-cell contents
|
/// then every cell object list (the same <c>PView::DrawCells</c>
|
||||||
/// and a building's look-in). This hook fires at that later point, so
|
/// 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"/>.
|
/// this is where a driver should actually draw <paramref name="cells"/>.
|
||||||
/// Building look-in floods are UNAFFECTED — retail calls
|
/// Building look-in floods are UNAFFECTED — retail calls
|
||||||
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no
|
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no
|
||||||
|
|
|
||||||
|
|
@ -104,13 +104,11 @@ internal interface IWalkFrameLeafRenderer
|
||||||
void DrawTerrainSlice(int sliceIndex);
|
void DrawTerrainSlice(int sliceIndex);
|
||||||
|
|
||||||
/// <summary>One committed cell's EnvCell shell —
|
/// <summary>One committed cell's EnvCell shell —
|
||||||
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe, which
|
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe. Retail first
|
||||||
/// precedes <c>DrawObjCellForDummies</c> @0x005a4b0d (the cell's static
|
/// draws ALL shells in reverse <c>cell_draw_list</c> order, then starts a
|
||||||
/// contents, appended to the stream instead — see
|
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
|
||||||
/// <see cref="WalkFrameDriver"/>'s type doc comment) for every cell of
|
/// The ordinary interior-root flood and every building look-in flood use
|
||||||
/// EVERY flood this stage drives (the ordinary interior root's own
|
/// this same two-pass discipline.</summary>
|
||||||
/// <c>DrawCells</c> AND a building's look-in <c>DrawCells</c> both walk
|
|
||||||
/// this same shell-then-contents order).</summary>
|
|
||||||
void DrawCellShell(uint cellId);
|
void DrawCellShell(uint cellId);
|
||||||
|
|
||||||
/// <summary>One landscape cell's or building shell's static-owner
|
/// <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
|
/// <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only gets a
|
||||||
/// mark ahead of whatever non-stream event comes next (the next building's
|
/// 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).
|
/// 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
|
/// retail's own building order (alpha barrier → portal pass → shell — see
|
||||||
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
|
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
|
||||||
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
|
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
|
||||||
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
|
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
|
||||||
/// comment; this is NOT the order the walk's EVENTS fire in, which is
|
/// 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
|
/// breakpoint-entry order matching the FW0 oracle traces), is what produces
|
||||||
/// every ordering constraint the plan's frame script names: [cell1 shell]
|
/// every ordering constraint the plan's frame script names: [far shell] …
|
||||||
/// [cell1 contents mark] [cell2 shell] …, [alpha barrier] [punch fan(s) +
|
/// [near shell] [far contents] … [near contents], [alpha barrier] [punch
|
||||||
/// look-in flood(s), each following the SAME shell-then-contents per-cell
|
/// fan(s) + look-in flood(s), each following the SAME reverse two-pass
|
||||||
/// discipline] [building shell content mark], [landscape (if exit views
|
/// discipline] [building shell content mark], [landscape (if exit views
|
||||||
/// survived)] [interior depth clear] [exit-portal seals] [the interior
|
/// survived)] [interior depth clear] [exit-portal seals] [the interior
|
||||||
/// root's own flood cells], and a final mark at Replay's prepare step. No
|
/// 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),
|
/// root <see cref="RetailFrameWalk.WalkFrame"/> already ports),
|
||||||
/// <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 (building-before-
|
/// <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 (building-before-
|
||||||
/// DrawObjCell per landscape cell), <c>PView::DrawCells</c> @0x005a4840
|
/// DrawObjCell per landscape cell), <c>PView::DrawCells</c> @0x005a4840
|
||||||
/// (<c>DrawEnvCell</c> @0x005a4abe before <c>DrawObjCellForDummies</c>
|
/// (the complete reverse <c>DrawEnvCell</c> loop @0x005a4a00–0x005a4ade
|
||||||
/// @0x005a4b0d per flooded cell; <c>LScape::draw</c> pc:432719, the depth
|
/// 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 —
|
/// clear pc:432731-432732, and the exit-portal seals pc:432785-432786 —
|
||||||
/// ALL strictly before the flood's own cells, though the event marking
|
/// ALL strictly before the flood's own cells, though the event marking
|
||||||
/// <c>DrawCells</c> entry fires before all three), <c>RenderDeviceD3D::DrawBuilding</c>
|
/// <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++)
|
for (int i = 0; i < cells.Count; i++)
|
||||||
InteriorFloodCells.Add(cells[i]);
|
InteriorFloodCells.Add(cells[i]);
|
||||||
|
|
||||||
for (int i = 0; i < cells.Count; i++)
|
EmitFloodTurns(WalkDrawStage.CellStatic, cells);
|
||||||
EmitCellTurn(WalkDrawStage.CellStatic, cells[i]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
@ -949,19 +948,36 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
// Any other stage (LookInStatic) is a building's look-in flood:
|
// Any other stage (LookInStatic) is a building's look-in flood:
|
||||||
// retail calls DrawCells re-entrantly there with no landscape/clear/
|
// retail calls DrawCells re-entrantly there with no landscape/clear/
|
||||||
// seal step, so its DC event already fires at the real draw point —
|
// seal step, so its DC event already fires at the real draw point —
|
||||||
// record immediately, unchanged from before this correction.
|
// record immediately. PView::DrawCells uses two complete reverse
|
||||||
for (int i = 0; i < cells.Count; i++)
|
// loops: every EnvCell shell first, then every cell object list.
|
||||||
EmitCellTurn(stage, cells[i]);
|
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();
|
MarkIfGrown();
|
||||||
_events.Add(WalkFrameEvent.CellShell(cellId));
|
_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)
|
||||||
|
{
|
||||||
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
||||||
_populator.PopulateCell(
|
_populator.PopulateCell(
|
||||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||||
_cameraWorldPosition, _viewProjection);
|
_cameraWorldPosition, _viewProjection);
|
||||||
|
MarkIfGrown();
|
||||||
if (stage == WalkDrawStage.LookInStatic)
|
if (stage == WalkDrawStage.LookInStatic)
|
||||||
{
|
{
|
||||||
// Retail draws a look-in cell's complete object list at this
|
// 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
|
// shells, which let the cathedral's 0x112 remote player overpaint
|
||||||
// opaque walls. Keep animation/fade in the packed route, but replay
|
// opaque walls. Keep animation/fade in the packed route, but replay
|
||||||
// it here between this cell's content and the building shell.
|
// it here between this cell's content and the building shell.
|
||||||
MarkIfGrown();
|
|
||||||
int routeIndex = _lookInRouteIndex++;
|
int routeIndex = _lookInRouteIndex++;
|
||||||
LookInCellTurns.Add(cellId);
|
LookInCellTurns.Add(cellId);
|
||||||
LookInCells.Add(cellId);
|
LookInCells.Add(cellId);
|
||||||
|
|
|
||||||
|
|
@ -193,10 +193,14 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
0x0100_0000_0000_0022,
|
0x0100_0000_0000_0022,
|
||||||
RenderProjectionClass.LiveDynamicRoot,
|
RenderProjectionClass.LiveDynamicRoot,
|
||||||
parentCell: lookInCell);
|
parentCell: lookInCell);
|
||||||
|
RenderProjectionRecord outdoorDynamic = Record(
|
||||||
|
0x0100_0000_0000_0023,
|
||||||
|
RenderProjectionClass.LiveDynamicRoot);
|
||||||
scene.Apply(
|
scene.Apply(
|
||||||
[
|
[
|
||||||
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
|
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
|
||||||
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
|
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
|
||||||
|
RenderProjectionDelta.Register(Generation, 3, outdoorDynamic),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
PortalVisibilityFrame portal = Portal(Cell);
|
PortalVisibilityFrame portal = Portal(Cell);
|
||||||
|
|
@ -235,6 +239,18 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
.ToArray()
|
.ToArray()
|
||||||
.Select(static record => record.Id));
|
.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(
|
RenderFrameCandidateRange dynamicLast = Assert.Single(
|
||||||
frame.RouteRanges.ToArray(),
|
frame.RouteRanges.ToArray(),
|
||||||
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
|
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
|
/// Campaign FW stage FW3.2b-1: <see cref="WalkFrameDriver"/>'s headless
|
||||||
/// referee suite — proves that driving <see cref="RetailFrameWalk"/> with the
|
/// referee suite — proves that driving <see cref="RetailFrameWalk"/> with the
|
||||||
/// driver as its <see cref="IWalkEventSink"/> produces retail's own turn
|
/// 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"/>
|
/// content-before-punch) through the REAL <see cref="WbDrawDispatcher.SubmitOrderedStream"/>
|
||||||
/// onto a <see cref="RecordingGpuDevice"/> — never a mock of the submission
|
/// onto a <see cref="RecordingGpuDevice"/> — never a mock of the submission
|
||||||
/// path itself. No production wiring is exercised (<c>WorldSceneRenderer</c>
|
/// 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),
|
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Deliverable: RunFrame drives an interior two-cell flood; shell
|
// ── Deliverable: RunFrame drives an interior two-cell flood using
|
||||||
// precedes contents per cell, and a flush happens exactly at the point
|
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
|
||||||
// the NEXT cell's shell needs the stream clear (never before, never
|
// then ALL object cells far-to-near. ─────────────────────────────────
|
||||||
// batched across cells within this stage's turn-by-turn discipline). ──
|
|
||||||
|
|
||||||
// ── Deliverable (2026-08-30 decomp correction): PView::DrawCells
|
// ── Deliverable (2026-08-30 decomp correction): PView::DrawCells
|
||||||
// @0x005a4840's actual DRAW order for an interior root's OWN flood is
|
// @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.
|
// (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
|
// 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
|
// (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]
|
[Fact]
|
||||||
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
|
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
|
||||||
|
|
@ -258,7 +258,8 @@ public sealed class WalkFrameDriverTests
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"SKY", "TERRAIN:0", "CLEAR", "SEALS",
|
"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);
|
log);
|
||||||
|
|
||||||
|
|
@ -329,7 +330,11 @@ public sealed class WalkFrameDriverTests
|
||||||
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
|
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
|
||||||
// at all — but CLEAR/SEALS still fire unconditionally.
|
// at all — but CLEAR/SEALS still fire unconditionally.
|
||||||
Assert.Equal(
|
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);
|
log);
|
||||||
|
|
||||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue