fix(render): S3 chunk 1 round 2 — one weather OC per frame (pinned), literal EC/OC comparison, comment/cost/wording items
Campaign OVERHAUL S3 chunk 1 fix round 2 (docs/research/2026-09-01-overhaul/ s3-walk-ownership-map.md §11.6), applied on top of 36be6b598 after the three-lens re-review. H1-H6, the last round the plan allows. H1 — the weather OC printed 2-4 times per interior-rooted frame instead of once: the print (and the `_sky.RenderWeather` mesh draw it sits beside) lived inside `DrawLandscapeSliceLate`, which `RetailPViewRenderer` calls once per active `OutsideViewSlices` entry. Moved both into a new `RetailPViewPassExecutor.DrawWeatherOnce`, called ONCE, unclipped/no-scissor, after the slice loop in `DrawLandscapeDynamicsPhase` — retail's own `GameSky::Draw(1)` runs once, after `LScape::draw`'s whole landblock loop. The gate is extracted as a pure `ShouldDrawWeatherOnce(bool,bool,uint)` predicate (retail's `SmartBox::is_player_outside` ANDed with the two render toggles) so `RetailPViewPassExecutorTests` can pin "no OC while the player stands indoors" without a live GL/DAT `SkyRenderer`; two structural (CompiledCallGraph) tests prove the call moved out of the per-slice loop and that the mesh draw + print each fire exactly once per invocation — this codebase has no existing runtime-construction fixture for `RetailPViewPassExecutor`, so the pin is structural + a testable pure gate rather than an end-to-end GL drive. One deliberate deviation from the literal "gated exactly as today": the print now runs through `_sky?.RenderWeather(...)` (null-conditional, matching every other `_sky?.RenderSky(...)` call site in this codebase) instead of an explicit `if (_sky is not null)` wrapper — a missing sky asset no longer also suppresses the transcript print, since the print's only job is trace fidelity and retail's own `GameSky` is never null. H2 — WalkFrameDriver's EC-print comment still claimed "EC and OC counts are always exactly equal"; replaced with the real citation (holtburg-doorway-still.walk.log:1126,1131,1134,1137 — four EC prints for one cell across four look-in DC turns), matching WalkTranscriptDump. PrintEnvCellShell's own comment (already corrected in round 1). H3 — the eight-kind signature's ORACLE side (WalkTraceReplayContext.Signature8(WalkOracleFrame)) derived EC/OC from each DC's cell list — the SAME derivation the REPLAY side's Recorder already used, so the comparison could never disagree with itself on EC/OC placement or content (how G7's SC-ordering regression shipped green with LC/SC). Now reads its own literally captured EC/OC events, excluding only the trailing per-frame weather OC — refined beyond the spec's literal "last event + P-cell id" rule with an additional "P cell itself looks outdoor" check, after cathedral-arrival and foundry-deep both proved the naive rule also strips an interior root's OWN real trailing object-list turn when its nearest cell (the reversed flood loop's last draw) happens to be the camera's own root cell. All four kit-pose rows still reproduce: three exactly; #458 (this round's new issue, formerly an inline-only note) re-verified at the SAME token index 165 under the new literal comparison. H4 — OnSortCellTurn ran RequireOpenFrame + two range validations before testing the flag. Since the hook is print-only (no stream side effect, unlike OnLandCellTurn's unconditional WalkFrameEvent record), the flag check now runs FIRST and returns immediately when off — flag-off cost drops to one interface dispatch per visited land cell. H5 — launch-options.md's ACDREAM_DUMP_WALK_TRANSCRIPT row: measured ≈1,200-1,400 lines per outdoor frame (terrace-edge 1,384; cathedral-arrival 1,269; doorway 1,187), replacing the earlier "600-800" estimate; documents H4's residual flag-off interface-dispatch cost. H6 — filed docs/ISSUES.md #458 for round 1's LOD-boundary land-cell divergence (previously only an inline test comment); the InstalledDat lane's known-failure set is now four (two #383 layout tests, TowerAscent, #458) — confirmed by a clean run. MUTATION CHECKS (both restored after confirming failure): - H1: deleting the OC print inside DrawWeatherOnce made DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce fail with "Assert.Single() Failure: The collection did not contain any matching items". - H3: reversing AppendFloodTurns (OC before EC) made Still_fixture_first_frame_reproduces_exactly(cathedral-arrival.walk) fail ("walk diverged from retail (cathedral-arrival.walk)"), diverging at token index 1241: expected "EC:f4180112" vs actual "OC:f4180112". Gates: hermetic App suite 6823/6823 passed; InstalledDat lane 244/249 passed with exactly the four known failures (two #383 layout tests, TowerAscent, Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9b55d78a28
commit
88c70072e0
8 changed files with 333 additions and 99 deletions
|
|
@ -226,39 +226,16 @@ public RetailPViewPassExecutor(
|
|||
// DrawLandscapeStaticParticles after the slice loop (retail: one
|
||||
// unclipped alpha-list insertion per emitter), not per slice here.
|
||||
EnableClipDistances();
|
||||
// Retail GameSky::Draw @0x00506ff0 gates the WEATHER pass (arg2==1)
|
||||
// on SmartBox::is_player_outside @0x00451e80 — Ghidra-arbitrated:
|
||||
// (player objcell_id & 0xFFFF) < 0x100. The sky pass always draws;
|
||||
// the rain draws ONLY while the PLAYER stands in an outdoor cell.
|
||||
// That one boolean is retail's entire rain confinement (the
|
||||
// owner-reported indoor/seam rain at the cathedral: rain on the
|
||||
// ledges, none the instant the player crosses into an interior
|
||||
// cell) — no depth or view-clip mechanism is involved.
|
||||
bool playerOutside = (frame.PlayerCellId & 0xFFFFu) < 0x100u;
|
||||
if (frame.RenderSky && frame.RenderWeather && playerOutside)
|
||||
// S3 chunk 1 fix round 2 (§11.6 H1): the sky/rain MESH draw and its
|
||||
// OC print used to live HERE, re-running once per active landscape
|
||||
// view (2-4 times for an interior root's exit views) — retail draws
|
||||
// it ONCE per frame, after LScape::draw's whole landblock loop
|
||||
// completes (see DrawWeatherOnce's own doc comment). Only the
|
||||
// per-slice rain PARTICLE emitters stay here — they legitimately
|
||||
// clip against this slice's own view (ParticleRenderPass.
|
||||
// SkyPostScene, clipSlot=slice.Slot), unlike the unclipped mesh.
|
||||
if (ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
|
||||
{
|
||||
if (_sky is not null)
|
||||
{
|
||||
_sky.RenderWeather(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
frame.DayFraction,
|
||||
frame.ActiveDayGroup,
|
||||
frame.SkyKeyframe,
|
||||
frame.EnvironOverrideActive);
|
||||
|
||||
// S3 chunk 1 fix round 1 (§11.5 G8): GameSky::Draw
|
||||
// @0x00506ff0's weather branch (arg2==1) calls
|
||||
// DrawObjCellForDummies(after_sky_cell) @0x005070da when
|
||||
// LScape::weather_enabled != 0 — the after-sky cell's DID is
|
||||
// the viewer's own land cell, the SAME id the frame-root "P"
|
||||
// line already carries (terrace-edge.walk.log:1416
|
||||
// "OC f418000b" = the P cell, once per complete outdoor
|
||||
// frame). Print-only, at the point this pass actually calls
|
||||
// RenderWeather — never gates weather rendering itself.
|
||||
AcDream.App.Rendering.Walk.WalkTranscriptDump.PrintObjectCellTurn(
|
||||
frame.ViewerCellId);
|
||||
}
|
||||
DisableClipDistances();
|
||||
if (_particles is not null && _particleRenderer is not null)
|
||||
{
|
||||
|
|
@ -280,6 +257,73 @@ public RetailPViewPassExecutor(
|
|||
DisableClipDistances();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass
|
||||
/// EXACTLY ONCE per frame — <c>GameSky::Draw</c> @0x00506ff0 with
|
||||
/// <c>arg2==1</c> runs AFTER <c>LScape::draw</c>'s landblock loop
|
||||
/// finishes (@0x00506396), not once per active landscape view. The old
|
||||
/// call site inside <see cref="DrawLandscapeSliceLate"/> ran once per
|
||||
/// <c>RetailPViewRenderer</c>'s <c>OutsideViewSlices</c> entry — 2-4 for
|
||||
/// an interior root's exit views — printing 2-4 "OC" lines and
|
||||
/// re-submitting the rain mesh that many times; every capture shows
|
||||
/// exactly one. This call is UNCLIPPED and sets no scissor (S3 chunk 4
|
||||
/// deletes the slice loop's remaining scissor/clip apparatus outright);
|
||||
/// callers invoke it ONCE, after the slice loop that still runs
|
||||
/// <see cref="DrawLandscapeSliceLate"/> per view for the per-slice rain
|
||||
/// particles and dynamics.
|
||||
/// <para>
|
||||
/// Gated exactly as before —
|
||||
/// <see cref="ShouldDrawWeatherOnce(bool,bool,uint)"/>, retail's
|
||||
/// <c>SmartBox::is_player_outside</c> @0x00451e80 check ANDed with the
|
||||
/// two render toggles — via <c>_sky?.RenderWeather(...)</c> (null-
|
||||
/// conditional, matching every other <c>_sky?.RenderSky(...)</c> call
|
||||
/// site in this codebase): a missing sky asset silently skips the GL
|
||||
/// draw while the print below still fires, because the print's only
|
||||
/// job is transcript fidelity to retail's own trace, which never has a
|
||||
/// null <c>GameSky</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void DrawWeatherOnce(RetailPViewFrameInput frame)
|
||||
{
|
||||
if (!ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
|
||||
return;
|
||||
|
||||
DisableClipDistances();
|
||||
_sky?.RenderWeather(
|
||||
frame.Camera,
|
||||
frame.CameraWorldPosition,
|
||||
frame.DayFraction,
|
||||
frame.ActiveDayGroup,
|
||||
frame.SkyKeyframe,
|
||||
frame.EnvironOverrideActive);
|
||||
|
||||
// GameSky::Draw @0x00506ff0's weather branch (arg2==1) calls
|
||||
// DrawObjCellForDummies(after_sky_cell) @0x005070da when
|
||||
// LScape::weather_enabled != 0 — the after-sky cell's DID is the
|
||||
// viewer's own land cell, the SAME id the frame-root "P" line
|
||||
// already carries (terrace-edge.walk.log:1416 "OC f418000b" = the P
|
||||
// cell, once per complete outdoor frame — S3 chunk 1 fix round 1
|
||||
// §11.5 G8's original finding; this round fixes the call site that
|
||||
// reported it 2-4 times). Print-only — never gates weather
|
||||
// rendering itself.
|
||||
AcDream.App.Rendering.Walk.WalkTranscriptDump.PrintObjectCellTurn(frame.ViewerCellId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather pass's gate, extracted
|
||||
/// as a pure predicate — retail's <c>SmartBox::is_player_outside</c>
|
||||
/// @0x00451e80 check, <c>(player objcell_id & 0xFFFF) < 0x100</c>,
|
||||
/// ANDed with the two render toggles. Internal (not private) so a test
|
||||
/// can pin "no weather while the player stands indoors" and "no weather
|
||||
/// with either toggle off" without a live GL/DAT <see cref="SkyRenderer"/>
|
||||
/// — <see cref="DrawWeatherOnce"/> itself still needs one to actually
|
||||
/// draw, but this predicate alone decides whether it would even try (and
|
||||
/// therefore whether the OC print fires).
|
||||
/// </summary>
|
||||
internal static bool ShouldDrawWeatherOnce(
|
||||
bool renderSky, bool renderWeather, uint playerCellId)
|
||||
=> renderSky && renderWeather && (playerCellId & 0xFFFFu) < 0x100u;
|
||||
|
||||
public void DrawLandscapeStaticParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
uint cellId)
|
||||
|
|
|
|||
|
|
@ -641,10 +641,10 @@ internal sealed class RetailPViewRenderer
|
|||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||
|
||||
// GameSky's weather pass still runs through each active landscape
|
||||
// view so doorway scissor/clip state matches the old executor. The
|
||||
// dynamics collection is intentionally empty: the walk already drew
|
||||
// those records at OnLandscapeCellTurn.
|
||||
// The per-slice rain PARTICLE emitters still run through each
|
||||
// active landscape view so doorway scissor/clip state matches the
|
||||
// old executor. The dynamics collection is intentionally empty: the
|
||||
// walk already drew those records at OnLandscapeCellTurn.
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
|
|
@ -656,6 +656,12 @@ internal sealed class RetailPViewRenderer
|
|||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
|
||||
// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
||||
// print run ONCE here, after the slice loop above — not once per
|
||||
// slice inside it (DrawWeatherOnce's own doc comment has the
|
||||
// decomp citation and the retail evidence).
|
||||
passes.DrawWeatherOnce(ctx);
|
||||
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1410,9 +1410,22 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
/// <see cref="IWalkEventSink.OnLandscapeCellTurn(uint,int,int)"/> AFTER
|
||||
/// DrawBuilding had already run). Print-only — no stream-side effect of
|
||||
/// its own; the object-list turn stays entirely in
|
||||
/// <c>OnLandscapeCellTurn</c> below, unchanged.</summary>
|
||||
/// <c>OnLandscapeCellTurn</c> below, unchanged.
|
||||
/// <para>
|
||||
/// S3 chunk 1 fix round 2 (§11.6 H4): UNLIKE <see cref="OnLandCellTurn"/>
|
||||
/// (whose <c>RequireOpenFrame</c>/range validation stay unconditional —
|
||||
/// that hook always records a real <c>WalkFrameEvent</c>), this hook has
|
||||
/// NO side effect beyond the print, so the flag gate wraps the ENTIRE
|
||||
/// body, not just the <c>LodCellId</c> computation: an ordinary launch
|
||||
/// pays exactly one interface dispatch per visited land cell and nothing
|
||||
/// else (docs/launch-options.md).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
void IWalkEventSink.OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex)
|
||||
{
|
||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
||||
return;
|
||||
|
||||
RequireOpenFrame();
|
||||
if (sideCellCount is not (1 or 2 or 4 or 8))
|
||||
{
|
||||
|
|
@ -1424,13 +1437,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
|
||||
throw new ArgumentOutOfRangeException(nameof(cellIndex));
|
||||
|
||||
// S3 chunk 1 fix round 1 (G2): guard BEFORE LodCellId, matching
|
||||
// OnLandCellTurn's own fix — see that method's comment.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
||||
{
|
||||
WalkTranscriptDump.PrintSortCell(
|
||||
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
||||
}
|
||||
WalkTranscriptDump.PrintSortCell(
|
||||
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
|
||||
|
|
@ -1781,10 +1789,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// SetDrawnThisFrame) sits INSIDE DrawEnvCell, past this
|
||||
// breakpoint's address, so every flood visit prints an EC line
|
||||
// even when the visit's actual shell submission is suppressed —
|
||||
// the OH captures' EC and OC counts are always exactly equal per
|
||||
// pose (e.g. 45/45 at holtburg-doorway-still), which only holds
|
||||
// if EC is visit-scoped like OC, not stamp-deduped like the
|
||||
// WalkFrameEvent.CellShell submission below.
|
||||
// proof: holtburg-doorway-still.walk.log:1126,1131,1134,1137
|
||||
// print "EC a9b40100" four times in ONE frame across four
|
||||
// look-in DC turns that all redraw the same cell. S3 chunk 1 fix
|
||||
// round 2 (§11.6 H2): "EC and OC counts are always exactly
|
||||
// equal" (an earlier version of this comment) is FALSE —
|
||||
// terrace-edge has 12 EC vs 16 OC, the extra 4 being the
|
||||
// trailing per-frame weather OC (§11.6 H1), which has no EC
|
||||
// counterpart at all; see WalkTranscriptDump.PrintEnvCellShell's
|
||||
// own doc comment for the same citation.
|
||||
WalkTranscriptDump.PrintEnvCellShell(cells[i]);
|
||||
if (_cellShellsDrawnThisFrame.Add(cells[i]))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -168,10 +168,14 @@ internal static class WalkTranscriptDump
|
|||
/// entry — the interior/look-in object-list turn (retail's outdoor
|
||||
/// <c>DrawObjCell</c> is a different function, folded into the "SC"
|
||||
/// line's own DrawSortCell call; it has no separate per-cell OC hook of
|
||||
/// its OWN). S3 chunk 1 fix round 1 (G8): this SAME line kind ALSO
|
||||
/// prints once more per COMPLETE outdoor frame, from a second call site
|
||||
/// entirely — <c>RetailPViewPassExecutor.DrawLandscapeSliceLate</c>'s
|
||||
/// weather pass, matching retail's <c>GameSky::Draw</c> @0x00506ff0
|
||||
/// its OWN). S3 chunk 1 fix round 1 (G8), corrected round 2 (§11.6 H1):
|
||||
/// this SAME line kind ALSO prints EXACTLY once more per COMPLETE
|
||||
/// outdoor frame, from a second call site entirely —
|
||||
/// <c>RetailPViewPassExecutor.DrawWeatherOnce</c>, called ONCE after the
|
||||
/// slice loop (round 1 had this inside
|
||||
/// <c>DrawLandscapeSliceLate</c>'s per-slice loop, printing 2-4 times
|
||||
/// for an interior root's exit views — a mismatch every capture
|
||||
/// disproves) — matching retail's <c>GameSky::Draw</c> @0x00506ff0
|
||||
/// (<c>arg2==1</c>) calling <c>DrawObjCellForDummies(after_sky_cell)</c>
|
||||
/// @0x005070da when the player stands outside — the after-sky cell's DID
|
||||
/// is the viewer's own land cell (the SAME id the frame-root "P" line
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue