389 lines
16 KiB
C#
389 lines
16 KiB
C#
using System.Reflection;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.Tests.Architecture;
|
|
using AcDream.App.UI;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class GameWindowRenderLeafCompositionTests
|
|
{
|
|
private const BindingFlags Declared = BindingFlags.Instance
|
|
| BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
|
| BindingFlags.DeclaredOnly;
|
|
|
|
[Fact]
|
|
public void ProductionRender_PreservesPrivatePresentationAndCaptureOrder()
|
|
{
|
|
MethodInfo presentation = RequiredMethod(
|
|
typeof(PrivatePresentationRenderer),
|
|
nameof(PrivatePresentationRenderer.Render));
|
|
AssertCallOrder(
|
|
presentation,
|
|
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
|
(typeof(IPrivateEntityViewportFrame), nameof(IPrivateEntityViewportFrame.Render)),
|
|
(typeof(IRetainedGameplayUiFrame), nameof(IRetainedGameplayUiFrame.Render)),
|
|
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.Render)),
|
|
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)));
|
|
|
|
MethodInfo orchestrator = RequiredMethod(
|
|
typeof(RenderFrameOrchestrator),
|
|
nameof(RenderFrameOrchestrator.Render));
|
|
AssertCallOrder(
|
|
orchestrator,
|
|
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
|
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
|
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionRender_Prepares_resources_then_devtools_weather_and_world()
|
|
{
|
|
MethodInfo prepare = RequiredMethod(
|
|
typeof(RenderFramePreparationController),
|
|
nameof(RenderFramePreparationController.Prepare));
|
|
AssertCallOrder(
|
|
prepare,
|
|
(typeof(IRenderFrameResourcePhase), nameof(IRenderFrameResourcePhase.Prepare)),
|
|
(typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.BeginFrame)),
|
|
(typeof(IRenderWeatherFramePhase), nameof(IRenderWeatherFramePhase.Tick)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers()
|
|
{
|
|
MethodInfo complete = RequiredMethod(
|
|
typeof(SessionPlayerCompositionPhase),
|
|
"CompleteSessionPlayer");
|
|
AssertCallOrder(
|
|
complete,
|
|
(typeof(TransferableResourceSlot<>), "Transfer"),
|
|
(typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"),
|
|
(typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer"));
|
|
|
|
MethodBase tunnelFactory = ReferencedMethodConstructing(
|
|
complete,
|
|
typeof(LocalPlayerTeleportPresentation));
|
|
Assert.NotNull(ReferencedMethodConstructing(
|
|
tunnelFactory,
|
|
typeof(LocalPlayerTeleportController)));
|
|
|
|
MethodInfo onLoad = RequiredMethod(typeof(GameWindow), "OnLoad");
|
|
IReadOnlyList<CompiledCall> loadReferences =
|
|
CompiledCallGraph.ReadMethodReferences(onLoad);
|
|
CompiledCall sessionPhase = Assert.Single(
|
|
loadReferences,
|
|
reference => Constructs(
|
|
reference.Target,
|
|
typeof(SessionPlayerCompositionPhase)));
|
|
CompiledCall framePhase = Assert.Single(
|
|
loadReferences,
|
|
reference => Constructs(
|
|
reference.Target,
|
|
typeof(FrameRootCompositionPhase)));
|
|
Assert.True(sessionPhase.Offset < framePhase.Offset);
|
|
|
|
MethodInfo composeFrame = RequiredMethod(
|
|
typeof(FrameRootCompositionPhase),
|
|
"ComposeCore");
|
|
AssertCallOrder(
|
|
composeFrame,
|
|
(typeof(LocalPlayerTeleportRenderStateSource), ".ctor"),
|
|
(typeof(RenderFrameResourceController), ".ctor"),
|
|
(typeof(PrivatePresentationRenderer), ".ctor"),
|
|
(typeof(RenderFrameOrchestrator), ".ctor"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionComposition_RemovesLegacyLeafOwnership()
|
|
{
|
|
FieldInfo[] windowFields = typeof(GameWindow).GetFields(Declared);
|
|
HashSet<string> removedFieldNames =
|
|
[
|
|
"_imguiBootstrap",
|
|
"_panelHost",
|
|
"_paperdollDollDirty",
|
|
"_lastRenderSignature",
|
|
"_lastVisibleLandblocks",
|
|
"_perfAccum",
|
|
"_entityUploadTiming",
|
|
"_weatherAccum",
|
|
];
|
|
HashSet<string> removedFieldTypes =
|
|
[
|
|
nameof(WorldRenderDiagnostics),
|
|
nameof(WorldRenderFrameBuilder),
|
|
nameof(SkyPesFrameController),
|
|
nameof(RetailPViewRenderer),
|
|
nameof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewCellSource),
|
|
nameof(TerrainDrawDiagnosticsController),
|
|
];
|
|
Assert.DoesNotContain(
|
|
windowFields,
|
|
field => removedFieldNames.Contains(field.Name)
|
|
|| removedFieldTypes.Contains(field.FieldType.Name));
|
|
|
|
HashSet<string> removedMethods =
|
|
[
|
|
"RefreshPaperdollDoll",
|
|
"ApplyPaperdollPose",
|
|
"ResolvePaperdollPoseDid",
|
|
"EnumerateDebugPanel",
|
|
"ResetPanelLayout",
|
|
"SetPanelLayout",
|
|
"EmitRenderSignatureIfChanged",
|
|
"EmitRetailPViewDiagnostics",
|
|
"EmitGlStateTripwireIfChanged",
|
|
"EmitClipRouteScissorProbe",
|
|
"TryGetLoginWorldCell",
|
|
"ApplyFramePacingPreference",
|
|
"RefreshActiveMonitorFramePacing",
|
|
"OnFrameRendered",
|
|
];
|
|
Assert.DoesNotContain(
|
|
typeof(GameWindow).GetMethods(Declared),
|
|
method => removedMethods.Contains(method.Name));
|
|
|
|
Assert.Contains(
|
|
CompiledCallGraph.ReadDeclared(typeof(LivePresentationCompositionPhase)),
|
|
call => call.Target.DeclaringType == typeof(PaperdollFramePresenter)
|
|
&& call.Target.IsConstructor);
|
|
IReadOnlyList<CompiledCall> frameCalls =
|
|
CompiledCallGraph.ReadDeclared(typeof(FrameRootCompositionPhase));
|
|
Assert.All(
|
|
new[]
|
|
{
|
|
typeof(RenderFrameResourceController),
|
|
typeof(RenderWeatherFrameController),
|
|
typeof(PrivatePresentationRenderer),
|
|
typeof(RenderFrameOrchestrator),
|
|
},
|
|
type => Assert.Contains(
|
|
frameCalls,
|
|
call => call.Target.DeclaringType == type
|
|
&& call.Target.IsConstructor));
|
|
Assert.Contains(
|
|
CompiledCallGraph.ReadDeclared(typeof(GameWindow)),
|
|
call => call.Target.DeclaringType == typeof(DisplayFramePacingController)
|
|
&& call.Target.IsConstructor);
|
|
Assert.Contains(
|
|
CompiledCallGraph.ReadDeclared(typeof(CameraPointerInputController)),
|
|
call => call.Target.DeclaringType == typeof(IInputCaptureSource)
|
|
&& call.Target.Name == "get_WantCaptureMouse");
|
|
}
|
|
|
|
[Fact]
|
|
public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder()
|
|
{
|
|
MethodInfo create = RequiredMethod(
|
|
typeof(GameWindowShutdownManifest),
|
|
nameof(GameWindowShutdownManifest.Create));
|
|
IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(create);
|
|
|
|
AssertLabelOrder(
|
|
labels,
|
|
"submitted GPU work",
|
|
"render frontends",
|
|
"portal tunnel",
|
|
"paperdoll viewport",
|
|
"graphics API context");
|
|
AssertLabelOrder(
|
|
labels,
|
|
"frame borrowers",
|
|
"world frame composition",
|
|
"frame-root bindings",
|
|
"session dependents");
|
|
AssertLabelOrder(labels, "frame pacing", "frame profiler");
|
|
AssertLabelOrder(
|
|
labels,
|
|
"world frame composition",
|
|
"render frontends",
|
|
"input context",
|
|
"graphics API context");
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionOutcome_UsesObservedWorldAndScreenshotFacts()
|
|
{
|
|
MethodInfo presentation = RequiredMethod(
|
|
typeof(PrivatePresentationRenderer),
|
|
nameof(PrivatePresentationRenderer.Render));
|
|
AssertCallOrder(
|
|
presentation,
|
|
(typeof(RenderFrameFoundation), "get_PortalViewportVisible"),
|
|
(typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)),
|
|
(typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)),
|
|
(typeof(PrivatePresentationFrameOutcome), ".ctor"));
|
|
|
|
MethodInfo orchestrator = RequiredMethod(
|
|
typeof(RenderFrameOrchestrator),
|
|
nameof(RenderFrameOrchestrator.Render));
|
|
AssertCallOrder(
|
|
orchestrator,
|
|
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.BeginFrame)),
|
|
(typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)),
|
|
(typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)),
|
|
(typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.EndFrame)),
|
|
(typeof(RenderFrameOutcome), ".ctor"),
|
|
(typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish)));
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindow_OnRenderIsOneImmutableOrchestratorHandoff()
|
|
{
|
|
MethodInfo onRender = RequiredMethod(typeof(GameWindow), "OnRender");
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(onRender);
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(GameFrameGraphSlot)
|
|
&& call.Target.Name == nameof(GameFrameGraphSlot.Render));
|
|
|
|
HashSet<string> forbiddenFields =
|
|
[
|
|
"_gpuFrameFlights",
|
|
"_worldScene",
|
|
"_devToolsFramePresenter",
|
|
"_retailUiRuntime",
|
|
"_frameScreenshots",
|
|
];
|
|
Assert.DoesNotContain(
|
|
CompiledCallGraph.ReadFieldReferences(onRender),
|
|
reference => forbiddenFields.Contains(reference.Field.Name));
|
|
}
|
|
|
|
[Fact]
|
|
public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface()
|
|
{
|
|
MethodBase compose = MethodConstructing(
|
|
typeof(LivePresentationCompositionPhase),
|
|
typeof(PaperdollFramePresenter));
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
CompiledCall viewport = Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailUiRuntime)
|
|
&& call.Target.Name == "get_PaperdollViewportWidget");
|
|
CompiledCall inventory = Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailUiRuntime)
|
|
&& call.Target.Name == "get_InventoryFrame");
|
|
CompiledCall presenter = Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(PaperdollFramePresenter)
|
|
&& call.Target.IsConstructor);
|
|
Assert.True(viewport.Offset < inventory.Offset);
|
|
Assert.True(inventory.Offset < presenter.Offset);
|
|
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(compose);
|
|
Assert.Contains(
|
|
branches,
|
|
branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch
|
|
&& branch.Offset > viewport.Offset
|
|
&& branch.Offset < inventory.Offset
|
|
&& branch.TargetOffset > presenter.Offset);
|
|
Assert.Contains(
|
|
branches,
|
|
branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch
|
|
&& branch.Offset > inventory.Offset
|
|
&& branch.Offset < presenter.Offset
|
|
&& branch.TargetOffset > presenter.Offset);
|
|
Assert.DoesNotContain(
|
|
CompiledCallGraph.ReadStringLiterals(compose),
|
|
value => value == "Paperdoll inventory frame is required.");
|
|
}
|
|
|
|
[Fact]
|
|
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
|
|
{
|
|
MethodInfo compose = RequiredMethod(
|
|
typeof(FrameRootCompositionPhase),
|
|
"ComposeCore");
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
AssertCallOrder(
|
|
compose,
|
|
(typeof(TerrainDrawDiagnosticsController), ".ctor"),
|
|
(typeof(WorldScenePassExecutor), ".ctor"));
|
|
Assert.DoesNotContain(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
|
&& (call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin)
|
|
|| call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete)));
|
|
}
|
|
|
|
private static MethodInfo RequiredMethod(Type type, string name) =>
|
|
type.GetMethod(
|
|
name,
|
|
BindingFlags.Instance | BindingFlags.Static
|
|
| BindingFlags.Public | BindingFlags.NonPublic)
|
|
?? throw new MissingMethodException(type.FullName, name);
|
|
|
|
private static MethodBase MethodConstructing(Type owner, Type constructed) =>
|
|
Assert.Single(
|
|
owner.GetMethods(Declared)
|
|
.Cast<MethodBase>()
|
|
.Concat(owner.GetConstructors(Declared))
|
|
.Where(method => method.GetMethodBody() is not null),
|
|
method => Constructs(method, constructed));
|
|
|
|
private static MethodBase ReferencedMethodConstructing(
|
|
MethodBase owner,
|
|
Type constructed) =>
|
|
Assert.Single(
|
|
CompiledCallGraph.ReadMethodReferences(owner)
|
|
.Select(reference => reference.Target)
|
|
.Where(method => method.GetMethodBody() is not null)
|
|
.Distinct(),
|
|
method => Constructs(method, constructed));
|
|
|
|
private static bool Constructs(MethodBase method, Type type) =>
|
|
method.GetMethodBody() is not null
|
|
&& CompiledCallGraph.Read(method).Any(call =>
|
|
call.Target.DeclaringType == type && call.Target.IsConstructor);
|
|
|
|
private static void AssertCallOrder(
|
|
MethodBase method,
|
|
params (Type Type, string Method)[] expected)
|
|
{
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int cursor = -1;
|
|
foreach ((Type type, string name) in expected)
|
|
{
|
|
int found = calls
|
|
.Select((call, index) => (call, index))
|
|
.Where(pair => pair.index > cursor)
|
|
.Select(pair => pair.call)
|
|
.Select((call, index) => (call, index: index + cursor + 1))
|
|
.FirstOrDefault(
|
|
pair => MatchesType(pair.call.Target.DeclaringType, type)
|
|
&& pair.call.Target.Name == name,
|
|
defaultValue: (default, -1))
|
|
.index;
|
|
Assert.True(
|
|
found > cursor,
|
|
$"Missing compiled edge after index {cursor}: {type.FullName}.{name}.");
|
|
cursor = found;
|
|
}
|
|
}
|
|
|
|
private static bool MatchesType(Type? actual, Type expected) =>
|
|
actual == expected
|
|
|| expected.IsGenericTypeDefinition
|
|
&& actual?.IsGenericType == true
|
|
&& actual.GetGenericTypeDefinition() == expected;
|
|
|
|
private static void AssertLabelOrder(
|
|
IReadOnlyList<string> labels,
|
|
params string[] expected)
|
|
{
|
|
int cursor = -1;
|
|
foreach (string label in expected)
|
|
{
|
|
int found = Enumerable.Range(cursor + 1, labels.Count - cursor - 1)
|
|
.FirstOrDefault(index => labels[index] == label, -1);
|
|
Assert.True(found > cursor, $"Missing shutdown label after {cursor}: {label}.");
|
|
cursor = found;
|
|
}
|
|
}
|
|
}
|