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
|
|
@ -272,7 +272,7 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
|
||||||
| `ACDREAM_DUMP_GFXOBJS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` — Companion output-directory knob for ACDREAM_DUMP_GFXOBJS. | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` |
|
| `ACDREAM_DUMP_GFXOBJS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` — Companion output-directory knob for ACDREAM_DUMP_GFXOBJS. | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` |
|
||||||
| `ACDREAM_DUMP_SKY` | `=1` | Print-only: dumps decoded `SkyDesc` raw values on region load (`SkyDescLoader.cs`) and per-GfxObj `Surface.Type`/translucency flags on first upload (`SkyRenderer.cs`), plus gates a `TimeSync` console diagnostic in `GameWindow`. Built to resolve specific open questions about retail sky units and GfxObjReplace timing (2026-04-23 research), now answered but the dumps remain wired. — Generic sky-keyframe isolation dump (introduced with the phase-1 tint revert); a tool, not a bug probe. | Three independent reads of the SAME env var, only one of which (`RuntimeOptions.DumpSky`) goes through the typed options object; the other two are raw scattered reads (see Notes). `SkyRenderer.cs:582`'s raw read is in the App layer and has no architectural excuse for bypassing `RuntimeOptions` — `_options.DumpSky` was already available to that composition. `print-only` in all three sites. | off/unset | `RuntimeOptions.DumpSky` (typed) → `GameWindow.cs:704` (`TimeSyncDiagnostic`); **also** two independent raw `Environment.GetEnvironmentVariable` reads at `SkyDescLoader.cs:392` (Core) and `SkyRenderer.cs:582` (App) |
|
| `ACDREAM_DUMP_SKY` | `=1` | Print-only: dumps decoded `SkyDesc` raw values on region load (`SkyDescLoader.cs`) and per-GfxObj `Surface.Type`/translucency flags on first upload (`SkyRenderer.cs`), plus gates a `TimeSync` console diagnostic in `GameWindow`. Built to resolve specific open questions about retail sky units and GfxObjReplace timing (2026-04-23 research), now answered but the dumps remain wired. — Generic sky-keyframe isolation dump (introduced with the phase-1 tint revert); a tool, not a bug probe. | Three independent reads of the SAME env var, only one of which (`RuntimeOptions.DumpSky`) goes through the typed options object; the other two are raw scattered reads (see Notes). `SkyRenderer.cs:582`'s raw read is in the App layer and has no architectural excuse for bypassing `RuntimeOptions` — `_options.DumpSky` was already available to that composition. `print-only` in all three sites. | off/unset | `RuntimeOptions.DumpSky` (typed) → `GameWindow.cs:704` (`TimeSyncDiagnostic`); **also** two independent raw `Environment.GetEnvironmentVariable` reads at `SkyDescLoader.cs:392` (Core) and `SkyRenderer.cs:582` (App) |
|
||||||
| `ACDREAM_DUMP_STEEP_ROOF` | `=1` | gates `[steep-roof] KILL-VELOCITY-APPLIED` in `PhysicsEngine.ResolveWithTransition` when retail's `kill_velocity` zeroes body velocity on steep-slope impact, plus per-frame plane-normal traces in `TransitionTypes`/`PlayerMovementController` — KEEP: observes LIVE divergence-register row AD-56 (the plumb-fall freeze on steep-but-walkable polys, restored 2026-08-07). The only runtime lens on that active divergence; delete only with the AD-56 row itself. | print-only | off/unset | `PhysicsDiagnostics.DumpSteepRoofEnabled` |
|
| `ACDREAM_DUMP_STEEP_ROOF` | `=1` | gates `[steep-roof] KILL-VELOCITY-APPLIED` in `PhysicsEngine.ResolveWithTransition` when retail's `kill_velocity` zeroes body velocity on steep-slope impact, plus per-frame plane-normal traces in `TransitionTypes`/`PlayerMovementController` — KEEP: observes LIVE divergence-register row AD-56 (the plumb-fall freeze on steep-but-walkable polys, restored 2026-08-07). The only runtime lens on that active divergence; delete only with the AD-56 row itself. | print-only | off/unset | `PhysicsDiagnostics.DumpSteepRoofEnabled` |
|
||||||
| `ACDREAM_DUMP_WALK_TRANSCRIPT` | `=1` | Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the production frame walk (`RetailFrameWalk` + `WalkFrameDriver`) prints the OH oracle-trace line kinds — `F`/`P`/`LS`/`LC`/`SC`/`BLD`/`DI`/`DC`/`EC`/`OC` — to `Console.Out` at the exact points retail's cdb breakpoints sit (`tools/walk-oracle/oh/oh-capture-walk.cdb.template`), so a Release run's own transcript can be diffed offline against a live retail capture (frame-by-frame, at the DI/DC/BLD/LS/LC/SC/EC/OC level — the test-side `WalkTranscriptSignatureDiff` helper does the diff, not this flag). | flag off = one bool read per print site, nothing else; flag on = one console line per walk turn (≈600–800 lines per outdoor frame) — a print-only diagnostic for offline diffing, never for ordinary runs. Every print sits AFTER the walk has already decided to emit the corresponding turn — never gates admission, depth state, or draw order. Off, the emitter (`WalkTranscriptDump`) allocates nothing (`WalkFrameDriverTests.TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp`). | off/unset | `RuntimeOptions.DumpWalkTranscript` (typed, sole env read) → `GameWindow.cs` (one-time handoff at construction) → `RenderingDiagnostics.DumpWalkTranscriptEnabled` (the actual gate every call site checks) → `WalkTranscriptDump` / `WalkFrameDriver.cs` |
|
| `ACDREAM_DUMP_WALK_TRANSCRIPT` | `=1` | Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the production frame walk (`RetailFrameWalk` + `WalkFrameDriver`) prints the OH oracle-trace line kinds — `F`/`P`/`LS`/`LC`/`SC`/`BLD`/`DI`/`DC`/`EC`/`OC` — to `Console.Out` at the exact points retail's cdb breakpoints sit (`tools/walk-oracle/oh/oh-capture-walk.cdb.template`), so a Release run's own transcript can be diffed offline against a live retail capture (frame-by-frame, at the DI/DC/BLD/LS/LC/SC/EC/OC level — the test-side `WalkTranscriptSignatureDiff` helper does the diff, not this flag). | flag off = one bool read per print site, PLUS one `IWalkEventSink.OnSortCellTurn` interface dispatch per visited land cell (S3 chunk 1 fix round 2 §11.6 H4: that hook is print-only, so its own flag check sits FIRST and skips `RequireOpenFrame`/range validation too — the dispatch itself is the only cost the gate cannot avoid); nothing else. Flag on = one console line per walk turn — measured ≈1,200–1,400 lines per outdoor frame (terrace-edge 1,384; cathedral-arrival 1,269; doorway 1,187) — a print-only diagnostic for offline diffing, never for ordinary runs. Every print sits AFTER the walk has already decided to emit the corresponding turn — never gates admission, depth state, or draw order. Off, the emitter (`WalkTranscriptDump`) allocates nothing (`WalkFrameDriverTests.TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp`). | off/unset | `RuntimeOptions.DumpWalkTranscript` (typed, sole env read) → `GameWindow.cs` (one-time handoff at construction) → `RenderingDiagnostics.DumpWalkTranscriptEnabled` (the actual gate every call site checks) → `WalkTranscriptDump` / `WalkFrameDriver.cs` |
|
||||||
| `ACDREAM_HIDE_PART` | `=<int>` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. — Generic model-part isolation tool (issue #37 lineage but general-purpose since); a tool, not a bug probe. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | off/unset | `RuntimeOptions.HidePartIndex` → `LivePresentationComposition.cs:608` → `LiveEntityAnimationPresenter.cs:21,38,243` |
|
| `ACDREAM_HIDE_PART` | `=<int>` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. — Generic model-part isolation tool (issue #37 lineage but general-purpose since); a tool, not a bug probe. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | off/unset | `RuntimeOptions.HidePartIndex` → `LivePresentationComposition.cs:608` → `LiveEntityAnimationPresenter.cs:21,38,243` |
|
||||||
| `ACDREAM_PROBE_CELL` | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) — Standing cell-transit tracer (L.2a slice 1), pair of the permanent ACDREAM_PROBE_RESOLVE; recurs in every membership investigation. | print-only; low volume (only on actual cell crossings) | off/unset | `PhysicsDiagnostics.ProbeCellEnabled` |
|
| `ACDREAM_PROBE_CELL` | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) — Standing cell-transit tracer (L.2a slice 1), pair of the permanent ACDREAM_PROBE_RESOLVE; recurs in every membership investigation. | print-only; low volume (only on actual cell crossings) | off/unset | `PhysicsDiagnostics.ProbeCellEnabled` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,39 +226,16 @@ public RetailPViewPassExecutor(
|
||||||
// DrawLandscapeStaticParticles after the slice loop (retail: one
|
// DrawLandscapeStaticParticles after the slice loop (retail: one
|
||||||
// unclipped alpha-list insertion per emitter), not per slice here.
|
// unclipped alpha-list insertion per emitter), not per slice here.
|
||||||
EnableClipDistances();
|
EnableClipDistances();
|
||||||
// Retail GameSky::Draw @0x00506ff0 gates the WEATHER pass (arg2==1)
|
// S3 chunk 1 fix round 2 (§11.6 H1): the sky/rain MESH draw and its
|
||||||
// on SmartBox::is_player_outside @0x00451e80 — Ghidra-arbitrated:
|
// OC print used to live HERE, re-running once per active landscape
|
||||||
// (player objcell_id & 0xFFFF) < 0x100. The sky pass always draws;
|
// view (2-4 times for an interior root's exit views) — retail draws
|
||||||
// the rain draws ONLY while the PLAYER stands in an outdoor cell.
|
// it ONCE per frame, after LScape::draw's whole landblock loop
|
||||||
// That one boolean is retail's entire rain confinement (the
|
// completes (see DrawWeatherOnce's own doc comment). Only the
|
||||||
// owner-reported indoor/seam rain at the cathedral: rain on the
|
// per-slice rain PARTICLE emitters stay here — they legitimately
|
||||||
// ledges, none the instant the player crosses into an interior
|
// clip against this slice's own view (ParticleRenderPass.
|
||||||
// cell) — no depth or view-clip mechanism is involved.
|
// SkyPostScene, clipSlot=slice.Slot), unlike the unclipped mesh.
|
||||||
bool playerOutside = (frame.PlayerCellId & 0xFFFFu) < 0x100u;
|
if (ShouldDrawWeatherOnce(frame.RenderSky, frame.RenderWeather, frame.PlayerCellId))
|
||||||
if (frame.RenderSky && frame.RenderWeather && playerOutside)
|
|
||||||
{
|
{
|
||||||
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();
|
DisableClipDistances();
|
||||||
if (_particles is not null && _particleRenderer is not null)
|
if (_particles is not null && _particleRenderer is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -280,6 +257,73 @@ public RetailPViewPassExecutor(
|
||||||
DisableClipDistances();
|
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(
|
public void DrawLandscapeStaticParticles(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
uint cellId)
|
uint cellId)
|
||||||
|
|
|
||||||
|
|
@ -641,10 +641,10 @@ internal sealed class RetailPViewRenderer
|
||||||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||||
|
|
||||||
// GameSky's weather pass still runs through each active landscape
|
// The per-slice rain PARTICLE emitters still run through each
|
||||||
// view so doorway scissor/clip state matches the old executor. The
|
// active landscape view so doorway scissor/clip state matches the
|
||||||
// dynamics collection is intentionally empty: the walk already drew
|
// old executor. The dynamics collection is intentionally empty: the
|
||||||
// those records at OnLandscapeCellTurn.
|
// walk already drew those records at OnLandscapeCellTurn.
|
||||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||||
{
|
{
|
||||||
passes.SetTerrainClip(slice.Planes);
|
passes.SetTerrainClip(slice.Planes);
|
||||||
|
|
@ -656,6 +656,12 @@ internal sealed class RetailPViewRenderer
|
||||||
Array.Empty<WorldEntity>()));
|
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();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1410,9 +1410,22 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
/// <see cref="IWalkEventSink.OnLandscapeCellTurn(uint,int,int)"/> AFTER
|
/// <see cref="IWalkEventSink.OnLandscapeCellTurn(uint,int,int)"/> AFTER
|
||||||
/// DrawBuilding had already run). Print-only — no stream-side effect of
|
/// DrawBuilding had already run). Print-only — no stream-side effect of
|
||||||
/// its own; the object-list turn stays entirely in
|
/// 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)
|
void IWalkEventSink.OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex)
|
||||||
{
|
{
|
||||||
|
if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
RequireOpenFrame();
|
RequireOpenFrame();
|
||||||
if (sideCellCount is not (1 or 2 or 4 or 8))
|
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))
|
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
|
||||||
throw new ArgumentOutOfRangeException(nameof(cellIndex));
|
throw new ArgumentOutOfRangeException(nameof(cellIndex));
|
||||||
|
|
||||||
// S3 chunk 1 fix round 1 (G2): guard BEFORE LodCellId, matching
|
WalkTranscriptDump.PrintSortCell(
|
||||||
// OnLandCellTurn's own fix — see that method's comment.
|
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
|
||||||
{
|
|
||||||
WalkTranscriptDump.PrintSortCell(
|
|
||||||
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
|
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
|
||||||
|
|
@ -1781,10 +1789,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
// SetDrawnThisFrame) sits INSIDE DrawEnvCell, past this
|
// SetDrawnThisFrame) sits INSIDE DrawEnvCell, past this
|
||||||
// breakpoint's address, so every flood visit prints an EC line
|
// breakpoint's address, so every flood visit prints an EC line
|
||||||
// even when the visit's actual shell submission is suppressed —
|
// even when the visit's actual shell submission is suppressed —
|
||||||
// the OH captures' EC and OC counts are always exactly equal per
|
// proof: holtburg-doorway-still.walk.log:1126,1131,1134,1137
|
||||||
// pose (e.g. 45/45 at holtburg-doorway-still), which only holds
|
// print "EC a9b40100" four times in ONE frame across four
|
||||||
// if EC is visit-scoped like OC, not stamp-deduped like the
|
// look-in DC turns that all redraw the same cell. S3 chunk 1 fix
|
||||||
// WalkFrameEvent.CellShell submission below.
|
// 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]);
|
WalkTranscriptDump.PrintEnvCellShell(cells[i]);
|
||||||
if (_cellShellsDrawnThisFrame.Add(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
|
/// entry — the interior/look-in object-list turn (retail's outdoor
|
||||||
/// <c>DrawObjCell</c> is a different function, folded into the "SC"
|
/// <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
|
/// 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
|
/// its OWN). S3 chunk 1 fix round 1 (G8), corrected round 2 (§11.6 H1):
|
||||||
/// prints once more per COMPLETE outdoor frame, from a second call site
|
/// this SAME line kind ALSO prints EXACTLY once more per COMPLETE
|
||||||
/// entirely — <c>RetailPViewPassExecutor.DrawLandscapeSliceLate</c>'s
|
/// outdoor frame, from a second call site entirely —
|
||||||
/// weather pass, matching retail's <c>GameSky::Draw</c> @0x00506ff0
|
/// <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>
|
/// (<c>arg2==1</c>) calling <c>DrawObjCellForDummies(after_sky_cell)</c>
|
||||||
/// @0x005070da when the player stands outside — the after-sky cell's DID
|
/// @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
|
/// is the viewer's own land cell (the SAME id the frame-root "P" line
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Sky;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
@ -109,6 +111,129 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
&& call.Target.Name == ".ctor");
|
&& call.Target.Name == ".ctor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
||||||
|
/// print moved OUT of the per-slice loop into
|
||||||
|
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
|
||||||
|
/// (that method's own doc comment has the full decomp citation).
|
||||||
|
/// <see cref="RetailPViewPassExecutor.DrawLandscapeSliceLate"/> must no
|
||||||
|
/// longer call either <see cref="SkyRenderer.RenderWeather"/> or
|
||||||
|
/// <see cref="WalkTranscriptDump"/>'s OC print — only the per-slice
|
||||||
|
/// rain PARTICLE emitters (<c>ParticleRenderer.Draw</c>) stay here.
|
||||||
|
/// MUTATION: re-inlining either call back into this method makes the
|
||||||
|
/// corresponding <c>Assert.DoesNotContain</c> fail.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DrawLandscapeSliceLate_NoLongerDrawsOrPrintsTheWeatherPass()
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
||||||
|
nameof(RetailPViewPassExecutor.DrawLandscapeSliceLate),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public)!;
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(SkyRenderer)
|
||||||
|
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(WalkTranscriptDump)
|
||||||
|
&& call.Target.Name == "PrintObjectCellTurn");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S3 chunk 1 fix round 2 (§11.6 H1): the new call site itself — exactly
|
||||||
|
/// one <see cref="SkyRenderer.RenderWeather"/> draw and exactly one OC
|
||||||
|
/// print per invocation (retail draws the weather pass exactly once per
|
||||||
|
/// frame; this method is the ONE call site production now uses — see
|
||||||
|
/// <see cref="DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnceAfterTheSliceLoop"/>).
|
||||||
|
/// MUTATION: deleting either call makes the matching
|
||||||
|
/// <c>Assert.Single</c> fail (zero matches instead of one).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce()
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
||||||
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public)!;
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||||
|
|
||||||
|
Assert.Single(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(SkyRenderer)
|
||||||
|
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
||||||
|
Assert.Single(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(WalkTranscriptDump)
|
||||||
|
&& call.Target.Name == "PrintObjectCellTurn");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S3 chunk 1 fix round 2 (§11.6 H1): the actual wiring fix — production
|
||||||
|
/// must call <c>DrawWeatherOnce</c> exactly ONCE, positioned AFTER the
|
||||||
|
/// (single, in-loop) <c>DrawLandscapeSliceLate</c> call site. A
|
||||||
|
/// <c>foreach</c> loop compiles to ONE call instruction regardless of
|
||||||
|
/// how many <c>OutsideViewSlices</c> it iterates at runtime — an
|
||||||
|
/// interior root's 2-4 exit views would otherwise still print 2-4 OC
|
||||||
|
/// lines even after <see cref="DrawLandscapeSliceLate_NoLongerDrawsOrPrintsTheWeatherPass"/>
|
||||||
|
/// passes, if <c>DrawWeatherOnce</c> were called FROM INSIDE that same
|
||||||
|
/// loop instead of after it. IL offsets (not list position) prove
|
||||||
|
/// source order for this straight-line method. MUTATION: moving the
|
||||||
|
/// <c>DrawWeatherOnce</c> call back inside the <c>foreach</c> (or before
|
||||||
|
/// the loop) makes the offset-ordering assertion fail; adding a second
|
||||||
|
/// call site makes <c>Assert.Single</c> fail.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnceAfterTheSliceLoop()
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
||||||
|
"DrawLandscapeDynamicsPhase",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
||||||
|
|
||||||
|
CompiledCall sliceLate = Assert.Single(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||||
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawLandscapeSliceLate));
|
||||||
|
CompiledCall weatherOnce = Assert.Single(
|
||||||
|
calls,
|
||||||
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
||||||
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
sliceLate.Offset < weatherOnce.Offset,
|
||||||
|
"DrawWeatherOnce must be called AFTER the per-slice loop, not from inside it.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S3 chunk 1 fix round 2 (§11.6 H1): <see
|
||||||
|
/// cref="RetailPViewPassExecutor.ShouldDrawWeatherOnce"/> is
|
||||||
|
/// <see cref="DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce"/>'s
|
||||||
|
/// gate, extracted as a pure predicate so this suite can pin "no OC line
|
||||||
|
/// while the player stands indoors" without a live GL/DAT
|
||||||
|
/// <see cref="SkyRenderer"/> — combined with the two structural tests
|
||||||
|
/// above (moved out of the loop; drawn/printed exactly once per call),
|
||||||
|
/// this proves both halves of the spec's pin: an outdoor root (or an
|
||||||
|
/// interior root with several exit-view slices) prints exactly one OC
|
||||||
|
/// line, and an indoor player prints none. Retail's own check:
|
||||||
|
/// <c>SmartBox::is_player_outside</c> @0x00451e80,
|
||||||
|
/// <c>(player objcell_id & 0xFFFF) < 0x100</c>. MUTATION: negating
|
||||||
|
/// the <c>< 0x100</c> comparison (or dropping either bool AND) makes
|
||||||
|
/// one of the four rows below fail.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, true, 0xF4180003u, true)] // outdoor root, player outside a land cell -> draws
|
||||||
|
[InlineData(true, true, 0xA9B40100u, false)] // player indoors (local id >= 0x100) -> no draw
|
||||||
|
[InlineData(false, true, 0xF4180003u, false)] // RenderSky off -> no draw
|
||||||
|
[InlineData(true, false, 0xF4180003u, false)] // RenderWeather off -> no draw
|
||||||
|
public void ShouldDrawWeatherOnce_MatchesRetailIsPlayerOutsideGate(
|
||||||
|
bool renderSky, bool renderWeather, uint playerCellId, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
expected,
|
||||||
|
RetailPViewPassExecutor.ShouldDrawWeatherOnce(renderSky, renderWeather, playerCellId));
|
||||||
|
}
|
||||||
|
|
||||||
private static int RequiredCallIndex(
|
private static int RequiredCallIndex(
|
||||||
IReadOnlyList<CompiledCall> calls,
|
IReadOnlyList<CompiledCall> calls,
|
||||||
Type declaringType,
|
Type declaringType,
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,11 @@ public sealed class WalkTraceConformanceTests
|
||||||
/// flood defers its EC/OC to <see cref="OnInteriorFloodDrawTurn"/>),
|
/// flood defers its EC/OC to <see cref="OnInteriorFloodDrawTurn"/>),
|
||||||
/// flips true forever once a building's portal pass begins
|
/// flips true forever once a building's portal pass begins
|
||||||
/// (<see cref="OnBuildingTurn"/>) — see
|
/// (<see cref="OnBuildingTurn"/>) — see
|
||||||
/// <see cref="WalkTraceReplayContext.Signature8(WalkOracleFrame)"/>'s own
|
/// <see cref="WalkTraceReplayContext.AppendFloodTurns"/>'s own doc
|
||||||
/// doc comment for why this never resets mid-frame.</summary>
|
/// comment for why this never resets mid-frame. S3 chunk 1 fix round 2
|
||||||
|
/// (§11.6 H3): this REPLAY-side derivation is unchanged — only the
|
||||||
|
/// ORACLE side (<see cref="WalkTraceReplayContext.Signature8(WalkOracleFrame)"/>)
|
||||||
|
/// moved off derivation onto its own literal captured EC/OC lines.</summary>
|
||||||
private sealed class Recorder : IWalkEventSink
|
private sealed class Recorder : IWalkEventSink
|
||||||
{
|
{
|
||||||
public readonly List<WalkEvent> Events = new();
|
public readonly List<WalkEvent> Events = new();
|
||||||
|
|
@ -176,9 +179,15 @@ public sealed class WalkTraceConformanceTests
|
||||||
/// boundary near this pose — a real <c>WalkLandscape.CheckBlocks</c>
|
/// boundary near this pose — a real <c>WalkLandscape.CheckBlocks</c>
|
||||||
/// visibility question, not a print-site or ordering bug, and out of
|
/// visibility question, not a print-site or ordering bug, and out of
|
||||||
/// this round's print-only scope (§11.4) — needs the retail oracle
|
/// this round's print-only scope (§11.4) — needs the retail oracle
|
||||||
/// (decomp/cdb) to resolve. terrace-edge/cathedral-arrival/foundry-deep
|
/// (decomp/cdb) to resolve. Filed as docs/ISSUES.md #458.
|
||||||
/// all reproduce EXACTLY at this same eight-kind level; only this row's
|
/// terrace-edge/cathedral-arrival/foundry-deep all reproduce EXACTLY at
|
||||||
/// specific pose hits the boundary.
|
/// this same eight-kind level; only this row's specific pose hits the
|
||||||
|
/// boundary. S3 chunk 1 fix round 2 (§11.6 H3) switched the ORACLE side
|
||||||
|
/// of the eight-kind signature from a derived EC/OC to retail's own
|
||||||
|
/// LITERAL captured EC/OC lines — re-verified 2026-09-03 that this
|
||||||
|
/// divergence stays at the SAME token index 165 (an LC/SC-only mismatch,
|
||||||
|
/// upstream of any EC/OC content) under the new comparison; #458's
|
||||||
|
/// position is unchanged.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Status", "KnownFailure")]
|
[Trait("Status", "KnownFailure")]
|
||||||
|
|
|
||||||
|
|
@ -185,41 +185,50 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
/// them at this level would spuriously diverge on every frame (the
|
/// them at this level would spuriously diverge on every frame (the
|
||||||
/// replay always emits LC/SC; the FW0 oracle frame never has any).
|
/// replay always emits LC/SC; the FW0 oracle frame never has any).
|
||||||
/// <para>
|
/// <para>
|
||||||
/// LC/SC come straight from <paramref name="tokens"/>/<paramref
|
/// S3 chunk 1 fix round 2 (§11.6 H3): the two sides of this comparison
|
||||||
/// name="frame"/>'s own literal events — real turns with a real position
|
/// are DELIBERATELY ASYMMETRIC. LC/SC/EC/OC all come straight from
|
||||||
/// on both sides. EC/OC do NOT: <see cref="WalkEvent"/>'s four-kind
|
/// <paramref name="tokens"/>'s own literal per-hook calls — the REPLAY
|
||||||
/// vocabulary has no EC/OC analogue at all, and retail's own real
|
/// side's real turns at their real positions, EC/OC included (<see
|
||||||
/// position for them depends on which flood a DC belongs to
|
/// cref="WalkTraceConformanceTests"/>'s <c>Recorder</c> derives them via
|
||||||
/// (<c>PView::DrawCells</c>'s two complete reverse loops — shell
|
/// <see cref="AppendFloodTurns"/> at the exact hook that fires for each
|
||||||
/// far-to-near, then object-list far-to-near). A look-in flood's DC
|
/// flood — see that type's own doc comment). <see
|
||||||
/// draws them IMMEDIATELY, matching <c>RetailFrameWalk.DrawLandscape</c>'s
|
/// cref="Signature8(WalkOracleFrame)"/> below, the ORACLE side, used to
|
||||||
/// own recursive <c>DrawCells</c> call; the interior root's OWN flood
|
/// derive EC/OC the SAME way from each DC's declared cell list — but
|
||||||
/// DEFERS them past the whole landscape/look-in walk, to
|
/// that derivation can never disagree with itself, so it silently
|
||||||
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/> — evidence:
|
/// verified nothing about EC/OC placement or content (round 1's own
|
||||||
/// docs/research/2026-09-01-overhaul/oh-capture/cathedral-leak.walk.log
|
/// blind spot: G7's SC-ordering regression shipped green for the same
|
||||||
/// lines 4 and 1328: the root DC's own EC/OC print LAST, after every
|
/// reason with LC/SC). Round 2 fixes this: the oracle side now reads its
|
||||||
/// intervening look-in's own EC/OC pair. Both signature overloads below
|
/// own CAPTURED <c>EC</c>/<c>OC</c> lines verbatim, in the order retail's
|
||||||
/// derive EC/OC from each DC's own declared cell list under that SAME
|
/// cdb breakpoints actually recorded them — a real comparison against a
|
||||||
/// rule (reversed order, twice), rather than reading the oracle's
|
/// real trace, not a derivation compared to itself. The one exclusion is
|
||||||
/// literal EC/OC lines — this ALSO naturally excludes G8's trailing
|
/// the trailing per-frame weather <c>OC</c> (§11.6 H1): the LAST event
|
||||||
/// per-frame weather OC from the comparison: it owns no DC, so no
|
/// of the frame's raw list, when its cell id matches the frame's own "P"
|
||||||
/// derivation rule ever produces a token for it (see
|
/// line (<see cref="WalkOracleFrame.Pose"/>) AND that P cell itself looks
|
||||||
/// <c>WalkOracleTraceTests</c> for a dedicated assertion that a
|
/// outdoor (local id < 0x100 — retail's own weather gate,
|
||||||
/// COMPLETE outdoor frame's raw event list really does end with it).
|
/// <c>SmartBox::is_player_outside</c>) — an interior root's OWN flood can
|
||||||
|
/// ALSO end the frame with a real OC matching the P cell (its nearest
|
||||||
|
/// cell's object-list turn draws last, and that nearest cell is often
|
||||||
|
/// the camera's own root cell: cathedral-arrival.walk's real trailing
|
||||||
|
/// "OC f4180108" at DI:f4180108/P-cell f4180108), so position and value
|
||||||
|
/// alone are not enough — it owns no DC and is not a walk turn at all
|
||||||
|
/// (see <c>WalkOracleTraceTests</c> for a dedicated
|
||||||
|
/// assertion that a COMPLETE outdoor frame's raw event list really does
|
||||||
|
/// end with it).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Signature8(IReadOnlyList<string> tokens) => string.Join("|", tokens);
|
public static string Signature8(IReadOnlyList<string> tokens) => string.Join("|", tokens);
|
||||||
|
|
||||||
/// <summary>The oracle-frame half of <see cref="Signature8(IReadOnlyList{string})"/> —
|
/// <summary>The oracle-frame half of <see cref="Signature8(IReadOnlyList{string})"/> —
|
||||||
/// see that overload's doc comment for the shared derivation rule.</summary>
|
/// see that overload's doc comment for why this reads EC/OC literally
|
||||||
|
/// rather than deriving them.</summary>
|
||||||
public static string Signature8(WalkOracleFrame frame)
|
public static string Signature8(WalkOracleFrame frame)
|
||||||
{
|
{
|
||||||
var tokens = new List<string>();
|
var tokens = new List<string>();
|
||||||
bool? lookIn = null;
|
IReadOnlyList<WalkOracleEvent> events = frame.Events;
|
||||||
IReadOnlyList<uint>? pendingRootCells = null;
|
|
||||||
|
|
||||||
foreach (WalkOracleEvent e in frame.Events)
|
for (int i = 0; i < events.Count; i++)
|
||||||
{
|
{
|
||||||
|
WalkOracleEvent e = events[i];
|
||||||
switch (e.Kind)
|
switch (e.Kind)
|
||||||
{
|
{
|
||||||
case WalkOracleEventKind.Landscape:
|
case WalkOracleEventKind.Landscape:
|
||||||
|
|
@ -227,19 +236,13 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
break;
|
break;
|
||||||
case WalkOracleEventKind.Building:
|
case WalkOracleEventKind.Building:
|
||||||
tokens.Add($"BLD:{e.CellId!.Value:x8}");
|
tokens.Add($"BLD:{e.CellId!.Value:x8}");
|
||||||
lookIn = true;
|
|
||||||
break;
|
break;
|
||||||
case WalkOracleEventKind.DrawInside:
|
case WalkOracleEventKind.DrawInside:
|
||||||
tokens.Add($"DI:{e.CellId!.Value:x8}");
|
tokens.Add($"DI:{e.CellId!.Value:x8}");
|
||||||
lookIn = false;
|
|
||||||
break;
|
break;
|
||||||
case WalkOracleEventKind.DrawCells:
|
case WalkOracleEventKind.DrawCells:
|
||||||
tokens.Add(
|
tokens.Add(
|
||||||
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}");
|
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}");
|
||||||
if (lookIn == true)
|
|
||||||
AppendFloodTurns(tokens, e.Cells);
|
|
||||||
else
|
|
||||||
pendingRootCells = e.Cells;
|
|
||||||
break;
|
break;
|
||||||
case WalkOracleEventKind.LandCell:
|
case WalkOracleEventKind.LandCell:
|
||||||
tokens.Add($"LC:{e.CellId!.Value:x8}");
|
tokens.Add($"LC:{e.CellId!.Value:x8}");
|
||||||
|
|
@ -247,25 +250,55 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
case WalkOracleEventKind.SortCell:
|
case WalkOracleEventKind.SortCell:
|
||||||
tokens.Add($"SC:{e.CellId!.Value:x8}");
|
tokens.Add($"SC:{e.CellId!.Value:x8}");
|
||||||
break;
|
break;
|
||||||
|
case WalkOracleEventKind.EnvCellShell:
|
||||||
|
tokens.Add($"EC:{e.CellId!.Value:x8}");
|
||||||
|
break;
|
||||||
|
case WalkOracleEventKind.ObjectCellTurn:
|
||||||
|
// §11.6 H1's trailing weather OC: the LAST event of the
|
||||||
|
// whole frame, matching the frame's own "P" cell — not a
|
||||||
|
// flood turn, excluded from the comparison (see this
|
||||||
|
// method's own class doc for the citation). The extra
|
||||||
|
// "P cell is itself an outdoor land cell" check matters:
|
||||||
|
// an INTERIOR root's OWN flood draws its NEAREST cell's
|
||||||
|
// object list LAST (PView::DrawCells's reversed loop,
|
||||||
|
// index 0 = nearest = last) — when that nearest cell IS
|
||||||
|
// the camera's own root cell, its real OC turn ALSO sits
|
||||||
|
// at the frame's last position AND shares the P cell's
|
||||||
|
// id (cathedral-arrival.walk: DI:f4180108, P cell
|
||||||
|
// f4180108, real trailing "OC f4180108" — position AND
|
||||||
|
// value alone would misclassify it). Retail's weather
|
||||||
|
// never draws for an interior-rooted P cell at all
|
||||||
|
// (SmartBox::is_player_outside is false whenever the
|
||||||
|
// viewer's own cell id has local part >= 0x100), so
|
||||||
|
// requiring the P cell itself look outdoor closes that
|
||||||
|
// hole without reopening G8's original one.
|
||||||
|
bool trailingWeatherOc = i == events.Count - 1
|
||||||
|
&& frame.Pose is not null
|
||||||
|
&& e.CellId!.Value == frame.Pose.CellId
|
||||||
|
&& (frame.Pose.CellId & 0xFFFFu) < 0x100u;
|
||||||
|
if (!trailingWeatherOc)
|
||||||
|
tokens.Add($"OC:{e.CellId!.Value:x8}");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The interior root's OWN flood: its EC/OC derive last, matching
|
|
||||||
// OnInteriorFloodDrawTurn firing as DrawInside's terminal sink call
|
|
||||||
// (see class doc) — an outdoor-rooted frame never sets this at all.
|
|
||||||
if (pendingRootCells is not null)
|
|
||||||
AppendFloodTurns(tokens, pendingRootCells);
|
|
||||||
|
|
||||||
return string.Join("|", tokens);
|
return string.Join("|", tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary><c>PView::DrawCells</c>'s two complete reverse loops: every
|
/// <summary><c>PView::DrawCells</c>'s two complete reverse loops: every
|
||||||
/// EnvCell shell far-to-near, THEN every object-list turn far-to-near —
|
/// EnvCell shell far-to-near, THEN every object-list turn far-to-near —
|
||||||
/// the SAME reversed cell order twice (not one reversed EC/OC pair per
|
/// the SAME reversed cell order twice (not one reversed EC/OC pair per
|
||||||
/// cell) — see <see cref="Signature8(WalkOracleFrame)"/>'s own doc
|
/// cell). S3 chunk 1 fix round 2 (§11.6 H3): this is now ONLY the
|
||||||
/// comment for the evidence. Internal (not private): <see
|
/// REPLAY side's derivation — <see cref="WalkTraceConformanceTests"/>'s
|
||||||
/// cref="WalkTraceConformanceTests"/>'s own <c>Recorder</c> reuses this
|
/// <c>Recorder</c> calls this at the exact hook that fires for each
|
||||||
/// EXACT rule for the replay side rather than re-deriving it.</summary>
|
/// flood (a look-in's own <c>DrawCells</c> immediately; the interior
|
||||||
|
/// root's own flood at <c>OnInteriorFloodDrawTurn</c>), matching
|
||||||
|
/// <c>RetailFrameWalk</c>'s real two-reverse-loop order
|
||||||
|
/// (<c>WalkFrameDriver.EmitFloodTurns</c>'s own comment has the same
|
||||||
|
/// citation). <see cref="Signature8(WalkOracleFrame)"/> no longer calls
|
||||||
|
/// this — the ORACLE side reads its own captured EC/OC lines literally
|
||||||
|
/// instead of re-deriving them (see that method's own doc comment for
|
||||||
|
/// why: a derivation compared to itself proves nothing).</summary>
|
||||||
internal static void AppendFloodTurns(List<string> tokens, IReadOnlyList<uint> cells)
|
internal static void AppendFloodTurns(List<string> tokens, IReadOnlyList<uint> cells)
|
||||||
{
|
{
|
||||||
for (int i = cells.Count - 1; i >= 0; i--)
|
for (int i = cells.Count - 1; i >= 0; i--)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue