acdream/tests/AcDream.App.Tests/Rendering/CameraDiagnosticsCollection.cs
Erik 02a8288172 feat(render): S3 chunk 1 — print-only walk transcript, OH fixtures, offline signature diff
Campaign OVERHAUL S3 chunk 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§11): the transcript-kinds/fixtures/print-only-emitter half of the walk work,
built AFTER chunk 3 landed LC/SC (the per-land-cell interleave).

B1 — the emitter (print-only, never gates admission/depth/order):
- ACDREAM_DUMP_WALK_TRANSCRIPT=1 is read once into RuntimeOptions.DumpWalkTranscript
  (rule 4) and handed to RenderingDiagnostics.DumpWalkTranscriptEnabled (rule 5,
  a settable static, not a second env read) once at GameWindow construction — the
  deep walk call sites have no reachable RuntimeOptions reference.
- WalkTranscriptDump (new) prints the OH line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC —
  to Console at the exact points retail's cdb breakpoints sit
  (tools/walk-oracle/oh/oh-capture-walk.cdb.template), gated internally so every
  method bails out before any string work when the flag is off.
- Every call site lives in WalkFrameDriver.cs, at the point the driver already
  processes that turn: Collect (F/P, after BeginFrame), Emit's DI/LS/DC/BLD cases,
  OnLandCellTurn/OnLandscapeCellTurn (LC/SC, at LOD resolution via the new
  WalkTranscriptDump.LodCellId helper, before the 8x8-bucket expansion), and the
  EmitFloodTurns/EmitCellContentsTurn loops (EC/OC — both UNCONDITIONAL per flood
  visit, matching the OH captures' always-equal EC/OC counts; retail's own
  DrawEnvCell stamp dedupe sits past the breakpoint, inside the function).
- DC's "pv=" field encodes interior(0)/outdoor(1) as an 8-hex-digit 0/1 so it
  satisfies the same pv=[0-9a-f]{8} regex real captures use; derived from
  _currentDcStage at the DC event (CellStatic = interior pview, else outdoor).
- The frame-root pose (origin, quaternion) is a reasonable orthonormal basis built
  from the walk's own CyPlane.Normal forward vector and WalkLandscape's own
  ViewerWorldOriginX/Y block origin — self-consistent for the round-trip parser,
  not a byte-exact reproduction of retail's Frame (B4's diff never compares P).

B2 — WalkOracleTrace learns EC/OC event kinds (LC/SC already existed from chunk 3).
New WalkOraclePartsTrace.cs holds two small read-only parsers for the parts log
(PD/DM) and the alpha-depth log (AM/FL/PM/PC) — records only, no validator, no
canonical JSONL, no new tool.

B3 — fixtures: the five OH walk captures join WalkOracleTraceTests.AllFixtures
(now (root, name) pairs — FW0's own root plus the OH capture directory) for
parse + complete-frame pins. The four kit poses join WalkTraceConformanceTests'
still-fixture coverage as NEW rows (the OH cathedral-arrival root is f4180108,
not FW0's f4180106): terrace-edge/cathedral-arrival extend the existing theory
(now (root, fixture) parameterized); holtburg-doorway-still and foundry-deep get
dedicated tests mirroring their FW0 siblings' own structure. Finding: the OH
foundry-deep capture's own retail transcript draws 12 real town buildings through
its exit chain (unlike the FW0 capture, which apparently reached none at that
pose) — the FW0 test's stub 1x1 landscape undershoots it (first divergence:
nothing after "LS" vs retail's real BLD content); fixed by building the full
landscape/building world via WalkLandscapeDatBuilder.Build, matching the shared
theory's own approach, not by skipping or weakening the row.
WalkTraceReplayContext.Signature(WalkOracleFrame) now filters to the DI/DC/BLD/LS
kinds (LC/SC/EC/OC never had a WalkEvent analogue in RetailFrameWalk's own
four-kind vocabulary) instead of mapping them to a "?" placeholder, so the still-
fixture comparison stays apples-to-apples on both sides.

B4 — WalkTranscriptSignatureDiff (test-side only, no runner/tool): diffs two
transcripts (raw lines or parsed frames) at the full DI/DC/BLD/LS/LC/SC/EC/OC
level, reporting the first divergent event and position per frame. Proven over a
synthetic self-vs-self-minus-one-event pair (SignatureDiff_ReportsTheExactRemovedEvent).

Tests: T1 (flag off) is split into a unit-level zero-allocation/zero-output check
on WalkTranscriptDump itself (the walk's pre-existing allocation, e.g.
RetailFrameWalk.EmitDrawCells's per-call array, is untouched by this chunk and not
independently zero-alloc) and an integration-level Collect() check; both assert via
WalkOracleTrace.Parse returning zero frames rather than raw string equality, which
is robust to unrelated Console.WriteLine noise from other test classes running in
parallel (a real, observed hazard — WalkFrameDriverTests joins
CameraDiagnosticsCollection for the same reason CornerFloodReplayTests/
Issue181WallPressEquilibriumTests already do, issue #251). T2 proves the
emitter/parser round trip on a synthetic interior frame. T3's InstalledDat rows all
pass. T4: LaunchOptionsDocumentationTests green with the ACDREAM_DUMP_WALK_TRANSCRIPT
row (both directions).

Gates: hermetic lane 6,814/0 (was 6,795 baseline + new tests), three consecutive
clean runs; InstalledDat lane 245/3 known-failures (the two pre-existing #383
layout tests + TowerAscent) unchanged from baseline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 11:49:42 +02:00

70 lines
3.6 KiB
C#

using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Serializes every test class that shares the camera/render family of
/// <b>process-global mutable statics</b>. xUnit runs distinct test classes in
/// PARALLEL by default, and these statics live on the process, not on the test
/// — so two classes running concurrently observe each other's writes.
///
/// <para>
/// The per-class <c>try</c>/<c>finally</c> save/restore blocks are correct
/// <i>within</i> a class and remain necessary — but they are not sufficient.
/// A <c>finally</c> only bounds the mutation in TIME along its own thread; it
/// cannot stop another class from reading the static inside that window. Worse,
/// two overlapping save/restore pairs can interleave so the second restore
/// writes back the FIRST one's temporary value, leaving the global permanently
/// wrong for the rest of the run. Serializing the sharers is what actually makes
/// each class's <c>finally</c> sufficient.
/// </para>
///
/// <para>Three distinct globals are covered, and every one of them is a shared
/// edge between at least two classes in this collection:</para>
/// <list type="bullet">
/// <item><description><c>AcDream.Core.Rendering.CameraDiagnostics</c> —
/// <c>AlignToSlope</c>, <c>CollideCamera</c>, <c>TranslationStiffness</c>,
/// <c>RotationStiffness</c>, <c>UseRetailChaseCamera</c>. Read by
/// <c>RetailChaseCamera.Update</c>, <c>CameraController.Active</c>,
/// <c>CameraFrameController</c>, <c>WorldRenderFrameBuilder</c> and
/// <c>MouseLookController</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled</c>
/// — written by <c>CornerFloodReplayTests</c> and
/// <c>Issue181WallPressEquilibriumTests</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled</c>
/// (Campaign OVERHAUL S3 chunk 1) — written by
/// <c>WalkFrameDriverTests</c>' transcript-emitter tests
/// (<c>WalkFrameDriverTranscriptTests.cs</c>).</description></item>
/// <item><description><c>System.Console.Out</c> — redirected via
/// <c>Console.SetOut</c> by those same classes to capture probe output.
/// Interleaved redirection can restore a DISPOSED <c>StringWriter</c> as the
/// process-wide <c>Console.Out</c>, which then throws in unrelated
/// tests.</description></item>
/// </list>
///
/// <para>
/// Membership is deliberately narrow: only classes that write one of these
/// globals, or that drive production code which READS a knob another member
/// mutates away from its default (<c>RetailChaseCameraTests</c> sets
/// <c>AlignToSlope</c> and <c>CollideCamera</c> to <see langword="false"/>;
/// the <c>UseRetailChaseCamera</c> writers set it to <see langword="false"/>).
/// Classes that merely construct a <c>CameraController</c> without a retail
/// chase camera are NOT members — their reads are insensitive.
/// </para>
///
/// <para>
/// Marker only, no collection fixture: each member still needs its own
/// per-test values (several are <c>[Theory]</c> cases that set different knobs
/// per case), so a fixture cannot own the save/restore without rewriting every
/// member's internals. The existing per-test <c>finally</c> blocks already
/// return the defaults; this attribute supplies the mutual exclusion they were
/// missing. Follows the <c>WorldEnvironmentControllerCollection</c> precedent.
/// </para>
///
/// <para>Evidence + history: <c>docs/ISSUES.md</c> #251.</para>
/// </summary>
[CollectionDefinition(Name)]
public sealed class CameraDiagnosticsCollection
{
public const string Name = "Camera diagnostics globals";
}