Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
202 lines
7 KiB
C#
202 lines
7 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockRetirementCoordinatorTests
|
|
{
|
|
[Fact]
|
|
public void EntityStageFailure_DetachesImmediately_AndResumesAtFailedEntity()
|
|
{
|
|
const uint landblockId = 0x2020FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
new[] { Entity(1), Entity(2), Entity(3) }));
|
|
|
|
var calls = new Dictionary<uint, int>();
|
|
bool failEntityTwo = true;
|
|
var coordinator = new LandblockRetirementCoordinator(
|
|
state,
|
|
ticket => CompletePresentation(
|
|
ticket,
|
|
lighting: entity =>
|
|
{
|
|
calls[entity.Id] = calls.GetValueOrDefault(entity.Id) + 1;
|
|
if (entity.Id == 2 && failEntityTwo)
|
|
{
|
|
failEntityTwo = false;
|
|
throw new InvalidOperationException("injected owner failure");
|
|
}
|
|
}));
|
|
|
|
coordinator.BeginFull(landblockId);
|
|
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.Equal(1, coordinator.PendingCount);
|
|
Assert.Equal(1, calls[1]);
|
|
Assert.Equal(1, calls[2]);
|
|
Assert.False(calls.ContainsKey(3));
|
|
|
|
coordinator.Advance();
|
|
|
|
Assert.Equal(0, coordinator.PendingCount);
|
|
Assert.Equal(1, calls[1]);
|
|
Assert.Equal(2, calls[2]);
|
|
Assert.Equal(1, calls[3]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ForceReload_RetiresEveryLoadedId_WhenOneOwnerRemainsPending()
|
|
{
|
|
uint[] ids = [0x2020FFFFu, 0x2121FFFFu, 0x2222FFFFu];
|
|
var state = new GpuWorldState();
|
|
foreach (uint id in ids)
|
|
state.AddLandblock(new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
|
|
|
|
bool failOnce = true;
|
|
var coordinator = new LandblockRetirementCoordinator(
|
|
state,
|
|
ticket => CompletePresentation(
|
|
ticket,
|
|
terrain: () =>
|
|
{
|
|
if (ticket.LandblockId == ids[1] && failOnce)
|
|
{
|
|
failOnce = false;
|
|
throw new InvalidOperationException("injected terrain failure");
|
|
}
|
|
}));
|
|
var controller = Controller(state, coordinator);
|
|
|
|
controller.ForceReloadWindow();
|
|
|
|
foreach (uint id in ids)
|
|
Assert.False(state.IsLoaded(id));
|
|
Assert.Equal(1, coordinator.PendingCount);
|
|
|
|
coordinator.Advance();
|
|
Assert.Equal(0, coordinator.PendingCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReplacementPublication_WaitsForSameIdRetirementFence()
|
|
{
|
|
const uint landblockId = 0x3232FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
|
|
int terrainAttempts = 0;
|
|
var coordinator = new LandblockRetirementCoordinator(
|
|
state,
|
|
ticket => CompletePresentation(
|
|
ticket,
|
|
terrain: () =>
|
|
{
|
|
terrainAttempts++;
|
|
if (terrainAttempts <= 2)
|
|
throw new InvalidOperationException("injected terrain failure");
|
|
}));
|
|
|
|
var pending = new Queue<LandblockStreamResult>();
|
|
int applyCount = 0;
|
|
var controller = Controller(
|
|
state,
|
|
coordinator,
|
|
drain: max => Drain(pending, max),
|
|
apply: (_, _) => applyCount++);
|
|
controller.ForceReloadWindow(); // generation 1, first retirement attempt
|
|
|
|
var replacement = new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>());
|
|
pending.Enqueue(new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
replacement,
|
|
EmptyMesh(),
|
|
generation: 1));
|
|
|
|
controller.Tick(0x32, 0x32); // second failure; publication stays deferred
|
|
Assert.Equal(0, applyCount);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.True(coordinator.IsPending(landblockId));
|
|
|
|
controller.Tick(0x32, 0x32); // third attempt completes, then publishes once
|
|
Assert.Equal(1, applyCount);
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.False(coordinator.IsPending(landblockId));
|
|
}
|
|
|
|
private static StreamingController Controller(
|
|
GpuWorldState state,
|
|
LandblockRetirementCoordinator coordinator,
|
|
Func<int, IReadOnlyList<LandblockStreamResult>>? drain = null,
|
|
Action<LandblockBuild, LandblockMeshData>? apply = null) =>
|
|
new(
|
|
enqueueLoad: (_, _, _) => { },
|
|
enqueueUnload: (_, _) => { },
|
|
drainCompletions: drain ?? (_ => Array.Empty<LandblockStreamResult>()),
|
|
applyTerrain: apply ?? ((_, _) => { }),
|
|
state: state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
retirementCoordinator: coordinator);
|
|
|
|
private static IReadOnlyList<LandblockStreamResult> Drain(
|
|
Queue<LandblockStreamResult> pending,
|
|
int max)
|
|
{
|
|
var result = new List<LandblockStreamResult>(max);
|
|
while (result.Count < max && pending.TryDequeue(out LandblockStreamResult? item))
|
|
result.Add(item);
|
|
return result;
|
|
}
|
|
|
|
private static void CompletePresentation(
|
|
LandblockRetirementTicket ticket,
|
|
Action<WorldEntity>? lighting = null,
|
|
Action? terrain = null)
|
|
{
|
|
ticket.RunForEachEntity(
|
|
LandblockRetirementStage.EntityLighting,
|
|
static _ => true,
|
|
lighting ?? (_ => { }));
|
|
ticket.RunForEachEntity(
|
|
LandblockRetirementStage.EntityTranslucency,
|
|
static _ => true,
|
|
static _ => { });
|
|
ticket.RunForEachEntity(
|
|
LandblockRetirementStage.PluginProjection,
|
|
static entity => entity.ServerGuid == 0,
|
|
static _ => { });
|
|
if (ticket.Kind == LandblockRetirementKind.Full)
|
|
ticket.RunOnce(LandblockRetirementStage.Terrain, terrain ?? (() => { }));
|
|
ticket.RunOnce(LandblockRetirementStage.Physics, static () => { });
|
|
ticket.RunOnce(LandblockRetirementStage.CellVisibility, static () => { });
|
|
ticket.RunOnce(LandblockRetirementStage.BuildingRegistry, static () => { });
|
|
ticket.RunOnce(LandblockRetirementStage.EnvironmentCells, static () => { });
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x01000000u + id,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static LandblockMeshData EmptyMesh() => new(
|
|
Array.Empty<TerrainVertex>(),
|
|
Array.Empty<uint>());
|
|
}
|