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 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 to 83 while direct readers remain 57. The remaining 83 reconcile to the 22
approved retained policies/contracts and 61 staged replacements. 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;
using AcDream.App.Rendering.Residency; using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Vfx; using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Spells; using AcDream.App.Spells;
using AcDream.App.Tests.Architecture;
using AcDream.Content; using AcDream.Content;
using AcDream.Content.Vfx; using AcDream.Content.Vfx;
using AcDream.Core.Audio; using AcDream.Core.Audio;
using AcDream.Core.CharGen; using AcDream.Core.CharGen;
using AcDream.Core.Lighting; using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.Spells; using AcDream.Core.Spells;
@ -20,6 +23,7 @@ using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics; using AcDream.Runtime.Physics;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
using DatReaderWriter.Lib.IO;
using Silk.NET.Input; using Silk.NET.Input;
using Silk.NET.OpenAL; using Silk.NET.OpenAL;
@ -163,84 +167,73 @@ public sealed class ContentEffectsAudioCompositionTests
[Fact] [Fact]
public void GameWindowUsesTheProductionPhaseAndNoLongerConstructsItsBodyInline() public void GameWindowUsesTheProductionPhaseAndNoLongerConstructsItsBodyInline()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> calls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new ContentEffectsAudioCompositionPhase(", source, Assert.Single(
StringComparison.Ordinal); calls,
call => call.Target.DeclaringType
== typeof(ContentEffectsAudioCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain( Assert.DoesNotContain(
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);", calls,
source, call => call.Target.DeclaringType
StringComparison.Ordinal); == typeof(RuntimeDatCollectionFactory)
Assert.DoesNotContain("_hookRouter.Register(_particleSink)", source, && call.Target.Name == nameof(RuntimeDatCollectionFactory.OpenReadOnly));
StringComparison.Ordinal); Assert.DoesNotContain(
Assert.DoesNotContain("new AcDream.App.Audio.OpenAlAudioEngine()", source, calls,
StringComparison.Ordinal); 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] [Fact]
public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource() public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource()
{ {
string root = FindRepoRoot(); MethodInfo openPrepared = typeof(RetailContentEffectsAudioCompositionFactory)
string contentPhase = File.ReadAllText(Path.Combine( .GetMethod(nameof(
root, RetailContentEffectsAudioCompositionFactory.OpenPreparedAssetSource))!;
"src", Assert.Contains(
"AcDream.App", CompiledCallGraph.Read(openPrepared),
"Composition", call => call.Target.DeclaringType == typeof(PakPreparedAssetSource)
"ContentEffectsAudioComposition.cs")); && call.Target.IsConstructor);
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"));
Assert.Contains("new PakPreparedAssetSource(path, dats, diagnostic)", MethodInfo compose = typeof(WorldRenderCompositionPhase)
contentPhase, StringComparison.Ordinal); .GetMethod(nameof(WorldRenderCompositionPhase.Compose))!;
Assert.Contains("content.PreparedAssets", worldPhase, IReadOnlyList<CompiledCall> worldCalls = CompiledCallGraph.Read(compose);
StringComparison.Ordinal); Assert.Contains(
Assert.Contains("_preparedAssets.Read(request.Asset, ct)", manager, worldCalls,
StringComparison.Ordinal); call => call.Target.DeclaringType == typeof(ContentEffectsAudioResult)
Assert.DoesNotContain("MeshExtractor", manager, && call.Target.Name == "get_PreparedAssets");
StringComparison.Ordinal);
Assert.DoesNotContain("IDatReaderWriter", manager,
StringComparison.Ordinal);
Assert.DoesNotContain("GfxObjMesh.Build", adapter,
StringComparison.Ordinal);
int meshStage = lifetime.IndexOf( FieldInfo preparedAssets = Assert.Single(
"new ResourceShutdownStage(\"mesh adapter\"", typeof(ObjectMeshManager).GetFields(
StringComparison.Ordinal); BindingFlags.Instance | BindingFlags.NonPublic),
int preparedRelease = lifetime.IndexOf( field => field.FieldType == typeof(IPreparedAssetSource));
"Hard(\"prepared asset source\"", Assert.Equal("_preparedAssets", preparedAssets.Name);
StringComparison.Ordinal); Assert.DoesNotContain(
int datRelease = lifetime.IndexOf( typeof(ObjectMeshManager).GetConstructors(),
"Hard(\"DAT collection\"", constructor => constructor.GetParameters().Any(parameter =>
StringComparison.Ordinal); 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(meshStage >= 0);
Assert.True(preparedRelease > meshStage); Assert.True(preparedRelease > meshStage);
Assert.True(datRelease > preparedRelease); Assert.True(datRelease > preparedRelease);
@ -564,15 +557,4 @@ public sealed class ContentEffectsAudioCompositionTests
public void CloseDevice(nint device) { } 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 System.Reflection;
using AcDream.App.Composition; using AcDream.App.Composition;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering; 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; namespace AcDream.App.Tests.Composition;
@ -28,48 +34,77 @@ public sealed class FrameRootCompositionTests
[Fact] [Fact]
public void ProductionPhasePublishesOnlyAfterBothRootsExist() public void ProductionPhasePublishesOnlyAfterBothRootsExist()
{ {
string source = File.ReadAllText(Path.Combine( MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
FindRepoRoot(), "ComposeCore",
"src", BindingFlags.Instance | BindingFlags.NonPublic)
"AcDream.App", ?? throw new MissingMethodException(
"Composition", typeof(FrameRootCompositionPhase).FullName,
"FrameRootComposition.cs")); "ComposeCore");
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
AssertAppearsInOrder( int resources = CallIndex(calls, typeof(RenderFrameResourceController), ".ctor");
source, int comparison = CallIndex(
"new RenderFrameResourceController(", calls,
"new RenderSceneShadowComparisonController(", typeof(RenderSceneShadowComparisonController),
"new WorldSceneRenderer(", ".ctor",
"new WorldLifecycleAutomationController(", resources + 1);
"\"world lifecycle automation owner\"", int scene = CallIndex(calls, typeof(WorldSceneRenderer), ".ctor", comparison + 1);
"\"world lifecycle automation binding\"", int automation = CallIndex(
"new SerialRenderFramePostDiagnosticsPhase(", calls,
"new RenderFrameOrchestrator(", typeof(WorldLifecycleAutomationController),
"postDiagnostics,", ".ctor",
"new RetailLiveFrameCoordinator(", scene + 1);
"new UpdateFrameOrchestrator(", int diagnostics = CallIndex(
"d.FrameGraphs.PublishOwned(", calls,
"_publication.PublishFrameRoots(result);", typeof(SerialRenderFramePostDiagnosticsPhase),
"graphLease.Transfer();", ".ctor",
"bindingsLease.Transfer();"); 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] [Fact]
public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs() public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> calls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new FrameRootCompositionPhase(", source); Assert.Single(
Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source); calls,
Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source); call => call.Target.DeclaringType == typeof(FrameRootCompositionPhase)
Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source); && call.Target.IsConstructor);
Assert.DoesNotContain("_worldLifecycleAutomation", source); Assert.DoesNotContain(
Assert.DoesNotContain("_frameGraphs.Publish(", source); 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] [Fact]
@ -80,19 +115,37 @@ public sealed class FrameRootCompositionTests
BindingFlags.Instance | BindingFlags.NonPublic), BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow)); field => field.FieldType == typeof(GameWindow));
string snapshots = File.ReadAllText(Path.Combine( Assert.DoesNotContain(
FindRepoRoot(), typeof(WorldLifecycleResourceSnapshotSource).GetFields(
"src", BindingFlags.Instance | BindingFlags.NonPublic),
"AcDream.App", field => field.FieldType == typeof(GameWindow));
"Diagnostics",
"WorldLifecycleResourceSnapshotSource.cs")); MethodInfo capture = typeof(WorldLifecycleResourceSnapshotSource)
Assert.DoesNotContain("GameWindow", snapshots, StringComparison.Ordinal); .GetMethod(nameof(WorldLifecycleResourceSnapshotSource.Capture))!;
Assert.Contains("_liveEntities.PendingTeardownCount", snapshots); Assert.Equal(
Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots); typeof(RenderFrameOutcome),
Assert.Contains("_frameProfiler.LastReport", snapshots); Assert.Single(capture.GetParameters()).ParameterType);
Assert.Contains("Capture(RenderFrameOutcome outcome)", snapshots); IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(capture);
Assert.Contains("outcome.World.VisibleLandblocks", snapshots); Assert.Contains(
Assert.Contains("outcome.World.TotalLandblocks", snapshots); 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( 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; int index = CompiledCallGraph.IndexOf(
foreach (string value in values) calls,
{ declaringType,
int found = source.IndexOf(value, cursor, StringComparison.Ordinal); methodName,
Assert.True(found >= 0, $"Missing expected source fragment: {value}"); startIndex);
cursor = found + value.Length; Assert.True(
} index >= startIndex,
} $"Missing compiled edge {declaringType.FullName}.{methodName}.");
return index;
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

@ -4,6 +4,7 @@ using AcDream.App.Composition;
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Architecture;
using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu;
using AcDream.UI.Abstractions.Input; using AcDream.UI.Abstractions.Input;
using Silk.NET.Input; using Silk.NET.Input;
@ -67,21 +68,35 @@ public sealed class HostInputCameraCompositionTests
[Fact] [Fact]
public void GameWindowUsesTheExactPlatformPreludeAndPhaseOneType() public void GameWindowUsesTheExactPlatformPreludeAndPhaseOneType()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> declaredCalls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("GameWindowPlatformAcquisition.Acquire(", source, Assert.Single(
StringComparison.Ordinal); declaredCalls,
Assert.Contains("new HostInputCameraCompositionPhase(", source, call => call.Target.DeclaringType
StringComparison.Ordinal); == typeof(GameWindowPlatformAcquisition)
Assert.DoesNotContain("_gl = GL.GetApi(_window!)", source, && call.Target.Name == nameof(GameWindowPlatformAcquisition.Acquire));
StringComparison.Ordinal); Assert.Single(
Assert.DoesNotContain("_input = _window!.CreateInput()", source, declaredCalls,
StringComparison.Ordinal); 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 private sealed class Fixture : IDisposable
@ -450,16 +465,4 @@ public sealed class HostInputCameraCompositionTests
public void WriteLine(string message) { } 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 System.Runtime.CompilerServices;
using AcDream.App.Combat; using AcDream.App.Combat;
using AcDream.App.Composition; using AcDream.App.Composition;
@ -8,6 +9,7 @@ using AcDream.Content;
using AcDream.App.UI; using AcDream.App.UI;
using AcDream.App.UI.Layout; using AcDream.App.UI.Layout;
using AcDream.App.World; using AcDream.App.World;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Core.Spells; using AcDream.Core.Spells;
@ -190,19 +192,33 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact] [Fact]
public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody() public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> calls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Contains("new InteractionRetainedUiCompositionPhase(", source); Assert.Single(
Assert.DoesNotContain("new AcDream.App.UI.ItemInteractionController(", source); calls,
Assert.DoesNotContain("_retailUiLease.AcquireHost(", source); call => call.Target.DeclaringType
Assert.DoesNotContain("RetailUiRuntime.CreateUninitialized(", source); == typeof(InteractionRetainedUiCompositionPhase)
Assert.DoesNotContain("private void UseItemByGuid(", source); && call.Target.IsConstructor);
Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source); 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 private sealed class Fixture : IDisposable
@ -422,15 +438,4 @@ public sealed class InteractionRetainedUiCompositionTests
private static T Stub<T>() where T : class => private static T Stub<T>() where T : class =>
(T)RuntimeHelpers.GetUninitializedObject(typeof(T)); (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.Composition;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Physics.Motion; using AcDream.Core.Physics.Motion;
@ -125,71 +129,29 @@ public sealed class LivePresentationCompositionTests
[Fact] [Fact]
public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody() public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody()
{ {
string root = FindRepoRoot(); IReadOnlyList<CompiledCall> windowCalls =
string window = File.ReadAllText(Path.Combine( CompiledCallGraph.ReadDeclared(typeof(GameWindow));
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
Assert.Contains("new LivePresentationCompositionPhase(", window); Assert.Single(
Assert.DoesNotContain("new AcDream.App.World.LiveEntityRuntime(", window); windowCalls,
Assert.DoesNotContain("new AcDream.App.Rendering.Wb.WbDrawDispatcher(", window); call => call.Target.DeclaringType
Assert.DoesNotContain("new AcDream.App.Streaming.LandblockRenderPublisher(", window); == typeof(LivePresentationCompositionPhase)
Assert.DoesNotContain("_portalTunnelFallback.AcquirePrepared(", window); && call.Target.IsConstructor);
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.DoesNotContain( Assert.DoesNotContain(
"if (gl is not null && dispatcherLease.Resource is { } portalDispatcher)", windowCalls,
phase); call => call.Target.IsConstructor
Assert.DoesNotContain("NullLocalPlayerTeleportPresentation", session); && call.Target.DeclaringType is { } type
&& (type == typeof(LiveEntityRuntime)
|| type == typeof(WbDrawDispatcher)
|| type == typeof(LandblockRenderPublisher)));
Assert.DoesNotContain( Assert.DoesNotContain(
"NullLocalPlayerTeleportPresentation", windowCalls,
File.ReadAllText(Path.Combine( call => call.Target.Name == "AcquirePrepared");
root,
"src", Assert.DoesNotContain(
"AcDream.App", typeof(LivePresentationCompositionPhase).GetFields(
"Rendering", BindingFlags.Instance | BindingFlags.NonPublic),
"Gpu", field => field.FieldType == typeof(GameWindow));
"Vk",
"VulkanCompositionFramePhases.cs")));
} }
private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create( 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.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.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; using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Composition; namespace AcDream.App.Tests.Composition;
@ -66,115 +78,124 @@ public sealed class SessionPlayerCompositionTests
[Fact] [Fact]
public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody() public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody()
{ {
string root = FindRepoRoot(); IReadOnlyList<CompiledCall> windowCalls =
string window = File.ReadAllText(Path.Combine( CompiledCallGraph.ReadDeclared(typeof(GameWindow));
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
string phase = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
Assert.Contains("new SessionPlayerCompositionPhase(", window); Assert.Single(
Assert.DoesNotContain("LandblockStreamer.CreateForRequests(", window); windowCalls,
Assert.DoesNotContain("new AcDream.App.Net.LiveSessionController()", window); call => call.Target.DeclaringType
== typeof(SessionPlayerCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain( Assert.DoesNotContain(
"new AcDream.App.Streaming.LocalPlayerTeleportController(", windowCalls,
window); call => call.Target.DeclaringType == typeof(LandblockStreamer)
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window); && call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
Assert.Contains("LandblockStreamer.CreateForRequests(", phase); Assert.DoesNotContain(
Assert.Contains( windowCalls,
"LiveSessionController liveSession = d.Runtime.Session;", call => call.Target.IsConstructor
phase); && call.Target.DeclaringType is { } type
Assert.DoesNotContain("new LiveSessionController()", phase); && (type == typeof(LiveSessionController)
Assert.Contains("new LocalPlayerTeleportController(", phase); || type == typeof(LocalPlayerTeleportController)
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase); || type == typeof(DatSpawnClaimHydrationClassifier)));
IReadOnlyList<FieldInfo> phaseFields =
typeof(SessionPlayerCompositionPhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(
phaseFields,
field => field.FieldType == typeof(GameWindow));
} }
[Fact] [Fact]
public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast() public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast()
{ {
string phase = File.ReadAllText(Path.Combine( MethodInfo composeCore = RequiredMethod(
FindRepoRoot(), typeof(SessionPlayerCompositionPhase),
"src", "ComposeCore");
"AcDream.App", IReadOnlyList<CompiledCall> composeCalls =
"Composition", CompiledCallGraph.Read(composeCore);
"SessionPlayerComposition.cs")); AssertCallOrder(
composeCalls,
(typeof(LandblockStreamer), nameof(LandblockStreamer.Start)),
(typeof(StreamingController), ".ctor"),
(typeof(WorldRevealCoordinator), ".ctor"),
(typeof(SessionPlayerCompositionPhase), "CompleteSessionPlayer"));
AssertAppearsInOrder( IEnumerable<MethodBase> streamerFactories =
phase, CompiledCallGraph.ReadMethodReferences(composeCore)
"LandblockStreamer.CreateForRequests(", .Select(call => call.Target)
"streamerLease.Resource.Start();", .Where(method => method.GetMethodBody() is not null);
"new StreamingController(", MethodBase streamerFactory = Assert.Single(
"new WorldRevealCoordinator(", streamerFactories,
"LiveSessionController liveSession = d.Runtime.Session;", method => CompiledCallGraph.Read(method).Any(call =>
"new LiveEntityHydrationController(", call.Target.DeclaringType == typeof(LandblockStreamer)
"new LiveEntityNetworkUpdateController(", && call.Target.Name
"new GameplayInputFrameController(", == nameof(LandblockStreamer.CreateForRequests)));
"new PlayerModeController(", Assert.Contains(
"d.PortalTunnelFallback.Transfer(", CompiledCallGraph.Read(streamerFactory),
"d.TeleportSink.BindOwned(localTeleport)", call => call.Target.DeclaringType == typeof(LandblockStreamer)
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(", && call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine( MethodInfo complete = RequiredMethod(
FindRepoRoot(), typeof(SessionPlayerCompositionPhase),
"src", "CompleteSessionPlayer");
"AcDream.App", IReadOnlyList<CompiledCall> completeCalls =
"Net", CompiledCallGraph.Read(complete);
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal); 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] [Fact]
public void GraphicalCompositionPausesOnlyWhenCharacterSelectorIsAbsent() public void GraphicalCompositionPausesOnlyWhenCharacterSelectorIsAbsent()
{ {
string root = FindRepoRoot(); MethodInfo complete = RequiredMethod(
string phase = File.ReadAllText(Path.Combine( typeof(SessionPlayerCompositionPhase),
root, "CompleteSessionPlayer");
"src", IReadOnlyList<CompiledCall> sessionCalls =
"AcDream.App", CompiledCallGraph.Read(complete);
"Composition",
"SessionPlayerComposition.cs"));
string retainedUi = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"InteractionRetainedUiComposition.cs"));
Assert.Contains( Assert.Contains(
"AwaitCharacterSelection:", sessionCalls,
phase, call => call.Target.DeclaringType == typeof(RuntimeOptions)
StringComparison.Ordinal); && call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains( Assert.Contains(
"d.Options.LiveCharacterSelector is null", sessionCalls,
phase, call => call.Target.DeclaringType == typeof(LiveSessionConnectOptions)
StringComparison.Ordinal); && call.Target.IsConstructor);
Assert.DoesNotContain( Assert.DoesNotContain(
"CharacterList.TrySelectFirstAvailable", sessionCalls,
phase, call => call.Target.DeclaringType == typeof(CharacterList)
StringComparison.Ordinal); && call.Target.Name == nameof(CharacterList.TrySelectFirstAvailable));
MethodInfo retainedUi = typeof(RetailInteractionRetainedUiCompositionFactory)
.GetMethod(nameof(
RetailInteractionRetainedUiCompositionFactory.CreateRetainedUi))!;
IReadOnlyList<CompiledCall> uiCalls = CompiledCallGraph.Read(retainedUi);
Assert.Contains( Assert.Contains(
"CharacterSelection: d.Options.LiveCharacterSelector is null", uiCalls,
retainedUi, call => call.Target.DeclaringType == typeof(RuntimeOptions)
StringComparison.Ordinal); && call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains( Assert.Contains(
"() => late.GameRuntime.CharacterSelection", uiCalls,
retainedUi, call => call.Target.DeclaringType
StringComparison.Ordinal); == typeof(CharacterSelectionRuntimeBindings)
Assert.Contains( && call.Target.IsConstructor);
"late.GameRuntime.CharacterSelectionEnter",
retainedUi,
StringComparison.Ordinal);
} }
private sealed class RetryBinding( 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; int cursor = -1;
foreach (string value in values) foreach ((Type declaringType, string methodName) in expected)
{ {
int found = source.IndexOf(value, cursor, StringComparison.Ordinal); int found = calls
Assert.True(found >= 0, $"Missing expected source fragment: {value}"); .Select((call, index) => (call, index))
cursor = found + value.Length; .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() private static bool MatchesType(Type? actual, Type expected) =>
{ actual == expected
DirectoryInfo? directory = new(AppContext.BaseDirectory); || expected.IsGenericTypeDefinition
while (directory is not null) && actual?.IsGenericType == true
{ && actual.GetGenericTypeDefinition() == expected;
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

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