fix(streaming): commit EnvCell landblocks atomically (#214)
Carry one complete cell payload with each streaming completion and publish visibility, physics, and render state on the render thread. Remove the obsolete post-readiness login reload that triggered a duplicate dungeon build. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
85980fe13f
commit
b0c175afc0
26 changed files with 973 additions and 617 deletions
|
|
@ -0,0 +1,39 @@
|
|||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public class CellVisibilityCommitTests
|
||||
{
|
||||
[Fact]
|
||||
public void CommitLandblock_ReplacesOnlyThatLandblocksCompleteCellSet()
|
||||
{
|
||||
var visibility = new CellVisibility();
|
||||
visibility.CommitLandblock(0x8C04FFFFu, new[]
|
||||
{
|
||||
Cell(0x8C040100u),
|
||||
Cell(0x8C040101u),
|
||||
});
|
||||
visibility.CommitLandblock(0x0007FFFFu, new[] { Cell(0x00070100u) });
|
||||
|
||||
visibility.CommitLandblock(0x8C04FFFFu, new[] { Cell(0x8C040200u) });
|
||||
|
||||
Assert.False(visibility.TryGetCell(0x8C040100u, out _));
|
||||
Assert.False(visibility.TryGetCell(0x8C040101u, out _));
|
||||
Assert.True(visibility.TryGetCell(0x8C040200u, out _));
|
||||
Assert.True(visibility.TryGetCell(0x00070100u, out _));
|
||||
Assert.Single(visibility.GetCellsForLandblock(0x8C04u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitLandblock_RejectsForeignCellBeforeChangingCurrentState()
|
||||
{
|
||||
var visibility = new CellVisibility();
|
||||
visibility.CommitLandblock(0x8C04FFFFu, new[] { Cell(0x8C040100u) });
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
visibility.CommitLandblock(0x8C04FFFFu, new[] { Cell(0x00070100u) }));
|
||||
Assert.True(visibility.TryGetCell(0x8C040100u, out _));
|
||||
}
|
||||
|
||||
private static LoadedCell Cell(uint id) => new() { CellId = id };
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// CellVisibilityPortalPolygonsTests.cs — verifies BuildLoadedCell preserves
|
||||
// CellVisibilityPortalPolygonsTests.cs — verifies the cell-build transaction preserves
|
||||
// portal polygon vertices in LoadedCell.PortalPolygons.
|
||||
//
|
||||
// Phase A8 — Indoor-cell visibility culling. Issue #78.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace AcDream.App.Tests.Rendering;
|
|||
/// the neighbour room through the 0171↔0173↔0172 doorway chain. The user still sees
|
||||
/// BACKGROUND at certain angles — so the defect is in the FLOOD/CLIP output for those
|
||||
/// eye positions. This harness drives the REAL <see cref="PortalVisibilityBuilder.Build"/>
|
||||
/// over the REAL Holtburg building cells (dat-loaded, mirroring GameWindow.BuildLoadedCell)
|
||||
/// over the REAL Holtburg building cells (dat-loaded, mirroring EnvCellLandblockBuildBuilder)
|
||||
/// along the captured corner eye path, and prints which cells the flood keeps/drops per
|
||||
/// step. The player's room (0172) dropping while the player is visible on screen is the
|
||||
/// background defect; the step where it happens pins the mechanism.
|
||||
|
|
@ -50,7 +50,7 @@ public class CornerFloodReplayTests
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dat → LoadedCell, mirroring GameWindow.BuildLoadedCell (GameWindow.cs:5636-5776)
|
||||
/// Dat → LoadedCell, mirroring EnvCellLandblockBuildBuilder.
|
||||
/// field-for-field: portals (with OtherPortalId back-link), clip planes from the
|
||||
/// portal polygon's first 3 verts + centroid InsideSide, full portal polygons in
|
||||
/// cell-local space, local AABB, world transform from EnvCell.Position.
|
||||
|
|
@ -106,7 +106,7 @@ public class CornerFloodReplayTests
|
|||
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
|
||||
float d = -Vector3.Dot(normal, p0);
|
||||
// InsideSide from the dat PortalSide bit — mirrors the production fix
|
||||
// (GameWindow.BuildLoadedCell): retail InitCell (0x005a4b70) + physics
|
||||
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
|
||||
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
|
||||
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
|
||||
clipPlanes.Add(new PortalClipPlane
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class Issue113MeetingHallFloodTests
|
|||
return Directory.Exists(def) ? def : null;
|
||||
}
|
||||
|
||||
// Mirrors CornerFloodReplayTests.LoadCell (GameWindow.BuildLoadedCell shape).
|
||||
// Mirrors CornerFloodReplayTests.LoadCell (EnvCellLandblockBuildBuilder shape).
|
||||
private static LoadedCell LoadCell(DatCollection dats, uint cellId)
|
||||
{
|
||||
var envCell = dats.Get<DatEnvCell>(cellId)
|
||||
|
|
@ -95,7 +95,7 @@ public class Issue113MeetingHallFloodTests
|
|||
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
|
||||
float d = -Vector3.Dot(normal, p0);
|
||||
// InsideSide from the dat PortalSide bit — mirrors the production fix
|
||||
// (GameWindow.BuildLoadedCell): retail InitCell (0x005a4b70) + physics
|
||||
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
|
||||
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
|
||||
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
|
||||
clipPlanes.Add(new PortalClipPlane
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public class EnvCellLandblockBuildTests
|
||||
{
|
||||
private const uint LandblockId = 0x8C04FFFFu;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SnapshotsInputCollections()
|
||||
{
|
||||
var cells = new List<LoadedCell> { new() { CellId = 0x8C040100u } };
|
||||
var shells = new List<EnvCellShellPlacement> { Shell(0x8C040100u, 1) };
|
||||
|
||||
var build = new EnvCellLandblockBuild(LandblockId, cells, shells);
|
||||
cells.Clear();
|
||||
shells.Clear();
|
||||
|
||||
Assert.Single(build.VisibilityCells);
|
||||
Assert.Single(build.Shells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RejectsCellFromAnotherLandblock()
|
||||
{
|
||||
var foreign = new LoadedCell { CellId = 0x00070100u };
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new EnvCellLandblockBuild(LandblockId, new[] { foreign }, Array.Empty<EnvCellShellPlacement>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCommittedSnapshot_ContainsEveryShellInOneCompleteReplacement()
|
||||
{
|
||||
var shells = Enumerable.Range(0, 1123)
|
||||
.Select(index => Shell(0x8C040100u + (uint)index, (ulong)(index % 17 + 1)))
|
||||
.ToArray();
|
||||
var build = new EnvCellLandblockBuild(
|
||||
LandblockId,
|
||||
Array.Empty<LoadedCell>(),
|
||||
shells);
|
||||
|
||||
var snapshot = EnvCellRenderer.CreateCommittedSnapshot(build);
|
||||
|
||||
Assert.Equal(1123, snapshot.Instances.Count);
|
||||
Assert.Equal(1123, snapshot.EnvCellBounds.Count);
|
||||
Assert.Equal(1123, snapshot.BuildingPartGroups.Values.Sum(group => group.Count));
|
||||
Assert.True(snapshot.InstancesReady);
|
||||
Assert.True(snapshot.MeshDataReady);
|
||||
Assert.True(snapshot.GpuReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndependentBuilds_NeverSharePendingStorage()
|
||||
{
|
||||
var complete = new EnvCellLandblockBuild(
|
||||
LandblockId,
|
||||
Array.Empty<LoadedCell>(),
|
||||
Enumerable.Range(0, 1123)
|
||||
.Select(index => Shell(0x8C040100u + (uint)index, 1)));
|
||||
var second = new EnvCellLandblockBuild(
|
||||
LandblockId,
|
||||
Array.Empty<LoadedCell>(),
|
||||
Enumerable.Range(0, 11)
|
||||
.Select(index => Shell(0x8C040500u + (uint)index, 2)));
|
||||
|
||||
var completeSnapshot = EnvCellRenderer.CreateCommittedSnapshot(complete);
|
||||
var secondSnapshot = EnvCellRenderer.CreateCommittedSnapshot(second);
|
||||
|
||||
Assert.Equal(1123, completeSnapshot.Instances.Count);
|
||||
Assert.Equal(11, secondSnapshot.Instances.Count);
|
||||
Assert.Equal(1123, completeSnapshot.Instances.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_CanPublishItsTransactionOnlyOnce()
|
||||
{
|
||||
var builder = new EnvCellLandblockBuildBuilder(LandblockId);
|
||||
|
||||
_ = builder.Build();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => builder.Build());
|
||||
}
|
||||
|
||||
private static EnvCellShellPlacement Shell(uint cellId, ulong geometryId)
|
||||
{
|
||||
var min = new Vector3(cellId & 0xFF, 0, 0);
|
||||
var bounds = new WbBoundingBox(min, min + Vector3.One);
|
||||
return new EnvCellShellPlacement(
|
||||
cellId,
|
||||
geometryId,
|
||||
EnvironmentId: 1,
|
||||
CellStructure: 0,
|
||||
Surfaces: ImmutableArray.Create<ushort>(1),
|
||||
WorldPosition: min,
|
||||
Rotation: Quaternion.Identity,
|
||||
Transform: Matrix4x4.CreateTranslation(min),
|
||||
LocalBounds: new WbBoundingBox(Vector3.Zero, Vector3.One),
|
||||
WorldBounds: bounds);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,12 +67,4 @@ public class EnvCellSceneryInstanceTests
|
|||
Assert.Equal(2, lb.BuildingPartGroups[0x01000001UL].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Landblock_PendingInstancesNullByDefault()
|
||||
{
|
||||
var lb = new EnvCellLandblock();
|
||||
Assert.Null(lb.PendingInstances);
|
||||
Assert.Null(lb.PendingEnvCellBounds);
|
||||
Assert.Null(lb.PendingSeenOutsideCells);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Xunit;
|
||||
|
|
@ -109,6 +110,35 @@ public class LandblockStreamerTests
|
|||
Assert.Equal(1, meshBuildCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_CarriesTheExactCompletedCellTransactionToTheConsumer()
|
||||
{
|
||||
var landblock = new LoadedLandblock(
|
||||
0x8C04FFFFu,
|
||||
new LandBlock(),
|
||||
System.Array.Empty<WorldEntity>());
|
||||
var cellBuild = new EnvCellLandblockBuild(
|
||||
landblock.LandblockId,
|
||||
System.Array.Empty<AcDream.App.Rendering.LoadedCell>(),
|
||||
System.Array.Empty<EnvCellShellPlacement>());
|
||||
var build = new LandblockBuild(landblock, cellBuild);
|
||||
var mesh = new AcDream.Core.Terrain.LandblockMeshData(
|
||||
System.Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
||||
System.Array.Empty<uint>());
|
||||
|
||||
using var streamer = new LandblockStreamer(
|
||||
loadLandblock: (_, _) => build,
|
||||
buildMeshOrNull: (_, _) => mesh);
|
||||
streamer.Start();
|
||||
streamer.EnqueueLoad(landblock.LandblockId, LandblockStreamJobKind.LoadNear);
|
||||
|
||||
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
|
||||
await DrainFirstAsync(streamer));
|
||||
|
||||
Assert.Same(build, loaded.Build);
|
||||
Assert.Same(cellBuild, loaded.Build.EnvCells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PromoteToNear_OvertakesAndSupersedesQueuedFarLoadForSameLandblock()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -163,6 +163,33 @@ public class StreamingControllerDungeonGateTests
|
|||
Assert.DoesNotContain(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadFar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeKnownLoginCenter_DungeonPerformsOneCollapseWithoutReloadChurn()
|
||||
{
|
||||
var h = Make();
|
||||
|
||||
h.Ctrl.InitializeKnownLoginCenter(0, 7, isSealedDungeon: true);
|
||||
h.Ctrl.InitializeKnownLoginCenter(0, 7, isSealedDungeon: true);
|
||||
|
||||
Assert.Single(h.Loads);
|
||||
Assert.Equal(Encode(0, 7), h.Loads[0].Id);
|
||||
Assert.Equal(1, h.ClearCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializeKnownLoginCenter_OutdoorWaitsForFirstCorrectlyCenteredTick()
|
||||
{
|
||||
var h = Make();
|
||||
|
||||
h.Ctrl.InitializeKnownLoginCenter(140, 4, isSealedDungeon: false);
|
||||
|
||||
Assert.Empty(h.Loads);
|
||||
Assert.Equal(0, h.ClearCalls());
|
||||
|
||||
h.Ctrl.Tick(140, 4, insideDungeon: false);
|
||||
Assert.Contains(h.Loads, load => load.Id == Encode(140, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreCollapse_AfterBootstrapTick_CancelsWindow_UnloadsResidentNeighbors_KeepsDungeon()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public class StreamingControllerTests
|
|||
var applied = new List<LoadedLandblock>();
|
||||
var controller = new StreamingController(
|
||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||
(lb, _) => applied.Add(lb), state, nearRadius: 2, farRadius: 2);
|
||||
(build, _) => applied.Add(build.Landblock), state, nearRadius: 2, farRadius: 2);
|
||||
|
||||
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
|
||||
// Entities (positional record). Adjust if the first positional arg
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ public class StreamingControllerTwoTierTests
|
|||
applyTerrain: (appliedLb, appliedMesh) =>
|
||||
{
|
||||
applyPromotedCount++;
|
||||
Assert.Same(promotedLb, appliedLb);
|
||||
Assert.Same(promotedLb, appliedLb.Landblock);
|
||||
Assert.Same(promotedMesh, appliedMesh);
|
||||
},
|
||||
state: state,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue