From 3628aeb520b6f952687e05a06bc97815f4aff255 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 22 Jul 2026 19:02:08 +0200 Subject: [PATCH] refactor(app): compose atomic frame roots Move the complete update/render construction graph into a typed Phase-8 owner, explicitly carry the content dependencies it consumes, and publish both roots through one exact lease. Extract lifecycle resource sampling and frame-owned late bindings so partial startup and shutdown withdraw the same generation without window callbacks. Co-authored-by: Codex --- docs/ISSUES.md | 6 +- docs/architecture/code-structure.md | 14 +- docs/plans/2026-04-11-roadmap.md | 8 +- docs/plans/2026-05-12-milestones.md | 10 +- ...lice-8-checkpoint-i-ordered-composition.md | 8 +- ...checkpoint-i8-frame-roots-session-start.md | 12 +- ...amewindow-slice-8-composition-lifecycle.md | 4 +- .../Composition/FrameRootComposition.cs | 516 ++++++++++++++++++ .../GameWindowCompositionPipeline.cs | 16 +- .../WorldLifecycleResourceSnapshotSource.cs | 119 ++++ .../Rendering/GameFrameGraphSlot.cs | 34 ++ src/AcDream.App/Rendering/GameWindow.cs | 376 +++---------- .../Composition/FrameRootCompositionTests.cs | 126 +++++ .../GameWindowCompositionPipelineTests.cs | 6 +- .../GameWindowRenderLeafCompositionTests.cs | 41 +- .../GameWindowSlice8BoundaryTests.cs | 44 +- .../Rendering/RetailPViewPassExecutorTests.cs | 8 +- .../Rendering/RuntimeResourceSlotTests.cs | 22 + .../Rendering/WorldRenderFrameBuilderTests.cs | 17 +- .../Rendering/WorldSceneRendererTests.cs | 8 +- .../World/UpdateFrameOrchestratorTests.cs | 31 +- 21 files changed, 1052 insertions(+), 374 deletions(-) create mode 100644 src/AcDream.App/Composition/FrameRootComposition.cs create mode 100644 src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs create mode 100644 tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 4853ad6e..20ddaeea 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -34,8 +34,10 @@ What does NOT go here: I.7 now owns streaming/session/hydration/local-player/teleport startup and every late edge explicitly. I.8a now owns the focused session factory, command targets, and sole gameplay input subscriber before frame publication. - `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration - handoffs and the class is 2,149 raw lines. I.8b–I.9 plus the remaining + I.8b now owns the complete update/render construction, exact atomic frame + publication, and lifecycle resource snapshot source. `GameWindow.OnUpdate` + and `GameWindow.OnRender` are typed orchestration handoffs and the class is + 1,919 raw lines. I.8c–I.9 plus the remaining shutdown/closeout cleanup is active in [`docs/architecture/code-structure.md`](architecture/code-structure.md). diff --git a/docs/architecture/code-structure.md b/docs/architecture/code-structure.md index 4b46210c..98f2df11 100644 --- a/docs/architecture/code-structure.md +++ b/docs/architecture/code-structure.md @@ -736,12 +736,14 @@ local-player/teleport construction phase and every late edge through named exact-owner tokens; the former 432-line inline body and spawn-claim memo are gone. I.8a moves the exact live-session reset/router graph, combat and diagnostic command targets, and sole gameplay input subscriber into Phase 7 -before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8b–L -remain active, with atomic frame-root publication next. The App gate passes -3,424 tests / 3 intentional skips and the complete Release suite passes 7,796 -tests / 5 intentional skips. The clean solution build retains only the 17 -test-project warnings tracked by #228; the I.8a behavior, architecture, and -adversarial reviews are clean. +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,919 raw lines. I.8c–L remain active, with terminal session start next. The +App gate passes 3,429 tests / 3 intentional skips and the complete Release suite +passes 7,801 tests / 5 intentional skips. The clean solution build retains only +the 17 test-project warnings tracked by #228; the I.8b corrected-diff review is +clean. ### 4.4 Exit criteria diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index bdf31e09..d94d8760 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -104,11 +104,11 @@ 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 2,149 raw lines after I.8a, which composes the session factory, command -targets, and sole gameplay input subscriber before frame publication. I.8b–I.9, +now 1,919 raw lines after I.8b, which also composes and atomically publishes the +complete update/render pair and extracts lifecycle resource sampling. I.8c–I.9, shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The -App gate passes 3,424 tests / 3 intentional skips, the complete Release suite -passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean. +App gate passes 3,429 tests / 3 intentional skips, the complete Release suite +passes 7,801 tests / 5 skips, and the I.8b 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 a4bb7c4d..6099b225 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -60,11 +60,13 @@ publishers, and portal/sky/particle resources have transactional phase ownership and exact-owner late bindings. I.7 now owns the complete streaming, session, hydration, local-player, combat, and teleport startup graph, including named reversible late edges. I.8a adds the focused live-session runtime factory -and attaches command/input owners before frame publication. `GameWindow` is -2,149 raw lines. I.8b–I.9 and checkpoints J–L remain active. Issue #232 tracks +and attaches command/input owners before frame publication. I.8b composes and +atomically publishes the complete frame pair and extracts lifecycle resource +sampling. `GameWindow` is 1,919 raw lines. I.8c–I.9 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,424 tests / 3 skips, the complete Release suite passes -7,796 tests / 5 skips, and the I.8a corrected-diff review is clean. +The App gate passes 3,429 tests / 3 skips, the complete Release suite passes +7,801 tests / 5 skips, and the I.8b 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 8e567ba5..7987ad4a 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 @@ -294,9 +294,11 @@ ledger only after the complete pipeline is live. detailed session-binding, atomic-root, and terminal-start plan is [`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md). I.8a is complete: the session factory, command targets, and sole gameplay - input subscriber now belong to Phase 7 and publish before any frame root; - `GameWindow.cs` is 2,149 raw lines. I.8b atomic frame-root composition is the - active cut. + input subscriber now belong to Phase 7 and publish before any frame root. + I.8b composes the unchanged update/render graph in a focused phase, publishes + the pair through an exact owned slot lease, and moves lifecycle resource + sampling out of the window. `GameWindow.cs` is 1,919 raw lines. I.8c terminal + session start is the active cut. Checkpoint I remains active until I.9 closes the complete nine-phase pipeline. ## 6. Automated acceptance diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md index bf225c3d..8e300d61 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md @@ -1,6 +1,6 @@ # GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start -**Status:** Active — I.8a complete; I.8b frame-root composition next +**Status:** Active — I.8a–I.8b complete; I.8c terminal start next **Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md` @@ -81,6 +81,16 @@ Phase-7 suffix in reverse order. ## 3. I.8b — atomic frame-root composition +**Completed 2026-07-22.** `FrameRootCompositionPhase` now constructs the +unchanged render and update graphs from explicit prior-phase results and +focused runtime owners. `GameFrameGraphSlot.PublishOwned` publishes and +withdraws one exact pair. Optional lifecycle automation has a Phase-8 binding +owner, and `WorldLifecycleResourceSnapshotSource` samples the same canonical +owners without window callbacks or mirrored counters. The Phase-8 contract now +receives the Phase-2 content result explicitly because scripts and particles +are real frame dependencies; this removes a hidden reach-back through the +window shell. + Add `FrameRootCompositionPhase` implementing the existing `IFrameRootCompositionPhase` contract. Its dependencies are focused App owners; prior phase results supply all resources created during startup. 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 5f919468..202d6d33 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 @@ -29,8 +29,8 @@ audit. - [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.7 and I.8a are complete; - I.8b atomic frame-root publication is active. Detailed plan: + transactional partial-acquisition rollback. I.1–I.8b are complete; I.8c + terminal session start is active. 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. diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs new file mode 100644 index 00000000..1b047cf1 --- /dev/null +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -0,0 +1,516 @@ +using AcDream.App.Diagnostics; +using AcDream.App.Input; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Vfx; +using AcDream.App.Settings; +using AcDream.App.Update; +using AcDream.App.World; +using AcDream.Core.Combat; +using AcDream.Core.Lighting; +using AcDream.Core.Physics; +using AcDream.Core.Selection; +using AcDream.Core.World; +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +namespace AcDream.App.Composition; + +internal sealed record FrameRootDependencies( + RuntimeOptions Options, + GL Gl, + IWindow Window, + IInputContext Input, + WorldTimeService WorldTime, + WeatherSystem Weather, + LightManager Lighting, + WorldEnvironmentController WorldEnvironment, + PhysicsEngine PhysicsEngine, + CellVisibility CellVisibility, + LocalPlayerModeState PlayerMode, + LocalPlayerIdentityState PlayerIdentity, + ChaseCameraInputState ChaseCameraInput, + LocalPlayerControllerSlot PlayerController, + LiveWorldOriginState WorldOrigin, + ParticleVisibilityController ParticleVisibility, + EntityEffectPoseRegistry EffectPoses, + WorldRenderRangeState RenderRange, + RuntimeSettingsController Settings, + DisplayFramePacingController DisplayFramePacing, + WorldSceneDebugState WorldSceneDebugState, + RetailAlphaQueue RetailAlphaQueue, + FrameProfiler FrameProfiler, + bool FrameDiagnosticsEnabled, + IRenderFrameDiagnosticLog RenderDiagnosticLog, + DebugVmRenderFactsPublisher DebugVmRenderFacts, + IInputCaptureSource InputCapture, + DispatcherCameraInputSource CameraInput, + SelectionState Selection, + LiveEntityAnimationRuntimeView Animations, + UpdateFrameClock UpdateClock, + CombatState Combat, + GameFrameGraphSlot FrameGraphs, + Action Log); + +internal sealed record FrameRootResult( + UpdateFrameOrchestrator Update, + RenderFrameOrchestrator Render, + WorldLifecycleAutomationController? LifecycleAutomation, + FrameRootRuntimeBindings RuntimeBindings, + IDisposable FrameGraphPublication, + AcDream.App.Net.LiveSessionHost SessionHost); + +internal interface IGameWindowFrameRootPublication +{ + void PublishFrameRoots(FrameRootResult result); +} + +internal enum FrameRootCompositionPoint +{ + RenderResourcesCreated, + WorldRendererCreated, + LifecycleAutomationBound, + RenderRootCreated, + UpdateRootCreated, + FrameGraphPublished, + ResultPublished, +} + +/// +/// Exact late edges created with the frame roots. Completed detach operations +/// are removed immediately so shutdown retry never replays them. +/// +internal sealed class FrameRootRuntimeBindings : IDisposable +{ + private readonly List<(string Name, IDisposable Binding)> _bindings = []; + private bool _deactivationStarted; + + public void Adopt(string name, IDisposable binding) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(binding); + ObjectDisposedException.ThrowIf(_deactivationStarted, this); + _bindings.Add((name, binding)); + } + + public void Dispose() + { + if (_deactivationStarted && _bindings.Count == 0) + return; + _deactivationStarted = true; + + List? failures = null; + for (int i = _bindings.Count - 1; i >= 0; i--) + { + (string name, IDisposable binding) = _bindings[i]; + try + { + binding.Dispose(); + _bindings.RemoveAt(i); + } + catch (Exception failure) + { + (failures ??= []).Add(new InvalidOperationException( + $"Frame-root binding '{name}' did not detach.", + failure)); + } + } + + if (failures is not null) + { + throw new AggregateException( + "Frame-root binding cleanup remains incomplete.", + failures); + } + } +} + +internal sealed class FrameRootCompositionPhase + : IFrameRootCompositionPhase< + HostInputCameraResult, + ContentEffectsAudioResult, + SettingsDevToolsResult, + WorldRenderResult, + InteractionRetainedUiResult, + LivePresentationResult, + SessionPlayerResult, + FrameRootResult> +{ + private readonly FrameRootDependencies _dependencies; + private readonly IGameWindowFrameRootPublication _publication; + private readonly Action? _faultInjection; + + public FrameRootCompositionPhase( + FrameRootDependencies dependencies, + IGameWindowFrameRootPublication publication, + Action? faultInjection = null) + { + _dependencies = dependencies + ?? throw new ArgumentNullException(nameof(dependencies)); + _publication = publication + ?? throw new ArgumentNullException(nameof(publication)); + _faultInjection = faultInjection; + } + + public FrameRootResult Compose( + HostInputCameraResult host, + ContentEffectsAudioResult content, + SettingsDevToolsResult settings, + WorldRenderResult world, + InteractionRetainedUiResult interaction, + LivePresentationResult live, + SessionPlayerResult session) + { + ArgumentNullException.ThrowIfNull(host); + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(settings); + ArgumentNullException.ThrowIfNull(world); + ArgumentNullException.ThrowIfNull(interaction); + ArgumentNullException.ThrowIfNull(live); + ArgumentNullException.ThrowIfNull(session); + + var scope = new CompositionAcquisitionScope(); + FrameRootRuntimeBindings? bindings = null; + bool bindingsOwnedByScope = false; + try + { + FrameRootResult result = ComposeCore( + host, + content, + settings, + world, + interaction, + live, + session, + scope, + ref bindings, + ref bindingsOwnedByScope); + scope.Complete(); + return result; + } + catch (Exception failure) + { + if (bindings is not null && !bindingsOwnedByScope) + { + scope.Own( + "frame-root runtime bindings", + bindings, + static value => value.Dispose()); + } + + scope.RollbackAndThrow(failure); + throw new System.Diagnostics.UnreachableException(); + } + } + + private FrameRootResult ComposeCore( + HostInputCameraResult host, + ContentEffectsAudioResult content, + SettingsDevToolsResult settings, + WorldRenderResult world, + InteractionRetainedUiResult interaction, + LivePresentationResult live, + SessionPlayerResult session, + CompositionAcquisitionScope scope, + ref FrameRootRuntimeBindings? bindings, + ref bool bindingsOwnedByScope) + { + FrameRootDependencies d = _dependencies; + WorldRenderFoundation foundation = world.Foundation; + var teleportRenderState = + new LocalPlayerTeleportRenderStateSource(session.LocalTeleport); + var renderLoginState = new RenderLoginStateSource( + d.Options.LiveMode, + d.PlayerMode); + var renderFrameGlState = new RenderFrameGlStateController( + new SilkRenderFrameGlStateApi(d.Gl)); + var renderFrameLivePreparation = + new RuntimeRenderFrameLivePreparation( + foundation.TextureCache, + foundation.MeshAdapter, + session.WorldReveal, + teleportRenderState, + renderLoginState, + new LiveLoginRevealCellSource( + live.LiveEntities, + d.PlayerIdentity), + live.ParticleRenderer, + d.FrameProfiler, + d.FrameDiagnosticsEnabled); + var renderFrameResources = new RenderFrameResourceController( + host.GpuFrameFlights, + new RuntimeRenderFrameBeginResources( + foundation.TextureCache, + live.DrawDispatcher, + live.EnvCellRenderer, + live.PortalDepthMask, + foundation.TextRenderer, + interaction.RetainedUi?.Host.TextRenderer, + live.ClipFrame, + foundation.Terrain, + foundation.SceneLighting, + d.FrameProfiler, + d.Gl), + new RuntimeRenderFrameClearPhase( + d.Gl, + d.WorldTime, + d.Weather, + teleportRenderState, + d.ParticleVisibility, + host.WorldRenderDiagnostics, + renderFrameGlState), + renderFrameLivePreparation); + Fault(FrameRootCompositionPoint.RenderResourcesCreated); + + var renderWeatherFrame = new RenderWeatherFrameController( + d.WorldTime, + d.Weather); + var skyPesFrame = new SkyPesFrameController( + content.ScriptRunner, + content.ParticleSink, + d.EffectPoses, + live.EntityEffects); + var worldFrameEnvironment = + new RuntimeWorldFrameEnvironmentPreparation( + d.Options, + d.WorldTime, + d.Lighting, + live.DrawDispatcher, + live.EnvCellRenderer, + foundation.SceneLighting, + d.RenderRange, + skyPesFrame); + var worldRenderFrameBuilder = new WorldRenderFrameBuilder( + new RuntimeWorldFrameCameraSource( + host.CameraController, + session.LocalTeleport), + new RuntimeWorldFrameVisibilityPreparation( + live.SelectionScene, + d.ParticleVisibility, + foundation.Terrain, + session.WorldReveal, + live.EnvCellFrustum), + new RuntimeWorldFrameSettingsPreview( + d.Settings, + content.Audio?.Engine, + host.CameraController, + d.DisplayFramePacing), + new RuntimeWorldFrameRootSource( + d.PhysicsEngine, + d.CellVisibility, + d.PlayerMode, + d.ChaseCameraInput, + d.PlayerController, + d.WorldOrigin), + worldFrameEnvironment, + new RuntimeWorldFrameAnimatedEntitySource( + d.Animations, + live.StaticAnimationScheduler, + live.EquippedChildren), + new RuntimeWorldFrameBuildingSource( + live.LandblockPipeline, + d.CellVisibility)); + var terrainDrawDiagnostics = new TerrainDrawDiagnosticsController( + d.FrameDiagnosticsEnabled, + host.WorldRenderDiagnostics, + new RuntimeFramePipelineDiagnosticFactsSource( + foundation.Terrain, + live.LandblockPipeline, + renderFrameLivePreparation, + live.DrawDispatcher, + session.Streaming, + live.LiveEntities, + live.WorldState), + d.RenderDiagnosticLog); + var retailPViewCells = new RetailPViewCellSource(d.CellVisibility); + var retailPViewPassExecutor = new RetailPViewPassExecutor( + d.Gl, + renderFrameGlState, + new SilkRetailPViewFramebufferSource(d.Window), + live.ClipFrame, + foundation.Terrain, + live.EnvCellRenderer, + live.DrawDispatcher, + live.SkyRenderer, + content.ParticleSystem, + live.ParticleRenderer, + live.PortalDepthMask, + d.RetailAlphaQueue, + host.WorldRenderDiagnostics, + terrainDrawDiagnostics); + var worldSceneDiagnostics = new WorldSceneDiagnosticsController( + host.WorldRenderDiagnostics, + new RuntimeWorldScenePViewDiagnosticSource( + d.PlayerController, + d.PhysicsEngine, + d.CellVisibility), + d.WorldSceneDebugState, + foundation.DebugLines, + d.PhysicsEngine, + d.PlayerMode, + d.PlayerController, + d.DebugVmRenderFacts, + settings.DevTools is not null); + var worldScenePasses = new WorldScenePassExecutor( + d.Gl, + renderFrameGlState, + live.ClipFrame, + live.DrawDispatcher, + live.EnvCellRenderer, + foundation.Terrain, + terrainDrawDiagnostics, + live.SkyRenderer, + content.ParticleSystem, + live.ParticleRenderer); + var worldSceneRenderer = new WorldSceneRenderer( + renderFrameResources, + renderLoginState, + d.WorldEnvironment, + worldRenderFrameBuilder, + new RuntimeWorldSceneEntitySource(live.WorldState), + live.SelectionScene, + d.RetailAlphaQueue, + d.ParticleVisibility, + new WorldScenePViewRenderer( + new RetailPViewRenderer(), + retailPViewPassExecutor, + retailPViewPassExecutor), + retailPViewCells, + worldScenePasses, + d.RenderRange, + worldSceneDiagnostics); + Fault(FrameRootCompositionPoint.WorldRendererCreated); + + bindings = new FrameRootRuntimeBindings(); + WorldLifecycleAutomationController? lifecycleAutomation = null; + if (interaction.RetainedUi?.Screenshots is { } screenshots + && d.Options.AutomationArtifactDirectory is { } artifactDirectory) + { + var resourceSnapshots = + new WorldLifecycleResourceSnapshotSource( + live.WorldState, + d.Animations, + live.FrameDiagnostics, + live.LiveEntities, + session.Streaming, + content.ParticleSystem, + content.ParticleSink, + live.EntityEffects, + live.Lights, + content.ScriptRunner, + foundation.MeshAdapter, + foundation.TextureCache, + live.DrawDispatcher, + d.FrameProfiler); + lifecycleAutomation = + new WorldLifecycleAutomationController( + () => session.WorldReveal.Snapshot, + () => session.WorldReveal.PortalMaterializationCount, + resourceSnapshots.Capture, + screenshots, + artifactDirectory, + message => d.Log("[UI-PROBE] " + message)); + bindings.Adopt( + "world lifecycle automation", + interaction.LateBindings.Automation.Bind( + lifecycleAutomation)); + } + Fault(FrameRootCompositionPoint.LifecycleAutomationBound); + + IRetainedGameplayUiFrame? retainedGameplayUi = + d.Options.RetailUi && interaction.RetainedUi is { } retained + ? new RetainedGameplayUiFrame(retained.Runtime, d.Input) + : null; + IPrivateFrameScreenshot? privateScreenshot = + interaction.RetainedUi?.Screenshots is { } frameScreenshots + ? new PrivateFrameScreenshot(frameScreenshots) + : null; + var privatePresentation = new PrivatePresentationRenderer( + new LocalPlayerPortalViewport( + session.LocalTeleport, + host.CameraController), + renderFrameResources, + live.PaperdollPresenter, + retainedGameplayUi, + settings.DevTools?.Presenter, + privateScreenshot); + var framePreparation = new RenderFramePreparationController( + renderFrameResources, + settings.DevTools?.Presenter, + renderWeatherFrame); + var renderFrame = new RenderFrameOrchestrator( + host.GpuFrameFlights, + framePreparation, + worldSceneRenderer, + privatePresentation, + live.FrameDiagnostics, + (IRenderFrameFailureRecovery?)settings.DevTools?.Presenter + ?? NullRenderFrameFailureRecovery.Instance); + Fault(FrameRootCompositionPoint.RenderRootCreated); + + var liveFrameCoordinator = new RetailLiveFrameCoordinator( + session.LiveObjectFrame, + live.WorldState, + session.LiveSession, + session.LocalPlayerFrame, + session.LiveSpatialReconciler); + var cameraFrame = new CameraFrameController( + host.CameraController, + d.InputCapture, + d.CameraInput, + session.LocalPlayerFrameRuntime, + d.ChaseCameraInput, + session.LocalPlayerFrame, + session.LiveSpatialReconciler, + new AcDream.App.Combat.CombatCameraTargetSource( + d.Settings, + d.Combat, + d.Selection, + live.SelectionQuery)); + var updateFrame = new UpdateFrameOrchestrator( + new LiveEntityTeardownFramePhase(live.LiveEntities), + new ConsoleUpdateFrameFailureSink(), + d.UpdateClock, + new PhysicsScriptClockPublisher(content.ScriptRunner), + session.StreamingFrame, + session.GameplayInput, + liveFrameCoordinator, + new LiveEntityLivenessFramePhase( + session.Liveness, + new StopwatchClientMonotonicTimeSource()), + session.LocalTeleport, + new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry), + cameraFrame); + Fault(FrameRootCompositionPoint.UpdateRootCreated); + + var bindingsLease = scope.Own( + "frame-root runtime bindings", + bindings, + static value => value.Dispose()); + bindingsOwnedByScope = true; + IDisposable frameGraphPublication = d.FrameGraphs.PublishOwned( + updateFrame, + renderFrame); + var graphLease = scope.Own( + "game frame graph publication", + frameGraphPublication, + static value => value.Dispose()); + Fault(FrameRootCompositionPoint.FrameGraphPublished); + + var result = new FrameRootResult( + updateFrame, + renderFrame, + lifecycleAutomation, + bindings, + frameGraphPublication, + session.SessionHost); + _publication.PublishFrameRoots(result); + graphLease.Transfer(); + bindingsLease.Transfer(); + Fault(FrameRootCompositionPoint.ResultPublished); + return result; + } + + private void Fault(FrameRootCompositionPoint point) => + _faultInjection?.Invoke(point); +} diff --git a/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs b/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs index 8a6b835c..b22168e5 100644 --- a/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs +++ b/src/AcDream.App/Composition/GameWindowCompositionPipeline.cs @@ -40,10 +40,11 @@ internal interface ISessionPlayerCompositionPhase +internal interface IFrameRootCompositionPhase { TResult Compose( THost host, + TContent content, TSettings settings, TWorld world, TInteraction interaction, @@ -79,7 +80,7 @@ internal sealed class GameWindowCompositionPipeline< private readonly IInteractionUiCompositionPhase _interaction; private readonly ILivePresentationCompositionPhase _live; private readonly ISessionPlayerCompositionPhase _session; - private readonly IFrameRootCompositionPhase _frame; + private readonly IFrameRootCompositionPhase _frame; private readonly ISessionStartCompositionPhase _start; public GameWindowCompositionPipeline( @@ -90,7 +91,7 @@ internal sealed class GameWindowCompositionPipeline< IInteractionUiCompositionPhase interaction, ILivePresentationCompositionPhase live, ISessionPlayerCompositionPhase session, - IFrameRootCompositionPhase frame, + IFrameRootCompositionPhase frame, ISessionStartCompositionPhase start) { _host = host ?? throw new ArgumentNullException(nameof(host)); @@ -113,7 +114,14 @@ internal sealed class GameWindowCompositionPipeline< 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); - TFrame frame = _frame.Compose(host, settings, world, interaction, live, session); + TFrame frame = _frame.Compose( + host, + content, + settings, + world, + interaction, + live, + session); _start.Start(frame); } } diff --git a/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs b/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs new file mode 100644 index 00000000..4c35aebd --- /dev/null +++ b/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs @@ -0,0 +1,119 @@ +using AcDream.App.Rendering; +using AcDream.App.Rendering.Vfx; +using AcDream.App.Rendering.Wb; +using AcDream.App.Streaming; +using AcDream.App.World; +using AcDream.Core.Vfx; + +namespace AcDream.App.Diagnostics; + +/// +/// Samples the canonical owners used by the connected lifecycle gate. This is +/// deliberately a read-only view: it owns no counters and keeps no mirrored +/// resource state between checkpoints. +/// +internal sealed class WorldLifecycleResourceSnapshotSource +{ + private readonly GpuWorldState _world; + private readonly LiveEntityAnimationRuntimeView + _animations; + private readonly RenderFrameDiagnosticsController _renderDiagnostics; + private readonly LiveEntityRuntime _liveEntities; + private readonly StreamingController _streaming; + private readonly ParticleSystem _particles; + private readonly ParticleHookSink _particleSink; + private readonly EntityEffectController _effects; + private readonly LiveEntityLightController _lights; + private readonly PhysicsScriptRunner _scripts; + private readonly WbMeshAdapter _meshes; + private readonly TextureCache _textures; + private readonly WbDrawDispatcher _dispatcher; + private readonly FrameProfiler _frameProfiler; + + public WorldLifecycleResourceSnapshotSource( + GpuWorldState world, + LiveEntityAnimationRuntimeView animations, + RenderFrameDiagnosticsController renderDiagnostics, + LiveEntityRuntime liveEntities, + StreamingController streaming, + ParticleSystem particles, + ParticleHookSink particleSink, + EntityEffectController effects, + LiveEntityLightController lights, + PhysicsScriptRunner scripts, + WbMeshAdapter meshes, + TextureCache textures, + WbDrawDispatcher dispatcher, + FrameProfiler frameProfiler) + { + _world = world ?? throw new ArgumentNullException(nameof(world)); + _animations = animations + ?? throw new ArgumentNullException(nameof(animations)); + _renderDiagnostics = renderDiagnostics + ?? throw new ArgumentNullException(nameof(renderDiagnostics)); + _liveEntities = liveEntities + ?? throw new ArgumentNullException(nameof(liveEntities)); + _streaming = streaming + ?? throw new ArgumentNullException(nameof(streaming)); + _particles = particles + ?? throw new ArgumentNullException(nameof(particles)); + _particleSink = particleSink + ?? throw new ArgumentNullException(nameof(particleSink)); + _effects = effects ?? throw new ArgumentNullException(nameof(effects)); + _lights = lights ?? throw new ArgumentNullException(nameof(lights)); + _scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); + _meshes = meshes ?? throw new ArgumentNullException(nameof(meshes)); + _textures = textures ?? throw new ArgumentNullException(nameof(textures)); + _dispatcher = dispatcher + ?? throw new ArgumentNullException(nameof(dispatcher)); + _frameProfiler = frameProfiler + ?? throw new ArgumentNullException(nameof(frameProfiler)); + } + + public WorldLifecycleResourceSnapshot Capture() + { + ObjectMeshManager meshManager = _meshes.MeshManager + ?? throw new InvalidOperationException( + "Lifecycle snapshots require the composed modern mesh manager."); + var mesh = meshManager.Diagnostics; + RenderFrameDiagnosticsSnapshot render = _renderDiagnostics.Snapshot; + GCMemoryInfo memory = GC.GetGCMemoryInfo(); + return new WorldLifecycleResourceSnapshot( + LoadedLandblocks: _world.LoadedLandblockIds.Count, + WorldEntities: _world.Entities.Count, + AnimatedEntities: _animations.Count, + VisibleLandblocks: render.VisibleLandblocks, + TotalLandblocks: render.TotalLandblocks, + LiveEntities: _liveEntities.Count, + MaterializedLiveEntities: _liveEntities.MaterializedCount, + PendingLiveTeardowns: _liveEntities.PendingTeardownCount, + PendingLandblockRetirements: _streaming.PendingRetirementCount, + ParticleEmitters: _particles.ActiveEmitterCount, + Particles: _particles.ActiveParticleCount, + ParticleBindings: _particleSink.ActiveBindingCount, + ParticleOwners: _particleSink.TrackedOwnerCount, + EffectOwners: _effects.ReadyOwnerCount, + LightOwners: _lights.TrackedOwnerCount, + ScriptOwners: _scripts.ActiveOwnerCount, + ActiveScripts: _scripts.ActiveScriptCount, + MeshRenderData: mesh.RenderData, + MeshAtlasArrays: mesh.AtlasArrays, + MeshEstimatedBytes: mesh.EstimatedBytes, + StagedMeshUploads: _meshes.StagedUploadBacklog, + StagedMeshBytes: _meshes.StagedUploadBytes, + TrackedGpuBytes: GpuMemoryTracker.AllocatedBytes, + TrackedGpuBuffers: GpuMemoryTracker.BufferCount, + TrackedGpuTextures: GpuMemoryTracker.TextureCount, + OwnedCompositeTextures: _textures.OwnedBindlessTextureCount, + CompositeTextureOwners: _textures.TextureOwnerCount, + ActiveParticleTextures: _textures.ActiveParticleTextureCount, + ParticleTextureOwners: _textures.ParticleTextureOwnerCount, + CompositeWarmupPending: + _dispatcher.LastCompositeWarmupPendingCount, + ManagedBytes: GC.GetTotalMemory(forceFullCollection: false), + ManagedCommittedBytes: memory.TotalCommittedBytes, + Fps: render.Fps, + FrameMilliseconds: render.FrameMilliseconds, + LastFrameProfile: _frameProfiler.LastReport); + } +} diff --git a/src/AcDream.App/Rendering/GameFrameGraphSlot.cs b/src/AcDream.App/Rendering/GameFrameGraphSlot.cs index bde9adc0..f31bcc66 100644 --- a/src/AcDream.App/Rendering/GameFrameGraphSlot.cs +++ b/src/AcDream.App/Rendering/GameFrameGraphSlot.cs @@ -29,12 +29,28 @@ internal sealed class GameFrameGraphSlot public bool IsPublished => Volatile.Read(ref _pair) is not null; public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render) + { + _ = PublishCore(update, render); + } + + public IDisposable PublishOwned( + IGameUpdateFrameRoot update, + IGameRenderFrameRoot render) + { + FrameGraphPair pair = PublishCore(update, render); + return new Publication(this, pair); + } + + private FrameGraphPair PublishCore( + IGameUpdateFrameRoot update, + IGameRenderFrameRoot render) { ArgumentNullException.ThrowIfNull(update); ArgumentNullException.ThrowIfNull(render); var pair = new FrameGraphPair(update, render); if (Interlocked.CompareExchange(ref _pair, pair, null) is not null) throw new InvalidOperationException("A game frame graph is already published."); + return pair; } public bool Tick(UpdateFrameInput input) @@ -64,4 +80,22 @@ internal sealed class GameFrameGraphSlot { Interlocked.Exchange(ref _pair, null); } + + private void Withdraw(FrameGraphPair expected) => + Interlocked.CompareExchange(ref _pair, null, expected); + + private sealed class Publication : IDisposable + { + private GameFrameGraphSlot? _owner; + private readonly FrameGraphPair _expected; + + public Publication(GameFrameGraphSlot owner, FrameGraphPair expected) + { + _owner = owner; + _expected = expected; + } + + public void Dispose() => + Interlocked.Exchange(ref _owner, null)?.Withdraw(_expected); + } } diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e5b35827..0fae2fd9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -22,7 +22,8 @@ public sealed class GameWindow : IGameWindowWorldRenderPublication, IGameWindowInteractionRetainedUiPublication, IGameWindowLivePresentationPublication, - IGameWindowSessionPlayerPublication + IGameWindowSessionPlayerPublication, + IGameWindowFrameRootPublication { private static double ClientTimerNow() => System.Diagnostics.Stopwatch.GetTimestamp() @@ -103,6 +104,8 @@ public sealed class GameWindow : private SessionPlayerRuntimeBindings? _sessionPlayerBindings; private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots; private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation; + private FrameRootRuntimeBindings? _frameRootBindings; + private IDisposable? _frameGraphPublication; private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights; private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameRenderResourceLifetime @@ -1040,6 +1043,23 @@ public sealed class GameWindow : _sessionPlayerBindings = result.RuntimeBindings; } + void IGameWindowFrameRootPublication.PublishFrameRoots( + FrameRootResult result) + { + ArgumentNullException.ThrowIfNull(result); + if (_worldLifecycleAutomation is not null + || _frameRootBindings is not null + || _frameGraphPublication is not null) + { + throw new InvalidOperationException( + "The GameWindow composition shell already owns frame roots."); + } + + _worldLifecycleAutomation = result.LifecycleAutomation; + _frameRootBindings = result.RuntimeBindings; + _frameGraphPublication = result.FrameGraphPublication; + } + private static void PublishCompositionOwner( ref T? destination, T value, @@ -1355,268 +1375,53 @@ public sealed class GameWindow : interactionUi, livePresentation); - var teleportRenderState = - new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource( - sessionPlayer.LocalTeleport); - var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource( - _options.LiveMode, - _localPlayerMode); - var renderFrameGlState = new AcDream.App.Rendering.RenderFrameGlStateController( - new AcDream.App.Rendering.SilkRenderFrameGlStateApi(_gl!)); - var renderFrameLivePreparation = - new AcDream.App.Rendering.RuntimeRenderFrameLivePreparation( - _textureCache, - _wbMeshAdapter, - _worldReveal, - teleportRenderState, - renderLoginState, - new AcDream.App.Rendering.LiveLoginRevealCellSource( - _liveEntities!, - _localPlayerIdentity), - _particleRenderer, - _frameProfiler, - _frameDiag); - var renderFrameResources = - new AcDream.App.Rendering.RenderFrameResourceController( - _gpuFrameFlights!, - new AcDream.App.Rendering.RuntimeRenderFrameBeginResources( - _textureCache, - _wbDrawDispatcher, - _envCellRenderer, - _portalDepthMask, - _textRenderer, - _uiHost?.TextRenderer, - _clipFrame, - _terrain, - _sceneLightingUbo, - _frameProfiler, - _gl!), - new AcDream.App.Rendering.RuntimeRenderFrameClearPhase( - _gl!, - WorldTime, - Weather, - teleportRenderState, - _particleVisibility, - worldRenderDiagnostics, - renderFrameGlState), - renderFrameLivePreparation); - var renderWeatherFrame = - new AcDream.App.Rendering.RenderWeatherFrameController( - WorldTime, - Weather); - var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController( - _scriptRunner!, - _particleSink!, - _effectPoses, - _entityEffects); - var worldFrameEnvironment = - new AcDream.App.Rendering.RuntimeWorldFrameEnvironmentPreparation( + FrameRootResult frameRoots = new FrameRootCompositionPhase( + new FrameRootDependencies( _options, - WorldTime, - Lighting, - _wbDrawDispatcher, - _envCellRenderer, - _sceneLightingUbo, - _renderRange, - skyPesFrame); - var worldRenderFrameBuilder = - new AcDream.App.Rendering.WorldRenderFrameBuilder( - new AcDream.App.Rendering.RuntimeWorldFrameCameraSource( - _cameraController!, - sessionPlayer.LocalTeleport), - new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation( - _retailSelectionScene, - _particleVisibility, - _terrain, - _worldReveal, - _envCellFrustum), - new AcDream.App.Rendering.RuntimeWorldFrameSettingsPreview( - _runtimeSettings, - _audioEngine, - _cameraController!, - _displayFramePacing), - new AcDream.App.Rendering.RuntimeWorldFrameRootSource( - _physicsEngine, - _cellVisibility, - _localPlayerMode, - _chaseCameraInput, - _playerControllerSlot, - _liveWorldOrigin), - worldFrameEnvironment, - new AcDream.App.Rendering.RuntimeWorldFrameAnimatedEntitySource( - _animatedEntities, - _staticAnimationScheduler, - _equippedChildRenderer), - new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource( - _landblockPresentationPipeline!, - _cellVisibility)); - var terrainDrawDiagnostics = - new AcDream.App.Rendering.TerrainDrawDiagnosticsController( - _frameDiag, - worldRenderDiagnostics, - new AcDream.App.Rendering.RuntimeFramePipelineDiagnosticFactsSource( - _terrain, - _landblockPresentationPipeline, - renderFrameLivePreparation, - _wbDrawDispatcher, - _streamingController, - _liveEntities!, - _worldState), - _renderDiagnosticLog); - var retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource( - _cellVisibility); - var retailPViewPassExecutor = - new AcDream.App.Rendering.RetailPViewPassExecutor( _gl!, - renderFrameGlState, - new AcDream.App.Rendering.SilkRetailPViewFramebufferSource( - _window!), - _clipFrame!, - _terrain, - _envCellRenderer!, - _wbDrawDispatcher!, - _skyRenderer, - _particleSystem, - _particleRenderer, - _portalDepthMask, - _retailAlphaQueue, - worldRenderDiagnostics, - terrainDrawDiagnostics); - var worldSceneDiagnostics = - new AcDream.App.Rendering.WorldSceneDiagnosticsController( - worldRenderDiagnostics, - new AcDream.App.Rendering.RuntimeWorldScenePViewDiagnosticSource( - _playerControllerSlot, - _physicsEngine, - _cellVisibility), - _worldSceneDebugState, - _debugLines, + _window!, + _input!, + WorldTime, + Weather, + Lighting, + _worldEnvironment, _physicsEngine, + _cellVisibility, _localPlayerMode, + _localPlayerIdentity, + _chaseCameraInput, _playerControllerSlot, - _debugVmRenderFacts, - _debugVm is not null); - var worldScenePasses = new AcDream.App.Rendering.WorldScenePassExecutor( - _gl!, - renderFrameGlState, - _clipFrame!, - _wbDrawDispatcher!, - _envCellRenderer!, - _terrain, - terrainDrawDiagnostics, - _skyRenderer, - _particleSystem, - _particleRenderer); - var worldSceneRenderer = new AcDream.App.Rendering.WorldSceneRenderer( - renderFrameResources, - renderLoginState, - _worldEnvironment, - worldRenderFrameBuilder, - new AcDream.App.Rendering.RuntimeWorldSceneEntitySource(_worldState), - _retailSelectionScene, - _retailAlphaQueue, - _particleVisibility, - new AcDream.App.Rendering.WorldScenePViewRenderer( - new AcDream.App.Rendering.RetailPViewRenderer(), - retailPViewPassExecutor, - retailPViewPassExecutor), - retailPViewCells, - worldScenePasses, - _renderRange, - worldSceneDiagnostics); - if (_frameScreenshots is { } screenshots - && _options.AutomationArtifactDirectory is { } artifactDirectory) - { - void UiProbeLog(string message) => - Console.WriteLine("[UI-PROBE] " + message); - _worldLifecycleAutomation = - new AcDream.App.Diagnostics.WorldLifecycleAutomationController( - () => _worldReveal?.Snapshot ?? default, - () => _worldReveal?.PortalMaterializationCount ?? 0, - CaptureWorldLifecycleResourceSnapshot, - screenshots, - artifactDirectory, - UiProbeLog); - interactionUi.LateBindings.AdoptLateOwnerBinding( - "world lifecycle automation", - interactionUi.LateBindings.Automation.Bind( - _worldLifecycleAutomation)); - } - AcDream.App.Rendering.IRetainedGameplayUiFrame? retainedGameplayUi = - _options.RetailUi && _retailUiRuntime is not null - ? new AcDream.App.Rendering.RetainedGameplayUiFrame( - _retailUiRuntime, - _input) - : null; - AcDream.App.Rendering.IPrivateFrameScreenshot? privateScreenshot = - _frameScreenshots is not null - ? new AcDream.App.Rendering.PrivateFrameScreenshot( - _frameScreenshots) - : null; - var privatePresentation = - new AcDream.App.Rendering.PrivatePresentationRenderer( - new AcDream.App.Rendering.LocalPlayerPortalViewport( - sessionPlayer.LocalTeleport, - _cameraController!), - renderFrameResources, - _paperdollFramePresenter, - retainedGameplayUi, - _devToolsFramePresenter, - privateScreenshot); - var framePreparation = - new AcDream.App.Rendering.RenderFramePreparationController( - renderFrameResources, - _devToolsFramePresenter, - renderWeatherFrame); - var renderFrameOrchestrator = - new AcDream.App.Rendering.RenderFrameOrchestrator( - _gpuFrameFlights!, - framePreparation, - worldSceneRenderer, - privatePresentation, - _renderFrameDiagnostics!, - (AcDream.App.Rendering.IRenderFrameFailureRecovery?) - _devToolsFramePresenter - ?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance); - var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator( - sessionPlayer.LiveObjectFrame, - _worldState, - sessionPlayer.LiveSession, - sessionPlayer.LocalPlayerFrame, - sessionPlayer.LiveSpatialReconciler); - var cameraFrame = new AcDream.App.Rendering.CameraFrameController( - _cameraController!, - _inputCapture, - _cameraInput, - sessionPlayer.LocalPlayerFrameRuntime, - _chaseCameraInput, - sessionPlayer.LocalPlayerFrame, - sessionPlayer.LiveSpatialReconciler, - new AcDream.App.Combat.CombatCameraTargetSource( + _liveWorldOrigin, + _particleVisibility, + _effectPoses, + _renderRange, _runtimeSettings, - Combat, + _displayFramePacing, + _worldSceneDebugState, + _retailAlphaQueue, + _frameProfiler, + _frameDiag, + _renderDiagnosticLog, + _debugVmRenderFacts, + _inputCapture, + _cameraInput, _selection, - _worldSelectionQuery!)); - var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator( - new AcDream.App.Update.LiveEntityTeardownFramePhase( - livePresentation.LiveEntities), - new AcDream.App.Update.ConsoleUpdateFrameFailureSink(), - _updateFrameClock, - new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!), - sessionPlayer.StreamingFrame, - sessionPlayer.GameplayInput, - liveFrameCoordinator, - new AcDream.App.Update.LiveEntityLivenessFramePhase( - sessionPlayer.Liveness, - new AcDream.App.Update.StopwatchClientMonotonicTimeSource()), - sessionPlayer.LocalTeleport, - new AcDream.App.Update.PlayerModeAutoEntryFramePhase( - sessionPlayer.PlayerModeAutoEntry), - cameraFrame); - _frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator); + _animatedEntities, + _updateFrameClock, + Combat, + _frameGraphs, + Console.WriteLine), + this).Compose( + hostInputCamera, + contentEffectsAudio, + settingsDevTools, + worldRender, + interactionUi, + livePresentation, + sessionPlayer); AcDream.App.Net.LiveSessionStartResult liveStart = - sessionPlayer.SessionHost.Start(_options); + frameRoots.SessionHost.Start(_options); switch (liveStart.Status) { case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials: @@ -1649,54 +1454,6 @@ public sealed class GameWindow : out _); } - private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot - CaptureWorldLifecycleResourceSnapshot() - { - var meshManager = _wbMeshAdapter?.MeshManager; - var mesh = meshManager?.Diagnostics ?? default; - AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot render = - _renderFrameDiagnostics?.Snapshot - ?? AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot.Initial; - GCMemoryInfo memory = GC.GetGCMemoryInfo(); - return new AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot( - LoadedLandblocks: _worldState.LoadedLandblockIds.Count, - WorldEntities: _worldState.Entities.Count, - AnimatedEntities: _animatedEntities.Count, - VisibleLandblocks: render.VisibleLandblocks, - TotalLandblocks: render.TotalLandblocks, - LiveEntities: _liveEntities?.Count ?? 0, - MaterializedLiveEntities: _liveEntities?.MaterializedCount ?? 0, - PendingLiveTeardowns: _liveEntities?.PendingTeardownCount ?? 0, - PendingLandblockRetirements: - _streamingController?.PendingRetirementCount ?? 0, - ParticleEmitters: _particleSystem?.ActiveEmitterCount ?? 0, - Particles: _particleSystem?.ActiveParticleCount ?? 0, - ParticleBindings: _particleSink?.ActiveBindingCount ?? 0, - ParticleOwners: _particleSink?.TrackedOwnerCount ?? 0, - EffectOwners: _entityEffects?.ReadyOwnerCount ?? 0, - LightOwners: _liveEntityLights?.TrackedOwnerCount ?? 0, - ScriptOwners: _scriptRunner?.ActiveOwnerCount ?? 0, - ActiveScripts: _scriptRunner?.ActiveScriptCount ?? 0, - MeshRenderData: mesh.RenderData, - MeshAtlasArrays: mesh.AtlasArrays, - MeshEstimatedBytes: mesh.EstimatedBytes, - StagedMeshUploads: _wbMeshAdapter?.StagedUploadBacklog ?? 0, - StagedMeshBytes: _wbMeshAdapter?.StagedUploadBytes ?? 0, - TrackedGpuBytes: AcDream.App.Rendering.Wb.GpuMemoryTracker.AllocatedBytes, - TrackedGpuBuffers: AcDream.App.Rendering.Wb.GpuMemoryTracker.BufferCount, - TrackedGpuTextures: AcDream.App.Rendering.Wb.GpuMemoryTracker.TextureCount, - OwnedCompositeTextures: _textureCache?.OwnedBindlessTextureCount ?? 0, - CompositeTextureOwners: _textureCache?.TextureOwnerCount ?? 0, - ActiveParticleTextures: _textureCache?.ActiveParticleTextureCount ?? 0, - ParticleTextureOwners: _textureCache?.ParticleTextureOwnerCount ?? 0, - CompositeWarmupPending: _wbDrawDispatcher?.LastCompositeWarmupPendingCount ?? 0, - ManagedBytes: GC.GetTotalMemory(forceFullCollection: false), - ManagedCommittedBytes: memory.TotalCommittedBytes, - Fps: render.Fps, - FrameMilliseconds: render.FrameMilliseconds, - LastFrameProfile: _frameProfiler.LastReport); - } - // IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass // narrowing that dropped settled-open doors / faded-out walls back onto the // stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the @@ -1871,7 +1628,20 @@ public sealed class GameWindow : [ new("world frame composition", () => { - _frameGraphs.Withdraw(); + IDisposable? publication = _frameGraphPublication; + if (publication is null) + return; + publication.Dispose(); + _frameGraphPublication = null; + }), + new("frame-root bindings", () => + { + FrameRootRuntimeBindings? bindings = _frameRootBindings; + if (bindings is null) + return; + bindings.Dispose(); + _frameRootBindings = null; + _worldLifecycleAutomation = null; }), new("session/player bindings", () => { diff --git a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs new file mode 100644 index 00000000..b2af0485 --- /dev/null +++ b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs @@ -0,0 +1,126 @@ +using System.Reflection; +using AcDream.App.Composition; +using AcDream.App.Rendering; + +namespace AcDream.App.Tests.Composition; + +public sealed class FrameRootCompositionTests +{ + [Fact] + public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges() + { + var calls = new List(); + var bindings = new FrameRootRuntimeBindings(); + bindings.Adopt("first", new RetryBinding("first", calls, 0)); + bindings.Adopt("second", new RetryBinding("second", calls, 1)); + + Assert.Throws(bindings.Dispose); + Assert.Equal(["second", "first"], calls); + + bindings.Dispose(); + bindings.Dispose(); + + Assert.Equal(["second", "first", "second"], calls); + Assert.Throws(() => + bindings.Adopt("late", new RetryBinding("late", calls, 0))); + } + + [Fact] + public void ProductionPhasePublishesOnlyAfterBothRootsExist() + { + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); + + AssertAppearsInOrder( + source, + "new RenderFrameResourceController(", + "new WorldSceneRenderer(", + "new RenderFrameOrchestrator(", + "new RetailLiveFrameCoordinator(", + "new UpdateFrameOrchestrator(", + "d.FrameGraphs.PublishOwned(", + "_publication.PublishFrameRoots(result);", + "graphLease.Transfer();", + "bindingsLease.Transfer();"); + } + + [Fact] + public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs() + { + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "GameWindow.cs")); + + Assert.Contains("new FrameRootCompositionPhase(", source); + Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source); + Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source); + Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source); + Assert.DoesNotContain("_frameGraphs.Publish(", source); + } + + [Fact] + public void FramePhaseAndSnapshotSourceRetainNoWindowOwner() + { + Assert.DoesNotContain( + typeof(FrameRootCompositionPhase).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.FieldType == typeof(GameWindow)); + + string snapshots = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Diagnostics", + "WorldLifecycleResourceSnapshotSource.cs")); + Assert.DoesNotContain("GameWindow", snapshots, StringComparison.Ordinal); + Assert.Contains("_liveEntities.PendingTeardownCount", snapshots); + Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots); + Assert.Contains("_frameProfiler.LastReport", snapshots); + } + + private sealed class RetryBinding( + string name, + List calls, + int failures) : IDisposable + { + private int _failures = failures; + + public void Dispose() + { + calls.Add(name); + if (_failures-- > 0) + throw new InvalidOperationException("retry"); + } + } + + private static void AssertAppearsInOrder(string source, params string[] values) + { + int cursor = 0; + foreach (string value in values) + { + int found = source.IndexOf(value, cursor, StringComparison.Ordinal); + Assert.True(found >= 0, $"Missing expected source fragment: {value}"); + cursor = found + value.Length; + } + } + + private static string FindRepoRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + return directory.FullName; + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not find AcDream.slnx."); + } +} diff --git a/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs b/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs index 56bf2695..15683ee0 100644 --- a/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs +++ b/tests/AcDream.App.Tests/Composition/GameWindowCompositionPipelineTests.cs @@ -52,7 +52,7 @@ public sealed class GameWindowCompositionPipelineTests IInteractionUiCompositionPhase, ILivePresentationCompositionPhase, ISessionPlayerCompositionPhase, - IFrameRootCompositionPhase, + IFrameRootCompositionPhase, ISessionStartCompositionPhase { public Token Platform { get; } = new(0); @@ -155,8 +155,9 @@ public sealed class GameWindowCompositionPipelineTests return _session; } - Token IFrameRootCompositionPhase.Compose( + Token IFrameRootCompositionPhase.Compose( Token host, + Token content, Token settings, Token world, Token interaction, @@ -164,6 +165,7 @@ public sealed class GameWindowCompositionPipelineTests Token session) { Assert.Same(_host, host); + Assert.Same(_content, content); Assert.Same(_settings, settings); Assert.Same(_world, world); Assert.Same(_interaction, interaction); diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs index d8cd3968..5024d5ce 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs @@ -46,6 +46,7 @@ public sealed class GameWindowRenderLeafCompositionTests "Composition", "SessionPlayerComposition.cs")); string source = GameWindowSource(); + string framePhase = FrameRootSource(); AssertAppearsInOrder( phase, @@ -57,10 +58,13 @@ public sealed class GameWindowRenderLeafCompositionTests AssertAppearsInOrder( source, "new SessionPlayerCompositionPhase(", - "new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(", - "new AcDream.App.Rendering.RenderFrameResourceController(", - "new AcDream.App.Rendering.PrivatePresentationRenderer(", - "new AcDream.App.Rendering.RenderFrameOrchestrator("); + "new FrameRootCompositionPhase("); + AssertAppearsInOrder( + framePhase, + "new LocalPlayerTeleportRenderStateSource(", + "new RenderFrameResourceController(", + "new PrivatePresentationRenderer(", + "new RenderFrameOrchestrator("); } [Fact] @@ -111,10 +115,11 @@ public sealed class GameWindowRenderLeafCompositionTests "Composition", "SettingsDevToolsComposition.cs")); Assert.Contains("new DevToolsFramePresenter(", settingsComposition); - Assert.Contains("new AcDream.App.Rendering.RenderFrameResourceController(", source); - Assert.Contains("new AcDream.App.Rendering.RenderWeatherFrameController(", source); - Assert.Contains("new AcDream.App.Rendering.PrivatePresentationRenderer(", source); - Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source); + string framePhase = FrameRootSource(); + Assert.Contains("new RenderFrameResourceController(", framePhase); + Assert.Contains("new RenderWeatherFrameController(", framePhase); + Assert.Contains("new PrivatePresentationRenderer(", framePhase); + Assert.Contains("new RenderFrameOrchestrator(", framePhase); Assert.Contains("new DisplayFramePacingController(", source); string pointerSource = File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -142,7 +147,8 @@ public sealed class GameWindowRenderLeafCompositionTests AssertAppearsInOrder( source, "new ResourceShutdownStage(\"frame borrowers\"", - "_frameGraphs.Withdraw();", + "publication.Dispose();", + "bindings.Dispose();", "new ResourceShutdownStage(\"session dependents\""); AssertAppearsInOrder( source, @@ -150,7 +156,7 @@ public sealed class GameWindowRenderLeafCompositionTests "new(\"frame profiler\", _frameProfiler.Dispose)"); AssertAppearsInOrder( source, - "_frameGraphs.Withdraw();", + "publication.Dispose();", "owner.DisposeFrontend()", "new ResourceShutdownStage(\"input\"", "_input?.Dispose();", @@ -186,7 +192,7 @@ public sealed class GameWindowRenderLeafCompositionTests "private void OnRender(double deltaSeconds)", StringComparison.Ordinal); int end = source.IndexOf( - "private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot", + "private void OnFramebufferResize(", start, StringComparison.Ordinal); Assert.True(start >= 0 && end > start); @@ -216,12 +222,12 @@ public sealed class GameWindowRenderLeafCompositionTests [Fact] public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner() { - string source = GameWindowSource(); + string source = FrameRootSource(); Assert.Contains( - "new AcDream.App.Rendering.TerrainDrawDiagnosticsController(", + "new TerrainDrawDiagnosticsController(", source); - Assert.Contains("new AcDream.App.Rendering.WorldScenePassExecutor(", source); + Assert.Contains("new WorldScenePassExecutor(", source); Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source); Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source); } @@ -240,6 +246,13 @@ public sealed class GameWindowRenderLeafCompositionTests "Composition", "LivePresentationComposition.cs")); + private static string FrameRootSource() => File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); + private static string Source(string fileName) => File.ReadAllText(Path.Combine( FindRepoRoot(), "src", diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index 3f081ef2..1730082f 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -65,10 +65,21 @@ public sealed class GameWindowSlice8BoundaryTests "new LivePresentationCompositionPhase(", "this).Compose(", "new SessionPlayerCompositionPhase(", - "var renderFrameOrchestrator =", - "var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(", - "_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);", - "sessionPlayer.SessionHost.Start(_options)"); + "FrameRootResult frameRoots = new FrameRootCompositionPhase(", + "frameRoots.SessionHost.Start(_options)"); + + 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);"); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -86,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests "_publication.PublishSessionPlayer(result);"); Assert.Equal(1, CountOccurrences( body, - "sessionPlayer.SessionHost.Start(_options)")); + "frameRoots.SessionHost.Start(_options)")); string phaseOne = Slice( File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -107,7 +118,7 @@ public sealed class GameWindowSlice8BoundaryTests phaseOne, StringComparison.Ordinal); int start = body.IndexOf( - "sessionPlayer.SessionHost.Start(_options)", + "frameRoots.SessionHost.Start(_options)", StringComparison.Ordinal); string postStart = body[start..]; Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); @@ -331,7 +342,8 @@ public sealed class GameWindowSlice8BoundaryTests "this).Compose(platform, hostInputCamera, contentEffectsAudio);", "new WorldRenderCompositionPhase(", "new SessionPlayerCompositionPhase(", - "sessionPlayer.SessionHost.Start(_options)"); + "new FrameRootCompositionPhase(", + "frameRoots.SessionHost.Start(_options)"); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", @@ -385,7 +397,7 @@ public sealed class GameWindowSlice8BoundaryTests string render = Slice( source, "private void OnRender(double deltaSeconds)", - "private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot"); + "private void OnFramebufferResize("); string focus = Slice( source, "private void OnFocusChanged(bool focused)", @@ -524,6 +536,12 @@ public sealed class GameWindowSlice8BoundaryTests "AcDream.App", "Composition", "SessionPlayerComposition.cs")); + string framePhase = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); AssertAppearsInOrder( livePhase, @@ -536,7 +554,12 @@ public sealed class GameWindowSlice8BoundaryTests "new WorldRenderCompositionPhase(", "new LivePresentationCompositionPhase(", "new SessionPlayerCompositionPhase(", - "_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);"); + "new FrameRootCompositionPhase("); + AssertAppearsInOrder( + framePhase, + "new RenderFrameOrchestrator(", + "new UpdateFrameOrchestrator(", + "d.FrameGraphs.PublishOwned("); AssertAppearsInOrder( sessionPhase, "d.PortalTunnelFallback.Transfer(", @@ -550,7 +573,8 @@ public sealed class GameWindowSlice8BoundaryTests "TerrainModernRenderer terrain = AcquireAndPublish("); AssertAppearsInOrder( shutdown, - "_frameGraphs.Withdraw();", + "publication.Dispose();", + "bindings.Dispose();", "_retailUiLease.Dispose();", "_localPlayerTeleport?.Dispose();", "_portalTunnelFallback.ReleaseFallback();", diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index f771b229..d60fb5af 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -225,11 +225,11 @@ public sealed class RetailPViewPassExecutorTests FindRepoRoot(), "src", "AcDream.App", - "Rendering", - "GameWindow.cs")); + "Composition", + "FrameRootComposition.cs")); - Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source); - Assert.Contains("new AcDream.App.Rendering.WorldScenePViewRenderer(", source); + Assert.Contains("new RetailPViewPassExecutor(", source); + Assert.Contains("new WorldScenePViewRenderer(", source); Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source); Assert.DoesNotContain("RetailPViewDrawContext", source); Assert.DoesNotContain("DrawLandscapeSlice =", source); diff --git a/tests/AcDream.App.Tests/Rendering/RuntimeResourceSlotTests.cs b/tests/AcDream.App.Tests/Rendering/RuntimeResourceSlotTests.cs index e19804df..22229c41 100644 --- a/tests/AcDream.App.Tests/Rendering/RuntimeResourceSlotTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RuntimeResourceSlotTests.cs @@ -152,6 +152,28 @@ public sealed class RuntimeResourceSlotTests Assert.Equal(["update:2"], calls); } + [Fact] + public void OwnedFramePublicationWithdrawsOnlyItsExactPair() + { + var calls = new List(); + var slot = new GameFrameGraphSlot(); + IDisposable stale = slot.PublishOwned( + new RecordingUpdateRoot(calls), + new RecordingRenderRoot(calls)); + + stale.Dispose(); + IDisposable current = slot.PublishOwned( + new RecordingUpdateRoot(calls), + new RecordingRenderRoot(calls)); + stale.Dispose(); + + Assert.True(slot.Tick(new UpdateFrameInput(3))); + current.Dispose(); + current.Dispose(); + Assert.False(slot.Tick(new UpdateFrameInput(4))); + Assert.Equal(["update:3"], calls); + } + [Fact] public void OwnedSlotRejectsReentrantAcquireWithoutLosingOuterResource() { diff --git a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs index cacabedd..e99adb3d 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs @@ -328,6 +328,12 @@ public sealed class WorldRenderFrameBuilderTests public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition() { string gameWindow = GameWindowSource(); + string frameRoot = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); string worldScene = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", @@ -357,11 +363,14 @@ public sealed class WorldRenderFrameBuilderTests "_passes.DrawFlatTerrain(", "NormalWorldDrawn: true"); AssertAppearsInOrder( - gameWindow, - "var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(", + frameRoot, + "var skyPesFrame = new SkyPesFrameController(", "var worldRenderFrameBuilder =", - "new AcDream.App.Rendering.RenderFrameOrchestrator(", - "_frameGraphs.Withdraw();", + "new RenderFrameOrchestrator(", + "d.FrameGraphs.PublishOwned("); + AssertAppearsInOrder( + gameWindow, + "publication.Dispose();", "new(\"equipped children\"", "new(\"effect network state\"", "new(\"audio\"", diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index de2cd9fc..0fb918dc 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -279,11 +279,11 @@ public sealed class WorldSceneRendererTests FindRepoRoot(), "src", "AcDream.App", - "Rendering", - "GameWindow.cs")); + "Composition", + "FrameRootComposition.cs")); - Assert.Contains("new AcDream.App.Rendering.WorldSceneRenderer(", source); - Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source); + Assert.Contains("new WorldSceneRenderer(", source); + Assert.Contains("new RenderFrameOrchestrator(", source); Assert.DoesNotContain("_worldSceneRenderer", source); Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source); Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source); diff --git a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs index 66a732a9..27a25f4b 100644 --- a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs +++ b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs @@ -276,10 +276,16 @@ public sealed class UpdateFrameOrchestratorTests "AcDream.App", "Update", "UpdateFrameRuntimeAdapters.cs")); + string frameRoot = File.ReadAllText(Path.Combine( + root, + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); Assert.Equal(0, CountOccurrences(source, "PublishTime(")); Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime(")); - Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source, + Assert.Contains("new PhysicsScriptClockPublisher(", frameRoot, StringComparison.Ordinal); } @@ -349,10 +355,16 @@ public sealed class UpdateFrameOrchestratorTests "AcDream.App", "Composition", "SessionPlayerComposition.cs")); + string frameRoot = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); Assert.Contains("new LiveObjectFrameController(", sessionPlayer); Assert.Contains("new LiveSpatialPresentationReconciler(", sessionPlayer); - Assert.Contains("new AcDream.App.World.RetailLiveFrameCoordinator(", source); + Assert.Contains("new RetailLiveFrameCoordinator(", frameRoot); Assert.DoesNotContain("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal); Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source, StringComparison.Ordinal); @@ -375,7 +387,6 @@ public sealed class UpdateFrameOrchestratorTests "AcDream.App", "Composition", "SessionPlayerComposition.cs")); - Assert.Contains("new StreamingFrameController(", sessionPlayer); Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal); Assert.Equal(1, CountOccurrences( @@ -492,6 +503,12 @@ public sealed class UpdateFrameOrchestratorTests "AcDream.App", "Composition", "SessionPlayerComposition.cs")); + string frameRoot = File.ReadAllText(Path.Combine( + root, + "src", + "AcDream.App", + "Composition", + "FrameRootComposition.cs")); Assert.Contains( "new LocalPlayerTeleportController(", @@ -513,10 +530,10 @@ public sealed class UpdateFrameOrchestratorTests Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource, StringComparison.Ordinal); AssertAppearsInOrder( - source, - "new AcDream.App.Update.LiveEntityLivenessFramePhase(", - "sessionPlayer.LocalTeleport,", - "new AcDream.App.Update.PlayerModeAutoEntryFramePhase(", + frameRoot, + "new LiveEntityLivenessFramePhase(", + "session.LocalTeleport,", + "new PlayerModeAutoEntryFramePhase(", "cameraFrame);"); }