feat(render) Campaign FW3.1: production walk world data behind the seam
The retail frame walk's world model now materializes from production landblock-build owners through the legal IDatReaderWriter seam, with zero frame wiring (FW3.2 roots the frame): - WalkCellFactory: WalkCell built in the SAME pass as LoadedCell (EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal Flags/polygons/planes/stab lists already parsed there; stored as LoadedCell.Walk, committed atomically with the cell. The fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF exit widening) live here. - WalkBuildingFactory + WalkBuildingRegistry: the production WalkBuilding build (drawing BSP with PORT nodes, degrade ladder, portal sides/stab lists, sort center, model frame) from the SAME LandBlockInfo the streaming build already fetches, under the factory's existing DAT lock - closing the gap where BuildingLoader drops every walk field at load. - WalkLandscapeAssembler: the retail 51x51 viewer-centred grid (mid_radius 25) fed incrementally from landblock publish/retire; per-block z-slab (heightTable[max]+200 / [min]-1) computed worker-side in LandblockBuildFactory from the heights already in hand. O(1) SetViewer on same-block frames. - WalkProductionFrameContext: the walk's frame contexts over CellVisibility + WalkBuildingRegistry with a generic inverse-view-projection ray caster (rays feed cross products only - scale-free) and the znear=0.1 CY plane. - Publication: LandblockRenderPublisher owns both walk registries, publishing in the same AdvanceCompleteOne step as BuildingRegistry and retiring in RemoveBuildingRegistry - same commit, same retirement, no new ticket stage. Conformance: ALL TEN oracle fixtures replay identically through the PRODUCTION builders (WalkProductionWorldConformanceTests) - same signatures as the test adapter, first run. Known gap documented for FW3.2: far-tier landblocks carry no EnvCell transaction, so their z-slab never reaches the assembler. Suites: full Release build 0 warnings; Walk lane 186/1 skip; hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b95850defe
commit
b10ad662b0
14 changed files with 1711 additions and 2 deletions
|
|
@ -178,6 +178,15 @@ public sealed class LandblockBuildFactory
|
|||
worldOffset,
|
||||
_heightTable));
|
||||
var envCellBuild = new AcDream.App.Rendering.Wb.EnvCellLandblockBuildBuilder(landblockId);
|
||||
// Campaign FW3.1: the walk landscape's per-block z-slab
|
||||
// (CLandBlock::calc_lighting-adjacent unpack @0x0052f1d0). Every
|
||||
// near-tier landblock has outdoor terrain heights regardless of
|
||||
// whether it has interior cells or buildings, so this is
|
||||
// unconditional — unlike WalkBuildings (added below, inside
|
||||
// BuildInteriorEntitiesForStreaming, where LandBlockInfo is already
|
||||
// in hand).
|
||||
(float walkMaxZ, float walkMinZ) = ComputeWalkZSlab(baseLoaded.Heightmap.Height);
|
||||
envCellBuild.SetWalkZSlab(walkMaxZ, walkMinZ);
|
||||
merged.AddRange(BuildInteriorEntitiesForStreaming(
|
||||
landblockId,
|
||||
lbX,
|
||||
|
|
@ -448,6 +457,17 @@ public sealed class LandblockBuildFactory
|
|||
(lbY - origin.CenterY) * 192f,
|
||||
0f);
|
||||
|
||||
// Campaign FW3.1: the walk's building placements (portals + drawing
|
||||
// BSP + degrade ladder + sort center), read from the SAME
|
||||
// LandBlockInfo.Buildings this method already fetched above — no
|
||||
// extra DAT read. This closes the gap where Wb.BuildingLoader
|
||||
// .AddBuilding (update thread, no DAT access) keeps only
|
||||
// ModelId/Frame/portal-flags/stab-lists for its own BFS seeding and
|
||||
// drops everything the frame walk needs.
|
||||
envCellBuild.AddWalkBuildings(
|
||||
AcDream.App.Rendering.Walk.WalkBuildingFactory.Build(
|
||||
_dats, landblockId, lbInfo.Buildings, lbOffset));
|
||||
|
||||
// Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
|
||||
// for the full bit layout + history. Distinct from scenery (0x80000000+) and
|
||||
// landblock stabs (0xC0000000+, ids from LandblockLoader).
|
||||
|
|
@ -649,6 +669,24 @@ public sealed class LandblockBuildFactory
|
|||
}
|
||||
|
||||
|
||||
/// <summary>Campaign FW3.1: retail's per-block z-slab
|
||||
/// (<c>WalkLandscapeDatBuilder</c>'s port target —
|
||||
/// <c>CLandBlock::calc_lighting</c>-adjacent unpack @0x0052f1d0):
|
||||
/// <c>max_zval = heightTable[maxByte] + 200</c>,
|
||||
/// <c>min_zval = heightTable[minByte] - 1</c>, over the landblock's own
|
||||
/// 81-byte heightmap. Uses <see cref="_heightTable"/> — already a
|
||||
/// constructor field, so this needs no additional DAT read.</summary>
|
||||
private (float MaxZ, float MinZ) ComputeWalkZSlab(byte[] heights)
|
||||
{
|
||||
byte maxByte = 0, minByte = 255;
|
||||
foreach (byte h in heights)
|
||||
{
|
||||
if (h > maxByte) maxByte = h;
|
||||
if (h < minByte) minByte = h;
|
||||
}
|
||||
return (_heightTable[maxByte] + 200f, _heightTable[minByte] - 1f);
|
||||
}
|
||||
|
||||
private static float SampleTerrainZ(DatReaderWriter.DBObjs.LandBlock block, float[] heightTable, float localX, float localY)
|
||||
{
|
||||
uint landblockX = (block.Id >> 24) & 0xFFu;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Diagnostics;
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.Terrain;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
|
@ -102,6 +103,17 @@ public sealed class LandblockRenderPublisher
|
|||
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
|
||||
private readonly Action<uint>? _removeEnvCells;
|
||||
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
|
||||
// Campaign FW3.1: the walk's production world-data siblings of
|
||||
// _buildingRegistries. Owned here (not injected) exactly like
|
||||
// _buildingRegistries — no caller constructs a LandblockRenderPublisher
|
||||
// with pre-existing walk state. Committed/retired at the same points as
|
||||
// the building registry (see AdvanceCompleteOne / RemoveBuildingRegistry
|
||||
// below) because both come from the same landblock's BuildingInfo array
|
||||
// at the same commit. No frame reads these yet (FW3.2 wires the walk
|
||||
// into the render loop) — publication happens now so FW3.1's
|
||||
// conformance gate exercises the production commit path.
|
||||
private readonly WalkBuildingRegistry _walkBuildingRegistry = new();
|
||||
private readonly WalkLandscapeAssembler _walkLandscape = new();
|
||||
|
||||
private long _beginCount;
|
||||
private long _completeCount;
|
||||
|
|
@ -148,6 +160,15 @@ public sealed class LandblockRenderPublisher
|
|||
public IReadOnlyCollection<BuildingRegistry> BuildingRegistries =>
|
||||
_buildingRegistries.Values;
|
||||
|
||||
/// <summary>Campaign FW3.1: the walk's production building placements,
|
||||
/// keyed by landblock, committed alongside <see cref="BuildingRegistries"/>.</summary>
|
||||
public WalkBuildingRegistry WalkBuildings => _walkBuildingRegistry;
|
||||
|
||||
/// <summary>Campaign FW3.1: the walk's production viewer-centred
|
||||
/// landscape grid, fed from the same landblock commits as
|
||||
/// <see cref="WalkBuildings"/>.</summary>
|
||||
public WalkLandscapeAssembler WalkLandscape => _walkLandscape;
|
||||
|
||||
public LandblockRenderPublisherDiagnostics Diagnostics => new(
|
||||
_beginCount,
|
||||
_completeCount,
|
||||
|
|
@ -339,6 +360,21 @@ public sealed class LandblockRenderPublisher
|
|||
_buildingRegistries[registryKey] =
|
||||
completedBuildings.Registry;
|
||||
}
|
||||
// Campaign FW3.1: publish this landblock's walk world data in
|
||||
// the same step — build.EnvCells carries WalkBuildings/WalkMaxZ
|
||||
// /WalkMinZ (computed worker-side by LandblockBuildFactory) for
|
||||
// every near-tier build, even one with zero interior cells
|
||||
// (the z-slab is unconditional; WalkBuildings is naturally empty
|
||||
// when LandBlockInfo has none).
|
||||
if (build.EnvCells is { } walkEnvCells)
|
||||
{
|
||||
_walkBuildingRegistry.Publish(landblockId, walkEnvCells.WalkBuildings);
|
||||
_walkLandscape.PublishLandblock(
|
||||
landblockId,
|
||||
walkEnvCells.WalkMaxZ,
|
||||
walkEnvCells.WalkMinZ,
|
||||
walkEnvCells.WalkBuildings);
|
||||
}
|
||||
publication.BuildingRegistryCommitted = true;
|
||||
}
|
||||
else if (publication.EnvCellPublication is { } envCellPublication
|
||||
|
|
@ -397,6 +433,14 @@ public sealed class LandblockRenderPublisher
|
|||
public void RemoveBuildingRegistry(uint landblockId)
|
||||
{
|
||||
_buildingRegistries.Remove(landblockId & 0xFFFF0000u);
|
||||
// Campaign FW3.1: retire the walk's siblings at the SAME retirement
|
||||
// stage — they were committed together above, so they retire
|
||||
// together (no new LandblockRetirementStage; folding into the
|
||||
// existing BuildingRegistry stage keeps the retirement ticket state
|
||||
// machine unchanged, which is the minimal/additive choice for a
|
||||
// slice that does no frame wiring yet).
|
||||
_walkBuildingRegistry.Retire(landblockId);
|
||||
_walkLandscape.RetireLandblock(landblockId);
|
||||
_buildingRegistryRemovalCount++;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue