fix #483: restore far terrain and close OVERHAUL with #484 deferred
Some checks failed
CI / linux-portable (push) Successful in 3m36s
CI / windows-gate (push) Failing after 6m52s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-09-06 10:22:51 +02:00
parent e0ba3e7f2c
commit 3ebb120dd2
20 changed files with 700 additions and 100 deletions

View file

@ -136,6 +136,7 @@ public sealed class LandblockBuildFactoryTests
result.EnvCells);
Assert.Equal(240f, envCells.WalkMaxZ); // heightTable[200] + 200
Assert.Equal(4f, envCells.WalkMinZ); // heightTable[10] - 1
Assert.Equal(new LandblockTerrainBounds(240f, 4f), result.TerrainBounds);
AcDream.App.Rendering.Walk.WalkBuildingFactory.Entry buildingEntry =
Assert.Single(envCells.WalkBuildings);
// origin (12,12) -> cellX=cellY=0 -> low word 0*8+0+1 = 1.
@ -189,20 +190,28 @@ public sealed class LandblockBuildFactoryTests
}
[Fact]
public void BuildFar_NeverPopulatesWalkDataBecauseEnvCellsIsNull()
public void BuildFar_CarriesNonFlatAuthoredBoundsWithoutNearDataOrExtraReads()
{
// Documents the known FW3.1 scope gap: far-tier (LoadFar) landblocks
// carry no EnvCellLandblockBuild transaction at all (terrain-only),
// so their z-slab/buildings never reach WalkLandscapeAssembler until
// a follow-up wires the far-tier path too.
var dat = CreateDat(out RecordingDatProxy proxy);
proxy.Add(LandblockId, new LandBlock { Id = LandblockId });
var factory = Factory(dat, new object());
var heights = Enumerable.Repeat((byte)10, 81).ToArray();
heights[80] = 200;
proxy.Add(LandblockId, new LandBlock { Id = LandblockId, Height = heights });
var heightTable = new float[256];
heightTable[10] = 5.25f;
heightTable[200] = 40.5f;
var source = new TestPreparedCollisionSource(PreparedAssetReadStatus.Missing);
var factory = new LandblockBuildFactory(dat, source, new object(), heightTable);
LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadFar));
Assert.NotNull(result);
Assert.Null(result.EnvCells);
Assert.Equal(new LandblockTerrainBounds(240.5f, 4.25f), result.TerrainBounds);
Assert.Empty(result.Landblock.Entities);
Assert.Same(PhysicsDatBundle.Empty, result.Landblock.PhysicsDats);
Assert.Null(result.Collisions);
Assert.Equal(0, source.Reads);
Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads);
}
[Fact]

View file

@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
@ -527,6 +528,223 @@ public sealed class LandblockConcretePresentationPipelineTests
Assert.Equal(1, fixture.Render.Diagnostics.EnvCellRemovalCount);
}
[Fact]
public async Task FarLoaded_WorkerAndRealWalkPreserveTerrainBeyondNearRadiusWithoutNearPopulation()
{
ConcreteFixture fixture = Fixture(new List<string>());
LandblockBuild far = Build() with
{
EnvCells = null,
Origin = new LandblockBuildOrigin(0xA3, 0xB4),
TerrainBounds = new(240.5f, 4.25f),
};
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: _ => far,
buildMeshOrNull: (_, _) => EmptyMesh(),
workerCount: 1);
streamer.EnqueueLoad(new LandblockBuildRequest(
LandblockId, LandblockStreamJobKind.LoadFar, Generation: 483, far.Origin));
streamer.Start();
LandblockStreamResult? completion = null;
for (int attempt = 0; attempt < 200 && completion is null; attempt++)
{
IReadOnlyList<LandblockStreamResult> drained = streamer.DrainCompletions();
if (drained.Count > 0)
completion = Assert.Single(drained);
else
await Task.Delay(10);
}
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(completion);
Assert.Equal(483ul, loaded.Generation);
Assert.Equal(far.Origin, loaded.Build.Origin);
Assert.Equal(far.TerrainBounds, loaded.Build.TerrainBounds);
Assert.Null(loaded.Build.EnvCells);
Assert.Same(PhysicsDatBundle.Empty, loaded.Landblock.PhysicsDats);
Pipeline(fixture).PublishLoaded(loaded);
AssertFarTerrain(fixture);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AcceptedNearAsFar_BothOverloadsPreserveTerrainAndStripNearPopulation(bool metered)
{
var calls = new List<string>();
ConcreteFixture fixture = Fixture(calls);
var pipeline = Pipeline(fixture);
LandblockBuild near = BuildWithWalkBuilding() with
{
Origin = new LandblockBuildOrigin(0xA3, 0xB4),
};
LandblockStreamResult.Loaded accepted = Result(near);
if (metered)
{
// One mutation per frame forces the real retained publication path.
var budget = new StreamingWorkBudget(
TimeSpan.FromSeconds(1),
maxCompletionAdmissions: 64,
maxAdoptedCpuBytes: 1_000_000,
maxEntityOperations: 1,
maxGpuUploadBytes: 1_000_000,
maxGlRetireOperations: 64,
destinationReserveFraction: 0.75f);
LandblockPublicationAdvance advance = default;
for (int frame = 0; frame < 64 && !advance.Completed; frame++)
{
var meter = new StreamingWorkMeter(budget);
advance = pipeline.PublishAsFar(
accepted, near, accepted.MeshData, meter, ensureProgress: false);
meter.FinishFrame();
Assert.True(meter.Snapshot.Used.EntityOperations <= 1);
}
Assert.True(advance.Completed);
}
else
{
pipeline.PublishAsFar(accepted, near, accepted.MeshData);
}
Assert.False(pipeline.HasPendingPublication(accepted));
Assert.Equal(1, calls.Count(call => call == "terrain"));
Assert.Equal(1, fixture.Render.Diagnostics.CompleteCount);
AssertFarTerrain(fixture);
}
[Fact]
public void ConcreteRetirement_DemotionKeepsTerrain_PromotionAndRevisitRestoreBuildings()
{
ConcreteFixture fixture = Fixture(new List<string>());
var pipeline = Pipeline(fixture);
LandblockBuild near = BuildWithWalkBuilding();
WalkBuilding building = Assert.Single(near.EnvCells!.WalkBuildings).Building;
pipeline.PublishLoaded(Result(near));
AssertNearTerrain(fixture, building);
pipeline.BeginNearLayerRetirement(LandblockId);
pipeline.BeginNearLayerRetirement(LandblockId);
Assert.Equal(0, pipeline.PendingRetirementCount);
Assert.True(fixture.State.IsLoaded(LandblockId));
Assert.False(fixture.State.IsNearTier(LandblockId));
Assert.Equal(0, fixture.Render.Diagnostics.TerrainRemovalCount);
fixture.Render.RemoveBuildingRegistry(LandblockId); // repeated near cleanup
AssertTerrainBounds(fixture.Render.WalkLandscape);
Assert.NotEmpty(DrawTerrain(fixture, farViewer: false).TerrainTurns);
Assert.Empty(fixture.Render.WalkBuildings.GetBuildings(LandblockId));
Assert.False(fixture.Render.WalkBuildings.TryGetEntry(building, out _));
Assert.All(PublishedBlock(fixture.Render.WalkLandscape).CellBuildings,
value => Assert.Null(value));
pipeline.PublishPromoted(
new LandblockStreamResult.Promoted(LandblockId, near, EmptyMesh()),
mergeIntoExistingLandblock: true);
AssertNearTerrain(fixture, building);
pipeline.BeginFullRetirement(LandblockId);
pipeline.BeginFullRetirement(LandblockId);
Assert.Equal(0, pipeline.PendingRetirementCount);
Assert.False(fixture.State.IsLoaded(LandblockId));
fixture.Render.RemoveTerrain(LandblockId); // duplicate full cleanup
fixture.Render.RemoveBuildingRegistry(LandblockId); // late stage after terrain removal
Assert.Empty(DrawTerrain(fixture, farViewer: false).TerrainTurns);
Assert.All(fixture.Render.WalkLandscape.Landscape.Blocks, value => Assert.Null(value));
Assert.Empty(fixture.Render.WalkBuildings.GetBuildings(LandblockId));
pipeline.PublishLoaded(Result(near));
AssertNearTerrain(fixture, building);
}
private static LandblockBuild BuildWithWalkBuilding()
{
var building = new WalkBuilding { PositionCellId = 0xA9B40009u };
var entry = new WalkBuildingFactory.Entry(
building, Matrix4x4.Identity, Matrix4x4.Identity);
LandblockBuild build = Build(Entity(0x80A9B401u));
return build with
{
EnvCells = new EnvCellLandblockBuild(
LandblockId, [], [], [entry], walkMaxZ: 240.5f, walkMinZ: 4.25f),
TerrainBounds = new(240.5f, 4.25f),
};
}
private static void AssertFarTerrain(ConcreteFixture fixture)
{
TerrainTurnRecorder turns = DrawTerrain(fixture, farViewer: true);
// Ring six uses one coarse terrain cell. This fails if the far block
// has a mesh/state entry but is missing from the sole landscape walk.
Assert.Equal((LandblockId & 0xFFFF0000u, 1, 0), Assert.Single(turns.TerrainTurns));
AssertTerrainBounds(fixture.Render.WalkLandscape);
Assert.Empty(turns.Buildings);
Assert.Empty(fixture.Render.WalkBuildings.GetBuildings(LandblockId));
Assert.All(PublishedBlock(fixture.Render.WalkLandscape).CellBuildings,
value => Assert.Null(value));
Assert.True(fixture.State.IsLoaded(LandblockId));
Assert.False(fixture.State.IsNearTier(LandblockId));
Assert.Empty(fixture.State.Entities);
Assert.Empty(fixture.World.Entities);
}
private static void AssertNearTerrain(ConcreteFixture fixture, WalkBuilding building)
{
TerrainTurnRecorder turns = DrawTerrain(fixture, farViewer: false);
Assert.NotEmpty(turns.TerrainTurns);
Assert.Contains(building.PositionCellId, turns.Buildings);
AssertTerrainBounds(fixture.Render.WalkLandscape);
Assert.Same(building, PublishedBlock(fixture.Render.WalkLandscape).CellBuildings[8]);
Assert.Same(building, Assert.Single(
fixture.Render.WalkBuildings.GetBuildings(LandblockId)).Building);
Assert.True(fixture.State.IsNearTier(LandblockId));
}
private static void AssertTerrainBounds(WalkLandscapeAssembler assembler)
{
WalkLandBlock block = PublishedBlock(assembler);
Assert.Equal(240.5f, block.MaxZ);
Assert.Equal(4.25f, block.MinZ);
}
private static WalkLandBlock PublishedBlock(WalkLandscapeAssembler assembler) =>
Assert.Single(assembler.Landscape.Blocks.OfType<WalkLandBlock>());
private static TerrainTurnRecorder DrawTerrain(ConcreteFixture fixture, bool farViewer)
{
// The far-only block is six blocks east of the viewer, beyond NearRadius4.
uint cameraCell = farViewer ? 0xA3B40001u : 0xA9B40001u;
var eye = new Vector3(12f, 12f, 100f);
var target = new Vector3(farViewer ? 6 * 192f + 96f : 144f, 12f, 20f);
Vector3 forward = Vector3.Normalize(target - eye);
Matrix4x4 viewProjection = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 16f / 9f, 0.1f, 10_000f);
fixture.Render.WalkLandscape.SetViewer(cameraCell, eye);
var context = new WalkProductionFrameContext(
new CellVisibility(), fixture.Render.WalkBuildings,
eye, forward, viewProjection, 1600f, 900f, cameraCell);
var recorder = new TerrainTurnRecorder();
new RetailFrameWalk().WalkFrame(
cameraCell, null, fixture.Render.WalkLandscape.Landscape, context, recorder);
return recorder;
}
private sealed class TerrainTurnRecorder : IWalkEventSink
{
public List<(uint LandblockId, int SideCellCount, int CellIndex)> TerrainTurns { get; } = [];
public List<uint> Buildings { get; } = [];
public void Emit(in WalkEvent walkEvent)
{
if (walkEvent.Kind == WalkEventKind.Building)
Buildings.Add(walkEvent.CellId);
}
public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
TerrainTurns.Add((landblockId, sideCellCount, cellIndex));
}
private static LandblockPresentationPipeline Pipeline(
ConcreteFixture fixture) => new(
fixture.Render,