Ports RenderDeviceD3D::DrawBlock @0x005a17c0's real per-cell order: loop 1 (@0x005a1876) prepares shadow lists; loop 2 (@0x005a197d) DrawLandCell(cell) @0x005a19c0 fires ONLY when the cell is in view, STRICTLY BEFORE DrawSortCell(cell) @0x005a19e6, which fires whenever alwaysDrawObjects (retail default 1 @0x00820ed4) or the cell is in view. RetailFrameWalk.DrawLandscape now emits sink.OnLandCellTurn( landblockId, side, cellIndex) at that exact point, per admitted cell, before the existing DrawBuilding + OnLandscapeCellTurn (the DrawSortCell half). WalkFrameDriver records one LandCell frame event per turn and deletes the whole-stage TerrainSlice(0) emission and the DrawTerrainSlice leaf outright — drawing all terrain before every building let a nearer building's far-Z punch survive under farther terrain drawn afterward, the doorway-behind-a-hill fragment bug from the owner's G2 Holtburg screenshot; this chunk removes it by ORDER alone, with no depth-compare change (S4's punch z-func question is untouched, per the contract). Index-run arithmetic (S3 §9.1 R3): the terrain mesh is cell-major (LandblockMesh.Build: cy outer, cx inner, 6 indices/cell, 384/land- block). A retail LOD cell (side n, LOD coords X,Y) covers cx in [X*8/n,(X+1)*8/n), cy in [Y*8/n,(Y+1)*8/n) — one contiguous run per covered cy row: side 8 -> one run of 6, side 4 -> two runs of 12, side 2 -> four runs of 24; side 1's single coarse cell covers every row contiguously so its 8 per-row runs collapse into ONE run of all 384 indices. TerrainModernRenderer.AppendCellIndexRuns (pure, no GPU, no baked table) and DrawLandCellRuns (resolves the landblock's slot, builds one DrawElementsIndirectCommand per run, reuses the existing DrawRhi bind-and-submit path) implement this; TerrainModernRenderer .Draw(...) is untouched and keeps serving non-walk callers (directional- shadow receivers, the flat terrain path). Order-preserving batching (S3 §9.2 B2): WalkFrameDriver.Replay merges consecutive same-landblock LandCell events with no intervening event into ONE DrawLandCellBatch leaf call; any other event splits the batch. In production this rarely fires because retail's own DrawSortCell (AlwaysDrawObjects=true) always interposes an object- list turn between one cell's LandCell event and the next's — see perfNote in the task report for the resulting command-count increase. Deleted as dead: the _walkTerrainInViewLandcells field and SetWalkTerrainInViewLandcells setter on RetailPViewPassExecutor (fed only DrawWalkTerrainSlice's inViewLandcells filter, which no longer exists — the walk's own per-cell CellInView admission is now the sole terrain-visibility authority) and its two call sites in RetailPViewRenderer.DrawWalkDrivenStatics. Weather placement (S3 §9.1 R5): confirmed unchanged. GameSky's weather pass (RenderWeather, gated on is_player_outside) already runs after every LandCell event for both root kinds — for an outdoor root, DrawLandscapeDynamicsPhase is called directly after driver.Replay() completes (which processes the whole per-cell _events list first); for an interior root with ov>0, it fires via the LandscapeFlush leaf (RetailPViewRenderer.FlushWalkLandscape -> _walkPreClearDynamics), and OnInteriorFloodDrawTurn only emits LandscapeFlush AFTER DrawLandscape's per-cell loop has fully run and recorded every LandCell event ahead of it in the same _events list Replay walks in order. No code change needed; verified by reading the call sites. Tests: WalkEvents/RetailFrameWalk/WalkFrameDriver's existing pins updated (every "TERRAIN:0" expectation deleted, matching the deleted event); new coverage for T1 (WalkLandCellOrderTests — LandWalkOrder + WalkLandscape.CalcDrawOrder + WalkLandscapeAssembler .SideCellCountForRing reproduce both cathedral captures' frame-2 LC/SC sequences byte-for-byte, 533/698 arrival and 531/757 leak, cross- verified against the retail ring-to-LOD table), T2 (a synthetic two- block landscape with a real WalkVisibilityMath-driven out-of-view column, proving LC-before-building/statics, far-to-near, no-LC-but- keeps-SC for the excluded cells, and no TerrainSlice event of any kind), T3 (TerrainLandCellIndexRunsTests — the index-run arithmetic for every side/cell, disjoint and exhaustive over the 384-index landblock), T4 (batching merge/split), and the RetailPViewPassExecutor CompiledCallGraph pin retargeted at DrawWalkLandCellBatch / DrawLandCellRuns. WalkOracleTrace gains LC/SC event kinds and a Load(root, name) overload for the OH capture directory — the one parser change this chunk needs, no validator, no other infrastructure. App hermetic lane: 6,786/6,786 (6,765 baseline + 21 new). InstalledDat lane: 241 passed, 3 accepted failures (2 pre-existing #383 layout fixture-drift tests, 1 TowerAscent Status=KnownFailure) — unchanged from baseline. Core terrain tests: 116/116 (Core untouched by this chunk). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
181 lines
8 KiB
C#
181 lines
8 KiB
C#
using AcDream.App.Rendering.Walk;
|
||
|
||
namespace AcDream.App.Tests.Rendering.Walk;
|
||
|
||
/// <summary>
|
||
/// S3 chunk 3 (§9.3 T1): <see cref="LandWalkOrder"/> +
|
||
/// <see cref="WalkLandscape.CalcDrawOrder"/> +
|
||
/// <see cref="WalkLandscapeAssembler.SideCellCountForRing"/> reproduce frame
|
||
/// 2's <c>LC</c> and <c>SC</c> sequences of BOTH OH cathedral captures
|
||
/// exactly — order, block membership, and per-block LOD side all pinned in
|
||
/// one pass, at the depth the two owner-verified transcripts (S3 §9.1 R2)
|
||
/// recorded them.
|
||
///
|
||
/// <para>Viewer: block <c>f4/18</c>, sq cell <c>(1, 1)</c> (the frame-2 `P`
|
||
/// line: x 36.6, y 24.0 — <c>floor(36.6/24)=1</c>, <c>floor(24.0/24)=1</c>).
|
||
/// Grid radius <see cref="WalkLandscapeAssembler.MidRadius"/> (25) — the
|
||
/// captured blocks span Chebyshev ring 24 (§9.1 R2 fact i), comfortably
|
||
/// inside it. Only the SET of blocks/cells the transcript itself visited is
|
||
/// placed on the grid (a real 51×51 window's un-streamed slots stay null
|
||
/// exactly the same way in production — <see cref="WalkLandscape.DrawLandscape"/>
|
||
/// skips a null/Outside slot regardless of why it's empty); each block's
|
||
/// <see cref="WalkLandBlock.CellInView"/> is set DIRECTLY from the
|
||
/// transcript's own LC membership (this test is an ORDER pin, not a second
|
||
/// visibility-math proof — that is <c>WalkVisibilityMathTests</c>'s
|
||
/// job) so <see cref="WalkLandscape.CalcDrawOrder"/> is the ONLY production
|
||
/// order machinery under test.</para>
|
||
/// </summary>
|
||
public sealed class WalkLandCellOrderTests
|
||
{
|
||
private const int ViewerBlockX = 0xf4;
|
||
private const int ViewerBlockY = 0x18;
|
||
private const int ViewerSqX = 1;
|
||
private const int ViewerSqY = 1;
|
||
|
||
[Theory]
|
||
[InlineData("cathedral-arrival")]
|
||
[InlineData("cathedral-leak")]
|
||
public void Frame2_ProducedLandCellSequence_MatchesTheCapturedRetailOrderExactly(
|
||
string fixtureName)
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(
|
||
"docs/research/2026-09-01-overhaul/oh-capture", fixtureName + ".walk");
|
||
WalkOracleFrame frame2 = Assert.Single(frames, f => f.Number == 2);
|
||
|
||
List<WalkOracleEvent> landCellEvents = frame2.Events
|
||
.Where(e => e.Kind is WalkOracleEventKind.LandCell or WalkOracleEventKind.SortCell)
|
||
.ToList();
|
||
Assert.NotEmpty(landCellEvents);
|
||
|
||
// Extract the block visit order (first-appearance order = retail's
|
||
// OWN far-to-near block sequence) and, per block, its side (from
|
||
// the observed SC cell count — every SC-visited block emits
|
||
// side² SC turns under AlwaysDrawObjects, R1) and its LC membership.
|
||
var blockOrder = new List<uint>();
|
||
var blockSeen = new HashSet<uint>();
|
||
var sideByBlock = new Dictionary<uint, int>();
|
||
var scCountByBlock = new Dictionary<uint, int>();
|
||
var lcMembersByBlock = new Dictionary<uint, HashSet<uint>>();
|
||
|
||
foreach (WalkOracleEvent e in landCellEvents)
|
||
{
|
||
uint cellId = e.CellId!.Value;
|
||
uint blockPrefix = cellId & 0xFFFF0000u;
|
||
uint lodId = cellId & 0xFFFFu;
|
||
if (blockSeen.Add(blockPrefix))
|
||
{
|
||
blockOrder.Add(blockPrefix);
|
||
lcMembersByBlock[blockPrefix] = new HashSet<uint>();
|
||
}
|
||
|
||
if (e.Kind == WalkOracleEventKind.SortCell)
|
||
{
|
||
scCountByBlock[blockPrefix] = scCountByBlock.GetValueOrDefault(blockPrefix) + 1;
|
||
}
|
||
else
|
||
{
|
||
lcMembersByBlock[blockPrefix].Add(lodId);
|
||
}
|
||
}
|
||
|
||
foreach ((uint blockPrefix, int scCount) in scCountByBlock)
|
||
{
|
||
int side = (int)Math.Round(Math.Sqrt(scCount));
|
||
Assert.Equal(scCount, side * side);
|
||
sideByBlock[blockPrefix] = side;
|
||
|
||
// Cross-check against the LOD-by-ring pyramid the ownership
|
||
// map's R2 names explicitly (ring 0-1 -> 8, ring 2 -> 4, ring
|
||
// 3-4 -> 2, ring >= 5 -> 1) — an independent confirmation that
|
||
// the transcript's own per-block cell count matches production's
|
||
// ring rule, not just an internally-consistent side count.
|
||
int bx = (int)(blockPrefix >> 24) & 0xFF;
|
||
int by = (int)(blockPrefix >> 16) & 0xFF;
|
||
int ring = Math.Max(Math.Abs(bx - ViewerBlockX), Math.Abs(by - ViewerBlockY));
|
||
Assert.Equal(WalkLandscapeAssembler.SideCellCountForRing(ring), side);
|
||
}
|
||
|
||
// Build the synthetic grid: only the transcript-visited blocks are
|
||
// populated (production leaves everything else null too), each at
|
||
// its REAL grid offset from the viewer block, with the REAL side
|
||
// WalkLandscapeAssembler.SideCellCountForRing computed above.
|
||
const int midWidth = WalkLandscapeAssembler.GridWidth;
|
||
const int midRadius = WalkLandscapeAssembler.MidRadius;
|
||
var landscape = new WalkLandscape
|
||
{
|
||
MidWidth = midWidth,
|
||
Blocks = new WalkLandBlock?[midWidth * midWidth],
|
||
ViewerBlockX = midRadius,
|
||
ViewerBlockY = midRadius,
|
||
ViewerCellX = ViewerSqX,
|
||
ViewerCellY = ViewerSqY,
|
||
};
|
||
|
||
foreach (uint blockPrefix in blockOrder)
|
||
{
|
||
int bx = (int)(blockPrefix >> 24) & 0xFF;
|
||
int by = (int)(blockPrefix >> 16) & 0xFF;
|
||
int gridX = bx - ViewerBlockX + midRadius;
|
||
int gridY = by - ViewerBlockY + midRadius;
|
||
Assert.InRange(gridX, 0, midWidth - 1);
|
||
Assert.InRange(gridY, 0, midWidth - 1);
|
||
|
||
int side = sideByBlock[blockPrefix];
|
||
var block = new WalkLandBlock
|
||
{
|
||
LandblockId = blockPrefix, SideCellCount = side, MaxZ = 0f, MinZ = 0f,
|
||
};
|
||
block.EnsureCellArrays();
|
||
HashSet<uint> lcMembers = lcMembersByBlock[blockPrefix];
|
||
for (int cellIndex = 0; cellIndex < side * side; cellIndex++)
|
||
{
|
||
int coarseX = cellIndex / side;
|
||
int coarseY = cellIndex % side;
|
||
uint retailLodId = (uint)(coarseX * 8 + coarseY + 1);
|
||
block.CellInView[cellIndex] = lcMembers.Contains(retailLodId)
|
||
? WalkBoundingType.EntirelyInside
|
||
: WalkBoundingType.Outside;
|
||
}
|
||
block.InView = WalkBoundingType.PartiallyInside; // admitted; never Outside
|
||
landscape.Blocks[gridX * midWidth + gridY] = block;
|
||
}
|
||
|
||
// LandWalkOrder.GetBlockOrder + FillCellOrderFarToNear (the ONLY
|
||
// production order machinery under test — CheckBlocks is
|
||
// deliberately not called; CellInView above is authoritative).
|
||
landscape.CalcDrawOrder();
|
||
|
||
var producedLc = new List<uint>();
|
||
var producedSc = new List<uint>();
|
||
for (int i = landscape.BlockDrawCount - 1; i >= 0; i--)
|
||
{
|
||
WalkLandBlock? block = landscape.Blocks[landscape.BlockDrawList[i]];
|
||
if (block is null)
|
||
continue;
|
||
|
||
int cellCount = block.SideCellCount * block.SideCellCount;
|
||
for (int k = 0; k < cellCount; k++)
|
||
{
|
||
int cellIndex = block.DrawArray[k];
|
||
int coarseX = cellIndex / block.SideCellCount;
|
||
int coarseY = cellIndex % block.SideCellCount;
|
||
uint fullId = block.LandblockId | (uint)(coarseX * 8 + coarseY + 1);
|
||
if (block.CellInView[cellIndex] != WalkBoundingType.Outside)
|
||
producedLc.Add(fullId);
|
||
producedSc.Add(fullId); // AlwaysDrawObjects: every cell of an admitted block.
|
||
}
|
||
}
|
||
|
||
List<uint> expectedLc = landCellEvents
|
||
.Where(e => e.Kind == WalkOracleEventKind.LandCell)
|
||
.Select(e => e.CellId!.Value)
|
||
.ToList();
|
||
List<uint> expectedSc = landCellEvents
|
||
.Where(e => e.Kind == WalkOracleEventKind.SortCell)
|
||
.Select(e => e.CellId!.Value)
|
||
.ToList();
|
||
|
||
Assert.Equal(expectedSc, producedSc);
|
||
Assert.Equal(expectedLc, producedLc);
|
||
}
|
||
}
|