acdream/src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs
Erik 88c70072e0 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>
2026-09-03 12:30:30 +02:00

208 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using System.Text;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the print-only walk-transcript
/// emitter. Prints the OH oracle-trace line kinds — <c>F</c>/<c>P</c>/
/// <c>LS</c>/<c>LC</c>/<c>SC</c>/<c>BLD</c>/<c>DI</c>/<c>DC</c>/<c>EC</c>/
/// <c>OC</c> — to <see cref="Console"/> at the EXACT points retail's cdb
/// breakpoints sit
/// (<c>tools/walk-oracle/oh/oh-capture-walk.cdb.template</c>), so a Release
/// run's output can be diffed offline against a live retail capture (§11.2
/// B4). Gated by
/// <see cref="AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled"/>
/// (rule 5) — every method here returns immediately, before any string
/// work, when the flag is off (T1: zero output, zero extra allocation on
/// the walk path).
///
/// <para>Every hex value prints lowercase, 8 digits
/// (<c>value.ToString("x8")</c>) to match <c>WalkOracleTrace</c>'s
/// <c>[0-9a-f]{8}</c> regex family — an uppercase or short-padded print
/// silently fails to parse.</para>
///
/// <para>This class never influences admission, depth state, or draw
/// order — every call site in <see cref="WalkFrameDriver"/> invokes it
/// AFTER the walk has already decided to emit the corresponding turn.</para>
/// </summary>
internal static class WalkTranscriptDump
{
private static bool Enabled =>
AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled;
/// <summary>
/// The frame-root pair — "F n" then "P …" — printed once per
/// <see cref="WalkFrameDriver.Collect"/> call (retail
/// <c>SmartBox::RenderNormalMode</c> @0x00453aa0).
/// <paramref name="forward"/> is the walk's own CY-plane normal
/// (<c>ctx.CyPlane.Normal</c>) — the SAME forward direction the walk
/// already derived for this frame (<c>WalkProductionFrameContext</c>/
/// <c>WalkTraceReplayContext</c> both store it there), so this costs no
/// extra camera-state lookup. The printed quaternion is a reasonable
/// orthonormal basis built FROM that forward vector (right = forward ×
/// worldUp, up = right × forward) in retail's w,x,y,z storage order — an
/// internally self-consistent pose for the round-trip parser (T2), not a
/// byte-exact reproduction of retail's own internal Frame (the B4
/// signature diff never compares the P line — see
/// <c>WalkTraceReplayContext.Signature</c>'s DI/DC/BLD/LS/LC/SC/EC/OC
/// scope). <paramref name="landblockLocalOrigin"/> is precomputed by the
/// caller from <c>WalkLandscape.ViewerWorldOriginX/Y</c> — the SAME 192 m
/// block origin the walk's own terrain/scenery placement already uses —
/// so this method does no coordinate math of its own.
/// </summary>
internal static void PrintFrameRoot(
int frameNumber,
uint cameraCellId,
Vector3 landblockLocalOrigin,
Vector3 forward)
{
if (!Enabled) return;
// A forward nearly parallel to world-up degenerates the usual
// forward×worldUp cross product (an outdoor root looking straight
// down, or a synthetic test fixture); fall back to a different
// reference axis rather than emit a NaN/zero-length basis.
Vector3 worldUp = MathF.Abs(Vector3.Dot(forward, Vector3.UnitZ)) > 0.999f
? Vector3.UnitY
: Vector3.UnitZ;
Vector3 right = Vector3.Normalize(Vector3.Cross(forward, worldUp));
Vector3 up = Vector3.Cross(right, forward);
// Row-vector convention (System.Numerics: v' = v * M, so
// Vector3.Transform(axis, M) reads out ROW axis of M): row1→right
// (UnitX), row2→forward (UnitY), row3→up (UnitZ) — the SAME
// convention WalkTraceReplayContext decodes a P line with
// (forward = Transform(UnitY, rotation), up = Transform(UnitZ, …),
// right = Transform(UnitX, …)), so a self-produced P line round-
// trips through that SAME decoding, not just through the parser.
var basis = new Matrix4x4(
right.X, right.Y, right.Z, 0f,
forward.X, forward.Y, forward.Z, 0f,
up.X, up.Y, up.Z, 0f,
0f, 0f, 0f, 1f);
Quaternion q = Quaternion.CreateFromRotationMatrix(basis);
Console.WriteLine($"F {frameNumber}");
Console.WriteLine(
$"P {cameraCellId.ToString("x8")} "
+ $"{HexOf(landblockLocalOrigin.X)} {HexOf(landblockLocalOrigin.Y)} {HexOf(landblockLocalOrigin.Z)} "
+ $"{HexOf(q.W)} {HexOf(q.X)} {HexOf(q.Y)} {HexOf(q.Z)}");
}
/// <summary><c>LScape::draw</c> @0x00506330 entry.</summary>
internal static void PrintLandscape()
{
if (!Enabled) return;
Console.WriteLine("LS");
}
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 entry.</summary>
internal static void PrintBuilding(uint positionCellId)
{
if (!Enabled) return;
Console.WriteLine($"BLD {positionCellId.ToString("x8")}");
}
/// <summary><c>PView::DrawInside</c> @0x005a5860 entry.</summary>
internal static void PrintDrawInside(uint cellId)
{
if (!Enabled) return;
Console.WriteLine($"DI {cellId.ToString("x8")}");
}
/// <summary>
/// <c>PView::DrawCells</c> @0x005a4840 entry. <paramref name="outdoorPview"/>
/// is false for the interior pview's own root flood, true for a
/// building look-in through the outdoor pview (the caller derives this
/// from which stage is active at the DC event — see
/// <c>WalkFrameDriver.Emit</c>'s <c>DrawCells</c> case). The "pv=" field
/// prints as an 8-hex-digit 0/1 (<c>00000000</c>/<c>00000001</c>)
/// rather than a real pointer so it satisfies the SAME
/// <c>pv=[0-9a-f]{8}</c> regex the retail captures use — the parser
/// ignores its value either way (§11.2 B1).
/// </summary>
internal static void PrintDrawCells(
bool outdoorPview, int outsideViewCount, IReadOnlyList<uint> cells)
{
if (!Enabled) return;
var sb = new StringBuilder(48 + cells.Count * 9);
sb.Append("DC pv=").Append(outdoorPview ? "00000001" : "00000000");
sb.Append(" ov=").Append(outsideViewCount);
sb.Append(" n=").Append(cells.Count).Append(':');
for (int i = 0; i < cells.Count; i++)
sb.Append(' ').Append(cells[i].ToString("x8"));
Console.WriteLine(sb.ToString());
}
/// <summary><c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120 entry.</summary>
internal static void PrintLandCell(uint cellId)
{
if (!Enabled) return;
Console.WriteLine($"LC {cellId.ToString("x8")}");
}
/// <summary><c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 entry.</summary>
internal static void PrintSortCell(uint cellId)
{
if (!Enabled) return;
Console.WriteLine($"SC {cellId.ToString("x8")}");
}
/// <summary><c>RenderDeviceD3D::DrawEnvCell</c> @0x0059f170 entry — fires
/// 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 <c>EC a9b40100</c> four times in ONE frame across
/// four look-in DC turns that all redraw the same cell.</summary>
internal static void PrintEnvCellShell(uint cellId)
{
if (!Enabled) return;
Console.WriteLine($"EC {cellId.ToString("x8")}");
}
/// <summary><c>RenderDeviceD3D::DrawObjCellForDummies</c> @0x005a0760
/// entry — the interior/look-in object-list turn (retail's outdoor
/// <c>DrawObjCell</c> is a different function, folded into the "SC"
/// line's own DrawSortCell call; it has no separate per-cell OC hook of
/// its OWN). S3 chunk 1 fix round 1 (G8), corrected round 2 (§11.6 H1):
/// this SAME line kind ALSO prints EXACTLY once more per COMPLETE
/// outdoor frame, from a second call site entirely —
/// <c>RetailPViewPassExecutor.DrawWeatherOnce</c>, called ONCE after the
/// slice loop (round 1 had this inside
/// <c>DrawLandscapeSliceLate</c>'s per-slice loop, printing 2-4 times
/// for an interior root's exit views — a mismatch every capture
/// disproves) — matching retail's <c>GameSky::Draw</c> @0x00506ff0
/// (<c>arg2==1</c>) calling <c>DrawObjCellForDummies(after_sky_cell)</c>
/// @0x005070da when the player stands outside — the after-sky cell's DID
/// is the viewer's own land cell (the SAME id the frame-root "P" line
/// 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.</summary>
internal static void PrintObjectCellTurn(uint cellId)
{
if (!Enabled) return;
Console.WriteLine($"OC {cellId.ToString("x8")}");
}
/// <summary>S3 §9.1 R2's LOD cell id: <c>x*8+y+1</c> in LOD coordinates
/// (a side-2 block's cells are 1, 2, 9, 10), composed onto the
/// landblock prefix — the SAME id
/// <c>WalkFrameDriver</c>'s <c>OnLandscapeCellTurn(landblockId,8,cellIndex)</c>
/// fast path already produces for side==8, generalized to every LOD.
/// <paramref name="sideCellCount"/>/<paramref name="cellIndex"/> travel
/// PACKED (LOD resolution), matching <c>OnLandCellTurn</c>'s own
/// contract — never the expanded 8×8-bucket resolution.</summary>
internal static uint LodCellId(uint landblockId, int sideCellCount, int cellIndex)
{
int x = cellIndex / sideCellCount;
int y = cellIndex % sideCellCount;
return (landblockId & 0xFFFF0000u) | checked((uint)(x * 8 + y + 1));
}
private static string HexOf(float value)
=> unchecked((uint)BitConverter.SingleToInt32Bits(value)).ToString("x8");
}