diff --git a/docs/launch-options.md b/docs/launch-options.md index dd4c1447..187d00be 100644 --- a/docs/launch-options.md +++ b/docs/launch-options.md @@ -272,7 +272,7 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv" | `ACDREAM_DUMP_GFXOBJS_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_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). | print-only; 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`) does no string work and 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, 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_HIDE_PART` | `=` | 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` | diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 04b7b8fc..43429f7f 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -237,13 +237,28 @@ public RetailPViewPassExecutor( bool playerOutside = (frame.PlayerCellId & 0xFFFFu) < 0x100u; if (frame.RenderSky && frame.RenderWeather && playerOutside) { - _sky?.RenderWeather( - frame.Camera, - frame.CameraWorldPosition, - frame.DayFraction, - frame.ActiveDayGroup, - frame.SkyKeyframe, - frame.EnvironOverrideActive); + 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) { diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs index 32ac7217..dd48ab0a 100644 --- a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs +++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs @@ -149,9 +149,12 @@ public sealed class RetailFrameWalk /// LScape::draw: visibility per active view, then blocks /// far-to-near, cells far-to-near; per cell, S3 chunk 3's /// (this cell's terrain, - /// in-view cells only) fires BEFORE the building's turn and the - /// object-list turn (, - /// every cell under alwaysDrawObjects). + /// in-view cells only) fires BEFORE S3 chunk 1 fix round 1's + /// (DrawSortCell entry, + /// every cell under alwaysDrawObjects), which in turn fires + /// BEFORE the building's turn and the object-list turn + /// (, + /// DrawSortCell's own far end) — retail's LC → SC → BLD order. public void DrawLandscape( WalkLandscape landscape, WalkPortalView activeViews, IRetailFrameWalkContext ctx, IWalkEventSink sink) @@ -189,6 +192,17 @@ public sealed class RetailFrameWalk if (!AlwaysDrawObjects && !cellInView) continue; + // S3 chunk 1 fix round 1 (G7): RenderDeviceD3D::DrawSortCell + // @0x0059f140 ENTRY — before its own DrawBuilding(building) + // call below (terrace-edge.walk.log:2331-2333 order: + // LC → SC → BLD). The three-lens review's transcript pass + // caught the previous placement (the sink fired this AFTER + // DrawBuilding, via OnLandscapeCellTurn) as a mismatch — see + // that hook's own doc comment for the far-end (post-building) + // turn this one brackets. + sink.OnSortCellTurn( + block.LandblockId, block.SideCellCount, cellIndex); + // RenderDeviceD3D::DrawSortCell @0x0059f140 (decomp- // confirmed 2026-08-30): DrawBuilding(building) FIRST, then // DrawObjCell(cell) UNCONDITIONALLY — the building's turn diff --git a/src/AcDream.App/Rendering/Walk/WalkEvents.cs b/src/AcDream.App/Rendering/Walk/WalkEvents.cs index 037166b8..bc2ccc1f 100644 --- a/src/AcDream.App/Rendering/Walk/WalkEvents.cs +++ b/src/AcDream.App/Rendering/Walk/WalkEvents.cs @@ -108,6 +108,21 @@ public interface IWalkEventSink /// void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) { } + /// + /// S3 chunk 1 fix round 1 (G7): fires once per visited landscape cell, + /// at RenderDeviceD3D::DrawSortCell @0x0059f140 ENTRY — BEFORE + /// that same turn's own DrawBuilding(building) call (retail's + /// terrace-edge.walk.log:2331-2333 order: LC → SC → BLD; a not-in-view + /// cell under alwaysDrawObjects prints SC then BLD with no LC, + /// :1042-1044). Fires under the SAME alwaysDrawObjects || cellInView + /// gate as (that hook + /// fires at the FAR end of the same DrawSortCell turn, after the + /// building) — the two together bracket retail's real call order, + /// terrain-only DrawLandCell aside. Default no-op — every pre-S3-chunk-1 + /// fix-round-1 sink continues to compile and behave identically. + /// + void OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) { } + /// /// Fires once per visited landscape cell, AFTER that cell's building /// turn (if any) — RenderDeviceD3D::DrawSortCell @0x0059f140 diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs index ee5520d0..a0469c5e 100644 --- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs +++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs @@ -1383,9 +1383,17 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawLandCell @0x0059f120 // entry — one LC line per admitted LOD land cell, at LOD resolution // (never expanded to the 8x8 owner-bucket ids OnLandscapeCellTurn - // uses for object-list content). - WalkTranscriptDump.PrintLandCell( - WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex)); + // uses for object-list content). S3 chunk 1 fix round 1 (G2): the + // flag gate wraps the LodCellId computation itself, not just + // WalkTranscriptDump.PrintLandCell's own internal check — an + // ordinary launch (flag off) must not pay for the division/modulo/ + // checked-conversion work on ~1,300 land-cell turns/frame just to + // discard the result. + if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) + { + WalkTranscriptDump.PrintLandCell( + WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex)); + } // The one mark rule (this type's own doc comment): flush any // already-queued stream content ahead of this leaf-renderer event. @@ -1393,9 +1401,45 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource _events.Add(WalkFrameEvent.LandCell(landblockId, sideCellCount, cellIndex)); } + /// S3 chunk 1 fix round 1 (G7): RenderDeviceD3D:: + /// DrawSortCell @0x0059f140 ENTRY — fired by + /// BEFORE that turn's own DrawBuilding call, so the SC line + /// prints between LC and BLD (retail's own order — see the interface + /// member's own doc comment; the three-lens review's transcript pass + /// caught the earlier placement, which printed SC from + /// AFTER + /// DrawBuilding had already run). Print-only — no stream-side effect of + /// its own; the object-list turn stays entirely in + /// OnLandscapeCellTurn below, unchanged. + void IWalkEventSink.OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) + { + RequireOpenFrame(); + if (sideCellCount is not (1 or 2 or 4 or 8)) + { + throw new ArgumentOutOfRangeException( + nameof(sideCellCount), + sideCellCount, + "A landscape LOD grid must be 1, 2, 4, or 8 cells per side."); + } + 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)); + } + } + void IWalkEventSink.OnLandscapeCellTurn(uint cellId) => HandleLandscapeCellTurn(cellId); + /// The object-list half of retail's DrawSortCell turn — see + /// for the SC transcript + /// line, which now fires separately, BEFORE the building's own turn + /// (S3 chunk 1 fix round 1, G7); this method never prints one. void IWalkEventSink.OnLandscapeCellTurn( uint landblockId, int sideCellCount, @@ -1411,13 +1455,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount)) throw new ArgumentOutOfRangeException(nameof(cellIndex)); - // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawSortCell @0x0059f140 - // entry — ONE SC line per LOD cell (printed before the 8x8-bucket - // expansion below, which is an acdream object-list-content detail - // with no retail-breakpoint analogue of its own). - WalkTranscriptDump.PrintSortCell( - WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex)); - uint blockPrefix = landblockId & 0xFFFF0000u; if (sideCellCount == 8) { diff --git a/src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs b/src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs index f92c109a..caefa075 100644 --- a/src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs +++ b/src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs @@ -149,12 +149,15 @@ internal static class WalkTranscriptDump } /// RenderDeviceD3D::DrawEnvCell @0x0059f170 entry — fires - /// once per (flood, cell) VISIT, not once per render stamp: the retail - /// captures' EC and OC counts are always exactly equal per pose (e.g. - /// 45/45 at holtburg-doorway-still), which only holds if EC — like OC — - /// is unconditional at every flood visit, with the per-stamp dedupe - /// happening INSIDE DrawEnvCell, after this breakpoint's address, not in - /// the calling loop. + /// once per (flood, cell) VISIT, not once per render stamp. S3 chunk 1 + /// fix round 1 (G9): the visit-scoped conclusion is right, but "EC and + /// OC counts are always exactly equal per pose" is NOT — terrace-edge + /// has 12 EC vs 16 OC (the extra 4 are G8's trailing per-frame weather + /// OC, which has no EC counterpart at all). The per-render-stamp dedupe + /// still sits INSIDE DrawEnvCell, past this breakpoint's address, not in + /// the calling loop — 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. internal static void PrintEnvCellShell(uint cellId) { if (!Enabled) return; @@ -164,7 +167,17 @@ internal static class WalkTranscriptDump /// RenderDeviceD3D::DrawObjCellForDummies @0x005a0760 /// entry — the interior/look-in object-list turn (retail's outdoor /// DrawObjCell is a different function, folded into the "SC" - /// line's own DrawSortCell call; it has no separate OC hook). + /// 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 — RetailPViewPassExecutor.DrawLandscapeSliceLate's + /// weather pass, matching retail's GameSky::Draw @0x00506ff0 + /// (arg2==1) calling DrawObjCellForDummies(after_sky_cell) + /// @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 + /// already carries). That trailing OC is NOT a walk turn (no owning DC, + /// no reversal derivation applies to it) — see this method's own two + /// call sites for the addresses. internal static void PrintObjectCellTurn(uint cellId) { if (!Enabled) return; diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs index 9055202b..9a5cc4f7 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTranscriptTests.cs @@ -116,7 +116,19 @@ public sealed partial class WalkFrameDriverTests /// T1's integration half: a full synthetic interior frame, /// driven through the SAME path /// production uses, produces literally NO transcript output when the - /// flag is off. + /// flag is off. S3 chunk 1 fix round 1 (G1): TWO Collect calls, + /// not one — structurally discards + /// the last (open) frame (its own class doc: the capture harness + /// detaches at the frame marker, so the final "F n" never records its + /// own draws), so a SINGLE-frame transcript parses to zero frames + /// whether or not the flag actually gated anything — a one-Collect-call + /// version of this test is vacuous (it would pass exactly as well with + /// the flag gate forced true). Two Collect calls give the parser a real + /// COMPLETE frame to see if the gate leaked (MUTATION CHECK: forcing + /// RenderingDiagnostics.DumpWalkTranscriptEnabled true right + /// after setting it false here makes AssertNoTranscriptLines + /// fail, both on the parsed-frame-count assertion and the prefix + /// scan). [Fact] public void Collect_TranscriptFlagOff_ProducesNoConsoleOutput() { @@ -138,6 +150,8 @@ public sealed partial class WalkFrameDriverTests var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame); var walk = new RetailFrameWalk(); + driver.Collect( + walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero); driver.Collect( walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero); @@ -150,6 +164,16 @@ public sealed partial class WalkFrameDriverTests } } + /// + /// The ten transcript line-kind prefixes + /// ever emits — F /P /LS/LC /SC / + /// BLD /DI /DC /EC /OC — matching + /// each print method's own exact Console.WriteLine format + /// (S3 chunk 1 fix round 1, G1). + /// + private static readonly string[] TranscriptLinePrefixes = + ["F ", "P ", "LS", "LC ", "SC ", "BLD ", "DI ", "DC ", "EC ", "OC "]; + /// /// Asserts NONE of 's own line kinds /// appear in — robust to unrelated @@ -166,7 +190,13 @@ public sealed partial class WalkFrameDriverTests /// unrelated text can never satisfy that regex, so a truly EMPTY parsed /// frame list is exactly as strong a proof that THIS code printed /// nothing, without being sensitive to what else shares the process - /// console. + /// console. S3 chunk 1 fix round 1 (G1) adds a second, independent + /// check: no captured line starts with any of the ten transcript + /// prefixes — those exact tokens never begin an unrelated test class's + /// own console noise, so this stays robust to the SAME parallel hazard + /// while also catching a leak the parser's own drop-last-frame quirk + /// could otherwise hide (a caller that (mistakenly) drives only ONE + /// frame). /// private static void AssertNoTranscriptLines(StringWriter capture) { @@ -175,6 +205,10 @@ public sealed partial class WalkFrameDriverTests .Select(l => l.TrimEnd('\r')) .ToArray(); Assert.Empty(WalkOracleTrace.Parse(lines)); + Assert.DoesNotContain( + lines, + l => Array.Exists( + TranscriptLinePrefixes, p => l.StartsWith(p, StringComparison.Ordinal))); } /// T2 (§11.3): flag on, one synthetic interior frame — the @@ -241,8 +275,12 @@ public sealed partial class WalkFrameDriverTests // The interior root's own flood visits both cells for a shell // AND an object-list turn — EC/OC pair, one line per cell, no - // dedupe (WalkTranscriptDump.PrintEnvCellShell's own doc - // comment: EC/OC counts are always exactly equal per pose). + // dedupe (this is the interior root's OWN flood — the ONE case + // WalkTranscriptDump.PrintEnvCellShell's own doc comment (G9) + // still guarantees EC==OC count for; a look-in flood matches + // too, but G8's per-frame trailing weather OC does not, and + // never fires here since this fixture's landscape has zero + // blocks/buildings). List ec = frame.Events .Where(e => e.Kind == WalkOracleEventKind.EnvCellShell) .Select(e => e.CellId!.Value).ToList(); @@ -255,9 +293,11 @@ public sealed partial class WalkFrameDriverTests Assert.Equal(2, oc.Count); // No landblocks were published (the stub 1x1 landscape), so no - // LC/SC/BLD turns exist this frame — the transcript format for - // those three kinds is proven separately, against real retail - // data, by WalkOracleTraceTests.AllFixtures and + // LC/SC/BLD turns exist this frame — the transcript format and + // relative order for those three kinds is proven by + // OutdoorRoot_TranscriptFlagOn_PrintsLandscapeThenLandCellThenSortCellThenBuilding_InOrder + // below (S3 chunk 1 fix round 1, G3), and separately against + // real retail data by WalkOracleTraceTests.AllFixtures and // WalkLandCellOrderTests. Assert.DoesNotContain(frame.Events, e => e.Kind is WalkOracleEventKind.LandCell or WalkOracleEventKind.SortCell @@ -270,6 +310,90 @@ public sealed partial class WalkFrameDriverTests } } + /// S3 chunk 1 fix round 1 (G3): the outdoor-root half T2 never + /// covered — the flag-on round trip above uses a stub 1x1 landscape with + /// zero blocks, so LC/SC/BLD never fire at all. Drives + /// directly with the SAME + /// deterministic "CY-only" admission + /// technique OutdoorRoot_LandCellPrecedesItsOwnCellsObjectTurn_ThenFlushesAtReplaysEnd + /// uses (a real root's default + /// full-viewport view depends on this suite's synthetic ray caster, + /// which does not model screen geometry faithfully enough to predict + /// block/cell admission from — see that test's own doc comment), with a + /// building attached to the one admitted cell so BLD fires too. Checked + /// against the RAW captured lines directly (not + /// ) — this test only needs kind + /// presence and relative order, not a complete F/P-framed capture. + /// MUTATION CHECK: deleting WalkTranscriptDump.PrintLandCell's + /// call site (or its body) makes the "expected an LC line" assertion + /// fail. + [Fact] + public void OutdoorRoot_TranscriptFlagOn_PrintsLandscapeThenLandCellThenSortCellThenBuilding_InOrder() + { + bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled; + RenderingDiagnostics.DumpWalkTranscriptEnabled = true; + TextWriter originalOut = Console.Out; + var capture = new StringWriter(); + try + { + Console.SetOut(capture); + + using var fx = new DispatcherFixture(); + var leaf = new RecordingLeafRenderer(new List()); + leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u)); + var ctx = new TestContext(); + var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData()); + var walk = new RetailFrameWalk(); + var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] }; + var block = new WalkLandBlock + { + LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f, + }; + block.EnsureCellArrays(); + block.CellBuildings[0] = new WalkBuilding { PositionCellId = 0xF4180000u }; + landscape.Blocks[0] = block; + + driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero); + walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver); + driver.EndFrame(); + + Console.Out.Flush(); + string[] lines = capture.ToString() + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.TrimEnd('\r')) + .ToArray(); + + int lsIndex = Array.IndexOf(lines, "LS"); + int lcIndex = Array.FindIndex( + lines, l => l.StartsWith("LC ", StringComparison.Ordinal)); + int scIndex = Array.FindIndex( + lines, l => l.StartsWith("SC ", StringComparison.Ordinal)); + int bldIndex = Array.FindIndex( + lines, l => l.StartsWith("BLD ", StringComparison.Ordinal)); + + Assert.True(lsIndex >= 0, "expected an LS line"); + Assert.True(lcIndex >= 0, "expected an LC line"); + Assert.True(scIndex >= 0, "expected an SC line"); + Assert.True(bldIndex >= 0, "expected a BLD line"); + Assert.True(lsIndex < lcIndex, "LS must precede LC"); + Assert.True( + lcIndex < scIndex, + "LC must precede SC — RenderDeviceD3D::DrawLandCell before DrawSortCell"); + Assert.True( + scIndex < bldIndex, + "SC must precede BLD — S3 chunk 1 fix round 1 (G7): DrawSortCell entry, " + + "before its own DrawBuilding call"); + Assert.Equal("LC f4180001", lines[lcIndex]); + Assert.Equal("SC f4180001", lines[scIndex]); + Assert.Equal("BLD f4180000", lines[bldIndex]); + } + finally + { + Console.SetOut(originalOut); + RenderingDiagnostics.DumpWalkTranscriptEnabled = previous; + } + } + /// §11.2 B4: the offline signature diff, self-checked over a /// synthetic pair — the SAME captured T2 transcript versus itself with /// its own LAST event removed. Reuses Collect_TranscriptFlagOn_…'s diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs index 57367bce..6ded2f7e 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs @@ -110,6 +110,15 @@ public static class WalkLandscapeDatBuilder int sideCellCount = SideCellCountForRing(ring); var block = new WalkLandBlock { + // S3 chunk 1 fix round 1 (G10): this field was never + // set — every LC/SC token this world's blocks ever + // produced read landblockId=0 (LodCellId's own + // "(landblockId & 0xFFFF0000) | ..." formula degenerates + // to a bare 8×8-bucket index), latent because the OLD + // four-kind Signature filtered LC/SC out entirely; G10's + // eight-kind comparison is the first thing that ever + // reads this field back out of a DAT-built world. + LandblockId = landblockId, SideCellCount = sideCellCount, MaxZ = heightTable[maxByte] + 200f, MinZ = heightTable[minByte] - 1f, diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTraceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTraceTests.cs index b3f51465..0e4ec25a 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTraceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTraceTests.cs @@ -35,6 +35,12 @@ public sealed class WalkOraclePartsTraceTests Assert.Contains(frames, f => f.MeshDraws.Count > 0); } + /// S3 chunk 1 fix round 1 (G4): besides Flushes (FL) and + /// DrawCellsSamples (PC), also assert MeshAdds (AM) and PortalPolyDraws + /// (PM) are non-empty — §6b lists real counts for every committed + /// capture (e.g. terrace-edge: AM=6880, PM=10), so a regex that + /// silently stopped matching either kind must fail this test, not pass + /// it silently. [Theory] [MemberData(nameof(AllPoses))] public void Alpha_depth_log_parses_with_nonempty_samples(string pose) @@ -45,7 +51,9 @@ public sealed class WalkOraclePartsTraceTests Assert.NotEmpty(frames); Assert.Equal(1, frames[0].Number); Assert.Equal(frames.Count, frames[^1].Number); + Assert.Contains(frames, f => f.MeshAdds.Count > 0); Assert.Contains(frames, f => f.Flushes.Count > 0); + Assert.Contains(frames, f => f.PortalPolyDraws.Count > 0); Assert.Contains(frames, f => f.DrawCellsSamples.Count > 0); } } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs index 69a0fa09..71e1d240 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs @@ -55,6 +55,31 @@ public sealed class WalkOracleTraceTests Assert.All(frames, f => Assert.NotEmpty(f.Events)); } + /// S3 chunk 1 fix round 1 (G8/G10): the "assert separately" + /// half of 's + /// own doc comment — that method's derivation never produces the + /// trailing weather OC (G8: GameSky::Draw @0x00506ff0's weather + /// branch calling DrawObjCellForDummies(after_sky_cell) + /// @0x005070da), so its presence/position is pinned here instead, + /// directly against the OH capture (the FW0 terrace-edge fixture + /// predates the OC breakpoint and carries none). Every complete outdoor + /// frame's LAST event is an OC whose cell id equals that SAME + /// frame's own P line cell id (the viewer's land cell). + [Fact] + public void Oh_terrace_edge_outdoor_frame_ends_with_the_weather_ObjectCellTurn() + { + IReadOnlyList frames = WalkOracleTrace.Load(OhRoot, "terrace-edge.walk"); + + Assert.NotEmpty(frames); + Assert.All(frames, f => + { + Assert.NotNull(f.Pose); + WalkOracleEvent last = f.Events[^1]; + Assert.Equal(WalkOracleEventKind.ObjectCellTurn, last.Kind); + Assert.Equal(f.Pose!.CellId, last.CellId); + }); + } + [Fact] public void Terrace_edge_draws_the_far_building_every_outdoor_frame() { diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs index 5ea2c414..0e09ea38 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs @@ -16,10 +16,70 @@ namespace AcDream.App.Tests.Rendering.Walk; [Trait("Lane", "InstalledDat")] public sealed class WalkTraceConformanceTests { + /// S3 chunk 1 fix round 1 (G10): besides the plain + /// list the four-kind + /// + /// still uses, also builds the full eight-kind token stream — LS/BLD/ + /// DI/DC/LC/SC/EC/OC — AS each hook fires, for + /// . + /// mirrors WalkFrameDriver's own private + /// _currentDcStage: null until the frame's first + /// (the interior root's own + /// flood defers its EC/OC to ), + /// flips true forever once a building's portal pass begins + /// () — see + /// 's own + /// doc comment for why this never resets mid-frame. private sealed class Recorder : IWalkEventSink { public readonly List Events = new(); - public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent); + public readonly List Tokens = new(); + + private bool? _lookIn; + + public void Emit(in WalkEvent walkEvent) + { + Events.Add(walkEvent); + switch (walkEvent.Kind) + { + case WalkEventKind.Landscape: + Tokens.Add("LS"); + break; + case WalkEventKind.Building: + Tokens.Add($"BLD:{walkEvent.CellId:x8}"); + break; + case WalkEventKind.DrawInside: + Tokens.Add($"DI:{walkEvent.CellId:x8}"); + _lookIn = false; + break; + case WalkEventKind.DrawCells: + Tokens.Add( + $"DC:ov={walkEvent.OutsideViewCount}:" + + string.Join(',', walkEvent.Cells.Select(c => c.ToString("x8")))); + // A look-in flood's DC draws its EC/OC immediately + // (RetailFrameWalk.HandleDrawCellsTurn calls + // EmitFloodTurns right here for WalkDrawStage. + // LookInStatic); the interior root's OWN flood (stage + // CellStatic, _lookIn still false) defers to + // OnInteriorFloodDrawTurn below — see that method. + if (_lookIn == true) + WalkTraceReplayContext.AppendFloodTurns(Tokens, walkEvent.Cells); + break; + } + } + + public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) + => Tokens.Add( + $"LC:{WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex):x8}"); + + public void OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) + => Tokens.Add( + $"SC:{WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex):x8}"); + + public void OnBuildingTurn(WalkBuilding building) => _lookIn = true; + + public void OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount) + => WalkTraceReplayContext.AppendFloodTurns(Tokens, cells); } private static DatCollection OpenDats() @@ -100,13 +160,39 @@ public sealed class WalkTraceConformanceTests $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}"); } + /// + /// PINNED RED 2026-09-03 (S3 chunk 1 fix round 1, G10 — un-skip with the + /// fix): the eight-kind signature is the FIRST comparison ever run at + /// LC/SC placement/content against a DAT-built world for this fixture + /// (the old four-kind Signature filtered LC/SC out entirely — how the + /// G7 SC-ordering regression shipped green). One real divergence + /// survives after fixing the LandblockId=0 fixture bug this same round + /// uncovered (WalkLandscapeDatBuilder.cs — a genuinely separate, + /// already-fixed defect): at token index 165, the replay draws ONE + /// EXTRA land-cell pair retail never does — + /// …SC:a8c90001|LC:a8c90001|SC:a8c90001| retail jumps straight to + /// LC:95c60001; the replay inserts LC:a9c90001|SC:a9c90001 + /// first. A single spurious block admission at a ring-2/ring-3 LOD + /// boundary near this pose — a real WalkLandscape.CheckBlocks + /// visibility question, not a print-site or ordering bug, and out of + /// this round's print-only scope (§11.4) — needs the retail oracle + /// (decomp/cdb) to resolve. terrace-edge/cathedral-arrival/foundry-deep + /// all reproduce EXACTLY at this same eight-kind level; only this row's + /// specific pose hits the boundary. + /// [Fact] + [Trait("Status", "KnownFailure")] public void Oh_doorway_still_first_frame_diff() { // S3 chunk 1 (§11.2 B3): the OH kit pose's own doorway-still capture - // — DI f4180108... no, a9b4013f, DC(ov=2, n=3) per §6b. Mirrors + // — DI a9b4013f, DC(ov=2, n=3) per §6b. Mirrors // Doorway_still_first_frame_diff's own structure (this pose needs // the interior camera cell, unlike the plain outdoor/theory rows). + // S3 chunk 1 fix round 1 (G6): kept as its own Fact rather than + // folded into Still_fixture_first_frame_reproduces_exactly's Theory + // — it and Oh_foundry_deep_... predate that Theory's OH rows and + // mirror their own FW0 siblings' structure; either shape works, no + // functional reason blocks merging them later. IReadOnlyList frames = WalkOracleTrace.Load( "docs/research/2026-09-01-overhaul/oh-capture", "holtburg-doorway-still.walk"); Assert.NotEmpty(frames); @@ -125,8 +211,11 @@ public sealed class WalkTraceConformanceTests walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); - string expected = WalkTraceReplayContext.Signature(frame); - string actual = WalkTraceReplayContext.Signature(recorder.Events); + // S3 chunk 1 fix round 1 (G10): the OH kit pose carries LC/SC/EC/OC + // — compare at the full eight-kind level (see Signature8's own doc + // comment). + string expected = WalkTraceReplayContext.Signature8(frame); + string actual = WalkTraceReplayContext.Signature8(recorder.Tokens); Assert.True( expected == actual, $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}"); @@ -169,8 +258,17 @@ public sealed class WalkTraceConformanceTests walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); - string expected = WalkTraceReplayContext.Signature(frame); - string actual = WalkTraceReplayContext.Signature(recorder.Events); + // S3 chunk 1 fix round 1 (G10): the OH kit-pose rows carry + // LC/SC/EC/OC — compare THOSE two rows at the full eight-kind level + // (Signature8's own doc comment); the FW0 rows have none of those + // lines at all (captured before those breakpoints existed), so they + // stay on the original four-kind comparison. + string expected = root == OhCaptureRoot + ? WalkTraceReplayContext.Signature8(frame) + : WalkTraceReplayContext.Signature(frame); + string actual = root == OhCaptureRoot + ? WalkTraceReplayContext.Signature8(recorder.Tokens) + : WalkTraceReplayContext.Signature(recorder.Events); Assert.True( expected == actual, $"walk diverged from retail ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}"); @@ -323,9 +421,11 @@ public sealed class WalkTraceConformanceTests walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); + // S3 chunk 1 fix round 1 (G10): full eight-kind comparison — + // see Signature8's own doc comment. Assert.Equal( - WalkTraceReplayContext.Signature(frame), - WalkTraceReplayContext.Signature(recorder.Events)); + WalkTraceReplayContext.Signature8(frame), + WalkTraceReplayContext.Signature8(recorder.Tokens)); } } } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs index 51077542..5e4087a8 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs @@ -174,4 +174,103 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk $"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}", _ => "?", })); + + /// + /// S3 chunk 1 fix round 1 (G10): the OH kit-pose captures' full + /// eight-kind vocabulary — LS/BLD/DI/DC/LC/SC/EC/OC. ONLY the OH-rooted + /// conformance rows use this (WalkTraceConformanceTests' + /// OhCaptureRoot rows) — the older FW0 fixtures + /// (docs/research/2026-08-30-fw-walk-oracle/) predate the LC/SC/EC/OC + /// cdb breakpoints and carry none of those lines at all, so comparing + /// them at this level would spuriously diverge on every frame (the + /// replay always emits LC/SC; the FW0 oracle frame never has any). + /// + /// LC/SC come straight from /'s own literal events — real turns with a real position + /// on both sides. EC/OC do NOT: 's four-kind + /// vocabulary has no EC/OC analogue at all, and retail's own real + /// position for them depends on which flood a DC belongs to + /// (PView::DrawCells's two complete reverse loops — shell + /// far-to-near, then object-list far-to-near). A look-in flood's DC + /// draws them IMMEDIATELY, matching RetailFrameWalk.DrawLandscape's + /// own recursive DrawCells call; the interior root's OWN flood + /// DEFERS them past the whole landscape/look-in walk, to + /// — evidence: + /// docs/research/2026-09-01-overhaul/oh-capture/cathedral-leak.walk.log + /// lines 4 and 1328: the root DC's own EC/OC print LAST, after every + /// intervening look-in's own EC/OC pair. Both signature overloads below + /// derive EC/OC from each DC's own declared cell list under that SAME + /// rule (reversed order, twice), rather than reading the oracle's + /// literal EC/OC lines — this ALSO naturally excludes G8's trailing + /// per-frame weather OC from the comparison: it owns no DC, so no + /// derivation rule ever produces a token for it (see + /// WalkOracleTraceTests for a dedicated assertion that a + /// COMPLETE outdoor frame's raw event list really does end with it). + /// + /// + public static string Signature8(IReadOnlyList tokens) => string.Join("|", tokens); + + /// The oracle-frame half of — + /// see that overload's doc comment for the shared derivation rule. + public static string Signature8(WalkOracleFrame frame) + { + var tokens = new List(); + bool? lookIn = null; + IReadOnlyList? pendingRootCells = null; + + foreach (WalkOracleEvent e in frame.Events) + { + switch (e.Kind) + { + case WalkOracleEventKind.Landscape: + tokens.Add("LS"); + break; + case WalkOracleEventKind.Building: + tokens.Add($"BLD:{e.CellId!.Value:x8}"); + lookIn = true; + break; + case WalkOracleEventKind.DrawInside: + tokens.Add($"DI:{e.CellId!.Value:x8}"); + lookIn = false; + break; + case WalkOracleEventKind.DrawCells: + tokens.Add( + $"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; + case WalkOracleEventKind.LandCell: + tokens.Add($"LC:{e.CellId!.Value:x8}"); + break; + case WalkOracleEventKind.SortCell: + tokens.Add($"SC:{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); + } + + /// PView::DrawCells's two complete reverse loops: every + /// 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 + /// cell) — see 's own doc + /// comment for the evidence. Internal (not private): 's own Recorder reuses this + /// EXACT rule for the replay side rather than re-deriving it. + internal static void AppendFloodTurns(List tokens, IReadOnlyList cells) + { + for (int i = cells.Count - 1; i >= 0; i--) + tokens.Add($"EC:{cells[i]:x8}"); + for (int i = cells.Count - 1; i >= 0; i--) + tokens.Add($"OC:{cells[i]:x8}"); + } }