From 9bd5d47c47e8b2b5c30927e1c7bd7418c5be73b4 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 18 Aug 2026 16:30:25 +0200 Subject: [PATCH] test: replace graphical host source freezes --- .../2026-08-18-r3-test-truth-ledger.md | 50 + .../Rendering/GameWindowHostBoundaryTests.cs | 1269 ++++++++--------- 2 files changed, 678 insertions(+), 641 deletions(-) diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md index ec0a708f..3534c5b4 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -1341,3 +1341,53 @@ Verification: attributed methods, and reduces direct/total source readers from 41/64 to 39/55. The remaining 55 reconcile to the 22 approved retained policies/contracts and 33 staged replacements. + +## Batch AC graphical-host and lifetime source-freeze replacement + +Batch AC converts all nine approved `GameWindowHostBoundaryTests` source +readers. No product source changes and no test is removed. + +Startup now follows the compiled settings snapshot, frame-pacing, native +window, callback-target, callback-binding, attachment, and frame-loop edges. +The callback delegate targets retain their exact load/update/render/closing/ +focus/resize order without treating local names, comments, or nested argument +spelling as behavior. `OnLoad` phase ordering is recovered from its compiled +delegate targets, while frame-root and session-player completion checks follow +their typed construction, binding, attachment, publication, and transfer +edges. + +The remaining owner checks reflect the one `WorldEnvironmentController` and +`RuntimeSettingsController`, reject their displaced window mirrors, and +follow environment routing, settings startup/runtime binding, terrain-atlas +acquisition, input-action attachment, and framebuffer-resize publication +through the built methods. Focus, update, render, close, and dispose are +verified as narrow handoffs. In particular, the render check uses semantic IL +execution order—window size, immutable input construction, then graph render— +rather than the misleading lexical order of a nested source expression. + +Shutdown now uses reflected root types, field stores, built call order, and +the compiled manifest's stage/operation labels. It preserves the twenty-stage +dependency order, plugin transfer before startup, ingress cleanup ordering, +Runtime/UI/resource release boundaries, `TryComplete` before native-window +release, and the final `_window` clear after the shared shutdown funnel. A +label such as `input context` may legitimately identify both an operation and +its stage; the replacement therefore verifies dependency order instead of +mistaking duplicate human-readable labels for duplicate ownership. + +The old checks for HostInputCamera local `if` spelling are retired rather than +translated. `HostInputCameraCompositionTests` already exercise the complete +production acquisition and every fault boundary; the names and arrangement +of compiler locals are not part of the host contract. All valuable ownership, +ordering, and cleanup rationale remains in the compiled guards above. + +Verification: + +- all nine focused graphical-host/lifetime methods pass; +- the complete locked Release build covers all 44 projects with zero warnings + and zero errors; +- the no-retry complete hermetic Release gate remains 14,346/14,346 with zero + skips or failures across all 12 test assemblies; and +- the regenerated 1,254-file inventory parses every file, remains at 11,414 + attributed methods, and reduces direct/total source readers from 39/55 to + 32/46. The remaining 46 reconcile to the 22 approved retained + policies/contracts and 24 staged replacements. diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowHostBoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowHostBoundaryTests.cs index 7a6243f1..4b085bb0 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowHostBoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowHostBoundaryTests.cs @@ -1,736 +1,723 @@ +using System.Reflection; +using System.Reflection.Emit; +using AcDream.App.Combat; +using AcDream.App.Composition; +using AcDream.App.Diagnostics; +using AcDream.App.Input; +using AcDream.App.Net; +using AcDream.App.Plugins; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Sky; +using AcDream.App.Settings; +using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; +using AcDream.App.UI; +using AcDream.App.Update; +using AcDream.App.World; +using AcDream.Runtime.Session; +using Silk.NET.Windowing; + namespace AcDream.App.Tests.Rendering; /// -/// Source-level architecture guards for the narrow graphical-host composition -/// boundary. R3 tracks these literal source-shape checks for staged replacement -/// with semantic owner and lifecycle contracts. +/// Compiled architecture guards for the narrow graphical-host composition +/// boundary. These checks protect ownership, publication, and lifecycle edges +/// without freezing comments, local-variable names, or source formatting. /// public sealed class GameWindowHostBoundaryTests { + private const BindingFlags Declared = BindingFlags.Instance + | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + [Fact] public void Run_PreservesNativeAttributeAndCallbackOrder() { - string body = MethodBody("public void Run()", "private void OnLoad()"); + MethodInfo run = RequiredMethod(typeof(GameWindow), nameof(GameWindow.Run)); + AssertCallOrder( + run, + (typeof(RuntimeSettingsController), "get_Startup"), + (typeof(DisplayFramePacingController), + nameof(DisplayFramePacingController.InitializeStartup)), + (typeof(Silk.NET.Windowing.Window), nameof(Silk.NET.Windowing.Window.Create)), + (typeof(DisplayFramePacingController), + nameof(DisplayFramePacingController.BindSurface)), + (typeof(WindowCallbackTargets), ".ctor"), + (typeof(SilkWindowCallbackBinding), nameof(SilkWindowCallbackBinding.Create)), + (typeof(SilkWindowCallbackBinding), nameof(SilkWindowCallbackBinding.Attach))); + AssertSilkWindowCallAfter( + run, + nameof(IWindow.Run), + typeof(SilkWindowCallbackBinding), + nameof(SilkWindowCallbackBinding.Attach)); - AssertAppearsInOrder( - body, - "RuntimeSettingsSnapshot startup = _runtimeSettings.Startup", - "_displayFramePacing.InitializeStartup(startup.Display.VSync)", - "VSync = startupPacing.UseVSync", - // Vulkan takes MSAA as an RHI attachment property rather than a - // window attribute. _startupQuality carries it forward instead. - "_startupQuality = startup.Quality;", - "Window.Create(options)", - "_displayFramePacing.BindSurface(", - "_windowCallbacks = SilkWindowCallbackBinding.Create(", - "new WindowCallbackTargets(", - "OnLoad,", - "OnUpdate,", - "OnRender,", - "OnClosing,", - "OnFocusChanged,", - "OnFramebufferResize),", - "_displayFramePacing,", - "_hostQuiescence);", - "_windowCallbacks.Attach();", - "_window.Run();"); - - Assert.Equal(1, CountOccurrences(body, "SilkWindowCallbackBinding.Create(")); - Assert.Equal(1, CountOccurrences(body, "_windowCallbacks.Attach();")); - Assert.DoesNotContain("_window.Load +=", body, StringComparison.Ordinal); - Assert.DoesNotContain("_window.Update +=", body, StringComparison.Ordinal); - Assert.DoesNotContain("_window.Render +=", body, StringComparison.Ordinal); - Assert.DoesNotContain("_window.Closing +=", body, StringComparison.Ordinal); + IReadOnlyList references = + CompiledCallGraph.ReadMethodReferences(run); + AssertTargetOrder( + references, + typeof(GameWindow), + "OnLoad", + "OnUpdate", + "OnRender", + "OnClosing", + "OnFocusChanged", + "OnFramebufferResize"); + Assert.DoesNotContain( + references, + call => call.Target.Name is "add_Load" or "add_Update" or "add_Render" + or "add_Closing"); } [Fact] public void SessionStart_FollowsFrameGraphPublication() { - string body = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); + AssertOnLoadPhaseOrder( + typeof(HostInputCameraCompositionPhase), + typeof(ContentEffectsAudioCompositionPhase), + typeof(SettingsDevToolsCompositionPhase), + typeof(WorldRenderCompositionPhase), + typeof(InteractionRetainedUiCompositionPhase), + typeof(LivePresentationCompositionPhase), + typeof(SessionPlayerCompositionPhase), + typeof(FrameRootCompositionPhase), + typeof(SessionStartCompositionPhase)); - AssertAppearsInOrder( - body, - "GameWindowPlatformResult platform = AcquirePlatform();", - "GameWindowCompositionPipeline.Run<", - "new HostInputCameraCompositionPhase(", - "this).Compose(platformResult),", - "new ContentEffectsAudioCompositionPhase(", - "this).Compose(platformResult, hostInputCamera),", - "new SettingsDevToolsCompositionPhase(", - ".Compose(platformResult, hostInputCamera, contentEffectsAudio),", - "new InteractionRetainedUiCompositionPhase(", - "this).Compose(", - "new LivePresentationCompositionPhase(", - "this).Compose(", - "new SessionPlayerCompositionPhase(", - "new FrameRootCompositionPhase(", - "new SessionStartCompositionPhase(", - ".Start(frameRoots));"); + AssertCallOrder( + RequiredMethod(typeof(FrameRootCompositionPhase), "ComposeCore"), + (typeof(RenderFrameOrchestrator), ".ctor"), + (typeof(UpdateFrameOrchestrator), ".ctor"), + (typeof(GameFrameGraphSlot), nameof(GameFrameGraphSlot.PublishOwned)), + (typeof(IGameWindowFrameRootPublication), + nameof(IGameWindowFrameRootPublication.PublishFrameRoots))); - string framePhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "FrameRootComposition.cs")); - AssertAppearsInOrder( - framePhase, - "new RenderFrameOrchestrator(", - "new UpdateFrameOrchestrator(", - "d.FrameGraphs.PublishOwned(", - "_publication.PublishFrameRoots(result);"); + MethodInfo complete = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + AssertCallOrder( + complete, + (typeof(LiveSessionRuntimeFactory), nameof(LiveSessionRuntimeFactory.Create)), + (typeof(LiveCombatModeCommandSlot), "BindOwned"), + (typeof(RuntimeDiagnosticCommandSlot), "BindOwned"), + (typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Create)), + (typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Attach)), + (typeof(IGameWindowSessionPlayerPublication), + nameof(IGameWindowSessionPlayerPublication.PublishSessionPlayer))); - string sessionPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - AssertAppearsInOrder( - sessionPhase, - "LiveSessionHost sessionHost = sessionRuntimeFactory.Create(", - "d.CombatModeCommands.BindOwned(combatCommand)", - "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)", - "GameplayInputActionRouter.Create(", - "gameplayActions.Attach();", - "_publication.PublishSessionPlayer(result);"); - Assert.Equal(1, CountOccurrences( - body, - ".Start(frameRoots));")); - string phaseOne = Slice( - File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "HostInputCameraComposition.cs")), - "private HostInputCameraResult ComposeCore(", - "private void Fault("); - Assert.Contains("if (firstKeyboard is not null)", phaseOne, StringComparison.Ordinal); - Assert.Contains("if (firstMouse is not null)", phaseOne, StringComparison.Ordinal); - Assert.Contains( - "if (keyboard is not null && mouse is not null)", - phaseOne, - StringComparison.Ordinal); - Assert.DoesNotContain( - "if (firstKeyboard is not null && firstMouse is not null)", - phaseOne, - StringComparison.Ordinal); - string startPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionStartComposition.cs")); - AssertAppearsInOrder( - startPhase, - "frame.GameRuntime.Session.Start(frame.GameRuntime.Generation)", - "switch (result.Status)", - "case RuntimeSessionStartStatus.MissingCredentials:", - "case RuntimeSessionStartStatus.Failed:"); - int start = body.IndexOf( - ".Start(frameRoots));", - StringComparison.Ordinal); - string postStart = body[(start + ".Start(frameRoots));".Length)..]; - Assert.Empty(System.Text.RegularExpressions.Regex.Matches( - postStart, - @"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)")); - Assert.DoesNotContain("new ", postStart, StringComparison.Ordinal); - Assert.DoesNotContain(".Mount(", postStart, StringComparison.Ordinal); - Assert.DoesNotContain(".Bind(", postStart, StringComparison.Ordinal); - Assert.DoesNotContain(".Attach(", postStart, StringComparison.Ordinal); - Assert.DoesNotContain(".Compose(", postStart, StringComparison.Ordinal); - Assert.DoesNotContain("PrepareResources(", postStart, StringComparison.Ordinal); + MethodInfo start = RequiredMethod(typeof(SessionStartCompositionPhase), "Start"); + IReadOnlyList startCalls = CompiledCallGraph.Read(start); + Assert.Contains(startCalls, call => call.Target.Name == "Start"); + Assert.Contains(startCalls, call => call.Target.Name == "Report"); } [Fact] public void WorldEnvironment_IsOwnedAndGameWindowOnlyComposesItsTypedEdges() { - string source = GameWindowSource(); - string load = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); - string sessionFactorySource = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Net", - "LiveSessionRuntimeFactory.cs")); - string sessionFactory = Slice( - sessionFactorySource, - "private ILiveSessionEventRouting CreateEventRouter(", - "private LiveInventorySessionBindings CreateInventoryBindings()"); - string worldPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "WorldRenderComposition.cs")); + AssertField(typeof(GameWindow), "_worldEnvironment", typeof(WorldEnvironmentController)); + MethodBase worldDelegate = OnLoadMethodConstructing( + typeof(WorldRenderCompositionPhase)); Assert.Contains( - "private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;", - source, - StringComparison.Ordinal); - Assert.Contains( - "new WorldRenderDependencies(\n _worldEnvironment,", - load.Replace("\r\n", "\n", StringComparison.Ordinal), - StringComparison.Ordinal); - string worldCompose = Slice( - worldPhase, - "public WorldRenderResult Compose(", - "private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources("); - AssertAppearsInOrder( - worldCompose, - "WorldRegionData region = _factory.LoadRegion(content.Dats);", - "_factory.InitializeEnvironment(_dependencies.Environment, region.Region);"); - Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal); - AssertAppearsInOrder( - sessionFactory, - "new LiveEnvironmentSessionSink(", - "_world.Environment.ApplyAdminEnvirons,", - "_world.Environment.SynchronizeFromServer)"); - Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal); - Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void OnEnvironChanged(", source, StringComparison.Ordinal); + CompiledCallGraph.ReadFieldReferences(worldDelegate), + reference => reference.Field.Name == "_worldEnvironment"); - Assert.DoesNotContain("private void CycleTimeOfDay()", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void CycleWeather()", source, StringComparison.Ordinal); - string sessionPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - AssertAppearsInOrder( - sessionPhase, - "new RuntimeDiagnosticCommandController(", - "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)"); + MethodInfo compose = RequiredMethod(typeof(WorldRenderCompositionPhase), "Compose"); + AssertCallOrder( + compose, + (typeof(IWorldRenderCompositionFactory), + nameof(IWorldRenderCompositionFactory.LoadRegion)), + (typeof(IWorldRenderCompositionFactory), + nameof(IWorldRenderCompositionFactory.InitializeEnvironment))); + Assert.Contains( + CompiledCallGraph.Read(RequiredMethod( + typeof(RetailWorldRenderCompositionFactory), + nameof(RetailWorldRenderCompositionFactory.InitializeEnvironment))), + call => call.Target.DeclaringType == typeof(WorldEnvironmentController) + && call.Target.Name == nameof(WorldEnvironmentController.Initialize)); + + MethodInfo router = RequiredMethod(typeof(LiveSessionRuntimeFactory), "CreateEventRouter"); + IReadOnlyList routerReferences = + CompiledCallGraph.ReadMethodReferences(router); + Assert.Contains( + routerReferences, + call => call.Target.DeclaringType == typeof(LiveEnvironmentSessionSink) + && call.Target.IsConstructor); + AssertTargetOrder( + routerReferences, + typeof(WorldEnvironmentController), + nameof(WorldEnvironmentController.ApplyAdminEnvirons), + nameof(WorldEnvironmentController.SynchronizeFromServer)); + + AssertMembersAbsent( + typeof(GameWindow), + ["_loadedSkyDesc", "_loadedSkyDayIndex"], + ["RefreshSkyForCurrentDay", "OnEnvironChanged", "CycleTimeOfDay", "CycleWeather"]); + + MethodInfo complete = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + AssertCallOrder( + complete, + (typeof(RuntimeDiagnosticCommandController), ".ctor"), + (typeof(RuntimeDiagnosticCommandSlot), "BindOwned")); } [Fact] public void InputAction_IsOneTypedOwnerHandoff() { - string source = GameWindowSource(); - string load = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); - string sessionPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - string shutdown = GameWindowLifetimeSource(); + AssertMembersAbsent( + typeof(GameWindow), + [], + ["OnInputAction", "SetInputCombatScope", "ToggleLiveCombatMode", + "OnUiDragReleasedOutside"]); + Assert.DoesNotContain( + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.Name == "add_DragReleasedOutsideUi"); - Assert.DoesNotContain("private void OnInputAction(", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void SetInputCombatScope(", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void ToggleLiveCombatMode()", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal); - Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal); - Assert.Equal(1, CountOccurrences( - sessionPhase, - "GameplayInputActionRouter.Create(")); - Assert.Equal(1, CountOccurrences( - sessionPhase, - "gameplayActions.Attach();")); - Assert.Equal(1, CountOccurrences( - LivePresentationSource(), - "RetainedUiGameplayBinding.Create(")); - Assert.Equal(1, CountOccurrences( - LivePresentationSource(), - "retainedGameplayLease.Resource.Attach();")); - AssertAppearsInOrder( - shutdown, - "Hard(\"combat command slot\", ingress.CombatCommands.Deactivate)", - "Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)", - "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", - "Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())", - "Hard(\"game runtime session\", ingress.Runtime.StopSession)", - "new ResourceShutdownStage(\"physical ingress cleanup\"", - "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", - "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", - "Soft(\"camera pointer\", () => DisposeCameraPointer(ingress.CameraPointer))", - "new ResourceShutdownStage(\"session dependents\"", - "Hard(\"mouse capture\", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement())"); + MethodInfo session = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + AssertSingleCall(session, typeof(GameplayInputActionRouter), + nameof(GameplayInputActionRouter.Create)); + AssertSingleCall(session, typeof(GameplayInputActionRouter), + nameof(GameplayInputActionRouter.Attach)); + + MethodInfo live = RequiredMethod( + typeof(LivePresentationCompositionPhase), + "CompletePresentation"); + MethodBase retainedFactory = ReferencedMethodConstructingOrCalling( + live, + typeof(RetainedUiGameplayBinding), + nameof(RetainedUiGameplayBinding.Create)); + AssertSingleCall(retainedFactory, typeof(RetainedUiGameplayBinding), + nameof(RetainedUiGameplayBinding.Create)); + Assert.Contains( + CompiledCallGraph.Read(live), + call => call.Target.DeclaringType == typeof(RetainedUiGameplayBinding) + && call.Target.Name == nameof(RetainedUiGameplayBinding.Attach)); + + IReadOnlyList labels = ShutdownLabels(); + AssertLabelOrder( + labels, + "combat command slot", + "diagnostic command slot", + "retained gameplay", + "gameplay actions", + "game runtime session", + "physical ingress cleanup", + "retained gameplay", + "gameplay actions", + "camera pointer", + "session dependents", + "mouse capture"); } [Fact] public void FramebufferResize_IsOneTypedOwnerHandoff() { - string load = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); - string body = MethodBody( - "private void OnFramebufferResize(", - "private void OnClosing()"); - - Assert.Contains("=> _framebufferResize.Resize(newSize);", body, StringComparison.Ordinal); - Assert.DoesNotContain("Viewport(", body, StringComparison.Ordinal); - Assert.DoesNotContain("SetAspect(", body, StringComparison.Ordinal); - Assert.DoesNotContain("ResetLayout(", body, StringComparison.Ordinal); - string phaseOne = Slice( - File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "HostInputCameraComposition.cs")), - "private HostInputCameraResult ComposeCore(", - "private void Fault("); - AssertAppearsInOrder( - phaseOne, - "_dependencies.FramebufferResize.BindViewport(", - "_publication.PublishCameraController(camera)", - "_dependencies.FramebufferResize.BindCamera(", - "_dependencies.FramebufferResize.Resize("); + MethodInfo resize = RequiredMethod(typeof(GameWindow), "OnFramebufferResize"); + AssertSingleCall( + resize, + typeof(FramebufferResizeController), + nameof(FramebufferResizeController.Resize)); Assert.DoesNotContain( - "_viewportAspect.Update(_window", - load, - StringComparison.Ordinal); + CompiledCallGraph.Read(resize), + call => call.Target.Name is "Viewport" or "SetAspect" or "ResetLayout"); + + AssertCallOrder( + RequiredMethod(typeof(HostInputCameraCompositionPhase), "ComposeCore"), + (typeof(FramebufferResizeController), + nameof(FramebufferResizeController.BindViewport)), + (typeof(IGameWindowHostInputCameraPublication), + nameof(IGameWindowHostInputCameraPublication.PublishCameraController)), + (typeof(FramebufferResizeController), + nameof(FramebufferResizeController.BindCamera)), + (typeof(FramebufferResizeController), nameof(FramebufferResizeController.Resize))); + + Assert.DoesNotContain( + CompiledCallGraph.ReadMethodReferences(RequiredMethod(typeof(GameWindow), "OnLoad")), + call => call.Target.Name == "Update" + && call.Target.DeclaringType?.Name == "ViewportAspectState"); } [Fact] public void RuntimeSettings_IsOneTwoPhaseOwnerWithoutWindowStateMirrors() { - string source = GameWindowSource(); - string run = MethodBody("public void Run()", "private void OnLoad()"); - string load = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); - string shutdown = GameWindowLifetimeSource(); + AssertField(typeof(GameWindow), "_runtimeSettings", typeof(RuntimeSettingsController)); + AssertSingleCall( + FindConstructorConstructing(typeof(GameWindow), typeof(RuntimeSettingsController)), + typeof(RuntimeSettingsController), + ".ctor"); + Assert.DoesNotContain( + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.DeclaringType?.Name == "SettingsStore" + || call.Target.Name is "LoadDisplay" or "LoadAudio" or "LoadGameplay" + or "LoadChat" or "LoadCharacter"); + AssertMembersAbsent( + typeof(GameWindow), + ["_persistedDisplay", "_persistedAudio", "_persistedGameplay", + "_persistedChat", "_persistedCharacter", "_activeToonKey", + "_settingsStore", "_settingsVm"], + ["LoadAndApplyPersistedSettings", "ApplyDisplayWindowState", + "ReapplyQualityPreset"]); + MethodInfo session = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "ComposeCore"); Assert.Contains( - "private readonly RuntimeSettingsController _runtimeSettings;", - source, - StringComparison.Ordinal); + CompiledCallGraph.Read(session), + call => call.Target.DeclaringType == typeof(RuntimeSettingsController) + && call.Target.Name == nameof(RuntimeSettingsController.BindRuntimeTargetsOwned)); Assert.Contains( - "_runtimeSettings = new RuntimeSettingsController(", - source, - StringComparison.Ordinal); - Assert.DoesNotContain("new SettingsStore(", source, StringComparison.Ordinal); - Assert.DoesNotContain("new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(", source, StringComparison.Ordinal); - Assert.DoesNotContain(".LoadDisplay()", source, StringComparison.Ordinal); - Assert.DoesNotContain(".LoadAudio()", source, StringComparison.Ordinal); - Assert.DoesNotContain(".LoadGameplay()", source, StringComparison.Ordinal); - Assert.DoesNotContain(".LoadChat()", source, StringComparison.Ordinal); - Assert.DoesNotContain(".LoadCharacter(", source, StringComparison.Ordinal); - Assert.DoesNotContain("_persistedDisplay", source, StringComparison.Ordinal); - Assert.DoesNotContain("_persistedAudio", source, StringComparison.Ordinal); - Assert.DoesNotContain("_persistedGameplay", source, StringComparison.Ordinal); - Assert.DoesNotContain("_persistedChat", source, StringComparison.Ordinal); - Assert.DoesNotContain("_persistedCharacter", source, StringComparison.Ordinal); - Assert.DoesNotContain("_activeToonKey", source, StringComparison.Ordinal); - Assert.DoesNotContain("_settingsStore", source, StringComparison.Ordinal); - Assert.DoesNotContain("_settingsVm", source, StringComparison.Ordinal); - Assert.DoesNotContain("LoadAndApplyPersistedSettings", source, StringComparison.Ordinal); - Assert.DoesNotContain("private void ApplyDisplayWindowState", source, StringComparison.Ordinal); - Assert.DoesNotContain("public void ReapplyQualityPreset", source, StringComparison.Ordinal); + CompiledCallGraph.Read(RequiredMethod( + typeof(SettingsDevToolsCompositionPhase), + "Compose")), + call => call.Target.DeclaringType == typeof(RuntimeSettingsController) + && call.Target.Name == nameof(RuntimeSettingsController.ApplyStartup)); - AssertAppearsInOrder( - run, - "RuntimeSettingsSnapshot startup = _runtimeSettings.Startup", - "_displayFramePacing.InitializeStartup(startup.Display.VSync)", - // Campaign V slice V11: Vulkan needs a client-API-less window and - // takes neither MSAA nor the stencil bit count as a window - // attribute (both are RHI attachment properties instead), so the - // raw-GL "Samples = ..." window option this used to assert is - // gone. _startupQuality carries MsaaSamples forward instead, into - // CreateGraphics' VulkanGraphicsContext.Acquire call. - "_startupQuality = startup.Quality;", - "Window.Create(options)"); - AssertAppearsInOrder( - load, - "GameWindowCompositionPipeline.Run<", - "new SettingsDevToolsCompositionPhase(", - ".Compose(platformResult, hostInputCamera, contentEffectsAudio),", - "new WorldRenderCompositionPhase(", - "new SessionPlayerCompositionPhase(", - "new FrameRootCompositionPhase(", - "new SessionStartCompositionPhase(", - ".Start(frameRoots));"); - string sessionPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); + MethodInfo acquireAtlas = RequiredMethod( + typeof(RetailWorldRenderCompositionFactory), + nameof(RetailWorldRenderCompositionFactory.AcquireBackendNeutralTerrainAtlas)); Assert.Contains( - "d.Settings.BindRuntimeTargetsOwned(settingsTargets)", - sessionPhase, - StringComparison.Ordinal); - string settingsPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SettingsDevToolsComposition.cs")); - Assert.Contains( - "_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);", - settingsPhase, - StringComparison.Ordinal); - string worldPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "WorldRenderComposition.cs")); - AssertAppearsInOrder( - worldPhase, - "TerrainAtlas.BuildBackendNeutral(device, dats)", - "settings.ResolvedQuality.AnisotropicLevel", - "_factory.CreateBackendNeutralTerrain("); - AssertAppearsInOrder( - shutdown, - "new ResourceShutdownStage(\"physical ingress cleanup\"", - "new ResourceShutdownStage(\"frame borrowers\"", - "Hard(\"frame-root bindings\", () => frame.FrameBindings?.Dispose())", - "Hard(\"retail UI\", () => DisposeRetailUi(live.RetailUi))", - "Hard(\"streamer\", () => live.Streamer?.Dispose())", - "Hard(\"mesh draw dispatcher\", () => render.DrawDispatcher?.Dispose())", - "Hard(\"terrain\", () => render.Terrain?.Dispose())"); + CompiledCallGraph.Read(acquireAtlas), + call => call.Target.DeclaringType == typeof(IGameRenderResourceLifetime) + && call.Target.Name == nameof(IGameRenderResourceLifetime.AcquireTerrainAtlas)); + MethodBase atlasFactory = ReferencedMethodConstructingOrCalling( + acquireAtlas, + typeof(TerrainAtlas), + nameof(TerrainAtlas.BuildBackendNeutral)); + Assert.NotNull(atlasFactory); + + AssertLabelOrder( + ShutdownLabels(), + "physical ingress cleanup", + "frame borrowers", + "frame-root bindings", + "retail UI", + "streamer", + "mesh draw dispatcher", + "terrain"); } [Fact] public void UpdateRenderFocusAndCloseRemainNarrowHostEdges() { - string source = GameWindowSource(); - string update = Slice( - source, - "private void OnUpdate(double dt)", - "private void OnRender(double deltaSeconds)"); - string render = Slice( - source, - "private void OnRender(double deltaSeconds)", - "private void OnFramebufferResize("); - string focus = Slice( - source, - "private void OnFocusChanged(bool focused)", - "public void Dispose()"); - string close = Slice( - source, - "private void OnClosing()", - "private void OnFocusChanged(bool focused)"); + MethodInfo update = RequiredMethod(typeof(GameWindow), "OnUpdate"); + AssertSingleCall(update, typeof(GameFrameGraphSlot), nameof(GameFrameGraphSlot.Tick)); - Assert.Equal(1, CountOccurrences(update, "_frameGraphs.Tick(")); - Assert.Equal(1, CountOccurrences(render, "_frameGraphs.Render(")); - AssertAppearsInOrder( + MethodInfo render = RequiredMethod(typeof(GameWindow), "OnRender"); + AssertSilkWindowCallBefore(render, "get_Size", typeof(RenderFrameInput), ".ctor"); + AssertCallOrder( render, - "Vector2D size = _window!.Size;", - "_frameGraphs.Render(", - "new AcDream.App.Rendering.RenderFrameInput("); - Assert.DoesNotContain("FramebufferSize", render, StringComparison.Ordinal); - Assert.Contains( - "=> _cameraPointerInput?.HandleFocusChanged(focused);", - focus, - StringComparison.Ordinal); - Assert.Contains( - "private void OnClosing() => CompleteShutdown(releaseNativeWindow: false);", - close, - StringComparison.Ordinal); - AssertAppearsInOrder( - close, - "private void CompleteShutdown(bool releaseNativeWindow)", - "if (!_lifetime.HasShutdownRoots)", - "_lifetime.PublishShutdownRoots(CaptureShutdownRoots());", - "? _lifetime.CompleteAndReleaseNativeWindow()", - ": _lifetime.TryComplete();"); - Assert.DoesNotContain("new ResourceShutdownStage(", close, StringComparison.Ordinal); - Assert.DoesNotContain("CompleteOrThrow", close, StringComparison.Ordinal); + (typeof(RenderFrameInput), ".ctor"), + (typeof(GameFrameGraphSlot), nameof(GameFrameGraphSlot.Render))); + Assert.DoesNotContain( + CompiledCallGraph.Read(render), + call => call.Target.Name == "get_FramebufferSize"); + + AssertOnlyProductionCall( + RequiredMethod(typeof(GameWindow), "OnFocusChanged"), + typeof(CameraPointerInputController), + nameof(CameraPointerInputController.HandleFocusChanged)); + AssertOnlyProductionCall( + RequiredMethod(typeof(GameWindow), "OnClosing"), + typeof(GameWindow), + "CompleteShutdown"); + + MethodInfo shutdown = RequiredMethod(typeof(GameWindow), "CompleteShutdown"); + IReadOnlyList calls = CompiledCallGraph.Read(shutdown); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindowLifetime) + && call.Target.Name == "get_HasShutdownRoots"); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindow) + && call.Target.Name == "CaptureShutdownRoots"); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindowLifetime) + && call.Target.Name == nameof(GameWindowLifetime.PublishShutdownRoots)); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindowLifetime) + && call.Target.Name == nameof(GameWindowLifetime.CompleteAndReleaseNativeWindow)); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindowLifetime) + && call.Target.Name == nameof(GameWindowLifetime.TryComplete)); + Assert.DoesNotContain(calls, call => call.Target.DeclaringType?.Name == "ResourceShutdownStage" + || call.Target.Name == "CompleteOrThrow"); } [Fact] public void Shutdown_PreservesDependencyStagesAndNativeWindowLast() { - string source = GameWindowSource(); - string program = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Program.cs")); - string lifetime = GameWindowLifetimeSource(); - string manifest = Slice( - lifetime, - "internal static class GameWindowShutdownManifest", - "private static ResourceShutdownOperation Hard"); - string nativeRelease = Slice( - lifetime, - "public GameWindowLifetimeReport CompleteAndReleaseNativeWindow()", - "private void EnsureTransaction()"); - int disposeStart = source.IndexOf("public void Dispose()", StringComparison.Ordinal); - Assert.True(disposeStart >= 0); - string dispose = source[disposeStart..]; - + IReadOnlyList labels = ShutdownLabels(); string[] stages = [ - "new ResourceShutdownStage(\"host and session barriers\"", - "new ResourceShutdownStage(\"physical ingress cleanup\"", - "new ResourceShutdownStage(\"plugin host\"", - "new ResourceShutdownStage(\"frame borrowers\"", - "new ResourceShutdownStage(\"session dependents\"", - "new ResourceShutdownStage(\"live entities\"", - "new ResourceShutdownStage(\"effect dispatch edges\"", - "new ResourceShutdownStage(\"live entity dependents\"", - "new ResourceShutdownStage(\"submitted GPU work\"", - "new ResourceShutdownStage(\"render frontends\"", - "new ResourceShutdownStage(\"game runtime root\"", - "new ResourceShutdownStage(\"shared texture owners\"", - "new ResourceShutdownStage(\"mesh adapter\"", - "new ResourceShutdownStage(\"remaining render owners\"", - "new ResourceShutdownStage(\"dedicated render resources\"", - "new ResourceShutdownStage(\"failed render construction cleanup\"", - "new ResourceShutdownStage(\"frame flight owner\"", - "new ResourceShutdownStage(\"content mappings\"", - "new ResourceShutdownStage(\"input context\"", - "new ResourceShutdownStage(\"graphics API context\"", + "host and session barriers", + "physical ingress cleanup", + "plugin host", + "frame borrowers", + "session dependents", + "live entities", + "effect dispatch edges", + "live entity dependents", + "submitted GPU work", + "render frontends", + "game runtime root", + "shared texture owners", + "mesh adapter", + "remaining render owners", + "dedicated render resources", + "failed render construction cleanup", + "frame flight owner", + "content mappings", + "input context", + "graphics API context", ]; - AssertAppearsInOrder(manifest, stages); - Assert.Equal(stages.Length, CountOccurrences(manifest, "new ResourceShutdownStage(")); - foreach (string stage in stages) - Assert.Equal(1, CountOccurrences(manifest, stage)); - AssertAppearsInOrder( - manifest, - "Hard(\"combat command slot\", ingress.CombatCommands.Deactivate)", - "Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)", - "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", - "Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())", - "Hard(\"camera pointer\", () => ingress.CameraPointer?.Deactivate())", - "Hard(\"game runtime session\", ingress.Runtime.StopSession)", - "new ResourceShutdownStage(\"physical ingress cleanup\"", - "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", - "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", - "Soft(\"camera pointer\", () => DisposeCameraPointer(ingress.CameraPointer))", - "Soft(\"native window callbacks\", () => DisposeWindowCallbacks(ingress.WindowCallbacks))", - "new ResourceShutdownStage(\"plugin host\"", - "Hard(\"plugins\", () => ingress.Plugins?.Dispose())", - "new ResourceShutdownStage(\"session dependents\"", - "Hard(\"mouse capture\", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement())"); - Assert.Contains( - "new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue)", - lifetime, - StringComparison.Ordinal); - Assert.Contains( - "UiHost? RetainedUiHost,", - lifetime, - StringComparison.Ordinal); - Assert.Contains( - "IDisposable? Plugins,", - lifetime, - StringComparison.Ordinal); - Assert.Contains("_uiHost,", source, StringComparison.Ordinal); - Assert.Contains("_pluginSession,", source, StringComparison.Ordinal); - AssertAppearsInOrder( - program, - "GraphicalPluginSession pluginSession = GraphicalPluginSession.Create(", - "window.StartPluginHosting(pluginSession);", - "window.Run();"); - AssertAppearsInOrder( - source, - "_pluginSession = pluginSession;", - "pluginSession.Start();", - "public void Run()"); - AssertAppearsInOrder( - manifest, - "Hard(\"plugins\", () => ingress.Plugins?.Dispose())", - "Hard(\"retail UI\", () => DisposeRetailUi(live.RetailUi))", - "Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))"); - AssertAppearsInOrder( - nativeRelease, - "TryComplete();", - "ReleaseNativeWindow();"); - AssertAppearsInOrder( - dispose, - "CompleteShutdown(releaseNativeWindow: true);", - "_window = null;"); - Assert.Equal(1, CountOccurrences(dispose, "CompleteShutdown(releaseNativeWindow: true);")); - Assert.DoesNotContain("_window?.Dispose();", dispose, StringComparison.Ordinal); - Assert.Equal(1, CountOccurrences(dispose, "_window = null;")); - int nativeReleased = dispose.IndexOf("_window = null;", StringComparison.Ordinal) - + "_window = null;".Length; - string afterNativeRelease = dispose[nativeReleased..]; - int fallbackDocumentation = afterNativeRelease.IndexOf( - "/// ", - StringComparison.Ordinal); - if (fallbackDocumentation >= 0) - afterNativeRelease = afterNativeRelease[..fallbackDocumentation]; - Assert.All( - afterNativeRelease, - character => Assert.True(char.IsWhiteSpace(character) || character == '}')); + AssertLabelOrder(labels, stages); + AssertLabelOrder( + labels, + "combat command slot", + "diagnostic command slot", + "retained gameplay", + "gameplay actions", + "game runtime session", + "physical ingress cleanup"); + + Assert.Equal(typeof(UiHost), + Nullable.GetUnderlyingType(typeof(IngressShutdownRoots) + .GetProperty("RetainedUiHost", Declared)?.PropertyType + ?? throw new MissingMemberException()) + ?? typeof(IngressShutdownRoots) + .GetProperty("RetainedUiHost", Declared)!.PropertyType); + Assert.Equal(typeof(IDisposable), + typeof(IngressShutdownRoots).GetProperty("Plugins", Declared)?.PropertyType); + IReadOnlyList captureFields = + CompiledCallGraph.ReadFieldReferences( + RequiredMethod(typeof(GameWindow), "CaptureShutdownRoots")); + Assert.Contains(captureFields, field => field.Field.Name == "_uiHost"); + Assert.Contains(captureFields, field => field.Field.Name == "_pluginSession"); + + MethodBase entry = typeof(GameWindow).Assembly.GetTypes() + .Where(type => type.Name == "Program") + .SelectMany(type => type.GetMethods(Declared)) + .Single(method => CompiledCallGraph.Read(method).Any(call => + call.Target.DeclaringType == typeof(GraphicalPluginSession) + && call.Target.Name == nameof(GraphicalPluginSession.Create))); + AssertCallOrder( + entry, + (typeof(GraphicalPluginSession), nameof(GraphicalPluginSession.Create)), + (typeof(GameWindow), nameof(GameWindow.StartPluginHosting)), + (typeof(GameWindow), nameof(GameWindow.Run))); + + MethodInfo attach = RequiredMethod(typeof(GameWindow), nameof(GameWindow.StartPluginHosting)); + CompiledFieldReference store = Assert.Single( + CompiledCallGraph.ReadFieldReferences(attach), + reference => reference.Field.Name == "_pluginSession" + && reference.OpCode == OpCodes.Stfld); + CompiledCall start = Assert.Single( + CompiledCallGraph.Read(attach), + call => call.Target.DeclaringType == typeof(GraphicalPluginSession) + && call.Target.Name == nameof(GraphicalPluginSession.Start)); + Assert.True(store.Offset < start.Offset); + + AssertLabelOrder(labels, "plugins", "retail UI", "game runtime"); + AssertCallOrder( + RequiredMethod( + typeof(GameWindowLifetime), + nameof(GameWindowLifetime.CompleteAndReleaseNativeWindow)), + (typeof(GameWindowLifetime), nameof(GameWindowLifetime.TryComplete)), + (typeof(GameWindowLifetime), "ReleaseNativeWindow")); + + MethodInfo dispose = RequiredMethod(typeof(GameWindow), nameof(GameWindow.Dispose)); + CompiledCall complete = Assert.Single( + CompiledCallGraph.Read(dispose), + call => call.Target.DeclaringType == typeof(GameWindow) + && call.Target.Name == "CompleteShutdown"); + CompiledFieldReference clear = Assert.Single( + CompiledCallGraph.ReadFieldReferences(dispose), + reference => reference.Field.Name == "_window" + && reference.OpCode == OpCodes.Stfld); + Assert.True(complete.Offset < clear.Offset); + Assert.DoesNotContain( + CompiledCallGraph.Read(dispose), + call => call.Target.DeclaringType == typeof(IWindow) + && call.Target.Name == nameof(IDisposable.Dispose)); } [Fact] public void ResourceRootsAndFramePairHaveExplicitAcquireTransferAndReleaseBoundaries() { - string source = GameWindowSource(); - string load = MethodBody( - "private void OnLoad()", - "private void OnUpdate(double dt)"); - string shutdown = GameWindowLifetimeSource(); - string terrainAtlas = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "TerrainAtlas.cs")); - string worldPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "WorldRenderComposition.cs")); - string livePhase = LivePresentationSource(); - string sessionPhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - string framePhase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "FrameRootComposition.cs")); + MethodInfo live = RequiredMethod( + typeof(LivePresentationCompositionPhase), + "CompletePresentation"); + IReadOnlyList liveCalls = CompiledCallGraph.Read(live); + CompiledCall acquirePrepared = Assert.Single( + liveCalls, + call => call.Target.Name == "AcquirePrepared"); + CompiledCall skyFactory = Assert.Single( + CompiledCallGraph.ReadMethodReferences(live), + call => call.Offset > acquirePrepared.Offset + && call.Target.GetMethodBody() is not null + && Constructs(call.Target, typeof(SkyRenderer))); + Assert.True(acquirePrepared.Offset < skyFactory.Offset); + Assert.NotNull(ReferencedMethodConstructingOrCalling( + live, + typeof(PortalTunnelPresentation), + nameof(PortalTunnelPresentation.PrepareResources))); - AssertAppearsInOrder( - livePhase, - "d.PortalTunnelFallback.AcquirePrepared(", - "static tunnel => tunnel.PrepareResources());", - "new SkyRenderer("); - AssertAppearsInOrder( - load, - "new WorldRenderCompositionPhase(", - "new LivePresentationCompositionPhase(", - "new SessionPlayerCompositionPhase(", - "new FrameRootCompositionPhase("); - AssertAppearsInOrder( - framePhase, - "new RenderFrameOrchestrator(", - "new UpdateFrameOrchestrator(", - "d.FrameGraphs.PublishOwned("); - AssertAppearsInOrder( - sessionPhase, - "d.PortalTunnelFallback.Transfer(", - "new LocalPlayerTeleportController(", - "_publication.PublishSessionPlayer(result);"); - AssertAppearsInOrder( - worldPhase, - "lifetime.AcquireTerrainAtlas(", - "TerrainAtlas.BuildBackendNeutral(device, dats)", - "TerrainModernRenderer CreateBackendNeutralTerrain(", - // Campaign V slice V6j: terrain is composed on both arms, so its - // acquisition is unconditional. The boundary this test pins — that - // the atlas is acquired, then the factory names the renderer, then - // the renderer is acquired AND published in one step — is unchanged. - // The raw-GL arm (CreateTerrain) was deleted at slice V11. - "TerrainModernRenderer terrain = AcquireAndPublish("); - AssertAppearsInOrder( - shutdown, - "frame.FrameGraphPublication?.Dispose()", - "frame.FrameBindings?.Dispose()", - "DisposeRetailUi(live.RetailUi)", - "render.LocalTeleport?.Dispose();", - "render.PortalTunnelFallback.ReleaseFallback();", - "render.Sky?.Dispose()", - "render.Terrain?.Dispose()", - "render.DedicatedResources.ReleaseTerrainAtlas", - "render.ConstructionCleanup.Dispose", - "platform.Graphics?.Dispose()"); + AssertOnLoadPhaseOrder( + typeof(WorldRenderCompositionPhase), + typeof(LivePresentationCompositionPhase), + typeof(SessionPlayerCompositionPhase), + typeof(FrameRootCompositionPhase)); + AssertCallOrder( + RequiredMethod(typeof(FrameRootCompositionPhase), "ComposeCore"), + (typeof(RenderFrameOrchestrator), ".ctor"), + (typeof(UpdateFrameOrchestrator), ".ctor"), + (typeof(GameFrameGraphSlot), nameof(GameFrameGraphSlot.PublishOwned))); - Assert.DoesNotContain( - "RetailUiRuntime.Mount(", - load, - StringComparison.Ordinal); - Assert.DoesNotContain("gl.GenTexture()", terrainAtlas, StringComparison.Ordinal); - Assert.DoesNotContain( - "portalTunnel.PrepareResources();", - load, - StringComparison.Ordinal); - Assert.DoesNotContain("_portalTunnel =", source, StringComparison.Ordinal); - Assert.DoesNotContain("_renderFrameOrchestrator", source, StringComparison.Ordinal); - Assert.DoesNotContain("_updateFrameOrchestrator", source, StringComparison.Ordinal); - AssertAppearsInOrder( - source, - "_window.Run();", - "_constructionCleanup.RetainFrom(failure);", - "private GameWindowShutdownRoots CaptureShutdownRoots()", - "_constructionCleanup)"); + MethodInfo session = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + AssertCallOrder( + session, + (typeof(TransferableResourceSlot<>), "Transfer"), + (typeof(IGameWindowSessionPlayerPublication), + nameof(IGameWindowSessionPlayerPublication.PublishSessionPlayer))); + MethodBase teleportFactory = ReferencedMethodConstructingOrCalling( + session, + typeof(LocalPlayerTeleportPresentation), + ".ctor"); + Assert.NotNull(ReferencedMethodConstructingOrCalling( + teleportFactory, + typeof(LocalPlayerTeleportController), + ".ctor")); + + MethodInfo atlas = RequiredMethod( + typeof(RetailWorldRenderCompositionFactory), + nameof(RetailWorldRenderCompositionFactory.AcquireBackendNeutralTerrainAtlas)); + Assert.NotNull(ReferencedMethodConstructingOrCalling( + atlas, + typeof(TerrainAtlas), + nameof(TerrainAtlas.BuildBackendNeutral))); + + IReadOnlyList labels = ShutdownLabels(); + AssertLabelOrder( + labels, + "world frame composition", + "frame-root bindings", + "retail UI", + "portal tunnel", + "sky", + "terrain", + "terrain atlas", + "resource construction ledger", + "graphics API"); + + MethodInfo onLoad = RequiredMethod(typeof(GameWindow), "OnLoad"); + IReadOnlyList loadCalls = + CompiledCallGraph.ReadMethodReferences(onLoad); + Assert.DoesNotContain(loadCalls, call => + call.Target.DeclaringType == typeof(RetailUiRuntime) + && call.Target.Name == "Mount"); + Assert.DoesNotContain(loadCalls, call => + call.Target.DeclaringType == typeof(PortalTunnelPresentation) + && call.Target.Name == nameof(PortalTunnelPresentation.PrepareResources)); + AssertMembersAbsent( + typeof(GameWindow), + ["_portalTunnel", "_renderFrameOrchestrator", "_updateFrameOrchestrator"], + []); + + AssertCallOrder( + RequiredMethod(typeof(GameWindow), nameof(GameWindow.Run)), + (typeof(ResourceConstructionCleanupLedger), + nameof(ResourceConstructionCleanupLedger.RetainFrom))); + AssertSilkWindowCallBefore( + RequiredMethod(typeof(GameWindow), nameof(GameWindow.Run)), + nameof(IWindow.Run), + typeof(ResourceConstructionCleanupLedger), + nameof(ResourceConstructionCleanupLedger.RetainFrom)); Assert.Contains( - "Hard(\"resource construction ledger\", render.ConstructionCleanup.Dispose)", - shutdown, - StringComparison.Ordinal); + CompiledCallGraph.ReadFieldReferences( + RequiredMethod(typeof(GameWindow), "CaptureShutdownRoots")), + reference => reference.Field.Name == "_constructionCleanup"); + Assert.Contains(labels, label => label == "resource construction ledger"); } - private static string MethodBody(string start, string end) => - Slice(GameWindowSource(), start, end); + 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 string Slice(string source, string start, string end) + private static ConstructorInfo FindConstructorConstructing(Type owner, Type constructed) => + Assert.Single( + owner.GetConstructors(Declared), + constructor => Constructs(constructor, constructed)); + + private static void AssertField(Type owner, string name, Type expectedType) { - int first = source.IndexOf(start, StringComparison.Ordinal); - int last = source.IndexOf(end, first + 1, StringComparison.Ordinal); - Assert.True(first >= 0, $"Missing source boundary: {start}"); - Assert.True(last > first, $"Missing source boundary: {end}"); - return source[first..last]; + FieldInfo field = owner.GetField(name, Declared) + ?? throw new MissingFieldException(owner.FullName, name); + Assert.Equal(expectedType, field.FieldType); } - private static int CountOccurrences(string source, string value) + private static void AssertMembersAbsent( + Type owner, + IReadOnlyCollection fields, + IReadOnlyCollection methods) { - int count = 0; - int cursor = 0; - while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0) + Assert.DoesNotContain(owner.GetFields(Declared), field => fields.Contains(field.Name)); + Assert.DoesNotContain(owner.GetMethods(Declared), method => methods.Contains(method.Name)); + } + + private static void AssertOnLoadPhaseOrder(params Type[] phases) + { + IReadOnlyList references = CompiledCallGraph.ReadMethodReferences( + RequiredMethod(typeof(GameWindow), "OnLoad")); + int cursor = -1; + foreach (Type phase in phases) { - count++; - cursor += value.Length; + CompiledCall found = references + .Where(reference => reference.Offset > cursor) + .FirstOrDefault(reference => Constructs(reference.Target, phase)); + Assert.NotNull(found.Target); + Assert.True(found.Offset > cursor, $"Missing OnLoad phase: {phase.Name}."); + cursor = found.Offset; } - - return count; } - private static void AssertAppearsInOrder(string source, params string[] fragments) + private static MethodBase OnLoadMethodConstructing(Type type) => + Assert.Single( + CompiledCallGraph.ReadMethodReferences( + RequiredMethod(typeof(GameWindow), "OnLoad")) + .Select(reference => reference.Target) + .Where(method => method.GetMethodBody() is not null) + .Distinct(), + method => Constructs(method, type)); + + private static bool Constructs(MethodBase method, Type type) => + method.GetMethodBody() is not null + && CompiledCallGraph.Read(method).Any(call => + call.Target.DeclaringType == type && call.Target.IsConstructor); + + private static MethodBase ReferencedMethodConstructingOrCalling( + MethodBase owner, + Type type, + string methodName) => + Assert.Single( + CompiledCallGraph.ReadMethodReferences(owner) + .Select(reference => reference.Target) + .Where(method => method.GetMethodBody() is not null) + .Distinct(), + method => CompiledCallGraph.Read(method).Any(call => + call.Target.DeclaringType == type + && call.Target.Name == methodName)); + + private static void AssertSingleCall(MethodBase owner, Type type, string name) => + Assert.Single( + CompiledCallGraph.Read(owner), + call => MatchesType(call.Target.DeclaringType, type) + && call.Target.Name == name); + + private static void AssertOnlyProductionCall(MethodBase owner, Type type, string name) + { + IReadOnlyList calls = CompiledCallGraph.Read(owner) + .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith("AcDream", + StringComparison.Ordinal) == true) + .ToArray(); + CompiledCall call = Assert.Single(calls); + Assert.True(MatchesType(call.Target.DeclaringType, type)); + Assert.Equal(name, call.Target.Name); + } + + private static void AssertSilkWindowCallAfter( + MethodBase owner, + string silkMethod, + Type predecessorType, + string predecessorMethod) + { + IReadOnlyList calls = CompiledCallGraph.Read(owner); + CompiledCall predecessor = Assert.Single(calls, call => + call.Target.DeclaringType == predecessorType + && call.Target.Name == predecessorMethod); + CompiledCall silk = Assert.Single(calls, call => + call.Target.DeclaringType?.Namespace == "Silk.NET.Windowing" + && call.Target.Name == silkMethod); + Assert.True(predecessor.Offset < silk.Offset); + } + + private static void AssertSilkWindowCallBefore( + MethodBase owner, + string silkMethod, + Type successorType, + string successorMethod) + { + IReadOnlyList calls = CompiledCallGraph.Read(owner); + CompiledCall silk = Assert.Single(calls, call => + call.Target.DeclaringType?.Namespace == "Silk.NET.Windowing" + && call.Target.Name == silkMethod); + CompiledCall successor = Assert.Single(calls, call => + call.Target.DeclaringType == successorType + && call.Target.Name == successorMethod); + Assert.True(silk.Offset < successor.Offset); + } + + private static void AssertCallOrder( + MethodBase method, + params (Type Type, string Method)[] expected) + { + IReadOnlyList calls = CompiledCallGraph.Read(method); + int cursor = -1; + foreach ((Type type, string name) in expected) + { + int found = Enumerable.Range(cursor + 1, calls.Count - cursor - 1) + .FirstOrDefault(index => + MatchesType(calls[index].Target.DeclaringType, type) + && calls[index].Target.Name == name, + -1); + Assert.True(found > cursor, + $"Missing compiled edge after {cursor}: {type.FullName}.{name}. " + + $"Observed: {string.Join(", ", calls.Select(call => + $"{call.Target.DeclaringType?.Name}.{call.Target.Name}"))}"); + cursor = found; + } + } + + private static void AssertTargetOrder( + IReadOnlyList calls, + Type type, + params string[] names) { int cursor = -1; - foreach (string fragment in fragments) + foreach (string name in names) { - int next = source.IndexOf(fragment, cursor + 1, StringComparison.Ordinal); - Assert.True(next >= 0, $"Missing expected source fragment: {fragment}"); - Assert.True(next > cursor, $"Out-of-order source fragment: {fragment}"); - cursor = next; + int found = Enumerable.Range(cursor + 1, calls.Count - cursor - 1) + .FirstOrDefault(index => calls[index].Target.DeclaringType == type + && calls[index].Target.Name == name, -1); + Assert.True(found > cursor, + $"Missing referenced target after {cursor}: {type.FullName}.{name}."); + cursor = found; } } - private static string GameWindowSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + private static bool MatchesType(Type? actual, Type expected) => + actual == expected + || expected.IsGenericTypeDefinition + && actual?.IsGenericType == true + && actual.GetGenericTypeDefinition() == expected; - private static string GameWindowLifetimeSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindowLifetime.cs")); + private static IReadOnlyList ShutdownLabels() => + CompiledCallGraph.ReadStringLiterals(RequiredMethod( + typeof(GameWindowShutdownManifest), + nameof(GameWindowShutdownManifest.Create))); - private static string LivePresentationSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); - - private static string FindRepoRoot() + private static void AssertLabelOrder( + IReadOnlyList labels, + params string[] expected) { - DirectoryInfo? directory = new(AppContext.BaseDirectory); - while (directory is not null) + int cursor = -1; + foreach (string label in expected) { - if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - return directory.FullName; - directory = directory.Parent; + int found = Enumerable.Range(cursor + 1, labels.Count - cursor - 1) + .FirstOrDefault(index => labels[index] == label, -1); + Assert.True(found > cursor, + $"Missing shutdown label after {cursor}: {label}."); + cursor = found; } - - throw new DirectoryNotFoundException("Could not find AcDream.slnx."); } }