diff --git a/AGENTS.md b/AGENTS.md index 13c1c3e5..4afdb56e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,10 +150,14 @@ lifecycle/reconnect gate, and 395.2-second synchronized nine-stop soak pass. Checkpoint H adds sole terrain/sky render-resource roots, one retained Host/runtime lease, an atomic update/render frame-root pair, prepared portal fallback/transfer, and checked retryable GL construction/state boundaries. -`GameWindow` is 3,689 raw lines / 162 fields / 37 methods, down 12,034 lines -(76.5%) from baseline. All three final corrected-diff reviews are clean. Slice -8 checkpoints I–L—ordered composition, retryable shutdown, canonical soak -snapshots, and final connected gates—remain active. +Checkpoint I now routes production through the same executable nine-phase +pipeline as its failure oracle, carries exact platform/settings results through +typed phase inputs, atomically publishes frame roots, and makes session start +terminal. `GameWindow` is 1,945 raw lines, down 13,778 lines (87.6%) from +baseline. App Release passes 3,431 tests / 3 skips and the complete Release +suite passes 7,803 / 5 skips. Checkpoint I is complete; checkpoints J–L—focused +retryable shutdown, canonical soak snapshots, and final connected gates—remain +active. See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and `docs/architecture/code-structure.md`. **Carried:** #232, #153, #116, remaining diff --git a/CLAUDE.md b/CLAUDE.md index 8fae80e3..2aa07e45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,10 +148,14 @@ lifecycle/reconnect gate, and 395.2-second synchronized nine-stop soak pass. Checkpoint H adds sole terrain/sky render-resource roots, one retained Host/runtime lease, an atomic update/render frame-root pair, prepared portal fallback/transfer, and checked retryable GL construction/state boundaries. -`GameWindow` is 3,689 raw lines / 162 fields / 37 methods, down 12,034 lines -(76.5%) from baseline. All three final corrected-diff reviews are clean. Slice -8 checkpoints I–L—ordered composition, retryable shutdown, canonical soak -snapshots, and final connected gates—remain active. +Checkpoint I now routes production through the same executable nine-phase +pipeline as its failure oracle, carries exact platform/settings results through +typed phase inputs, atomically publishes frame roots, and makes session start +terminal. `GameWindow` is 1,945 raw lines, down 13,778 lines (87.6%) from +baseline. App Release passes 3,431 tests / 3 skips and the complete Release +suite passes 7,803 / 5 skips. Checkpoint I is complete; checkpoints J–L—focused +retryable shutdown, canonical soak snapshots, and final connected gates—remain +active. See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and `docs/architecture/code-structure.md`. **Carried:** #232, #153, #116, remaining diff --git a/docs/ISSUES.md b/docs/ISSUES.md index f000be6c..1ea954d5 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -36,11 +36,11 @@ What does NOT go here: command targets, and sole gameplay input subscriber before frame publication. I.8b now owns the complete update/render construction, exact atomic frame publication, and lifecycle resource snapshot source. I.8c makes session - start the terminal `OnLoad` operation. `GameWindow.OnUpdate` and - `GameWindow.OnRender` are typed orchestration handoffs and the class is - 1,910 raw lines. I.9 plus the remaining - shutdown/closeout - cleanup is active in + start the terminal `OnLoad` operation. I.9 corrected the production root to + invoke the exact tested pipeline and made every consumed platform/settings + result explicit. `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed + orchestration handoffs and the class is 1,945 raw lines. Checkpoint I is + complete; the shutdown/lifetime cutover and closeout remain active in [`docs/architecture/code-structure.md`](architecture/code-structure.md). This is the behavior-preserving prerequisite before new M4 feature bodies. - **Connected-gate fidelity:** `#232` adds canonical managed/GPU/cache-owner diff --git a/docs/architecture/code-structure.md b/docs/architecture/code-structure.md index edb42abb..753bf098 100644 --- a/docs/architecture/code-structure.md +++ b/docs/architecture/code-structure.md @@ -739,12 +739,14 @@ diagnostic command targets, and sole gameplay input subscriber into Phase 7 before frame publication. I.8b moves the complete update/render construction body into `FrameRootCompositionPhase`, publishes the pair through an exact lease, and gives lifecycle resource sampling a focused source. `GameWindow` is -1,910 raw lines after I.8c makes live-session start the terminal `OnLoad` -operation. I.9–L remain active, with the complete Checkpoint-I audit next. The -App gate passes 3,430 tests / 3 intentional skips and the complete Release suite -passes 7,802 tests / 5 intentional skips. The clean solution build retains only -the 17 test-project warnings tracked by #228; the I.8c corrected-diff review is -clean. +1,945 raw lines after I.9 corrects production to invoke the same executable +pipeline as the failure oracle, carries exact platform/settings results through +the typed contract, and keeps live-session start terminal. Checkpoint I is +complete; J–L remain active with lifetime/shutdown extraction next. The App gate +passes 3,431 tests / 3 intentional skips and the complete Release suite passes +7,803 tests / 5 intentional skips. The clean solution build retains only the 17 +test-project warnings tracked by #228; the complete Checkpoint-I behavior, +architecture, and adversarial corrected-diff passes are clean. ### 4.4 Exit criteria diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index b2e4d8f2..c83bc1d5 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -104,11 +104,12 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle resources have transactional phase ownership and exact-owner late bindings. I.7 adds the complete streaming/session/hydration/local-player/teleport phase, including explicit reversible ownership for every late edge. `GameWindow` is -now 1,910 raw lines after I.8c, which makes session start the terminal `OnLoad` -operation. I.9, shutdown, canonical soak snapshots, and closeout checkpoints -J–L remain. The App gate passes 3,430 tests / 3 intentional skips, the complete -Release suite passes 7,802 tests / 5 skips, and the I.8c corrected-diff review -is clean. +now 1,945 raw lines after I.9 makes production invoke the same typed pipeline +as the failure oracle and keeps session start terminal. Checkpoint I is +complete; shutdown, canonical soak snapshots, and closeout checkpoints J–L +remain. The App gate passes 3,431 tests / 3 intentional skips, the complete +Release suite passes 7,803 tests / 5 skips, and the complete Checkpoint-I +corrected-diff review is clean. This is a behavior-preserving structural program. Severe regressions still get root-cause fixes in separate commits; ordinary feature work resumes with M4 diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 762a2866..70c0ccb4 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -63,11 +63,13 @@ named reversible late edges. I.8a adds the focused live-session runtime factory and attaches command/input owners before frame publication. I.8b composes and atomically publishes the complete frame pair and extracts lifecycle resource sampling. I.8c makes session start the terminal `OnLoad` operation. -`GameWindow` is 1,910 raw lines. I.9 and checkpoints J–L remain active. Issue -#232 tracks +`GameWindow` is 1,945 raw lines after I.9 routes production through the exact +tested pipeline with explicit prior-result dependencies. Checkpoint I is +complete and checkpoints J–L remain active. Issue #232 tracks process-residency variance in the soak without loosening its leak threshold. -The App gate passes 3,430 tests / 3 skips, the complete Release suite passes -7,802 tests / 5 skips, and the I.8c corrected-diff review is clean. +The App gate passes 3,431 tests / 3 skips, the complete Release suite passes +7,803 tests / 5 skips, and the complete Checkpoint-I corrected-diff review is +clean. Carried: #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md index 72625f7f..46a1ccac 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md @@ -1,6 +1,6 @@ # GameWindow Slice 8 Checkpoint I — ordered production composition -**Status:** Active 2026-07-22. +**Status:** Complete 2026-07-22. **Parent:** [`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md), Checkpoint I. @@ -299,9 +299,19 @@ ledger only after the complete pipeline is live. the pair through an exact owned slot lease, and moves lifecycle resource sampling out of the window. I.8c makes the exact session start and its existing diagnostics the terminal `OnLoad` operation. `GameWindow.cs` is - 1,910 raw lines. I.9 corrected-diff review and documentation closeout is the - active cut. - Checkpoint I remains active until I.9 closes the complete nine-phase pipeline. + 1,910 raw lines. +- I.9 found and corrected one structural defect: production manually repeated + the phase order instead of invoking the executable pipeline used by failure + tests, and several later phases received real platform/settings results only + through surrounding captures. Production now enters the same fixed pipeline; + platform and settings are explicit typed phase inputs and concrete phases + reject mismatched result instances. Session start remains the terminal + operation. Three corrected-diff passes covering behavior order, architecture/ + ownership, and adversarial failure/reentrancy are clean. `GameWindow.cs` is + 1,945 raw lines. App Release passes 3,431 tests / 3 intentional skips; the + complete Release suite passes 7,803 tests / 5 intentional skips with only the + 17 pre-existing test-project warnings tracked by #228. No retail behavior or + divergence changed. Checkpoint I is complete; Checkpoint J is active. ## 6. Automated acceptance diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md index 01456b7a..061f4c55 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md @@ -28,9 +28,9 @@ audit. - [x] G — extract two-phase persisted settings/display/quality ownership. - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots explicit single ownership and transfer seams. -- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - transactional partial-acquisition rollback. I.1–I.8 are complete; I.9 - corrected-diff review and checkpoint closeout is active. Detailed plan: +- [x] I — group `OnLoad` into small ordered, fakeable composition phases with + transactional partial-acquisition rollback. Production and failure tests now + invoke the same pipeline and carry exact typed prior results. Detailed plan: [`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md). - [ ] J — move the exact retryable shutdown manifest to a focused lifetime owner and prove all partial-load/reentrant/retry paths. @@ -627,6 +627,16 @@ corrected-diff reviews are clean. The complete Release suite passes 7,606 tests teleport transfer, and frame-root publication. Each concrete phase also proves transactional rollback of its internal acquisition prefix. +Result: all nine production phases now run through the same executable pipeline +as the prefix/failure oracle. Platform and settings results are explicit typed +inputs wherever consumed; phases reject mismatched instances, all long-lived +owners publish to focused lifetime slots, the frame pair publishes atomically, +and session start is terminal. `GameWindow.cs` is 1,945 raw lines. Three +corrected-diff passes are clean; App Release passes 3,431 tests / 3 skips and +the complete Release suite passes 7,803 tests / 5 skips with the existing 17 +#228 warnings. No connected gate was required for the final structural +correction; Checkpoint L retains the connected lifecycle and soak gates. + ### J — lifetime cutover and shutdown failure policy - Move the teardown-only stage records and shutdown manifest out of GameWindow. diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 1b047cf1..ff479f25 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -127,6 +127,7 @@ internal sealed class FrameRootRuntimeBindings : IDisposable internal sealed class FrameRootCompositionPhase : IFrameRootCompositionPhase< + GameWindowPlatformResult, HostInputCameraResult, ContentEffectsAudioResult, SettingsDevToolsResult, @@ -153,6 +154,7 @@ internal sealed class FrameRootCompositionPhase } public FrameRootResult Compose( + GameWindowPlatformResult platform, HostInputCameraResult host, ContentEffectsAudioResult content, SettingsDevToolsResult settings, @@ -161,6 +163,7 @@ internal sealed class FrameRootCompositionPhase LivePresentationResult live, SessionPlayerResult session) { + ArgumentNullException.ThrowIfNull(platform); ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(content); ArgumentNullException.ThrowIfNull(settings); @@ -168,6 +171,12 @@ internal sealed class FrameRootCompositionPhase ArgumentNullException.ThrowIfNull(interaction); ArgumentNullException.ThrowIfNull(live); ArgumentNullException.ThrowIfNull(session); + if (!ReferenceEquals(_dependencies.Gl, platform.Graphics) + || !ReferenceEquals(_dependencies.Input, platform.Input)) + { + throw new InvalidOperationException( + "Frame-root dependencies do not match the ordered platform result."); + } var scope = new CompositionAcquisitionScope(); FrameRootRuntimeBindings? bindings = null; diff --git a/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs b/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs index b22168e5..9b86574d 100644 --- a/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs +++ b/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs @@ -20,29 +20,37 @@ internal interface IWorldRenderCompositionPhase +internal interface IInteractionUiCompositionPhase { - TResult Compose(THost host, TContent content, TSettings settings, TWorld world); + TResult Compose(TPlatform platform, THost host, TContent content, TSettings settings, TWorld world); } -internal interface ILivePresentationCompositionPhase +internal interface ILivePresentationCompositionPhase { - TResult Compose(THost host, TContent content, TWorld world, TInteraction interaction); + TResult Compose( + TPlatform platform, + THost host, + TContent content, + TSettings settings, + TWorld world, + TInteraction interaction); } -internal interface ISessionPlayerCompositionPhase +internal interface ISessionPlayerCompositionPhase { TResult Compose( THost host, TContent content, + TSettings settings, TWorld world, TInteraction interaction, TLive live); } -internal interface IFrameRootCompositionPhase +internal interface IFrameRootCompositionPhase { TResult Compose( + TPlatform platform, THost host, TContent content, TSettings settings, @@ -57,6 +65,149 @@ internal interface ISessionStartCompositionPhase void Start(TFrame frame); } +/// +/// Synchronous production entry point for the fixed composition topology. +/// The delegates exist only for the duration of ; this lets +/// the native host remain the write-only publication shell without retaining +/// it in any phase owner. +/// +internal static class GameWindowCompositionPipeline +{ + public static void Run< + TPlatform, + THost, + TContent, + TSettings, + TWorld, + TInteraction, + TLive, + TSession, + TFrame>( + TPlatform platform, + Func host, + Func content, + Func settings, + Func world, + Func interaction, + Func live, + Func session, + Func frame, + Action start) + { + new GameWindowCompositionPipeline< + TPlatform, + THost, + TContent, + TSettings, + TWorld, + TInteraction, + TLive, + TSession, + TFrame>( + new HostPhase(host), + new ContentPhase(content), + new SettingsPhase(settings), + new WorldPhase(world), + new InteractionPhase(interaction), + new LivePhase(live), + new SessionPhase(session), + new FramePhase(frame), + new StartPhase(start)) + .Run(platform); + } + + private sealed class HostPhase( + Func compose) + : IHostInputCameraCompositionPhase + { + public TResult Compose(TPlatform platform) => compose(platform); + } + + private sealed class ContentPhase( + Func compose) + : IContentEffectsAudioCompositionPhase + { + public TResult Compose(TPlatform platform, THost host) => compose(platform, host); + } + + private sealed class SettingsPhase( + Func compose) + : ISettingsDevToolsCompositionPhase + { + public TResult Compose(TPlatform platform, THost host, TContent content) => + compose(platform, host, content); + } + + private sealed class WorldPhase( + Func compose) + : IWorldRenderCompositionPhase + { + public TResult Compose(TPlatform platform, TContent content, TSettings settings) => + compose(platform, content, settings); + } + + private sealed class InteractionPhase( + Func compose) + : IInteractionUiCompositionPhase + { + public TResult Compose( + TPlatform platform, + THost host, + TContent content, + TSettings settings, + TWorld world) => compose(platform, host, content, settings, world); + } + + private sealed class LivePhase( + Func compose) + : ILivePresentationCompositionPhase + { + public TResult Compose( + TPlatform platform, + THost host, + TContent content, + TSettings settings, + TWorld world, + TInteraction interaction) => + compose(platform, host, content, settings, world, interaction); + } + + private sealed class SessionPhase( + Func compose) + : ISessionPlayerCompositionPhase + { + public TResult Compose( + THost host, + TContent content, + TSettings settings, + TWorld world, + TInteraction interaction, + TLive live) => compose(host, content, settings, world, interaction, live); + } + + private sealed class FramePhase( + Func compose) + : IFrameRootCompositionPhase + { + public TResult Compose( + TPlatform platform, + THost host, + TContent content, + TSettings settings, + TWorld world, + TInteraction interaction, + TLive live, + TSession session) => + compose(platform, host, content, settings, world, interaction, live, session); + } + + private sealed class StartPhase(Action start) + : ISessionStartCompositionPhase + { + public void Start(TFrame frame) => start(frame); + } +} + /// /// Fixed startup topology. It intentionally retains no cumulative context or /// final runtime graph; immutable phase results exist only as locals during @@ -77,10 +228,10 @@ internal sealed class GameWindowCompositionPipeline< private readonly IContentEffectsAudioCompositionPhase _content; private readonly ISettingsDevToolsCompositionPhase _settings; private readonly IWorldRenderCompositionPhase _world; - private readonly IInteractionUiCompositionPhase _interaction; - private readonly ILivePresentationCompositionPhase _live; - private readonly ISessionPlayerCompositionPhase _session; - private readonly IFrameRootCompositionPhase _frame; + private readonly IInteractionUiCompositionPhase _interaction; + private readonly ILivePresentationCompositionPhase _live; + private readonly ISessionPlayerCompositionPhase _session; + private readonly IFrameRootCompositionPhase _frame; private readonly ISessionStartCompositionPhase _start; public GameWindowCompositionPipeline( @@ -88,10 +239,10 @@ internal sealed class GameWindowCompositionPipeline< IContentEffectsAudioCompositionPhase content, ISettingsDevToolsCompositionPhase settings, IWorldRenderCompositionPhase world, - IInteractionUiCompositionPhase interaction, - ILivePresentationCompositionPhase live, - ISessionPlayerCompositionPhase session, - IFrameRootCompositionPhase frame, + IInteractionUiCompositionPhase interaction, + ILivePresentationCompositionPhase live, + ISessionPlayerCompositionPhase session, + IFrameRootCompositionPhase frame, ISessionStartCompositionPhase start) { _host = host ?? throw new ArgumentNullException(nameof(host)); @@ -111,10 +262,11 @@ internal sealed class GameWindowCompositionPipeline< TContent content = _content.Compose(platform, host); TSettings settings = _settings.Compose(platform, host, content); TWorld world = _world.Compose(platform, content, settings); - TInteraction interaction = _interaction.Compose(host, content, settings, world); - TLive live = _live.Compose(host, content, world, interaction); - TSession session = _session.Compose(host, content, world, interaction, live); + TInteraction interaction = _interaction.Compose(platform, host, content, settings, world); + TLive live = _live.Compose(platform, host, content, settings, world, interaction); + TSession session = _session.Compose(host, content, settings, world, interaction, live); TFrame frame = _frame.Compose( + platform, host, content, settings, diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index 1fb3392e..94389ba7 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -693,6 +693,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory internal sealed class InteractionRetainedUiCompositionPhase : IInteractionUiCompositionPhase< + GameWindowPlatformResult, HostInputCameraResult, ContentEffectsAudioResult, SettingsDevToolsResult, @@ -805,16 +806,20 @@ internal sealed class InteractionRetainedUiCompositionPhase } public InteractionRetainedUiResult Compose( + GameWindowPlatformResult platform, HostInputCameraResult host, ContentEffectsAudioResult content, SettingsDevToolsResult settings, WorldRenderResult world) { + ArgumentNullException.ThrowIfNull(platform); ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(content); ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(world); - if (!ReferenceEquals(_dependencies.InputDispatcher, host.InputDispatcher) + if (!ReferenceEquals(_dependencies.Gl, platform.Graphics) + || !ReferenceEquals(_dependencies.Input, platform.Input) + || !ReferenceEquals(_dependencies.InputDispatcher, host.InputDispatcher) || !ReferenceEquals(_dependencies.Dats, content.Dats) || !ReferenceEquals(_dependencies.MagicCatalog, content.MagicCatalog) || !ReferenceEquals(_dependencies.TextureCache, world.Foundation.TextureCache) diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index 93f932a6..6dc3af07 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -131,8 +131,10 @@ internal enum LivePresentationCompositionPoint internal sealed class LivePresentationCompositionPhase : ILivePresentationCompositionPhase< + GameWindowPlatformResult, HostInputCameraResult, ContentEffectsAudioResult, + SettingsDevToolsResult, WorldRenderResult, InteractionRetainedUiResult, LivePresentationResult> @@ -154,15 +156,24 @@ internal sealed class LivePresentationCompositionPhase } public LivePresentationResult Compose( + GameWindowPlatformResult platform, HostInputCameraResult host, ContentEffectsAudioResult content, + SettingsDevToolsResult settings, WorldRenderResult world, InteractionRetainedUiResult interaction) { + ArgumentNullException.ThrowIfNull(platform); ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(world); ArgumentNullException.ThrowIfNull(interaction); + if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)) + { + throw new InvalidOperationException( + "Live-presentation dependencies do not match the ordered platform result."); + } return ComposeCore(host, content, world, interaction); } diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 341dd6c9..8c6f99c5 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -148,6 +148,7 @@ internal sealed class SessionPlayerCompositionPhase : ISessionPlayerCompositionPhase< HostInputCameraResult, ContentEffectsAudioResult, + SettingsDevToolsResult, WorldRenderResult, InteractionRetainedUiResult, LivePresentationResult, @@ -172,15 +173,22 @@ internal sealed class SessionPlayerCompositionPhase public SessionPlayerResult Compose( HostInputCameraResult host, ContentEffectsAudioResult content, + SettingsDevToolsResult settings, WorldRenderResult world, InteractionRetainedUiResult interaction, LivePresentationResult live) { ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(world); ArgumentNullException.ThrowIfNull(interaction); ArgumentNullException.ThrowIfNull(live); + if (!ReferenceEquals(_dependencies.SettingsDevTools, settings)) + { + throw new InvalidOperationException( + "Session/player dependencies do not match the ordered settings result."); + } var scope = new CompositionAcquisitionScope(); SessionPlayerRuntimeBindings? bindings = null; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 9953d71d..1c8083cc 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1097,8 +1097,18 @@ public sealed class GameWindow : GameWindowPlatformResult platform = AcquirePlatform(); - HostInputCameraResult hostInputCamera = - new HostInputCameraCompositionPhase( + GameWindowCompositionPipeline.Run< + GameWindowPlatformResult, + HostInputCameraResult, + ContentEffectsAudioResult, + SettingsDevToolsResult, + WorldRenderResult, + InteractionRetainedUiResult, + LivePresentationResult, + SessionPlayerResult, + FrameRootResult>( + platform, + platformResult => new HostInputCameraCompositionPhase( new HostInputCameraDependencies( _framebufferResize, _window!.FramebufferSize, @@ -1111,12 +1121,9 @@ public sealed class GameWindow : _chaseCameraInput, _pointerPosition, _renderDiagnosticLog), - this).Compose(platform); - WorldRenderDiagnostics worldRenderDiagnostics = - hostInputCamera.WorldRenderDiagnostics; - - ContentEffectsAudioResult contentEffectsAudio = - new ContentEffectsAudioCompositionPhase( + this).Compose(platformResult), + (platformResult, hostInputCamera) => + new ContentEffectsAudioCompositionPhase( new ContentEffectsAudioDependencies( _datDir, _physicsDataCache, @@ -1130,91 +1137,95 @@ public sealed class GameWindow : _options.NoAudio, Console.WriteLine, Console.Error.WriteLine), - this).Compose(platform, hostInputCamera); - - SettingsDevToolsOptionalDependencies? optionalDevTools = null; - if (DevToolsEnabled) - { - var devToolsWorldEntities = - new DeferredCanonicalWorldEntityCountSource(); - var devToolsFrameDiagnostics = - new DeferredRenderFrameDiagnosticsSource(); - var devToolsPlayerModeCommands = - new DeferredDevToolsPlayerModeCommands(); - var devToolsFacts = new DevToolsRuntimeFacts( - _localPlayerMode, - _playerControllerSlot, - hostInputCamera.CameraController, - devToolsWorldEntities, - _animatedEntities, - _debugVmRenderFacts, - _physicsEngine, - _worldSceneDebugState, - _renderRange, - hostInputCamera.CameraPointerInput, - _worldEnvironment, - Lighting, - contentEffectsAudio.ParticleSystem, - devToolsFrameDiagnostics); - IRuntimeKeyBindingTarget? keyBindingTarget = - hostInputCamera.InputDispatcher is { } settingsDispatcher - ? new RuntimeKeyBindingTarget(settingsDispatcher) - : null; - optionalDevTools = new SettingsDevToolsOptionalDependencies( - devToolsFacts, - keyBindingTarget, - devToolsWorldEntities, - devToolsFrameDiagnostics, - devToolsPlayerModeCommands); - } - SettingsDevToolsResult settingsDevTools = - new SettingsDevToolsCompositionPhase( - new SettingsDevToolsDependencies( - _window!, - _runtimeSettings, - new RuntimeSettingsStartupTargets( - new SilkRuntimeDisplayWindowTarget(_window!), - _displayFramePacing, + this).Compose(platformResult, hostInputCamera), + (platformResult, hostInputCamera, contentEffectsAudio) => + { + SettingsDevToolsOptionalDependencies? optionalDevTools = null; + if (DevToolsEnabled) + { + var devToolsWorldEntities = + new DeferredCanonicalWorldEntityCountSource(); + var devToolsFrameDiagnostics = + new DeferredRenderFrameDiagnosticsSource(); + var devToolsPlayerModeCommands = + new DeferredDevToolsPlayerModeCommands(); + var devToolsFacts = new DevToolsRuntimeFacts( + _localPlayerMode, + _playerControllerSlot, hostInputCamera.CameraController, - contentEffectsAudio.Audio?.Engine), - _hostQuiescence, - Chat, - Combat, - LocalPlayer, - optionalDevTools, - _runtimeDiagnosticCommands, - _combatFeedback, - _keyBindings, - _frameProfiler, - _framebufferResize, - Console.WriteLine), - this).Compose(platform, hostInputCamera, contentEffectsAudio); - Action? compositionToast = settingsDevTools.DevTools is { } composedDevTools - ? text => composedDevTools.Debug.AddToast(text) - : null; + devToolsWorldEntities, + _animatedEntities, + _debugVmRenderFacts, + _physicsEngine, + _worldSceneDebugState, + _renderRange, + hostInputCamera.CameraPointerInput, + _worldEnvironment, + Lighting, + contentEffectsAudio.ParticleSystem, + devToolsFrameDiagnostics); + IRuntimeKeyBindingTarget? keyBindingTarget = + hostInputCamera.InputDispatcher is { } settingsDispatcher + ? new RuntimeKeyBindingTarget(settingsDispatcher) + : null; + optionalDevTools = new SettingsDevToolsOptionalDependencies( + devToolsFacts, + keyBindingTarget, + devToolsWorldEntities, + devToolsFrameDiagnostics, + devToolsPlayerModeCommands); + } - const uint initialCenterLandblockId = 0xA9B4FFFFu; - WorldRenderResult worldRender = new WorldRenderCompositionPhase( - new WorldRenderDependencies( - _worldEnvironment, - _renderResourceLifetime, - _gpuFrameFlights!, - initialCenterLandblockId, - Console.WriteLine), - this).Compose(platform, contentEffectsAudio, settingsDevTools); - string shadersDir = worldRender.Foundation.ShadersDirectory; - Console.WriteLine( - $"loading world view centered on " + - $"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}"); - - InteractionRetainedUiResult interactionUi = - new InteractionRetainedUiCompositionPhase( - new InteractionRetainedUiDependencies( + return new SettingsDevToolsCompositionPhase( + new SettingsDevToolsDependencies( + _window!, + _runtimeSettings, + new RuntimeSettingsStartupTargets( + new SilkRuntimeDisplayWindowTarget(_window!), + _displayFramePacing, + hostInputCamera.CameraController, + contentEffectsAudio.Audio?.Engine), + _hostQuiescence, + Chat, + Combat, + LocalPlayer, + optionalDevTools, + _runtimeDiagnosticCommands, + _combatFeedback, + _keyBindings, + _frameProfiler, + _framebufferResize, + Console.WriteLine), + this).Compose(platformResult, hostInputCamera, contentEffectsAudio); + }, + (platformResult, contentEffectsAudio, settingsDevTools) => + { + const uint initialCenterLandblockId = 0xA9B4FFFFu; + WorldRenderResult worldRender = new WorldRenderCompositionPhase( + new WorldRenderDependencies( + _worldEnvironment, + _renderResourceLifetime, + _gpuFrameFlights!, + initialCenterLandblockId, + Console.WriteLine), + this).Compose(platformResult, contentEffectsAudio, settingsDevTools); + Console.WriteLine( + $"loading world view centered on " + + $"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}"); + return worldRender; + }, + (platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) => + { + Action? compositionToast = settingsDevTools.DevTools is { } devTools + ? text => devTools.Debug.AddToast(text) + : null; + return new InteractionRetainedUiCompositionPhase( + new InteractionRetainedUiDependencies( _options, - platform.Graphics, + platformResult.Graphics, _window!, - platform.Input, - shadersDir, + platformResult.Input, + worldRender.Foundation.ShadersDirectory, contentEffectsAudio.Dats, _datLock, worldRender.Foundation.TextureCache, @@ -1252,16 +1263,26 @@ public sealed class GameWindow : Console.WriteLine), _retailUiLease, this).Compose( + platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender); - - LivePresentationResult livePresentation = - new LivePresentationCompositionPhase( - new LivePresentationDependencies( + }, + (platformResult, + hostInputCamera, + contentEffectsAudio, + settingsDevTools, + worldRender, + interactionUi) => + { + Action? compositionToast = settingsDevTools.DevTools is { } devTools + ? text => devTools.Debug.AddToast(text) + : null; + return new LivePresentationCompositionPhase( + new LivePresentationDependencies( _options, - platform.Graphics, + platformResult.Graphics, _window!, _datLock, _runtimeSettings, @@ -1299,13 +1320,20 @@ public sealed class GameWindow : _uiFrameDiagnostics, compositionToast), this).Compose( + platformResult, hostInputCamera, contentEffectsAudio, + settingsDevTools, worldRender, interactionUi); - - SessionPlayerResult sessionPlayer = - new SessionPlayerCompositionPhase( + }, + (hostInputCamera, + contentEffectsAudio, + settingsDevTools, + worldRender, + interactionUi, + livePresentation) => + new SessionPlayerCompositionPhase( new SessionPlayerDependencies( _options, _window!, @@ -1371,58 +1399,65 @@ public sealed class GameWindow : this).Compose( hostInputCamera, contentEffectsAudio, + settingsDevTools, worldRender, interactionUi, - livePresentation); - - FrameRootResult frameRoots = new FrameRootCompositionPhase( - new FrameRootDependencies( - _options, - _gl!, - _window!, - _input!, - WorldTime, - Weather, - Lighting, - _worldEnvironment, - _physicsEngine, - _cellVisibility, - _localPlayerMode, - _localPlayerIdentity, - _chaseCameraInput, - _playerControllerSlot, - _liveWorldOrigin, - _particleVisibility, - _effectPoses, - _renderRange, - _runtimeSettings, - _displayFramePacing, - _worldSceneDebugState, - _retailAlphaQueue, - _frameProfiler, - _frameDiag, - _renderDiagnosticLog, - _debugVmRenderFacts, - _inputCapture, - _cameraInput, - _selection, - _animatedEntities, - _updateFrameClock, - Combat, - _frameGraphs, - Console.WriteLine), - this).Compose( + livePresentation), + (platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender, interactionUi, livePresentation, - sessionPlayer); - - new SessionStartCompositionPhase( - new SessionStartDependencies(_options, Console.WriteLine)) - .Start(frameRoots); + sessionPlayer) => new FrameRootCompositionPhase( + new FrameRootDependencies( + _options, + platformResult.Graphics, + _window!, + platformResult.Input, + WorldTime, + Weather, + Lighting, + _worldEnvironment, + _physicsEngine, + _cellVisibility, + _localPlayerMode, + _localPlayerIdentity, + _chaseCameraInput, + _playerControllerSlot, + _liveWorldOrigin, + _particleVisibility, + _effectPoses, + _renderRange, + _runtimeSettings, + _displayFramePacing, + _worldSceneDebugState, + _retailAlphaQueue, + _frameProfiler, + _frameDiag, + _renderDiagnosticLog, + _debugVmRenderFacts, + _inputCapture, + _cameraInput, + _selection, + _animatedEntities, + _updateFrameClock, + Combat, + _frameGraphs, + Console.WriteLine), + this).Compose( + platformResult, + hostInputCamera, + contentEffectsAudio, + settingsDevTools, + worldRender, + interactionUi, + livePresentation, + sessionPlayer), + frameRoots => new SessionStartCompositionPhase( + new SessionStartDependencies(_options, Console.WriteLine)) + .Start(frameRoots)); } private void OnUpdate(double dt) diff --git a/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs b/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs index 15683ee0..540690fe 100644 --- a/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs +++ b/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs @@ -42,6 +42,65 @@ public sealed class GameWindowCompositionPipelineTests Assert.Equal("phase-9", phases.Calls[^1]); } + [Fact] + public void ProductionDelegateEntryUsesTheSamePipelineAndExactResults() + { + var calls = new List(); + var values = Enumerable.Range(0, 9).Select(value => new Token(value)).ToArray(); + + GameWindowCompositionPipeline.Run< + Token, + Token, + Token, + Token, + Token, + Token, + Token, + Token, + Token>( + values[0], + platform => Pass(1, values[1], platform), + (platform, host) => Pass(2, values[2], platform, host), + (platform, host, content) => Pass(3, values[3], platform, host, content), + (platform, content, settings) => Pass(4, values[4], platform, content, settings), + (platform, host, content, settings, world) => + Pass(5, values[5], platform, host, content, settings, world), + (platform, host, content, settings, world, interaction) => + Pass(6, values[6], platform, host, content, settings, world, interaction), + (host, content, settings, world, interaction, live) => + Pass(7, values[7], host, content, settings, world, interaction, live), + (platform, host, content, settings, world, interaction, live, session) => + Pass(8, values[8], platform, host, content, settings, world, interaction, live, session), + frame => + { + Assert.Same(values[8], frame); + calls.Add(9); + }); + + Assert.Equal(Enumerable.Range(1, 9), calls); + + Token Pass(int phase, Token result, params Token[] actual) + { + Token[] expected = phase switch + { + 1 => [values[0]], + 2 => [values[0], values[1]], + 3 => [values[0], values[1], values[2]], + 4 => [values[0], values[2], values[3]], + 5 => [values[0], values[1], values[2], values[3], values[4]], + 6 => [values[0], values[1], values[2], values[3], values[4], values[5]], + 7 => [values[1], values[2], values[3], values[4], values[5], values[6]], + 8 => [values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7]], + _ => throw new ArgumentOutOfRangeException(nameof(phase)), + }; + Assert.Equal(expected.Length, actual.Length); + for (int index = 0; index < expected.Length; index++) + Assert.Same(expected[index], actual[index]); + calls.Add(phase); + return result; + } + } + private sealed record Token(int Phase); private sealed class RecordingPhases(int failingPhase) : @@ -49,10 +108,10 @@ public sealed class GameWindowCompositionPipelineTests IContentEffectsAudioCompositionPhase, ISettingsDevToolsCompositionPhase, IWorldRenderCompositionPhase, - IInteractionUiCompositionPhase, - ILivePresentationCompositionPhase, - ISessionPlayerCompositionPhase, - IFrameRootCompositionPhase, + IInteractionUiCompositionPhase, + ILivePresentationCompositionPhase, + ISessionPlayerCompositionPhase, + IFrameRootCompositionPhase, ISessionStartCompositionPhase { public Token Platform { get; } = new(0); @@ -111,12 +170,14 @@ public sealed class GameWindowCompositionPipelineTests return _world; } - Token IInteractionUiCompositionPhase.Compose( + Token IInteractionUiCompositionPhase.Compose( + Token platform, Token host, Token content, Token settings, Token world) { + Assert.Same(Platform, platform); Assert.Same(_host, host); Assert.Same(_content, content); Assert.Same(_settings, settings); @@ -125,29 +186,35 @@ public sealed class GameWindowCompositionPipelineTests return _interaction; } - Token ILivePresentationCompositionPhase.Compose( + Token ILivePresentationCompositionPhase.Compose( + Token platform, Token host, Token content, + Token settings, Token world, Token interaction) { + Assert.Same(Platform, platform); Assert.Same(_host, host); Assert.Same(_content, content); + Assert.Same(_settings, settings); Assert.Same(_world, world); Assert.Same(_interaction, interaction); Record(6); return _live; } - Token ISessionPlayerCompositionPhase.Compose( + Token ISessionPlayerCompositionPhase.Compose( Token host, Token content, + Token settings, Token world, Token interaction, Token live) { Assert.Same(_host, host); Assert.Same(_content, content); + Assert.Same(_settings, settings); Assert.Same(_world, world); Assert.Same(_interaction, interaction); Assert.Same(_live, live); @@ -155,7 +222,8 @@ public sealed class GameWindowCompositionPipelineTests return _session; } - Token IFrameRootCompositionPhase.Compose( + Token IFrameRootCompositionPhase.Compose( + Token platform, Token host, Token content, Token settings, @@ -164,6 +232,7 @@ public sealed class GameWindowCompositionPipelineTests Token live, Token session) { + Assert.Same(Platform, platform); Assert.Same(_host, host); Assert.Same(_content, content); Assert.Same(_settings, settings); diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index a5484285..af545f9a 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -52,22 +52,21 @@ public sealed class GameWindowSlice8BoundaryTests AssertAppearsInOrder( body, "GameWindowPlatformResult platform = AcquirePlatform();", + "GameWindowCompositionPipeline.Run<", "new HostInputCameraCompositionPhase(", - "this).Compose(platform);", + "this).Compose(platformResult),", "new ContentEffectsAudioCompositionPhase(", - "this).Compose(platform, hostInputCamera);", + "this).Compose(platformResult, hostInputCamera),", "new SettingsDevToolsCompositionPhase(", - "this).Compose(platform, hostInputCamera, contentEffectsAudio);", - "InteractionRetainedUiResult interactionUi =", + "this).Compose(platformResult, hostInputCamera, contentEffectsAudio);", "new InteractionRetainedUiCompositionPhase(", "this).Compose(", - "LivePresentationResult livePresentation =", "new LivePresentationCompositionPhase(", "this).Compose(", "new SessionPlayerCompositionPhase(", - "FrameRootResult frameRoots = new FrameRootCompositionPhase(", + "new FrameRootCompositionPhase(", "new SessionStartCompositionPhase(", - ".Start(frameRoots);"); + ".Start(frameRoots));"); string framePhase = File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -98,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests "_publication.PublishSessionPlayer(result);"); Assert.Equal(1, CountOccurrences( body, - ".Start(frameRoots);")); + ".Start(frameRoots));")); string phaseOne = Slice( File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -131,9 +130,9 @@ public sealed class GameWindowSlice8BoundaryTests "case LiveSessionStartStatus.MissingCredentials:", "case LiveSessionStartStatus.Failed:"); int start = body.IndexOf( - ".Start(frameRoots);", + ".Start(frameRoots));", StringComparison.Ordinal); - string postStart = body[(start + ".Start(frameRoots);".Length)..]; + string postStart = body[(start + ".Start(frameRoots));".Length)..]; Assert.Empty(System.Text.RegularExpressions.Regex.Matches( postStart, @"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)")); @@ -174,7 +173,7 @@ public sealed class GameWindowSlice8BoundaryTests source, StringComparison.Ordinal); Assert.Contains( - "new WorldRenderDependencies(\n _worldEnvironment,", + "new WorldRenderDependencies(\n _worldEnvironment,", load.Replace("\r\n", "\n", StringComparison.Ordinal), StringComparison.Ordinal); string worldCompose = Slice( @@ -350,13 +349,14 @@ public sealed class GameWindowSlice8BoundaryTests "Window.Create(options)"); AssertAppearsInOrder( load, + "GameWindowCompositionPipeline.Run<", "new SettingsDevToolsCompositionPhase(", - "this).Compose(platform, hostInputCamera, contentEffectsAudio);", + "this).Compose(platformResult, hostInputCamera, contentEffectsAudio);", "new WorldRenderCompositionPhase(", "new SessionPlayerCompositionPhase(", "new FrameRootCompositionPhase(", "new SessionStartCompositionPhase(", - ".Start(frameRoots);"); + ".Start(frameRoots));"); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src",