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}");
+ }
}