feat(app): observe canonical placement receipts
This commit is contained in:
parent
378ca95a67
commit
f05ed5c3cd
14 changed files with 545 additions and 31 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue