refactor(runtime): acknowledge exact world host projections

This commit is contained in:
Erik 2026-07-26 18:27:41 +02:00
parent 73d0b54e38
commit 18d17d8bb1
17 changed files with 1677 additions and 72 deletions

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Runtime;
using AcDream.Runtime.World;
using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics;
@ -179,6 +180,19 @@ public sealed class WorldLifecycleAutomationControllerTests
Path.Combine(directory, "screenshots"));
var controller = new WorldLifecycleAutomationController(
() => reveal,
() => new RuntimeWorldEnvironmentOwnershipSnapshot(
IsInitialized: true,
DayGroupDefinitionCount: 4,
ActiveDayGroupCount: 1),
() => new RuntimeWorldTransitOwnershipSnapshot(
BufferedTeleportDestinationCount: 0,
PendingTeleportStartCount: 0,
ActiveTeleportCount: 0,
AcceptedTeleportDestinationCount: 0,
ActiveRevealCount: 0,
PendingDestinationReadinessCount: 0,
HostProjectionCount: 0,
PendingHostAcknowledgementCount: 0),
() => 3,
_ => resources,
screenshots,
@ -205,6 +219,15 @@ public sealed class WorldLifecycleAutomationControllerTests
using JsonDocument json = JsonDocument.Parse(line);
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
Assert.Equal(
4,
json.RootElement.GetProperty("environmentOwnership")
.GetProperty("dayGroupDefinitionCount").GetInt32());
Assert.Equal(
0,
json.RootElement.GetProperty("transitOwnership")
.GetProperty("pendingHostAcknowledgementCount")
.GetInt32());
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
.GetProperty("visibleLandblocks").GetInt32());
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
@ -373,6 +396,8 @@ public sealed class WorldLifecycleAutomationControllerTests
string directory,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
new(
() => default,
() => default,
() => default,
() => 0,
capture,

View file

@ -81,7 +81,34 @@ public sealed class RuntimeWorldTransitOwnershipTests
coordinatorFields,
field => field.Name is "_activeGeneration"
or "_worldSimulationReleased"
or "_lifecycle");
or "_lifecycle"
or "_reservationGeneration"
or "_worldViewportReleased"
or "_hostProjection");
Assert.DoesNotContain(
coordinatorFields,
field => field.FieldType == typeof(RuntimePortalSnapshot)
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|| field.FieldType == typeof(long)
|| field.FieldType == typeof(uint));
Type hostProjection = Assert.Single(
typeof(WorldRevealCoordinator).GetNestedTypes(
System.Reflection.BindingFlags.NonPublic),
type => type.Name == "HostProjection");
var hostFields = hostProjection.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Single(
hostFields,
field => field.FieldType
== typeof(RuntimeWorldHostProjectionToken));
Assert.DoesNotContain(
hostFields,
field => field.FieldType == typeof(RuntimePortalSnapshot)
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|| field.FieldType == typeof(long)
|| field.FieldType == typeof(uint));
var teleportFields = typeof(LocalPlayerTeleportController)
.GetFields(

View file

@ -144,6 +144,39 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
}
[Fact]
public void ReentrantBeginDuringAudioResume_ReconcilesToNewestEdge()
{
var transit = new RuntimeWorldTransitState();
var world = new GpuWorldState(
availability: new WorldGenerationAvailabilityState(transit));
var audio = new ReentrantAudioQuiescence();
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var edge = new WorldGenerationQuiescenceEdge(
ShouldApply: true,
ClearWorldSelection: false);
quiescence.CommitBegin(edge);
audio.OnResume = () =>
{
audio.OnResume = null;
quiescence.CommitBegin(edge);
};
quiescence.ObserveReleased();
Assert.Equal(2, audio.SuspendCalls);
Assert.Equal(1, audio.ResumeCalls);
quiescence.ObserveReleased();
Assert.Equal(2, audio.SuspendCalls);
Assert.Equal(2, audio.ResumeCalls);
}
private static RuntimeDestinationReadiness Ready(
long generation,
uint destinationCell) =>
@ -178,4 +211,19 @@ public sealed class WorldGenerationQuiescenceTests
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio() => ResumeCalls++;
}
private sealed class ReentrantAudioQuiescence : IWorldAudioQuiescence
{
public int SuspendCalls { get; private set; }
public int ResumeCalls { get; private set; }
public Action? OnResume { get; set; }
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio()
{
ResumeCalls++;
OnResume?.Invoke();
}
}
}

View file

@ -409,8 +409,9 @@ public sealed class WorldRevealCoordinatorTests
(second, 0x20210123u, 0),
],
scheduler.Begins);
coordinator.Complete();
Assert.Equal([second], scheduler.Ends);
Assert.Equal([first], scheduler.Ends);
coordinator.Cancel();
Assert.Equal([first, second], scheduler.Ends);
}
[Fact]
@ -434,10 +435,200 @@ public sealed class WorldRevealCoordinatorTests
sequence: 2);
Assert.Equal([first, second], resources.Begins);
Assert.Equal([first], resources.Ends);
coordinator.RevealWorldViewport();
coordinator.Complete();
Assert.Equal([second], resources.Ends);
Assert.Equal([first, second], resources.Ends);
}
[Fact]
public void RegistrationCallback_ReentrantReplacementRetiresExactOldSuffix()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = null!;
long replacement = 0;
scheduler.OnBegin = () =>
{
scheduler.OnBegin = null;
replacement = coordinator.BeginLogin(0x20210123u);
};
coordinator = state.Build(streaming: scheduler);
long first = coordinator.BeginLogin(0x11340021u);
Assert.NotEqual(first, replacement);
Assert.Equal(replacement, coordinator.Snapshot.Generation);
Assert.Equal(
[
(first, 0x11340021u, 1),
(replacement, 0x20210123u, 0),
],
scheduler.Begins);
Assert.Equal([first], scheduler.Ends);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
Assert.Equal([first, replacement], scheduler.Ends);
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
}
[Fact]
public void RegistrationFailure_RetriesExactSuffixWithoutReplayingBegin()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
var resources = new RecordingRenderResourceScheduler
{
ThrowBeginCount = 1,
};
WorldRevealCoordinator coordinator = state.Build(
streaming: scheduler,
renderResources: resources);
Assert.Throws<InvalidOperationException>(
() => coordinator.BeginLogin(0x11340021u));
Assert.Single(scheduler.Begins);
Assert.Equal(1, resources.BeginAttempts);
Assert.Empty(resources.Begins);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Single(scheduler.Begins);
Assert.Equal(2, resources.BeginAttempts);
Assert.Single(resources.Begins);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
}
[Fact]
public void ReservationReleaseFailure_RetriesOnlyUnfinishedCallback()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
var resources = new RecordingRenderResourceScheduler
{
ThrowEndCount = 1,
};
WorldRevealCoordinator coordinator = state.Build(
streaming: scheduler,
renderResources: resources);
long generation = coordinator.BeginLogin(0x11340021u);
Assert.Throws<InvalidOperationException>(
coordinator.RevealWorldViewport);
Assert.Equal([generation], scheduler.Ends);
Assert.Equal(1, resources.EndAttempts);
Assert.Empty(resources.Ends);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Equal([generation], scheduler.Ends);
Assert.Equal(2, resources.EndAttempts);
Assert.Equal([generation], resources.Ends);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
}
[Fact]
public void ResetFailure_RetainsWithdrawalAndConvergesOnRetry()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler
{
ThrowEndCount = 1,
};
WorldRevealCoordinator coordinator =
state.Build(streaming: scheduler);
coordinator.BeginLogin(0x11340021u);
Assert.Throws<InvalidOperationException>(coordinator.ResetSession);
Assert.True(coordinator.Snapshot.Cancelled);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.True(
coordinator.Ownership.PendingHostAcknowledgementCount > 0);
coordinator.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
Assert.True(coordinator.Ownership.IsSessionIdle);
Assert.Equal(2, scheduler.EndAttempts);
Assert.Single(scheduler.Ends);
}
[Fact]
public void SimulationReleaseFailure_RetriesAudioWithoutReplayingMaterialization()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence
{
ThrowResumeCount = 1,
};
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true,
areCompositeTexturesReady: () => true,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence);
const uint cell = 0x3032001Cu;
long generation = BeginPortal(
coordinator,
transit,
cell,
sequence: 1);
Assert.True(coordinator.Evaluate(cell).IsReady);
Assert.Throws<InvalidOperationException>(() =>
coordinator.ObserveMaterialized(
generation,
1,
cell));
Assert.True(coordinator.Snapshot.Materialized);
Assert.Equal(1, coordinator.PortalMaterializationCount);
Assert.Equal(1, audio.ResumeAttempts);
Assert.Equal(0, audio.ResumeCalls);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Equal(2, audio.ResumeAttempts);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal(1, coordinator.PortalMaterializationCount);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
}
private static long BeginPortal(
@ -471,9 +662,32 @@ public sealed class WorldRevealCoordinatorTests
{
public int SuspendCalls { get; private set; }
public int ResumeCalls { get; private set; }
public int SuspendAttempts { get; private set; }
public int ResumeAttempts { get; private set; }
public int ThrowSuspendCount { get; set; }
public int ThrowResumeCount { get; set; }
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio() => ResumeCalls++;
public void SuspendWorldAudio()
{
SuspendAttempts++;
if (ThrowSuspendCount > 0)
{
ThrowSuspendCount--;
throw new InvalidOperationException("suspend failed");
}
SuspendCalls++;
}
public void ResumeWorldAudio()
{
ResumeAttempts++;
if (ThrowResumeCount > 0)
{
ThrowResumeCount--;
throw new InvalidOperationException("resume failed");
}
ResumeCalls++;
}
}
private sealed class RecordingDestinationScheduler
@ -481,16 +695,40 @@ public sealed class WorldRevealCoordinatorTests
{
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
public List<long> Ends { get; } = [];
public int BeginAttempts { get; private set; }
public int EndAttempts { get; private set; }
public int ThrowBeginCount { get; set; }
public int ThrowEndCount { get; set; }
public Action? OnBegin { get; set; }
public Action? OnEnd { get; set; }
public void BeginDestinationReservation(
long revealGeneration,
uint destinationCell,
int requiredRenderRadius) =>
int requiredRenderRadius)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("stream begin failed");
}
Begins.Add(
(revealGeneration, destinationCell, requiredRenderRadius));
OnBegin?.Invoke();
}
public void EndDestinationReservation(long revealGeneration) =>
public void EndDestinationReservation(long revealGeneration)
{
EndAttempts++;
if (ThrowEndCount > 0)
{
ThrowEndCount--;
throw new InvalidOperationException("stream end failed");
}
Ends.Add(revealGeneration);
OnEnd?.Invoke();
}
}
private sealed class RecordingRenderResourceScheduler
@ -498,11 +736,31 @@ public sealed class WorldRevealCoordinatorTests
{
public List<long> Begins { get; } = [];
public List<long> Ends { get; } = [];
public int BeginAttempts { get; private set; }
public int EndAttempts { get; private set; }
public int ThrowBeginCount { get; set; }
public int ThrowEndCount { get; set; }
public void BeginDestinationReveal(long revealGeneration) =>
public void BeginDestinationReveal(long revealGeneration)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("render begin failed");
}
Begins.Add(revealGeneration);
}
public void EndDestinationReveal(long revealGeneration) =>
public void EndDestinationReveal(long revealGeneration)
{
EndAttempts++;
if (ThrowEndCount > 0)
{
ThrowEndCount--;
throw new InvalidOperationException("render end failed");
}
Ends.Add(revealGeneration);
}
}
}