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

@ -162,7 +162,7 @@ public sealed class GameWindowSlice8BoundaryTests
"LiveSessionRuntimeFactory.cs"));
string sessionFactory = Slice(
sessionFactorySource,
"private LiveSessionEventRouter CreateEventRouter(",
"private ILiveSessionEventRouting CreateEventRouter(",
"private LiveInventorySessionBindings CreateInventoryBindings()");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),

View file

@ -5,28 +5,36 @@ namespace AcDream.App.Tests.Runtime;
public sealed class RuntimePhysicsOwnershipTests
{
[Fact]
public void GraphicalPlacementProjectionRemainsDormantUntilCoordinatedCutover()
public void ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel()
{
string root = FindRepositoryRoot();
string relative = Path.Combine("src", "AcDream.App");
foreach (string file in Directory.EnumerateFiles(
Path.Combine(root, relative),
"*.cs",
SearchOption.AllDirectories))
foreach (string relative in new[]
{
Path.Combine("src", "AcDream.App"),
Path.Combine("src", "AcDream.Headless"),
})
{
string source = File.ReadAllText(file);
string[] sources = Directory.EnumerateFiles(
Path.Combine(root, relative),
"*.cs",
SearchOption.AllDirectories)
.Select(File.ReadAllText)
.ToArray();
Assert.DoesNotContain(
".Placements.",
source,
StringComparison.Ordinal);
sources,
source => source.Contains(
".Placements.",
StringComparison.Ordinal));
Assert.DoesNotContain(
"RuntimePlacementProjectionChannel",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimePlacementProjectionSubscription",
source,
StringComparison.Ordinal);
sources,
source => source.Contains(
"RuntimePlacementProjectionChannel",
StringComparison.Ordinal));
Assert.Contains(
sources,
source => source.Contains(
"RuntimePlacementProjectionSubscription",
StringComparison.Ordinal));
}
}

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
{

View file

@ -173,6 +173,28 @@ public sealed class RuntimePlacementProjectionSubscriptionTests
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
}
[Fact]
public void SubscribeWithoutDrainPublishesOnlyAfterExplicitRetry()
{
using var fixture = new Fixture();
RuntimeSetPositionOutcome pending = fixture.Place(
fixture.First,
new Vector3(18.5f, 18f, 7f));
var sink = new RecordingSink();
using var subscription = fixture.Subscribe(
sink,
retryPendingOnSubscribe: false);
Assert.Empty(sink.Applied);
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
Assert.True(subscription.RetryPending());
Assert.Equal(pending.Projection, Assert.Single(sink.Applied).Token);
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
}
[Fact]
public void SyntheticWithdrawalUsesTheSameExactSinkAndAckPath()
{
@ -263,10 +285,12 @@ public sealed class RuntimePlacementProjectionSubscriptionTests
internal RuntimeEntityRecord? Second { get; }
internal RuntimePlacementProjectionSubscription Subscribe(
IRuntimePlacementProjectionSink sink) => new(
IRuntimePlacementProjectionSink sink,
bool retryPendingOnSubscribe = true) => new(
Lifetime.Placements,
() => _generation,
sink);
sink,
retryPendingOnSubscribe);
internal RuntimeSetPositionOutcome Place(
RuntimeEntityRecord record,