test: replace composition source freezes

This commit is contained in:
Erik 2026-08-18 15:50:53 +02:00
parent caa5eb8b2b
commit 80c7b44457
8 changed files with 471 additions and 406 deletions

View file

@ -6,12 +6,15 @@ using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Spells;
using AcDream.App.Tests.Architecture;
using AcDream.Content;
using AcDream.Content.Vfx;
using AcDream.Core.Audio;
using AcDream.Core.CharGen;
using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.Spells;
@ -20,6 +23,7 @@ using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Lib.IO;
using Silk.NET.Input;
using Silk.NET.OpenAL;
@ -163,84 +167,73 @@ public sealed class ContentEffectsAudioCompositionTests
[Fact]
public void GameWindowUsesTheProductionPhaseAndNoLongerConstructsItsBodyInline()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new ContentEffectsAudioCompositionPhase(", source,
StringComparison.Ordinal);
Assert.Single(
calls,
call => call.Target.DeclaringType
== typeof(ContentEffectsAudioCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);",
source,
StringComparison.Ordinal);
Assert.DoesNotContain("_hookRouter.Register(_particleSink)", source,
StringComparison.Ordinal);
Assert.DoesNotContain("new AcDream.App.Audio.OpenAlAudioEngine()", source,
StringComparison.Ordinal);
calls,
call => call.Target.DeclaringType
== typeof(RuntimeDatCollectionFactory)
&& call.Target.Name == nameof(RuntimeDatCollectionFactory.OpenReadOnly));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(AnimationHookRouter)
&& call.Target.Name == nameof(AnimationHookRouter.Register));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(OpenAlAudioEngine)
&& call.Target.IsConstructor);
}
[Fact]
public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource()
{
string root = FindRepoRoot();
string contentPhase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"ContentEffectsAudioComposition.cs"));
string worldPhase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
string manager = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"ObjectMeshManager.cs"));
string adapter = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Wb",
"WbMeshAdapter.cs"));
string lifetime = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindowLifetime.cs"));
MethodInfo openPrepared = typeof(RetailContentEffectsAudioCompositionFactory)
.GetMethod(nameof(
RetailContentEffectsAudioCompositionFactory.OpenPreparedAssetSource))!;
Assert.Contains(
CompiledCallGraph.Read(openPrepared),
call => call.Target.DeclaringType == typeof(PakPreparedAssetSource)
&& call.Target.IsConstructor);
Assert.Contains("new PakPreparedAssetSource(path, dats, diagnostic)",
contentPhase, StringComparison.Ordinal);
Assert.Contains("content.PreparedAssets", worldPhase,
StringComparison.Ordinal);
Assert.Contains("_preparedAssets.Read(request.Asset, ct)", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("MeshExtractor", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("IDatReaderWriter", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("GfxObjMesh.Build", adapter,
StringComparison.Ordinal);
MethodInfo compose = typeof(WorldRenderCompositionPhase)
.GetMethod(nameof(WorldRenderCompositionPhase.Compose))!;
IReadOnlyList<CompiledCall> worldCalls = CompiledCallGraph.Read(compose);
Assert.Contains(
worldCalls,
call => call.Target.DeclaringType == typeof(ContentEffectsAudioResult)
&& call.Target.Name == "get_PreparedAssets");
int meshStage = lifetime.IndexOf(
"new ResourceShutdownStage(\"mesh adapter\"",
StringComparison.Ordinal);
int preparedRelease = lifetime.IndexOf(
"Hard(\"prepared asset source\"",
StringComparison.Ordinal);
int datRelease = lifetime.IndexOf(
"Hard(\"DAT collection\"",
StringComparison.Ordinal);
FieldInfo preparedAssets = Assert.Single(
typeof(ObjectMeshManager).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(IPreparedAssetSource));
Assert.Equal("_preparedAssets", preparedAssets.Name);
Assert.DoesNotContain(
typeof(ObjectMeshManager).GetConstructors(),
constructor => constructor.GetParameters().Any(parameter =>
parameter.ParameterType == typeof(IDatReaderWriter)));
Assert.Contains(
CompiledCallGraph.ReadDeclared(typeof(ObjectMeshManager)),
call => call.Target.DeclaringType == typeof(IPreparedAssetSource)
&& call.Target.Name == nameof(IPreparedAssetSource.Read));
Assert.DoesNotContain(
CompiledCallGraph.ReadDeclared(typeof(WbMeshAdapter)),
call => call.Target.DeclaringType == typeof(GfxObjMesh)
&& call.Target.Name == nameof(GfxObjMesh.Build));
MethodInfo shutdown = typeof(GameWindowShutdownManifest).GetMethod(
nameof(GameWindowShutdownManifest.Create))!;
List<string> labels =
CompiledCallGraph.ReadStringLiterals(shutdown).ToList();
int meshStage = labels.IndexOf("mesh adapter");
int preparedRelease = labels.IndexOf("prepared asset source");
int datRelease = labels.IndexOf("DAT collection");
Assert.True(meshStage >= 0);
Assert.True(preparedRelease > meshStage);
Assert.True(datRelease > preparedRelease);
@ -564,15 +557,4 @@ public sealed class ContentEffectsAudioCompositionTests
public void CloseDevice(nint device) { }
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,6 +1,12 @@
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;
@ -28,48 +34,77 @@ public sealed class FrameRootCompositionTests
[Fact]
public void ProductionPhasePublishesOnlyAfterBothRootsExist()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException(
typeof(FrameRootCompositionPhase).FullName,
"ComposeCore");
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
AssertAppearsInOrder(
source,
"new RenderFrameResourceController(",
"new RenderSceneShadowComparisonController(",
"new WorldSceneRenderer(",
"new WorldLifecycleAutomationController(",
"\"world lifecycle automation owner\"",
"\"world lifecycle automation binding\"",
"new SerialRenderFramePostDiagnosticsPhase(",
"new RenderFrameOrchestrator(",
"postDiagnostics,",
"new RetailLiveFrameCoordinator(",
"new UpdateFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(",
"_publication.PublishFrameRoots(result);",
"graphLease.Transfer();",
"bindingsLease.Transfer();");
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()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new FrameRootCompositionPhase(", source);
Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source);
Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source);
Assert.DoesNotContain("_worldLifecycleAutomation", source);
Assert.DoesNotContain("_frameGraphs.Publish(", source);
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]
@ -80,19 +115,37 @@ public sealed class FrameRootCompositionTests
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow));
string snapshots = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Diagnostics",
"WorldLifecycleResourceSnapshotSource.cs"));
Assert.DoesNotContain("GameWindow", snapshots, StringComparison.Ordinal);
Assert.Contains("_liveEntities.PendingTeardownCount", snapshots);
Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots);
Assert.Contains("_frameProfiler.LastReport", snapshots);
Assert.Contains("Capture(RenderFrameOutcome outcome)", snapshots);
Assert.Contains("outcome.World.VisibleLandblocks", snapshots);
Assert.Contains("outcome.World.TotalLandblocks", snapshots);
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(
@ -110,27 +163,20 @@ public sealed class FrameRootCompositionTests
}
}
private static void AssertAppearsInOrder(string source, params string[] values)
private static int CallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName,
int startIndex = 0)
{
int cursor = 0;
foreach (string value in values)
{
int found = source.IndexOf(value, cursor, StringComparison.Ordinal);
Assert.True(found >= 0, $"Missing expected source fragment: {value}");
cursor = found + value.Length;
}
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
int index = CompiledCallGraph.IndexOf(
calls,
declaringType,
methodName,
startIndex);
Assert.True(
index >= startIndex,
$"Missing compiled edge {declaringType.FullName}.{methodName}.");
return index;
}
}

View file

@ -4,6 +4,7 @@ using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Architecture;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
@ -67,21 +68,35 @@ public sealed class HostInputCameraCompositionTests
[Fact]
public void GameWindowUsesTheExactPlatformPreludeAndPhaseOneType()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> declaredCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("GameWindowPlatformAcquisition.Acquire(", source,
StringComparison.Ordinal);
Assert.Contains("new HostInputCameraCompositionPhase(", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_gl = GL.GetApi(_window!)", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_input = _window!.CreateInput()", source,
StringComparison.Ordinal);
Assert.Single(
declaredCalls,
call => call.Target.DeclaringType
== typeof(GameWindowPlatformAcquisition)
&& call.Target.Name == nameof(GameWindowPlatformAcquisition.Acquire));
Assert.Single(
declaredCalls,
call => call.Target.DeclaringType
== typeof(HostInputCameraCompositionPhase)
&& call.Target.IsConstructor);
MethodInfo onLoad = typeof(GameWindow).GetMethod(
"OnLoad",
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException(typeof(GameWindow).FullName, "OnLoad");
IReadOnlyList<CompiledCall> loadCalls = CompiledCallGraph.Read(onLoad);
Assert.Contains(
loadCalls,
call => call.Target.DeclaringType == typeof(GameWindow)
&& call.Target.Name == "AcquirePlatform");
Assert.Contains(
loadCalls,
call => call.Target.DeclaringType == typeof(GameWindowCompositionPipeline)
&& call.Target.Name == nameof(GameWindowCompositionPipeline.Run));
Assert.DoesNotContain(loadCalls, call => call.Target.Name == "GetApi");
Assert.DoesNotContain(loadCalls, call => call.Target.Name == "CreateInput");
}
private sealed class Fixture : IDisposable
@ -450,16 +465,4 @@ public sealed class HostInputCameraCompositionTests
public void WriteLine(string message) { }
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,3 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.App.Combat;
using AcDream.App.Composition;
@ -8,6 +9,7 @@ using AcDream.Content;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Spells;
@ -190,19 +192,33 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact]
public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new InteractionRetainedUiCompositionPhase(", source);
Assert.DoesNotContain("new AcDream.App.UI.ItemInteractionController(", source);
Assert.DoesNotContain("_retailUiLease.AcquireHost(", source);
Assert.DoesNotContain("RetailUiRuntime.CreateUninitialized(", source);
Assert.DoesNotContain("private void UseItemByGuid(", source);
Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source);
Assert.Single(
calls,
call => call.Target.DeclaringType
== typeof(InteractionRetainedUiCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType
== typeof(ItemInteractionController)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(RetailUiRuntimeLease)
&& call.Target.Name == "AcquireHost");
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(RetailUiRuntime)
&& call.Target.Name == nameof(RetailUiRuntime.CreateUninitialized));
Assert.Null(typeof(GameWindow).GetMethod(
"UseItemByGuid",
BindingFlags.Instance | BindingFlags.NonPublic));
Assert.Null(typeof(GameWindow).GetMethod(
"PickWorldGuidAtCursor",
BindingFlags.Instance | BindingFlags.NonPublic));
}
private sealed class Fixture : IDisposable
@ -422,15 +438,4 @@ public sealed class InteractionRetainedUiCompositionTests
private static T Stub<T>() where T : class =>
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,6 +1,10 @@
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Physics.Motion;
@ -125,71 +129,29 @@ public sealed class LivePresentationCompositionTests
[Fact]
public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody()
{
string root = FindRepoRoot();
string window = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new LivePresentationCompositionPhase(", window);
Assert.DoesNotContain("new AcDream.App.World.LiveEntityRuntime(", window);
Assert.DoesNotContain("new AcDream.App.Rendering.Wb.WbDrawDispatcher(", window);
Assert.DoesNotContain("new AcDream.App.Streaming.LandblockRenderPublisher(", window);
Assert.DoesNotContain("_portalTunnelFallback.AcquirePrepared(", window);
Assert.Contains("new LiveEntityRuntime(", phase);
Assert.Contains("new WbDrawDispatcher(", phase);
Assert.Contains("new LandblockRenderPublisher(", phase);
Assert.Contains("d.PortalTunnelFallback.AcquirePrepared(", phase);
}
/// <summary>
/// Campaign V slice V6m: portal space is composed on BOTH arms. It was the
/// last renderer this phase built only when a GL context existed, and the
/// Vulkan arm's portal-less teleport presentation went with the condition —
/// so both the gate and the stand-in are asserted absent here.
/// </summary>
[Fact]
public void PortalSpaceIsComposedOnBothBackendArms()
{
string root = FindRepoRoot();
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
string session = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
Assert.Contains(
"if (dispatcherLease.Resource is { } portalDispatcher)",
phase);
Assert.Single(
windowCalls,
call => call.Target.DeclaringType
== typeof(LivePresentationCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
"if (gl is not null && dispatcherLease.Resource is { } portalDispatcher)",
phase);
Assert.DoesNotContain("NullLocalPlayerTeleportPresentation", session);
windowCalls,
call => call.Target.IsConstructor
&& call.Target.DeclaringType is { } type
&& (type == typeof(LiveEntityRuntime)
|| type == typeof(WbDrawDispatcher)
|| type == typeof(LandblockRenderPublisher)));
Assert.DoesNotContain(
"NullLocalPlayerTeleportPresentation",
File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"Gpu",
"Vk",
"VulkanCompositionFramePhases.cs")));
windowCalls,
call => call.Target.Name == "AcquirePrepared");
Assert.DoesNotContain(
typeof(LivePresentationCompositionPhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow));
}
private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create(
@ -250,15 +212,4 @@ public sealed class LivePresentationCompositionTests
}
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,5 +1,17 @@
using System.Reflection;
using AcDream.App.Combat;
using AcDream.App.Composition;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.UI;
using AcDream.App.World;
using AcDream.Core.Net.Messages;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Composition;
@ -66,115 +78,124 @@ public sealed class SessionPlayerCompositionTests
[Fact]
public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody()
{
string root = FindRepoRoot();
string window = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new SessionPlayerCompositionPhase(", window);
Assert.DoesNotContain("LandblockStreamer.CreateForRequests(", window);
Assert.DoesNotContain("new AcDream.App.Net.LiveSessionController()", window);
Assert.Single(
windowCalls,
call => call.Target.DeclaringType
== typeof(SessionPlayerCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
window);
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window);
Assert.Contains("LandblockStreamer.CreateForRequests(", phase);
Assert.Contains(
"LiveSessionController liveSession = d.Runtime.Session;",
phase);
Assert.DoesNotContain("new LiveSessionController()", phase);
Assert.Contains("new LocalPlayerTeleportController(", phase);
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase);
windowCalls,
call => call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
Assert.DoesNotContain(
windowCalls,
call => call.Target.IsConstructor
&& call.Target.DeclaringType is { } type
&& (type == typeof(LiveSessionController)
|| type == typeof(LocalPlayerTeleportController)
|| type == typeof(DatSpawnClaimHydrationClassifier)));
IReadOnlyList<FieldInfo> phaseFields =
typeof(SessionPlayerCompositionPhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(
phaseFields,
field => field.FieldType == typeof(GameWindow));
}
[Fact]
public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast()
{
string phase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
MethodInfo composeCore = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"ComposeCore");
IReadOnlyList<CompiledCall> composeCalls =
CompiledCallGraph.Read(composeCore);
AssertCallOrder(
composeCalls,
(typeof(LandblockStreamer), nameof(LandblockStreamer.Start)),
(typeof(StreamingController), ".ctor"),
(typeof(WorldRevealCoordinator), ".ctor"),
(typeof(SessionPlayerCompositionPhase), "CompleteSessionPlayer"));
AssertAppearsInOrder(
phase,
"LandblockStreamer.CreateForRequests(",
"streamerLease.Resource.Start();",
"new StreamingController(",
"new WorldRevealCoordinator(",
"LiveSessionController liveSession = d.Runtime.Session;",
"new LiveEntityHydrationController(",
"new LiveEntityNetworkUpdateController(",
"new GameplayInputFrameController(",
"new PlayerModeController(",
"d.PortalTunnelFallback.Transfer(",
"d.TeleportSink.BindOwned(localTeleport)",
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
IEnumerable<MethodBase> streamerFactories =
CompiledCallGraph.ReadMethodReferences(composeCore)
.Select(call => call.Target)
.Where(method => method.GetMethodBody() is not null);
MethodBase streamerFactory = Assert.Single(
streamerFactories,
method => CompiledCallGraph.Read(method).Any(call =>
call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name
== nameof(LandblockStreamer.CreateForRequests)));
Assert.Contains(
CompiledCallGraph.Read(streamerFactory),
call => call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal);
MethodInfo complete = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"CompleteSessionPlayer");
IReadOnlyList<CompiledCall> completeCalls =
CompiledCallGraph.Read(complete);
AssertCallOrder(
completeCalls,
(typeof(LiveEntityHydrationController), ".ctor"),
(typeof(LiveEntityNetworkUpdateController), ".ctor"),
(typeof(GameplayInputFrameController), ".ctor"),
(typeof(PlayerModeController), ".ctor"),
(typeof(TransferableResourceSlot<>), "Transfer"),
(typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"),
(typeof(LiveSessionRuntimeFactory), nameof(LiveSessionRuntimeFactory.Create)),
(typeof(LiveCombatModeCommandSlot), "BindOwned"),
(typeof(RuntimeDiagnosticCommandSlot), "BindOwned"),
(typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Create)),
(typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Attach)),
(typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer"));
Assert.DoesNotContain(
typeof(LiveSessionRuntimeFactory).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow));
}
[Fact]
public void GraphicalCompositionPausesOnlyWhenCharacterSelectorIsAbsent()
{
string root = FindRepoRoot();
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
string retainedUi = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"InteractionRetainedUiComposition.cs"));
MethodInfo complete = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"CompleteSessionPlayer");
IReadOnlyList<CompiledCall> sessionCalls =
CompiledCallGraph.Read(complete);
Assert.Contains(
"AwaitCharacterSelection:",
phase,
StringComparison.Ordinal);
sessionCalls,
call => call.Target.DeclaringType == typeof(RuntimeOptions)
&& call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains(
"d.Options.LiveCharacterSelector is null",
phase,
StringComparison.Ordinal);
sessionCalls,
call => call.Target.DeclaringType == typeof(LiveSessionConnectOptions)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
"CharacterList.TrySelectFirstAvailable",
phase,
StringComparison.Ordinal);
sessionCalls,
call => call.Target.DeclaringType == typeof(CharacterList)
&& call.Target.Name == nameof(CharacterList.TrySelectFirstAvailable));
MethodInfo retainedUi = typeof(RetailInteractionRetainedUiCompositionFactory)
.GetMethod(nameof(
RetailInteractionRetainedUiCompositionFactory.CreateRetainedUi))!;
IReadOnlyList<CompiledCall> uiCalls = CompiledCallGraph.Read(retainedUi);
Assert.Contains(
"CharacterSelection: d.Options.LiveCharacterSelector is null",
retainedUi,
StringComparison.Ordinal);
uiCalls,
call => call.Target.DeclaringType == typeof(RuntimeOptions)
&& call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains(
"() => late.GameRuntime.CharacterSelection",
retainedUi,
StringComparison.Ordinal);
Assert.Contains(
"late.GameRuntime.CharacterSelectionEnter",
retainedUi,
StringComparison.Ordinal);
uiCalls,
call => call.Target.DeclaringType
== typeof(CharacterSelectionRuntimeBindings)
&& call.Target.IsConstructor);
}
private sealed class RetryBinding(
@ -192,27 +213,39 @@ public sealed class SessionPlayerCompositionTests
}
}
private static void AssertAppearsInOrder(string source, params string[] values)
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 void AssertCallOrder(
IReadOnlyList<CompiledCall> calls,
params (Type DeclaringType, string MethodName)[] expected)
{
int cursor = 0;
foreach (string value in values)
int cursor = -1;
foreach ((Type declaringType, string methodName) in expected)
{
int found = source.IndexOf(value, cursor, StringComparison.Ordinal);
Assert.True(found >= 0, $"Missing expected source fragment: {value}");
cursor = found + value.Length;
int found = calls
.Select((call, index) => (call, index))
.FirstOrDefault(
pair => pair.index > cursor
&& MatchesType(pair.call.Target.DeclaringType, declaringType)
&& pair.call.Target.Name == methodName,
defaultValue: (default, -1))
.index;
Assert.True(
found > cursor,
$"Missing compiled edge after index {cursor}: "
+ $"{declaringType.FullName}.{methodName}");
cursor = found;
}
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
private static bool MatchesType(Type? actual, Type expected) =>
actual == expected
|| expected.IsGenericTypeDefinition
&& actual?.IsGenericType == true
&& actual.GetGenericTypeDefinition() == expected;
}

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Residency;
using AcDream.App.World;
using AcDream.App.Tests.Architecture;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Physics;
@ -121,23 +122,21 @@ public sealed class WorldRenderCompositionTests
[Fact]
public void GameWindowUsesPhaseAndNoLongerBuildsWorldFoundationInline()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Contains("new WorldRenderCompositionPhase(", source,
StringComparison.Ordinal);
Assert.DoesNotContain("BindlessSupport.TryCreate(_gl", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_terrainModernShader = new Shader", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_wbMeshAdapter = new", source,
StringComparison.Ordinal);
Assert.DoesNotContain("_textureCache = new TextureCache", source,
StringComparison.Ordinal);
Assert.Single(
calls,
call => call.Target.DeclaringType
== typeof(WorldRenderCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
calls,
call => call.Target.IsConstructor
&& call.Target.DeclaringType is { } type
&& (type == typeof(TerrainModernRenderer)
|| type == typeof(WbMeshAdapter)
|| type == typeof(TextureCache)));
}
private sealed class Fixture
@ -385,15 +384,4 @@ public sealed class WorldRenderCompositionTests
private static T Stub<T>() where T : class =>
(T)RuntimeHelpers.GetUninitializedObject(typeof(T));
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}