feat(app): observe canonical placement receipts

This commit is contained in:
Erik 2026-08-01 15:22:52 +02:00
parent 378ca95a67
commit f05ed5c3cd
14 changed files with 545 additions and 31 deletions

View file

@ -1,6 +1,7 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Net;
@ -11,6 +12,7 @@ using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs;
@ -409,6 +411,108 @@ public sealed class RuntimePlacementPresentationSinkTests
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
}
[Fact]
public void GraphicalRoute_ReentrantInitialDrainTeardownLeavesHeadForReplacement()
{
using var fixture = SubscriptionFixture.Create();
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record.Canonical,
publishWithdrawal: true));
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
RuntimeGenerationToken generation = fixture.Generation;
var retries = new RuntimePlacementProjectionRetrySlot(
() => generation);
var firstEvents = new RecordingEventRoute();
GraphicalSessionEventRoute? first = null;
int applications = 0;
var sink = new DelegatePlacementSink(
(in RuntimePlacementProjectionSnapshot projection) =>
{
applications++;
bool applied = fixture.Sink.TryApply(in projection);
if (applications == 1)
first!.Dispose();
return applied;
});
first = new GraphicalSessionEventRoute(
firstEvents,
() => new RuntimePlacementProjectionSubscription(
fixture.Lifetime.Placements,
() => generation,
sink,
retryPendingOnSubscribe: false),
() => generation,
retries);
first.Attach();
Assert.Equal(1, applications);
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
Assert.Equal(0, retries.BindingCount);
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
Assert.Equal(1, firstEvents.DisposeCount);
var replacementEvents = new RecordingEventRoute();
var replacement = new GraphicalSessionEventRoute(
replacementEvents,
() => new RuntimePlacementProjectionSubscription(
fixture.Lifetime.Placements,
() => generation,
sink,
retryPendingOnSubscribe: false),
() => generation,
retries);
replacement.Attach();
Assert.Equal(2, applications);
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
Assert.Equal(1, retries.BindingCount);
Assert.Equal(1, fixture.Lifetime.Events.PlacementSubscriberCount);
replacement.Dispose();
Assert.Equal(0, retries.BindingCount);
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
Assert.Equal(1, replacementEvents.DisposeCount);
}
[Fact]
public void RetrySlot_InvokesOnlyExactCurrentGenerationBinding()
{
RuntimeGenerationToken generation = new(7UL);
var retries = new RuntimePlacementProjectionRetrySlot(
() => generation);
int firstCalls = 0;
IDisposable first = retries.BindOwned(
generation,
() =>
{
firstCalls++;
return true;
});
retries.RetryPending();
generation = new RuntimeGenerationToken(8UL);
retries.RetryPending();
first.Dispose();
int replacementCalls = 0;
using IDisposable replacement = retries.BindOwned(
generation,
() =>
{
replacementCalls++;
return true;
});
first.Dispose();
retries.RetryPending();
Assert.Equal(1, firstCalls);
Assert.Equal(1, replacementCalls);
Assert.Equal(1, retries.BindingCount);
}
private static RuntimePlacementProjectionSnapshot Placement(
Fixture fixture,
LiveEntityRecord record,
@ -805,6 +909,27 @@ public sealed class RuntimePlacementPresentationSinkTests
}
}
private sealed class DelegatePlacementSink(
PlacementApply apply) : IRuntimePlacementProjectionSink
{
public bool TryApply(
in RuntimePlacementProjectionSnapshot projection) =>
apply(in projection);
}
private delegate bool PlacementApply(
in RuntimePlacementProjectionSnapshot projection);
private sealed class RecordingEventRoute : ILiveSessionEventRouting
{
public int AttachCount { get; private set; }
public int DisposeCount { get; private set; }
public void Attach() => AttachCount++;
public void Dispose() => DisposeCount++;
}
private sealed class RecordingResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }

View file

@ -1,9 +1,11 @@
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.World;
@ -387,6 +389,65 @@ public sealed class UpdateFrameOrchestratorTests
Assert.Equal(1, CountOccurrences(source, "_scripts.Tick("));
}
[Fact]
public void PlacementRetryRunsAfterStreamingAndInboundBeforeCommandReconcile()
{
string root = FindRepoRoot();
string outer = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Update",
"UpdateFrameOrchestrator.cs"));
string live = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"World",
"RetailLiveFrameCoordinator.cs"));
AssertAppearsInOrder(
outer,
"_streaming.Tick();",
"_liveFrame.Tick(");
AssertAppearsInOrder(
live,
"_session.Tick();",
"_placementProjectionRetry?.RetryPending();",
"_localPlayer.RunPostNetworkCommandPhase();",
"_spatialReconciler.Reconcile();");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void LiveFrameRetriesPlacementAfterInboundEvenWhenWorldIsQuiesced(
bool worldAvailable)
{
var calls = new List<string>();
var phases = new RecordingLivePhases(calls)
{
IsWorldAvailable = worldAvailable,
};
var frame = new RetailLiveFrameCoordinator(
phases,
new GpuWorldState(),
phases,
phases,
phases,
phases,
phases,
phases);
frame.Tick(1f / 60f);
Assert.Equal(
worldAvailable
? ["objects", "network", "placement-retry", "commands", "reconcile"]
: ["network", "placement-retry", "commands", "render-sync"],
calls);
}
[Fact]
public void GameWindow_ComposesTheLiveFrameOwnersWithoutOwningTheirBodies()
{
@ -1048,6 +1109,26 @@ public sealed class UpdateFrameOrchestratorTests
}
}
private sealed class RecordingLivePhases(List<string> calls) :
ILiveObjectFramePhase,
IRuntimeLiveSessionFramePhase,
IPostNetworkCommandFramePhase,
ILiveSpatialReconcilePhase,
IRenderProjectionSyncPhase,
IWorldGenerationAvailability,
IRuntimePlacementProjectionRetryPhase
{
public bool IsWorldAvailable { get; set; }
public long QuiescedGeneration => IsWorldAvailable ? 0L : 1L;
public void Tick(float deltaSeconds) => calls.Add("objects");
public void Tick() => calls.Add("network");
public void RunPostNetworkCommandPhase() => calls.Add("commands");
public void Reconcile() => calls.Add("reconcile");
public void SynchronizeActiveSources() => calls.Add("render-sync");
public void RetryPending() => calls.Add("placement-retry");
}
private sealed class RecordingCommit(List<string> calls)
: IUpdateFrameCommitPhase
{