fix(render): S3 chunk 1 round 1 — SC at DrawSortCell entry, the weather OC, real pins with mutation checks
Campaign OVERHAUL S3 chunk 1 fix round 1 (docs/research/2026-09-01-overhaul/
s3-walk-ownership-map.md §11.5), applied on top of 079483bb6 after the
three-lens review. G1-G10, each verified against source and (where the item
adds a pin) mutation-checked.
G1 — the flag-off integration test was vacuous: Collect_TranscriptFlagOff_
ProducesNoConsoleOutput drove ONE frame, and WalkOracleTrace.Parse
structurally discards the last (open) frame, so it passed whether or not
the flag gate worked. Now drives TWO frames and asserts BOTH that Parse
sees zero frames AND that no captured line starts with any of the ten
transcript-kind prefixes (F /P /LS/LC /SC /BLD /DI /DC /EC /OC ) — robust
to unrelated console noise from parallel test classes.
G2 — LC/SC computed LodCellId before testing the flag (~1,300 land-cell
turns/frame of division/modulo/checked-conversion work paid even when the
flag is off). Both OnLandCellTurn and the new OnSortCellTurn now guard the
LodCellId call itself, not just WalkTranscriptDump's internal check.
G3 — the flag-on round trip never exercised LC/SC/BLD (the T2 fixture's
stub 1x1 landscape has zero blocks). New
OutdoorRoot_TranscriptFlagOn_PrintsLandscapeThenLandCellThenSortCellThen
Building_InOrder drives RetailFrameWalk.DrawLandscape directly (the same
deterministic "CY-only" admission technique the existing outdoor LandCell
test uses) over a landscape with one block + one building, and asserts
LS < LC < SC < BLD by position, with the exact cell/building ids.
G4 — the parts/alpha-depth smoke tests asserted FL/PC (Flushes/
DrawCellsSamples) but never AM/PM (MeshAdds/PortalPolyDraws) for the
alpha-depth log, even though PD/DM were already asserted for the parts
log. Added the missing Assert.Contains for MeshAdds and PortalPolyDraws.
G5 — launch-options.md's side-effects column now states the flag off/on
costs explicitly ("one bool read per print site, nothing else" / "one
console line per walk turn, ≈600-800 lines per outdoor frame").
G6 — removed the "DI f4180108... no, a9b4013f" self-correction in
Oh_doorway_still_first_frame_diff's comment; added a one-line note on why
it and Oh_foundry_deep_... stay bespoke Facts rather than folding into the
Theory (no functional reason, just history).
G7 — SC printed from OnLandscapeCellTurn, which RetailFrameWalk.
DrawLandscape calls AFTER DrawBuilding — retail's own order is
LC -> SC -> BLD (terrace-edge.walk.log:2331-2333), not LC -> BLD -> SC.
Fixed by adding IWalkEventSink.OnSortCellTurn, fired at the DrawSortCell
entry point (same alwaysDrawObjects||cellInView gate, before DrawBuilding);
the driver moves its SC print there and OnLandscapeCellTurn no longer
prints one.
G8 — the trailing "OC <viewer land cell>" retail prints once per complete
outdoor frame is GameSky::Draw's weather branch calling
DrawObjCellForDummies(after_sky_cell) @0x005070da, not a walk turn.
RetailPViewPassExecutor.DrawLandscapeSliceLate now prints it at the point
it actually calls RenderWeather; PrintObjectCellTurn's doc, which denied
this line existed, is corrected. New
Oh_terrace_edge_outdoor_frame_ends_with_the_weather_ObjectCellTurn pins
this directly against the OH capture: every complete outdoor frame's last
event is an OC whose cell id equals that same frame's own P-line cell id.
G9 — "EC and OC counts are always exactly equal per pose" was false
(terrace-edge: 12 EC vs 16 OC, the extra 4 being G8's weather OC, which
has no EC counterpart). The visit-scoped/stamp-dedupe conclusion was
right; only the count claim needed fixing. Both doc sites corrected.
G10 — the conformance signature dropped LC/SC/EC/OC entirely, which is
how G7 shipped green: no test ever compared their placement. Recorder now
tracks LC/SC via the new OnLandCellTurn/OnSortCellTurn hooks and derives
EC/OC per DC from PView::DrawCells's own two reverse loops (shell
far-to-near, then object-list far-to-near) — a look-in flood's DC derives
them immediately, the interior root's OWN flood defers to
OnInteriorFloodDrawTurn (verified against cathedral-leak.walk.log:4,1328,
where the root DC's own EC/OC print last). WalkTraceReplayContext.
Signature8 applies the identical derivation to oracle frames and never
produces the trailing weather OC (G8's own dedicated pin covers that
separately, matching the spec's "exclude from the comparison, assert
separately" instruction). Used ONLY for the four OH-capture-rooted
conformance rows (the older FW0 fixtures predate the LC/SC/EC/OC
breakpoints and carry none of those lines).
Applying the eight-kind check surfaced a real, separate bug: WalkLandscape
DatBuilder never set WalkLandBlock.LandblockId, so every LC/SC token any
DAT-built world ever produced read landblockId=0 — latent because the old
four-kind signature filtered LC/SC out. Fixed (one field, test-fixture-
only, no production behavior change). With that fixed, three of the four
OH rows (terrace-edge, cathedral-arrival, foundry-deep) reproduce EXACTLY
at the eight-kind level. Oh_doorway_still_first_frame_diff still diverges
by one extra LC/SC pair (token index 165: replay draws LC:a9c90001|
SC:a9c90001 that retail never does) — a real WalkLandscape.CheckBlocks
visibility question at a ring-2/ring-3 LOD boundary, out of this round's
print-only scope, needing the retail oracle to resolve. Marked
[Trait("Status","KnownFailure")] with the exact position recorded in its
own doc comment, matching TowerAscent's existing pattern — the InstalledDat
lane now shows FOUR known failures (the pre-existing two #383 layout tests
+ TowerAscent, plus this new finding), not three; never weakened the pin
itself.
Mutation checks (all four restored after observing the failure):
- G1: forcing WalkTranscriptDump.Enabled true made
Collect_TranscriptFlagOff_ProducesNoConsoleOutput fail with
"Assert.DoesNotContain() Failure: Filter matched in collection ... Collection:
[\"DI 00000100\", \"DC pv=00000000 ov=1 n=2: 00000100 00000101\", \"LS\", ...]".
- G3: deleting PrintLandCell's Console.WriteLine made the new outdoor test
fail with "expected an LC line".
- G4: making MeshAddPattern unmatchable made all five
Alpha_depth_log_parses_with_nonempty_samples rows fail with
"Assert.Contains() Failure: Filter not matched in collection".
- G7/G10: moving OnSortCellTurn back after DrawBuilding made the
terrace-edge conformance row fail with "walk diverged from retail
(terrace-edge.walk)", first divergence exactly at its first building
(f518002e): expected ...SC:f518002e|BLD:f518002e..., actual
...BLD:f518002e|SC:f518002e....
Gates: hermetic lane 6,816/0 (baseline 6,814 + G3's new test + G8's new
test); InstalledDat lane 244/4 known failures (two pre-existing #383
layout tests + TowerAscent + the new Oh_doorway_still_first_frame_diff
finding, documented above).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
02a8288172
commit
be81475cf9
12 changed files with 502 additions and 43 deletions
|
|
@ -116,7 +116,19 @@ public sealed partial class WalkFrameDriverTests
|
|||
/// <summary>T1's integration half: a full synthetic interior frame,
|
||||
/// driven through the SAME <see cref="WalkFrameDriver.Collect"/> path
|
||||
/// production uses, produces literally NO transcript output when the
|
||||
/// flag is off.</summary>
|
||||
/// flag is off. S3 chunk 1 fix round 1 (G1): TWO <c>Collect</c> calls,
|
||||
/// not one — <see cref="WalkOracleTrace.Parse"/> 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
|
||||
/// <c>RenderingDiagnostics.DumpWalkTranscriptEnabled</c> true right
|
||||
/// after setting it false here makes <c>AssertNoTranscriptLines</c>
|
||||
/// fail, both on the parsed-frame-count assertion and the prefix
|
||||
/// scan).</summary>
|
||||
[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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ten transcript line-kind prefixes <see cref="WalkTranscriptDump"/>
|
||||
/// ever emits — <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> — matching
|
||||
/// each print method's own exact <c>Console.WriteLine</c> format
|
||||
/// (S3 chunk 1 fix round 1, G1).
|
||||
/// </summary>
|
||||
private static readonly string[] TranscriptLinePrefixes =
|
||||
["F ", "P ", "LS", "LC ", "SC ", "BLD ", "DI ", "DC ", "EC ", "OC "];
|
||||
|
||||
/// <summary>
|
||||
/// Asserts NONE of <see cref="WalkTranscriptDump"/>'s own line kinds
|
||||
/// appear in <paramref name="capture"/> — 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).
|
||||
/// </summary>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>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<uint> 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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>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
|
||||
/// <see cref="RetailFrameWalk.DrawLandscape"/> directly with the SAME
|
||||
/// deterministic "CY-only" <see cref="OneDegenerateView"/> admission
|
||||
/// technique <c>OutdoorRoot_LandCellPrecedesItsOwnCellsObjectTurn_ThenFlushesAtReplaysEnd</c>
|
||||
/// uses (a real <see cref="RetailFrameWalk.WalkFrame"/> 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
|
||||
/// <see cref="WalkOracleTrace.Parse"/>) — this test only needs kind
|
||||
/// presence and relative order, not a complete F/P-framed capture.
|
||||
/// MUTATION CHECK: deleting <c>WalkTranscriptDump.PrintLandCell</c>'s
|
||||
/// call site (or its body) makes the "expected an LC line" assertion
|
||||
/// fail.</summary>
|
||||
[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<string>());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>§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 <c>Collect_TranscriptFlagOn_…</c>'s
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ public sealed class WalkOraclePartsTraceTests
|
|||
Assert.Contains(frames, f => f.MeshDraws.Count > 0);
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,31 @@ public sealed class WalkOracleTraceTests
|
|||
Assert.All(frames, f => Assert.NotEmpty(f.Events));
|
||||
}
|
||||
|
||||
/// <summary>S3 chunk 1 fix round 1 (G8/G10): the "assert separately"
|
||||
/// half of <see cref="WalkTraceReplayContext.Signature8(WalkOracleFrame)"/>'s
|
||||
/// own doc comment — that method's derivation never produces the
|
||||
/// trailing weather OC (G8: <c>GameSky::Draw</c> @0x00506ff0's weather
|
||||
/// branch calling <c>DrawObjCellForDummies(after_sky_cell)</c>
|
||||
/// @0x005070da), so its presence/position is pinned here instead,
|
||||
/// directly against the OH capture (the FW0 <c>terrace-edge</c> fixture
|
||||
/// predates the OC breakpoint and carries none). Every complete outdoor
|
||||
/// frame's LAST event is an <c>OC</c> whose cell id equals that SAME
|
||||
/// frame's own <c>P</c> line cell id (the viewer's land cell).</summary>
|
||||
[Fact]
|
||||
public void Oh_terrace_edge_outdoor_frame_ends_with_the_weather_ObjectCellTurn()
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> 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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,10 +16,70 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class WalkTraceConformanceTests
|
||||
{
|
||||
/// <summary>S3 chunk 1 fix round 1 (G10): besides the plain
|
||||
/// <see cref="WalkEvent"/> list the four-kind
|
||||
/// <see cref="WalkTraceReplayContext.Signature(IEnumerable{WalkEvent})"/>
|
||||
/// still uses, also builds the full eight-kind token stream — LS/BLD/
|
||||
/// DI/DC/LC/SC/EC/OC — AS each hook fires, for
|
||||
/// <see cref="WalkTraceReplayContext.Signature8(IReadOnlyList{string})"/>.
|
||||
/// <see cref="_lookIn"/> mirrors <c>WalkFrameDriver</c>'s own private
|
||||
/// <c>_currentDcStage</c>: null until the frame's first
|
||||
/// <see cref="WalkEventKind.DrawInside"/> (the interior root's own
|
||||
/// flood defers its EC/OC to <see cref="OnInteriorFloodDrawTurn"/>),
|
||||
/// flips true forever once a building's portal pass begins
|
||||
/// (<see cref="OnBuildingTurn"/>) — see
|
||||
/// <see cref="WalkTraceReplayContext.Signature8(WalkOracleFrame)"/>'s own
|
||||
/// doc comment for why this never resets mid-frame.</summary>
|
||||
private sealed class Recorder : IWalkEventSink
|
||||
{
|
||||
public readonly List<WalkEvent> Events = new();
|
||||
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
||||
public readonly List<string> 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<uint> 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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 —
|
||||
/// <c>…SC:a8c90001|LC:a8c90001|SC:a8c90001|</c> retail jumps straight to
|
||||
/// <c>LC:95c60001</c>; the replay inserts <c>LC:a9c90001|SC:a9c90001</c>
|
||||
/// first. A single spurious block admission at a ring-2/ring-3 LOD
|
||||
/// boundary near this pose — a real <c>WalkLandscape.CheckBlocks</c>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<WalkOracleFrame> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,4 +174,103 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
|||
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
|
||||
_ => "?",
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<c>WalkTraceConformanceTests</c>'
|
||||
/// <c>OhCaptureRoot</c> 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).
|
||||
/// <para>
|
||||
/// LC/SC come straight from <paramref name="tokens"/>/<paramref
|
||||
/// name="frame"/>'s own literal events — real turns with a real position
|
||||
/// on both sides. EC/OC do NOT: <see cref="WalkEvent"/>'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
|
||||
/// (<c>PView::DrawCells</c>'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 <c>RetailFrameWalk.DrawLandscape</c>'s
|
||||
/// own recursive <c>DrawCells</c> call; the interior root's OWN flood
|
||||
/// DEFERS them past the whole landscape/look-in walk, to
|
||||
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/> — 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
|
||||
/// <c>WalkOracleTraceTests</c> for a dedicated assertion that a
|
||||
/// COMPLETE outdoor frame's raw event list really does end with it).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static string Signature8(IReadOnlyList<string> tokens) => string.Join("|", tokens);
|
||||
|
||||
/// <summary>The oracle-frame half of <see cref="Signature8(IReadOnlyList{string})"/> —
|
||||
/// see that overload's doc comment for the shared derivation rule.</summary>
|
||||
public static string Signature8(WalkOracleFrame frame)
|
||||
{
|
||||
var tokens = new List<string>();
|
||||
bool? lookIn = null;
|
||||
IReadOnlyList<uint>? 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);
|
||||
}
|
||||
|
||||
/// <summary><c>PView::DrawCells</c>'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 <see cref="Signature8(WalkOracleFrame)"/>'s own doc
|
||||
/// comment for the evidence. Internal (not private): <see
|
||||
/// cref="WalkTraceConformanceTests"/>'s own <c>Recorder</c> reuses this
|
||||
/// EXACT rule for the replay side rather than re-deriving it.</summary>
|
||||
internal static void AppendFloodTurns(List<string> tokens, IReadOnlyList<uint> 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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue