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>
273 lines
12 KiB
C#
273 lines
12 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>
|
|
/// S3 chunk 1 (§11.2 B2): small, read-only parsers for the OH capture
|
|
/// family's two companion logs — the PARTS log
|
|
/// (<c>tools/walk-oracle/oh/oh-capture-parts.cdb.template</c>: PD/DM) and
|
|
/// the ALPHA-DEPTH log
|
|
/// (<c>tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template</c>: AM/FL/
|
|
/// PM/PC), both dumped from the SAME <c>docs/research/2026-09-01-overhaul/
|
|
/// oh-capture/</c> directory as the walk log <see cref="WalkOracleTrace"/>
|
|
/// already parses. These are records only — no validator, no canonical
|
|
/// JSONL, no new tool ([[feedback-evidence-infrastructure-sink]]) — a later
|
|
/// chunk/S4 names the first real consumer and pins specific values then.
|
|
/// </summary>
|
|
public static class WalkOraclePartsTrace
|
|
{
|
|
/// <summary>Parses a PARTS log (<c><pose>.parts.log</c>). Same
|
|
/// F/P framing and truncated-last-frame drop rule as
|
|
/// <see cref="WalkOracleTrace.Parse"/> — the harness detaches at the
|
|
/// frame marker, so the final "F n" never records its own PD/DM lines.</summary>
|
|
public static IReadOnlyList<WalkOraclePartsFrame> Parse(IEnumerable<string> lines)
|
|
{
|
|
var frames = new List<WalkOraclePartsFrame>();
|
|
List<WalkOraclePartDraw>? partDraws = null;
|
|
List<WalkOracleMeshDraw>? meshDraws = null;
|
|
int currentNumber = 0;
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
Match frameMatch = FramePattern.Match(line);
|
|
if (frameMatch.Success)
|
|
{
|
|
if (partDraws is not null)
|
|
{
|
|
frames.Add(new WalkOraclePartsFrame(currentNumber, partDraws, meshDraws!));
|
|
}
|
|
currentNumber = int.Parse(
|
|
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
partDraws = new List<WalkOraclePartDraw>();
|
|
meshDraws = new List<WalkOracleMeshDraw>();
|
|
continue;
|
|
}
|
|
if (partDraws is null)
|
|
continue;
|
|
|
|
Match pd = PartDrawPattern.Match(line);
|
|
if (pd.Success)
|
|
{
|
|
partDraws.Add(new WalkOraclePartDraw(
|
|
ParseHex(pd.Groups[1].Value),
|
|
ParseHex(pd.Groups[2].Value),
|
|
pd.Groups[3].Value != "0",
|
|
ParseHex(pd.Groups[4].Value)));
|
|
continue;
|
|
}
|
|
Match dm = MeshDrawPattern.Match(line);
|
|
if (dm.Success)
|
|
{
|
|
meshDraws!.Add(new WalkOracleMeshDraw(
|
|
ParseHex(dm.Groups[1].Value),
|
|
ParseHex(dm.Groups[2].Value),
|
|
dm.Groups[3].Value != "0",
|
|
int.Parse(dm.Groups[4].Value, CultureInfo.InvariantCulture),
|
|
ParseHex(dm.Groups[5].Value)));
|
|
continue;
|
|
}
|
|
// Anything else (the "P …" pose line included — parts fixtures
|
|
// carry no pose consumer today) is ignored, matching
|
|
// WalkOracleTrace's own "cdb chrome" tolerance.
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
public static IReadOnlyList<WalkOraclePartsFrame> Load(string root, string fixtureName)
|
|
=> Parse(File.ReadLines(FixturePath(root, fixtureName, ".parts.log")));
|
|
|
|
private static string FixturePath(string root, string fixtureName, string suffix)
|
|
{
|
|
string repoRoot = WalkOracleTraceRepoRoot.Find();
|
|
return Path.Combine(
|
|
repoRoot, Path.Combine(root.Split('/')), fixtureName + suffix);
|
|
}
|
|
|
|
private static uint ParseHex(string hex)
|
|
=> uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
|
|
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
|
|
|
// PD gfx=<hex8> did=<hex8> force=<0|1> cell=<hex8>
|
|
private static readonly Regex PartDrawPattern = new(
|
|
@"^PD gfx=([0-9a-f]{8}) did=([0-9a-f]{8}) force=(\d) cell=([0-9a-f]{8})\s*$",
|
|
RegexOptions.Compiled);
|
|
|
|
// DM gfx=<hex8> did=<hex8> force=<0|1> bound=<0|1|2> cell=<hex8>
|
|
private static readonly Regex MeshDrawPattern = new(
|
|
@"^DM gfx=([0-9a-f]{8}) did=([0-9a-f]{8}) force=(\d) bound=(\d) cell=([0-9a-f]{8})\s*$",
|
|
RegexOptions.Compiled);
|
|
}
|
|
|
|
/// <summary>One <c>CPhysicsPart::Draw</c> @0x0050D7A0 entry (PD line).</summary>
|
|
public sealed record WalkOraclePartDraw(uint Gfx, uint DataId, bool Force, uint Cell);
|
|
|
|
/// <summary>One <c>RenderDeviceD3D::DrawMeshInternal</c> @0x0059F360 entry
|
|
/// (DM line). <paramref name="Bound"/> is retail's BoundingType (0=OUTSIDE,
|
|
/// 1=PARTIALLY_INSIDE, 2=ENTIRELY_INSIDE).</summary>
|
|
public sealed record WalkOracleMeshDraw(uint Gfx, uint DataId, bool Force, int Bound, uint Cell);
|
|
|
|
public sealed record WalkOraclePartsFrame(
|
|
int Number,
|
|
IReadOnlyList<WalkOraclePartDraw> PartDraws,
|
|
IReadOnlyList<WalkOracleMeshDraw> MeshDraws);
|
|
|
|
/// <summary>
|
|
/// S3 chunk 1 (§11.2 B2): parses an ALPHA-DEPTH log
|
|
/// (<c><pose>.alphadepth.log</c>). Same F/P framing and
|
|
/// truncated-last-frame drop rule as <see cref="WalkOracleTrace.Parse"/>.
|
|
/// </summary>
|
|
public static class WalkOracleAlphaDepthTrace
|
|
{
|
|
public static IReadOnlyList<WalkOracleAlphaDepthFrame> Parse(IEnumerable<string> lines)
|
|
{
|
|
var frames = new List<WalkOracleAlphaDepthFrame>();
|
|
List<WalkOracleAlphaMeshAdd>? meshAdds = null;
|
|
List<WalkOracleAlphaFlush>? flushes = null;
|
|
List<WalkOraclePortalPolyDraw>? portalPolyDraws = null;
|
|
List<WalkOracleDrawCellsSample>? drawCellsSamples = null;
|
|
int currentNumber = 0;
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
Match frameMatch = FramePattern.Match(line);
|
|
if (frameMatch.Success)
|
|
{
|
|
if (meshAdds is not null)
|
|
{
|
|
frames.Add(new WalkOracleAlphaDepthFrame(
|
|
currentNumber, meshAdds, flushes!, portalPolyDraws!, drawCellsSamples!));
|
|
}
|
|
currentNumber = int.Parse(
|
|
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
meshAdds = new List<WalkOracleAlphaMeshAdd>();
|
|
flushes = new List<WalkOracleAlphaFlush>();
|
|
portalPolyDraws = new List<WalkOraclePortalPolyDraw>();
|
|
drawCellsSamples = new List<WalkOracleDrawCellsSample>();
|
|
continue;
|
|
}
|
|
if (meshAdds is null)
|
|
continue;
|
|
|
|
Match am = MeshAddPattern.Match(line);
|
|
if (am.Success)
|
|
{
|
|
meshAdds.Add(new WalkOracleAlphaMeshAdd(
|
|
ParseHex(am.Groups[1].Value),
|
|
int.Parse(am.Groups[2].Value, CultureInfo.InvariantCulture),
|
|
ParseHex(am.Groups[3].Value),
|
|
am.Groups[4].Value != "0",
|
|
am.Groups[5].Value != "0",
|
|
int.Parse(am.Groups[6].Value, CultureInfo.InvariantCulture)));
|
|
continue;
|
|
}
|
|
Match fl = FlushPattern.Match(line);
|
|
if (fl.Success)
|
|
{
|
|
flushes!.Add(new WalkOracleAlphaFlush(
|
|
ParseHex(fl.Groups[1].Value), ParseHex(fl.Groups[2].Value)));
|
|
continue;
|
|
}
|
|
Match pm = PortalPolyPattern.Match(line);
|
|
if (pm.Success)
|
|
{
|
|
portalPolyDraws!.Add(new WalkOraclePortalPolyDraw(
|
|
ParseHex(pm.Groups[1].Value),
|
|
int.Parse(pm.Groups[2].Value, CultureInfo.InvariantCulture),
|
|
int.Parse(pm.Groups[3].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
|
|
continue;
|
|
}
|
|
Match pc = DrawCellsSamplePattern.Match(line);
|
|
if (pc.Success)
|
|
{
|
|
drawCellsSamples!.Add(new WalkOracleDrawCellsSample(
|
|
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
|
pc.Groups[3].Value != "0"));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
public static IReadOnlyList<WalkOracleAlphaDepthFrame> Load(string root, string fixtureName)
|
|
=> Parse(File.ReadLines(Path.Combine(
|
|
WalkOracleTraceRepoRoot.Find(),
|
|
Path.Combine(root.Split('/')),
|
|
fixtureName + ".alphadepth.log")));
|
|
|
|
private static uint ParseHex(string hex)
|
|
=> uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
|
|
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
|
|
|
// AM mesh=<hex8> surf=<n> csurf=<hex8> new=<0|1> clip=<0|1> listSel=<n>
|
|
private static readonly Regex MeshAddPattern = new(
|
|
@"^AM mesh=([0-9a-f]{8}) surf=(\d+) csurf=([0-9a-f]{8}) new=(\d) clip=(\d) listSel=(\d+)\s*$",
|
|
RegexOptions.Compiled);
|
|
|
|
// FL thresh=<hex8> ret=<hex8>
|
|
private static readonly Regex FlushPattern = new(
|
|
@"^FL thresh=([0-9a-f]{8}) ret=([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
|
|
|
// PM poly=<hex8> mode=<n> counterBefore=<hex4>
|
|
private static readonly Regex PortalPolyPattern = new(
|
|
@"^PM poly=([0-9a-f]{8}) mode=(\d) counterBefore=([0-9a-f]{4})\s*$",
|
|
RegexOptions.Compiled);
|
|
|
|
// PC ov=<n> counter=<hex4> fc=<0|1>
|
|
private static readonly Regex DrawCellsSamplePattern = new(
|
|
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=(\d)\s*$", RegexOptions.Compiled);
|
|
}
|
|
|
|
/// <summary>One <c>D3DPolyRender::AddMeshToAlphaList</c> @0x0059C230 entry
|
|
/// (AM line). <paramref name="ListSelector"/> 0 selects the ALPHA list,
|
|
/// nonzero the CLIP list.</summary>
|
|
public sealed record WalkOracleAlphaMeshAdd(
|
|
uint Mesh, int Surface, uint ClipSurface, bool New, bool Clip, int ListSelector);
|
|
|
|
/// <summary>One <c>D3DPolyRender::FlushAlphaList</c> @0x0059D2E0 entry (FL
|
|
/// line). <paramref name="ThresholdBits"/> is the raw IEEE-754 bits of the
|
|
/// threshold float argument.</summary>
|
|
public sealed record WalkOracleAlphaFlush(uint ThresholdBits, uint ReturnAddress);
|
|
|
|
/// <summary>One <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059BC90
|
|
/// entry (PM line). <paramref name="Mode"/> 0 = true-depth/exit-seal,
|
|
/// nonzero = far-Z/building punch.</summary>
|
|
public sealed record WalkOraclePortalPolyDraw(uint Poly, int Mode, int CounterBefore);
|
|
|
|
/// <summary>One <c>PView::DrawCells</c> @0x005A4840 entry, sampled for the
|
|
/// persistent <c>portalsDrawnCount</c> depth-lifecycle state machine (PC
|
|
/// line) — same breakpoint address as the walk log's DC line, but this is a
|
|
/// SEPARATE capture that does not also record the cell roster.</summary>
|
|
public sealed record WalkOracleDrawCellsSample(int OutsideViewCount, int Counter, bool ForceClear);
|
|
|
|
public sealed record WalkOracleAlphaDepthFrame(
|
|
int Number,
|
|
IReadOnlyList<WalkOracleAlphaMeshAdd> MeshAdds,
|
|
IReadOnlyList<WalkOracleAlphaFlush> Flushes,
|
|
IReadOnlyList<WalkOraclePortalPolyDraw> PortalPolyDraws,
|
|
IReadOnlyList<WalkOracleDrawCellsSample> DrawCellsSamples);
|
|
|
|
/// <summary>Shared repo-root finder — the SAME walk-up-to-<c>AcDream.slnx</c>
|
|
/// logic <see cref="WalkOracleTrace"/> already has privately; factored out
|
|
/// so the parts/alpha-depth parsers don't duplicate it a second and third
|
|
/// time.</summary>
|
|
internal static class WalkOracleTraceRepoRoot
|
|
{
|
|
internal static string Find()
|
|
{
|
|
DirectoryInfo? dir = new(AppContext.BaseDirectory);
|
|
while (dir is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
|
|
return dir.FullName;
|
|
dir = dir.Parent;
|
|
}
|
|
throw new InvalidOperationException(
|
|
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
|
|
}
|
|
}
|