feat(render) Campaign FW1: the landscape replay world + first outdoor diff

WalkLandscapeDatBuilder assembles the replay grid with the LIVE-dumped
retail landscape model (LScape mid_radius=25 -> 51x51; the resolution
pyramid observed on the capture client: side_cell_count 8 in the 3x3
core, 4 at ring 2, 2 at rings 3-4, 1 beyond; buildings attach only to
full-res blocks - matching the traces roster), retail z-slabs
(heightTable[max]+200 / [min]-1 per CLandBlock unpack @0052f297), and
camera-block-local coordinates. The replay context gains the building
half (placements, active-view install, building-polygon clip). The
street-outdoor conformance diff now reaches real adjudication: the walk
over-culls six traced buildings (jagged boundary - not a clean frustum
edge) and the look-in floods differ; the driving test is parked Skip
while the visibility map is instrumented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 10:36:09 +02:00
parent e8df809c40
commit e3f0c6a1a1
5 changed files with 222 additions and 21 deletions

View file

@ -0,0 +1,120 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// Campaign FW1 — assembles the replay world for outdoor/landscape
/// fixtures: retail's 11×11 viewer-centered block grid (mid_radius 5) in
/// CAMERA-BLOCK-LOCAL coordinates (the same space the pose origin and the
/// per-block corner math use), each block with its retail z-slab
/// (<c>CLandBlock::calc_lighting</c>-adjacent unpack @0x0052f1d0:
/// max_zval = heightTable[maxByte] + 200, min_zval = heightTable[minByte]
/// 1), buildings placed at their landcell indices with landblock-local
/// transforms offset by the block delta, and every neighbor block's
/// interior cells merged into one registry.
/// </summary>
public static class WalkLandscapeDatBuilder
{
public sealed record BuiltWorld(
WalkLandscape Landscape,
Dictionary<uint, WalkCell> Cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings);
/// <summary>The capture client's live landscape grid (dumped 2026-08-30:
/// LScape mid_radius=25 → a 51×51 grid — the owner's landscape-radius
/// setting) with the resolution pyramid observed live: side_cell_count 8
/// in the 3×3 core, 4 at ring 2, 2 at rings 34, 1 beyond. Buildings
/// attach only to full-res blocks (the traces show no BLD beyond ±1).</summary>
public const int MidRadius = 25;
private static int SideCellCountForRing(int ring)
=> ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1;
public static BuiltWorld Build(DatCollection dats, uint cameraCellId)
{
Region region = (Region)dats.Get<Region>(0x13000000u)!;
float[] heightTable = region.LandDefs.LandHeightTable;
int cameraBlockX = (int)(cameraCellId >> 24);
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
const int midRadius = MidRadius;
const int midWidth = midRadius * 2 + 1;
var landscape = new WalkLandscape
{
MidWidth = midWidth,
Blocks = new WalkLandBlock?[midWidth * midWidth],
ViewerBlockX = midRadius,
ViewerBlockY = midRadius,
};
uint low = cameraCellId & 0xFFFFu;
if (low >= 1 && low <= 0x40)
{
int cellIndex = (int)low - 1;
landscape.ViewerCellX = cellIndex / 8;
landscape.ViewerCellY = cellIndex % 8;
}
var cells = new Dictionary<uint, WalkCell>();
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
for (int gx = 0; gx < midWidth; gx++)
{
for (int gy = 0; gy < midWidth; gy++)
{
int blockX = cameraBlockX + gx - midRadius;
int blockY = cameraBlockY + gy - midRadius;
if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
continue;
uint landblockId = (uint)((blockX << 24) | (blockY << 16));
if (dats.Get<LandBlock>(landblockId | 0xFFFFu) is not LandBlock landBlock)
continue;
byte maxByte = 0, minByte = 255;
foreach (byte h in landBlock.Height)
{
if (h > maxByte) maxByte = h;
if (h < minByte) minByte = h;
}
int ring = Math.Max(Math.Abs(gx - midRadius), Math.Abs(gy - midRadius));
int sideCellCount = SideCellCountForRing(ring);
var block = new WalkLandBlock
{
SideCellCount = sideCellCount,
MaxZ = heightTable[maxByte] + 200f,
MinZ = heightTable[minByte] - 1f,
};
block.EnsureCellArrays();
var blockOffset = new Vector3(
(gx - midRadius) * WalkLandscape.BlockLength,
(gy - midRadius) * WalkLandscape.BlockLength,
0f);
if (sideCellCount == 8)
{
foreach (WalkWorldDatAdapter.BuildingEntry entry
in WalkWorldDatAdapter.BuildBuildings(dats, landblockId))
{
Matrix4x4 world = entry.WorldTransform
* Matrix4x4.CreateTranslation(blockOffset);
Matrix4x4.Invert(world, out Matrix4x4 inverse);
var placed = new WalkWorldDatAdapter.BuildingEntry(
entry.Building, world, inverse);
buildings[entry.Building] = placed;
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
if (cellIndex >= 0 && cellIndex < 64)
block.CellBuildings[cellIndex] = entry.Building;
}
WalkWorldDatAdapter.BuildInteriorCells(
dats, landblockId, blockOffset, cells);
}
landscape.Blocks[gx * midWidth + gy] = block;
}
}
return new BuiltWorld(landscape, cells, buildings);
}
}

View file

@ -32,6 +32,37 @@ public sealed class WalkTraceConformanceTests
return new DatCollection(datDir!, DatAccessType.Read); return new DatCollection(datDir!, DatAccessType.Read);
} }
[Fact(Skip = "FW1 adjudication in progress: the walk over-culls six traced "
+ "buildings (incl. block a9b3) and the look-in floods differ — see the "
+ "campaign memory. Re-enable as the outdoor gate once adjudicated.")]
public void Street_outdoor_first_frame_diff()
{
// The first landscape-involving conformance case: diff-first (the
// assert prints both signatures on mismatch for adjudication).
IReadOnlyList<WalkOracleFrame> frames =
WalkOracleTrace.Load("posed/holtburg-street-outdoor");
Assert.NotEmpty(frames);
using DatCollection dats = OpenDats();
WalkOracleFrame frame = frames[1]; // frame 2: pose settled (marker timing)
Assert.NotNull(frame.Pose);
WalkLandscapeDatBuilder.BuiltWorld world =
WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId);
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
{
Buildings = world.Buildings,
};
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder);
string expected = WalkTraceReplayContext.Signature(frame);
string actual = WalkTraceReplayContext.Signature(recorder.Events);
Assert.True(
expected == actual,
$"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}");
}
[Fact] [Fact]
public void Foundry_deep_reproduces_every_complete_frame_exactly() public void Foundry_deep_reproduces_every_complete_frame_exactly()
{ {

View file

@ -72,6 +72,14 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f); CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f);
} }
/// <summary>Building placements (camera-block-local) for the landscape
/// fixtures; empty for the interior-only ones.</summary>
public Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings { get; set; }
= new();
private Vector2[] _activeViewVerts = new Vector2[32];
private int _activeViewVertCount;
public Vector3 ViewpointIn(WalkCell cell) public Vector3 ViewpointIn(WalkCell cell)
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform); => Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
@ -85,13 +93,33 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
public float ViewportHeight { get; } public float ViewportHeight { get; }
public WalkPlane CyPlane { get; } public WalkPlane CyPlane { get; }
public IWalkFrameContext CellContext => this; public IWalkFrameContext CellContext => this;
public void SetActiveView(WalkPortalView views, int index) { }
public Vector3 ViewpointInBuilding(WalkBuilding building) => WorldViewpoint; public void SetActiveView(WalkPortalView views, int index)
{
WalkViewPoly poly = views.View.Polys[index];
if (_activeViewVerts.Length < poly.VertexCount)
_activeViewVerts = new Vector2[poly.VertexCount];
for (int k = 0; k < poly.VertexCount; k++)
_activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point;
_activeViewVertCount = poly.VertexCount;
}
public Vector3 ViewpointInBuilding(WalkBuilding building)
=> Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform);
public int ClipBuildingPolygon( public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output) WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
=> 0; // building look-ins join the conformance surface with the landscape fixtures {
Matrix4x4 objectToClip = Buildings[building].WorldTransform * _viewProjection;
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
for (int i = 0; i < polygon.Vertices.Length; i++)
projected[i] = WalkScreenClip.TransformToScreen(
polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight);
if (side != 0)
projected.Reverse();
return WalkScreenClip.ClipAgainstView(
projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output);
}
// ---- signatures for comparing walk output to oracle frames ---- // ---- signatures for comparing walk output to oracle frames ----

View file

@ -32,7 +32,14 @@ using AcDream.App.Rendering.Walk;
/// </summary> /// </summary>
public static class WalkWorldDatAdapter public static class WalkWorldDatAdapter
{ {
/// <summary>One built building plus its landblock-local placement.</summary>
public sealed record BuildingEntry(
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
public static WalkCell? BuildCell(DatCollection dats, uint cellId) public static WalkCell? BuildCell(DatCollection dats, uint cellId)
=> BuildCell(dats, cellId, Vector3.Zero);
public static WalkCell? BuildCell(DatCollection dats, uint cellId, Vector3 blockOffset)
{ {
if (dats.Get<EnvCell>(cellId) is not EnvCell envCell) if (dats.Get<EnvCell>(cellId) is not EnvCell envCell)
return null; return null;
@ -67,9 +74,9 @@ public static class WalkWorldDatAdapter
Matrix4x4 worldTransform = Matrix4x4 worldTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation)
* Matrix4x4.CreateTranslation( * Matrix4x4.CreateTranslation(
envCell.Position.Origin.X, envCell.Position.Origin.X + blockOffset.X,
envCell.Position.Origin.Y, envCell.Position.Origin.Y + blockOffset.Y,
envCell.Position.Origin.Z); envCell.Position.Origin.Z + blockOffset.Z);
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse); Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
return new WalkCell return new WalkCell
{ {
@ -84,12 +91,17 @@ public static class WalkWorldDatAdapter
public static Dictionary<uint, WalkCell> BuildInteriorCells( public static Dictionary<uint, WalkCell> BuildInteriorCells(
DatCollection dats, uint landblockId) DatCollection dats, uint landblockId)
=> BuildInteriorCells(dats, landblockId, Vector3.Zero, null);
public static Dictionary<uint, WalkCell> BuildInteriorCells(
DatCollection dats, uint landblockId, Vector3 blockOffset,
Dictionary<uint, WalkCell>? into)
{ {
uint lbMask = landblockId & 0xFFFF0000u; uint lbMask = landblockId & 0xFFFF0000u;
var cells = new Dictionary<uint, WalkCell>(); Dictionary<uint, WalkCell> cells = into ?? new Dictionary<uint, WalkCell>();
for (uint low = 0x0100; low <= 0xFFFD; low++) for (uint low = 0x0100; low <= 0xFFFD; low++)
{ {
WalkCell? cell = BuildCell(dats, lbMask | low); WalkCell? cell = BuildCell(dats, lbMask | low, blockOffset);
if (cell is null) break; // interior cells are contiguous from 0x0100 if (cell is null) break; // interior cells are contiguous from 0x0100
cells[cell.CellId] = cell; cells[cell.CellId] = cell;
} }
@ -97,11 +109,12 @@ public static class WalkWorldDatAdapter
} }
/// <summary>Buildings of one landblock, with their walk event ids /// <summary>Buildings of one landblock, with their walk event ids
/// (containing landcell), portal records, and drawing-BSP portal trees.</summary> /// (containing landcell), portal records, drawing-BSP portal trees, and
public static List<WalkBuilding> BuildBuildings(DatCollection dats, uint landblockId) /// landblock-local placements.</summary>
public static List<BuildingEntry> BuildBuildings(DatCollection dats, uint landblockId)
{ {
uint lbMask = landblockId & 0xFFFF0000u; uint lbMask = landblockId & 0xFFFF0000u;
var result = new List<WalkBuilding>(); var result = new List<BuildingEntry>();
if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info
|| info.Buildings is null) || info.Buildings is null)
{ {
@ -138,12 +151,19 @@ public static class WalkWorldDatAdapter
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj) if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root); bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
result.Add(new WalkBuilding Matrix4x4 worldTransform =
{ Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
PositionCellId = positionCellId, * Matrix4x4.CreateTranslation(origin);
Portals = portals, Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
DrawingBsp = bsp, result.Add(new BuildingEntry(
}); new WalkBuilding
{
PositionCellId = positionCellId,
Portals = portals,
DrawingBsp = bsp,
},
worldTransform,
inverse));
} }
return result; return result;
} }

View file

@ -60,8 +60,9 @@ public sealed class WalkWorldDatAdapterTests
{ {
using DatCollection dats = OpenDats(); using DatCollection dats = OpenDats();
List<WalkBuilding> buildings = WalkWorldDatAdapter.BuildBuildings(dats, landblockId); List<WalkWorldDatAdapter.BuildingEntry> buildings =
var roster = buildings.Select(b => b.PositionCellId).ToHashSet(); WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
var roster = buildings.Select(e => e.Building.PositionCellId).ToHashSet();
foreach (uint traced in tracedBuildingIds) foreach (uint traced in tracedBuildingIds)
Assert.Contains(traced, roster); Assert.Contains(traced, roster);
@ -72,9 +73,10 @@ public sealed class WalkWorldDatAdapterTests
{ {
using DatCollection dats = OpenDats(); using DatCollection dats = OpenDats();
List<WalkBuilding> buildings = WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u); List<WalkWorldDatAdapter.BuildingEntry> buildings =
WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
WalkBuilding sanctuary = Assert.Single( WalkBuilding sanctuary = Assert.Single(
buildings, b => b.PositionCellId == 0xF418000Au); buildings, e => e.Building.PositionCellId == 0xF418000Au).Building;
Assert.NotEmpty(sanctuary.Portals); Assert.NotEmpty(sanctuary.Portals);
Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList)); Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));