parent
e0ba3e7f2c
commit
3ebb120dd2
20 changed files with 700 additions and 100 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue