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

@ -1208,3 +1208,60 @@ Verification:
attributed methods, and reduces total direct/helper source readers from 92
to 83 while direct readers remain 57. The remaining 83 reconcile to the 22
approved retained policies/contracts and 61 staged replacements.
## Batch Z composition-root source-freeze replacement
Batch Z converts 12 approved source-text readers across the seven graphical
composition-owner suites and removes one stale backend assertion. No product
source changes. The replacement tests use the built call graph, reflected
owner metadata, existing transactional fixtures, and typed composition results
instead of source fragments.
The exact decisions are:
- all seven phase-boundary tests now prove that `GameWindow` constructs exactly
one instance of the relevant composition phase and does not directly
construct the leaf owners that belong behind it. The phase and snapshot
types are also checked for the absence of a retained `GameWindow` owner;
- the platform prelude now verifies the compiled `OnLoad` route through
`AcquirePlatform` and `GameWindowCompositionPipeline.Run`, plus the single
`GameWindowPlatformAcquisition.Acquire` and host/input/camera phase edges;
- the prepared-asset contract follows the production factory, world phase,
`ObjectMeshManager`, `WbMeshAdapter`, and compiled shutdown manifest. It
proves one `IPreparedAssetSource` owner, no DAT-reader constructor seam in the
production mesh manager, prepared reads rather than live mesh building, and
mesh/prepared-source/DAT release order;
- the frame-root test follows the compiled construction order from render
resources and comparison state through the world/update roots, graph
publication, window publication, and final ownership transfers. The
lifecycle snapshot contract now verifies its exact `RenderFrameOutcome`
input and its typed live-entity, GPU-memory, profiler, and landblock metrics;
- the session phase follows the compiled streamer/start/reveal route and the
delegate target that constructs the request streamer. Its completion check
follows hydration, inbound routing, input/player mode, portal transfer,
session creation, command bindings, action attachment, and final publication
in order. Character selection now verifies the graphical selector option,
typed connect options, absence of automatic first-character selection, and
retained-UI runtime bindings;
- the interaction, live-presentation, and world-render boundaries retain their
existing behavioral phase fixtures while compiled metadata guards against
construction leaking back into `GameWindow`; and
- `PortalSpaceIsComposedOnBothBackendArms` is removed as stale. Campaign V
deleted the OpenGL backend, so a two-backend-arms source condition is no
longer an architectural contract; its negative assertion named an already
deleted null presentation. The valuable portal ownership rationale remains
in the live/session composition code, the compiled portal transfer order,
and the existing teleport/reveal behavior suites.
Verification:
- all 187 composition tests pass with zero skips or failures;
- the complete locked Release build covers all 44 projects with zero warnings
and zero errors;
- the no-retry complete hermetic Release gate passes 14,346/14,346 with zero
skips or failures across all 12 test assemblies. The one-case reduction is
the stale two-backend-arms assertion; and
- the regenerated 1,254-file inventory parses every file, reports 11,414
attributed methods, and reduces direct/total source readers from 57/83 to
44/70. The remaining 70 reconcile to the 22 approved retained
policies/contracts and 48 staged replacements.

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.");
}
}