182 lines
6.7 KiB
C#
182 lines
6.7 KiB
C#
using System.Reflection;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Tests.Architecture;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
public sealed class FrameRootCompositionTests
|
|
{
|
|
[Fact]
|
|
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
|
{
|
|
var calls = new List<string>();
|
|
var bindings = new FrameRootRuntimeBindings();
|
|
bindings.Adopt("first", new RetryBinding("first", calls, 0));
|
|
bindings.Adopt("second", new RetryBinding("second", calls, 1));
|
|
|
|
Assert.Throws<AggregateException>(bindings.Dispose);
|
|
Assert.Equal(["second", "first"], calls);
|
|
|
|
bindings.Dispose();
|
|
bindings.Dispose();
|
|
|
|
Assert.Equal(["second", "first", "second"], calls);
|
|
Assert.Throws<ObjectDisposedException>(() =>
|
|
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionPhasePublishesOnlyAfterBothRootsExist()
|
|
{
|
|
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
|
|
"ComposeCore",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?? throw new MissingMethodException(
|
|
typeof(FrameRootCompositionPhase).FullName,
|
|
"ComposeCore");
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
|
|
int resources = CallIndex(calls, typeof(RenderFrameResourceController), ".ctor");
|
|
int comparison = CallIndex(
|
|
calls,
|
|
typeof(RenderSceneShadowComparisonController),
|
|
".ctor",
|
|
resources + 1);
|
|
int scene = CallIndex(calls, typeof(WorldSceneRenderer), ".ctor", comparison + 1);
|
|
int automation = CallIndex(
|
|
calls,
|
|
typeof(WorldLifecycleAutomationController),
|
|
".ctor",
|
|
scene + 1);
|
|
int diagnostics = CallIndex(
|
|
calls,
|
|
typeof(SerialRenderFramePostDiagnosticsPhase),
|
|
".ctor",
|
|
automation + 1);
|
|
int render = CallIndex(calls, typeof(RenderFrameOrchestrator), ".ctor", diagnostics + 1);
|
|
int live = CallIndex(calls, typeof(RetailLiveFrameCoordinator), ".ctor", render + 1);
|
|
int update = CallIndex(calls, typeof(UpdateFrameOrchestrator), ".ctor", live + 1);
|
|
int graph = CallIndex(calls, typeof(GameFrameGraphSlot), "PublishOwned", update + 1);
|
|
int publish = CallIndex(
|
|
calls,
|
|
typeof(IGameWindowFrameRootPublication),
|
|
"PublishFrameRoots",
|
|
graph + 1);
|
|
int firstTransfer = calls
|
|
.Select((call, index) => (call, index))
|
|
.First(pair => pair.index > publish
|
|
&& pair.call.Target.Name == "Transfer")
|
|
.index;
|
|
int secondTransfer = calls
|
|
.Select((call, index) => (call, index))
|
|
.First(pair => pair.index > firstTransfer
|
|
&& pair.call.Target.Name == "Transfer")
|
|
.index;
|
|
|
|
Assert.True(secondTransfer > firstTransfer);
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs()
|
|
{
|
|
IReadOnlyList<CompiledCall> calls =
|
|
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
|
|
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(FrameRootCompositionPhase)
|
|
&& call.Target.IsConstructor);
|
|
Assert.DoesNotContain(
|
|
calls,
|
|
call => call.Target.IsConstructor
|
|
&& call.Target.DeclaringType is { } type
|
|
&& (type == typeof(WorldSceneRenderer)
|
|
|| type == typeof(UpdateFrameOrchestrator)
|
|
|| type == typeof(WorldLifecycleAutomationController)));
|
|
Assert.DoesNotContain(
|
|
calls,
|
|
call => call.Target.DeclaringType
|
|
== typeof(WorldLifecycleResourceSnapshotSource)
|
|
&& call.Target.Name == nameof(
|
|
WorldLifecycleResourceSnapshotSource.Capture));
|
|
}
|
|
|
|
[Fact]
|
|
public void FramePhaseAndSnapshotSourceRetainNoWindowOwner()
|
|
{
|
|
Assert.DoesNotContain(
|
|
typeof(FrameRootCompositionPhase).GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
|
field => field.FieldType == typeof(GameWindow));
|
|
|
|
Assert.DoesNotContain(
|
|
typeof(WorldLifecycleResourceSnapshotSource).GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic),
|
|
field => field.FieldType == typeof(GameWindow));
|
|
|
|
MethodInfo capture = typeof(WorldLifecycleResourceSnapshotSource)
|
|
.GetMethod(nameof(WorldLifecycleResourceSnapshotSource.Capture))!;
|
|
Assert.Equal(
|
|
typeof(RenderFrameOutcome),
|
|
Assert.Single(capture.GetParameters()).ParameterType);
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(capture);
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(LiveEntityRuntime)
|
|
&& call.Target.Name == "get_PendingTeardownCount");
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(GpuMemoryTracker)
|
|
&& call.Target.Name == "get_AllocatedBytes");
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(FrameProfiler)
|
|
&& call.Target.Name == "get_LastReport");
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WorldRenderFrameOutcome)
|
|
&& call.Target.Name == "get_VisibleLandblocks");
|
|
Assert.Contains(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WorldRenderFrameOutcome)
|
|
&& call.Target.Name == "get_TotalLandblocks");
|
|
}
|
|
|
|
private sealed class RetryBinding(
|
|
string name,
|
|
List<string> calls,
|
|
int failures) : IDisposable
|
|
{
|
|
private int _failures = failures;
|
|
|
|
public void Dispose()
|
|
{
|
|
calls.Add(name);
|
|
if (_failures-- > 0)
|
|
throw new InvalidOperationException("retry");
|
|
}
|
|
}
|
|
|
|
private static int CallIndex(
|
|
IReadOnlyList<CompiledCall> calls,
|
|
Type declaringType,
|
|
string methodName,
|
|
int startIndex = 0)
|
|
{
|
|
int index = CompiledCallGraph.IndexOf(
|
|
calls,
|
|
declaringType,
|
|
methodName,
|
|
startIndex);
|
|
Assert.True(
|
|
index >= startIndex,
|
|
$"Missing compiled edge {declaringType.FullName}.{methodName}.");
|
|
return index;
|
|
}
|
|
}
|