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 <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 19:02:08 +02:00
parent 826f9ea9b5
commit 3628aeb520
21 changed files with 1052 additions and 374 deletions

View file

@ -34,8 +34,10 @@ What does NOT go here:
I.7 now owns streaming/session/hydration/local-player/teleport startup and I.7 now owns streaming/session/hydration/local-player/teleport startup and
every late edge explicitly. I.8a now owns the focused session factory, every late edge explicitly. I.8a now owns the focused session factory,
command targets, and sole gameplay input subscriber before frame publication. command targets, and sole gameplay input subscriber before frame publication.
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration I.8b now owns the complete update/render construction, exact atomic frame
handoffs and the class is 2,149 raw lines. I.8bI.9 plus the remaining publication, and lifecycle resource snapshot source. `GameWindow.OnUpdate`
and `GameWindow.OnRender` are typed orchestration handoffs and the class is
1,919 raw lines. I.8cI.9 plus the remaining
shutdown/closeout shutdown/closeout
cleanup is active in cleanup is active in
[`docs/architecture/code-structure.md`](architecture/code-structure.md). [`docs/architecture/code-structure.md`](architecture/code-structure.md).

View file

@ -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 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 gone. I.8a moves the exact live-session reset/router graph, combat and
diagnostic command targets, and sole gameplay input subscriber into Phase 7 diagnostic command targets, and sole gameplay input subscriber into Phase 7
before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8bL before frame publication. I.8b moves the complete update/render construction
remain active, with atomic frame-root publication next. The App gate passes body into `FrameRootCompositionPhase`, publishes the pair through an exact
3,424 tests / 3 intentional skips and the complete Release suite passes 7,796 lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
tests / 5 intentional skips. The clean solution build retains only the 17 1,919 raw lines. I.8cL remain active, with terminal session start next. The
test-project warnings tracked by #228; the I.8a behavior, architecture, and App gate passes 3,429 tests / 3 intentional skips and the complete Release suite
adversarial reviews are clean. 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 ### 4.4 Exit criteria

View file

@ -104,11 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
resources have transactional phase ownership and exact-owner late bindings. resources have transactional phase ownership and exact-owner late bindings.
I.7 adds the complete streaming/session/hydration/local-player/teleport phase, I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
including explicit reversible ownership for every late edge. `GameWindow` is including explicit reversible ownership for every late edge. `GameWindow` is
now 2,149 raw lines after I.8a, which composes the session factory, command now 1,919 raw lines after I.8b, which also composes and atomically publishes the
targets, and sole gameplay input subscriber before frame publication. I.8bI.9, complete update/render pair and extracts lifecycle resource sampling. I.8cI.9,
shutdown, canonical soak snapshots, and closeout checkpoints JL remain. The shutdown, canonical soak snapshots, and closeout checkpoints JL remain. The
App gate passes 3,424 tests / 3 intentional skips, the complete Release suite App gate passes 3,429 tests / 3 intentional skips, the complete Release suite
passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean. 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 This is a behavior-preserving structural program. Severe regressions still get
root-cause fixes in separate commits; ordinary feature work resumes with M4 root-cause fixes in separate commits; ordinary feature work resumes with M4

View file

@ -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, ownership and exact-owner late bindings. I.7 now owns the complete streaming,
session, hydration, local-player, combat, and teleport startup graph, including session, hydration, local-player, combat, and teleport startup graph, including
named reversible late edges. I.8a adds the focused live-session runtime factory named reversible late edges. I.8a adds the focused live-session runtime factory
and attaches command/input owners before frame publication. `GameWindow` is and attaches command/input owners before frame publication. I.8b composes and
2,149 raw lines. I.8bI.9 and checkpoints JL remain active. Issue #232 tracks atomically publishes the complete frame pair and extracts lifecycle resource
sampling. `GameWindow` is 1,919 raw lines. I.8cI.9 and checkpoints JL remain
active. Issue #232 tracks
process-residency variance in the soak without loosening its leak threshold. 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 The App gate passes 3,429 tests / 3 skips, the complete Release suite passes
7,796 tests / 5 skips, and the I.8a corrected-diff review is clean. 7,801 tests / 5 skips, and the I.8b corrected-diff review is clean.
Carried: Carried:
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus

View file

@ -294,9 +294,11 @@ ledger only after the complete pipeline is live.
detailed session-binding, atomic-root, and terminal-start plan is 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). [`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 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; 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 I.8b composes the unchanged update/render graph in a focused phase, publishes
active cut. 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. Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
## 6. Automated acceptance ## 6. Automated acceptance

View file

@ -1,6 +1,6 @@
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start # 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.8aI.8b complete; I.8c terminal start next
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md` **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 ## 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 Add `FrameRootCompositionPhase` implementing the existing
`IFrameRootCompositionPhase` contract. Its dependencies are focused App owners; `IFrameRootCompositionPhase` contract. Its dependencies are focused App owners;
prior phase results supply all resources created during startup. prior phase results supply all resources created during startup.

View file

@ -29,8 +29,8 @@ audit.
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
explicit single ownership and transfer seams. explicit single ownership and transfer seams.
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. I.1I.7 and I.8a are complete; transactional partial-acquisition rollback. I.1I.8b are complete; I.8c
I.8b atomic frame-root publication is active. Detailed plan: 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). [`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 - [ ] J — move the exact retryable shutdown manifest to a focused lifetime
owner and prove all partial-load/reentrant/retry paths. owner and prove all partial-load/reentrant/retry paths.

View file

@ -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<LiveEntityAnimationState> Animations,
UpdateFrameClock UpdateClock,
CombatState Combat,
GameFrameGraphSlot FrameGraphs,
Action<string> 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,
}
/// <summary>
/// Exact late edges created with the frame roots. Completed detach operations
/// are removed immediately so shutdown retry never replays them.
/// </summary>
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<Exception>? 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<FrameRootCompositionPoint>? _faultInjection;
public FrameRootCompositionPhase(
FrameRootDependencies dependencies,
IGameWindowFrameRootPublication publication,
Action<FrameRootCompositionPoint>? 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);
}

View file

@ -40,10 +40,11 @@ internal interface ISessionPlayerCompositionPhase<in THost, in TContent, in TWor
TLive live); TLive live);
} }
internal interface IFrameRootCompositionPhase<in THost, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult> internal interface IFrameRootCompositionPhase<in THost, in TContent, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult>
{ {
TResult Compose( TResult Compose(
THost host, THost host,
TContent content,
TSettings settings, TSettings settings,
TWorld world, TWorld world,
TInteraction interaction, TInteraction interaction,
@ -79,7 +80,7 @@ internal sealed class GameWindowCompositionPipeline<
private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction; private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction;
private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live; private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live;
private readonly ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> _session; private readonly ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> _session;
private readonly IFrameRootCompositionPhase<THost, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame; private readonly IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame;
private readonly ISessionStartCompositionPhase<TFrame> _start; private readonly ISessionStartCompositionPhase<TFrame> _start;
public GameWindowCompositionPipeline( public GameWindowCompositionPipeline(
@ -90,7 +91,7 @@ internal sealed class GameWindowCompositionPipeline<
IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction, IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction,
ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live, ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live,
ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> session, ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> session,
IFrameRootCompositionPhase<THost, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame, IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
ISessionStartCompositionPhase<TFrame> start) ISessionStartCompositionPhase<TFrame> start)
{ {
_host = host ?? throw new ArgumentNullException(nameof(host)); _host = host ?? throw new ArgumentNullException(nameof(host));
@ -113,7 +114,14 @@ internal sealed class GameWindowCompositionPipeline<
TInteraction interaction = _interaction.Compose(host, content, settings, world); TInteraction interaction = _interaction.Compose(host, content, settings, world);
TLive live = _live.Compose(host, content, world, interaction); TLive live = _live.Compose(host, content, world, interaction);
TSession session = _session.Compose(host, content, world, interaction, live); 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); _start.Start(frame);
} }
} }

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
internal sealed class WorldLifecycleResourceSnapshotSource
{
private readonly GpuWorldState _world;
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState>
_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<LiveEntityAnimationState> 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);
}
}

View file

@ -29,12 +29,28 @@ internal sealed class GameFrameGraphSlot
public bool IsPublished => Volatile.Read(ref _pair) is not null; public bool IsPublished => Volatile.Read(ref _pair) is not null;
public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render) 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(update);
ArgumentNullException.ThrowIfNull(render); ArgumentNullException.ThrowIfNull(render);
var pair = new FrameGraphPair(update, render); var pair = new FrameGraphPair(update, render);
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null) if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
throw new InvalidOperationException("A game frame graph is already published."); throw new InvalidOperationException("A game frame graph is already published.");
return pair;
} }
public bool Tick(UpdateFrameInput input) public bool Tick(UpdateFrameInput input)
@ -64,4 +80,22 @@ internal sealed class GameFrameGraphSlot
{ {
Interlocked.Exchange(ref _pair, null); 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);
}
} }

View file

@ -22,7 +22,8 @@ public sealed class GameWindow :
IGameWindowWorldRenderPublication, IGameWindowWorldRenderPublication,
IGameWindowInteractionRetainedUiPublication, IGameWindowInteractionRetainedUiPublication,
IGameWindowLivePresentationPublication, IGameWindowLivePresentationPublication,
IGameWindowSessionPlayerPublication IGameWindowSessionPlayerPublication,
IGameWindowFrameRootPublication
{ {
private static double ClientTimerNow() => private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp() System.Diagnostics.Stopwatch.GetTimestamp()
@ -103,6 +104,8 @@ public sealed class GameWindow :
private SessionPlayerRuntimeBindings? _sessionPlayerBindings; private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots; private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation; private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private FrameRootRuntimeBindings? _frameRootBindings;
private IDisposable? _frameGraphPublication;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights; private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
private readonly AcDream.App.Rendering.GameRenderResourceLifetime private readonly AcDream.App.Rendering.GameRenderResourceLifetime
@ -1040,6 +1043,23 @@ public sealed class GameWindow :
_sessionPlayerBindings = result.RuntimeBindings; _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<T>( private static void PublishCompositionOwner<T>(
ref T? destination, ref T? destination,
T value, T value,
@ -1355,268 +1375,53 @@ public sealed class GameWindow :
interactionUi, interactionUi,
livePresentation); livePresentation);
var teleportRenderState = FrameRootResult frameRoots = new FrameRootCompositionPhase(
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource( new FrameRootDependencies(
sessionPlayer.LocalTeleport); _options,
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!, _gl!,
_window!,
_input!,
WorldTime, WorldTime,
Weather, 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(
_options,
WorldTime,
Lighting, Lighting,
_wbDrawDispatcher, _worldEnvironment,
_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, _physicsEngine,
_cellVisibility, _cellVisibility,
_localPlayerMode, _localPlayerMode,
_localPlayerIdentity,
_chaseCameraInput, _chaseCameraInput,
_playerControllerSlot, _playerControllerSlot,
_liveWorldOrigin), _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,
_physicsEngine,
_localPlayerMode,
_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, _particleVisibility,
new AcDream.App.Rendering.WorldScenePViewRenderer( _effectPoses,
new AcDream.App.Rendering.RetailPViewRenderer(),
retailPViewPassExecutor,
retailPViewPassExecutor),
retailPViewCells,
worldScenePasses,
_renderRange, _renderRange,
worldSceneDiagnostics); _runtimeSettings,
if (_frameScreenshots is { } screenshots _displayFramePacing,
&& _options.AutomationArtifactDirectory is { } artifactDirectory) _worldSceneDebugState,
{ _retailAlphaQueue,
void UiProbeLog(string message) => _frameProfiler,
Console.WriteLine("[UI-PROBE] " + message); _frameDiag,
_worldLifecycleAutomation = _renderDiagnosticLog,
new AcDream.App.Diagnostics.WorldLifecycleAutomationController( _debugVmRenderFacts,
() => _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, _inputCapture,
_cameraInput, _cameraInput,
sessionPlayer.LocalPlayerFrameRuntime,
_chaseCameraInput,
sessionPlayer.LocalPlayerFrame,
sessionPlayer.LiveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
_runtimeSettings,
Combat,
_selection, _selection,
_worldSelectionQuery!)); _animatedEntities,
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
new AcDream.App.Update.LiveEntityTeardownFramePhase(
livePresentation.LiveEntities),
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
_updateFrameClock, _updateFrameClock,
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!), Combat,
sessionPlayer.StreamingFrame, _frameGraphs,
sessionPlayer.GameplayInput, Console.WriteLine),
liveFrameCoordinator, this).Compose(
new AcDream.App.Update.LiveEntityLivenessFramePhase( hostInputCamera,
sessionPlayer.Liveness, contentEffectsAudio,
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()), settingsDevTools,
sessionPlayer.LocalTeleport, worldRender,
new AcDream.App.Update.PlayerModeAutoEntryFramePhase( interactionUi,
sessionPlayer.PlayerModeAutoEntry), livePresentation,
cameraFrame); sessionPlayer);
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
AcDream.App.Net.LiveSessionStartResult liveStart = AcDream.App.Net.LiveSessionStartResult liveStart =
sessionPlayer.SessionHost.Start(_options); frameRoots.SessionHost.Start(_options);
switch (liveStart.Status) switch (liveStart.Status)
{ {
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials: case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
@ -1649,54 +1454,6 @@ public sealed class GameWindow :
out _); 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 // IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
// narrowing that dropped settled-open doors / faded-out walls back onto the // 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 // 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", () => 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", () => new("session/player bindings", () =>
{ {

View file

@ -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<string>();
var bindings = new FrameRootRuntimeBindings();
bindings.Adopt("first", new RetryBinding("first", calls, 0));
bindings.Adopt("second", new RetryBinding("second", calls, 1));
Assert.Throws<AggregateException>(bindings.Dispose);
Assert.Equal(["second", "first"], calls);
bindings.Dispose();
bindings.Dispose();
Assert.Equal(["second", "first", "second"], calls);
Assert.Throws<ObjectDisposedException>(() =>
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<string> 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.");
}
}

View file

@ -52,7 +52,7 @@ public sealed class GameWindowCompositionPipelineTests
IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>, IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>,
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>, ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>,
ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token>, ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token>,
IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token>, IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>,
ISessionStartCompositionPhase<Token> ISessionStartCompositionPhase<Token>
{ {
public Token Platform { get; } = new(0); public Token Platform { get; } = new(0);
@ -155,8 +155,9 @@ public sealed class GameWindowCompositionPipelineTests
return _session; return _session;
} }
Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token>.Compose( Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>.Compose(
Token host, Token host,
Token content,
Token settings, Token settings,
Token world, Token world,
Token interaction, Token interaction,
@ -164,6 +165,7 @@ public sealed class GameWindowCompositionPipelineTests
Token session) Token session)
{ {
Assert.Same(_host, host); Assert.Same(_host, host);
Assert.Same(_content, content);
Assert.Same(_settings, settings); Assert.Same(_settings, settings);
Assert.Same(_world, world); Assert.Same(_world, world);
Assert.Same(_interaction, interaction); Assert.Same(_interaction, interaction);

View file

@ -46,6 +46,7 @@ public sealed class GameWindowRenderLeafCompositionTests
"Composition", "Composition",
"SessionPlayerComposition.cs")); "SessionPlayerComposition.cs"));
string source = GameWindowSource(); string source = GameWindowSource();
string framePhase = FrameRootSource();
AssertAppearsInOrder( AssertAppearsInOrder(
phase, phase,
@ -57,10 +58,13 @@ public sealed class GameWindowRenderLeafCompositionTests
AssertAppearsInOrder( AssertAppearsInOrder(
source, source,
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(", "new FrameRootCompositionPhase(");
"new AcDream.App.Rendering.RenderFrameResourceController(", AssertAppearsInOrder(
"new AcDream.App.Rendering.PrivatePresentationRenderer(", framePhase,
"new AcDream.App.Rendering.RenderFrameOrchestrator("); "new LocalPlayerTeleportRenderStateSource(",
"new RenderFrameResourceController(",
"new PrivatePresentationRenderer(",
"new RenderFrameOrchestrator(");
} }
[Fact] [Fact]
@ -111,10 +115,11 @@ public sealed class GameWindowRenderLeafCompositionTests
"Composition", "Composition",
"SettingsDevToolsComposition.cs")); "SettingsDevToolsComposition.cs"));
Assert.Contains("new DevToolsFramePresenter(", settingsComposition); Assert.Contains("new DevToolsFramePresenter(", settingsComposition);
Assert.Contains("new AcDream.App.Rendering.RenderFrameResourceController(", source); string framePhase = FrameRootSource();
Assert.Contains("new AcDream.App.Rendering.RenderWeatherFrameController(", source); Assert.Contains("new RenderFrameResourceController(", framePhase);
Assert.Contains("new AcDream.App.Rendering.PrivatePresentationRenderer(", source); Assert.Contains("new RenderWeatherFrameController(", framePhase);
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source); Assert.Contains("new PrivatePresentationRenderer(", framePhase);
Assert.Contains("new RenderFrameOrchestrator(", framePhase);
Assert.Contains("new DisplayFramePacingController(", source); Assert.Contains("new DisplayFramePacingController(", source);
string pointerSource = File.ReadAllText(Path.Combine( string pointerSource = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -142,7 +147,8 @@ public sealed class GameWindowRenderLeafCompositionTests
AssertAppearsInOrder( AssertAppearsInOrder(
source, source,
"new ResourceShutdownStage(\"frame borrowers\"", "new ResourceShutdownStage(\"frame borrowers\"",
"_frameGraphs.Withdraw();", "publication.Dispose();",
"bindings.Dispose();",
"new ResourceShutdownStage(\"session dependents\""); "new ResourceShutdownStage(\"session dependents\"");
AssertAppearsInOrder( AssertAppearsInOrder(
source, source,
@ -150,7 +156,7 @@ public sealed class GameWindowRenderLeafCompositionTests
"new(\"frame profiler\", _frameProfiler.Dispose)"); "new(\"frame profiler\", _frameProfiler.Dispose)");
AssertAppearsInOrder( AssertAppearsInOrder(
source, source,
"_frameGraphs.Withdraw();", "publication.Dispose();",
"owner.DisposeFrontend()", "owner.DisposeFrontend()",
"new ResourceShutdownStage(\"input\"", "new ResourceShutdownStage(\"input\"",
"_input?.Dispose();", "_input?.Dispose();",
@ -186,7 +192,7 @@ public sealed class GameWindowRenderLeafCompositionTests
"private void OnRender(double deltaSeconds)", "private void OnRender(double deltaSeconds)",
StringComparison.Ordinal); StringComparison.Ordinal);
int end = source.IndexOf( int end = source.IndexOf(
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot", "private void OnFramebufferResize(",
start, start,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(start >= 0 && end > start); Assert.True(start >= 0 && end > start);
@ -216,12 +222,12 @@ public sealed class GameWindowRenderLeafCompositionTests
[Fact] [Fact]
public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner() public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner()
{ {
string source = GameWindowSource(); string source = FrameRootSource();
Assert.Contains( Assert.Contains(
"new AcDream.App.Rendering.TerrainDrawDiagnosticsController(", "new TerrainDrawDiagnosticsController(",
source); source);
Assert.Contains("new AcDream.App.Rendering.WorldScenePassExecutor(", source); Assert.Contains("new WorldScenePassExecutor(", source);
Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source); Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source);
Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source); Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source);
} }
@ -240,6 +246,13 @@ public sealed class GameWindowRenderLeafCompositionTests
"Composition", "Composition",
"LivePresentationComposition.cs")); "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( private static string Source(string fileName) => File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",

View file

@ -65,10 +65,21 @@ public sealed class GameWindowSlice8BoundaryTests
"new LivePresentationCompositionPhase(", "new LivePresentationCompositionPhase(",
"this).Compose(", "this).Compose(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"var renderFrameOrchestrator =", "FrameRootResult frameRoots = new FrameRootCompositionPhase(",
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(", "frameRoots.SessionHost.Start(_options)");
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
"sessionPlayer.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( string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -86,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests
"_publication.PublishSessionPlayer(result);"); "_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences( Assert.Equal(1, CountOccurrences(
body, body,
"sessionPlayer.SessionHost.Start(_options)")); "frameRoots.SessionHost.Start(_options)"));
string phaseOne = Slice( string phaseOne = Slice(
File.ReadAllText(Path.Combine( File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -107,7 +118,7 @@ public sealed class GameWindowSlice8BoundaryTests
phaseOne, phaseOne,
StringComparison.Ordinal); StringComparison.Ordinal);
int start = body.IndexOf( int start = body.IndexOf(
"sessionPlayer.SessionHost.Start(_options)", "frameRoots.SessionHost.Start(_options)",
StringComparison.Ordinal); StringComparison.Ordinal);
string postStart = body[start..]; string postStart = body[start..];
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
@ -331,7 +342,8 @@ public sealed class GameWindowSlice8BoundaryTests
"this).Compose(platform, hostInputCamera, contentEffectsAudio);", "this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"new WorldRenderCompositionPhase(", "new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"sessionPlayer.SessionHost.Start(_options)"); "new FrameRootCompositionPhase(",
"frameRoots.SessionHost.Start(_options)");
string sessionPhase = File.ReadAllText(Path.Combine( string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",
@ -385,7 +397,7 @@ public sealed class GameWindowSlice8BoundaryTests
string render = Slice( string render = Slice(
source, source,
"private void OnRender(double deltaSeconds)", "private void OnRender(double deltaSeconds)",
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot"); "private void OnFramebufferResize(");
string focus = Slice( string focus = Slice(
source, source,
"private void OnFocusChanged(bool focused)", "private void OnFocusChanged(bool focused)",
@ -524,6 +536,12 @@ public sealed class GameWindowSlice8BoundaryTests
"AcDream.App", "AcDream.App",
"Composition", "Composition",
"SessionPlayerComposition.cs")); "SessionPlayerComposition.cs"));
string framePhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
AssertAppearsInOrder( AssertAppearsInOrder(
livePhase, livePhase,
@ -536,7 +554,12 @@ public sealed class GameWindowSlice8BoundaryTests
"new WorldRenderCompositionPhase(", "new WorldRenderCompositionPhase(",
"new LivePresentationCompositionPhase(", "new LivePresentationCompositionPhase(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);"); "new FrameRootCompositionPhase(");
AssertAppearsInOrder(
framePhase,
"new RenderFrameOrchestrator(",
"new UpdateFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(");
AssertAppearsInOrder( AssertAppearsInOrder(
sessionPhase, sessionPhase,
"d.PortalTunnelFallback.Transfer(", "d.PortalTunnelFallback.Transfer(",
@ -550,7 +573,8 @@ public sealed class GameWindowSlice8BoundaryTests
"TerrainModernRenderer terrain = AcquireAndPublish("); "TerrainModernRenderer terrain = AcquireAndPublish(");
AssertAppearsInOrder( AssertAppearsInOrder(
shutdown, shutdown,
"_frameGraphs.Withdraw();", "publication.Dispose();",
"bindings.Dispose();",
"_retailUiLease.Dispose();", "_retailUiLease.Dispose();",
"_localPlayerTeleport?.Dispose();", "_localPlayerTeleport?.Dispose();",
"_portalTunnelFallback.ReleaseFallback();", "_portalTunnelFallback.ReleaseFallback();",

View file

@ -225,11 +225,11 @@ public sealed class RetailPViewPassExecutorTests
FindRepoRoot(), FindRepoRoot(),
"src", "src",
"AcDream.App", "AcDream.App",
"Rendering", "Composition",
"GameWindow.cs")); "FrameRootComposition.cs"));
Assert.Contains("new AcDream.App.Rendering.RetailPViewPassExecutor(", source); Assert.Contains("new RetailPViewPassExecutor(", source);
Assert.Contains("new AcDream.App.Rendering.WorldScenePViewRenderer(", source); Assert.Contains("new WorldScenePViewRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source); Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.DoesNotContain("RetailPViewDrawContext", source); Assert.DoesNotContain("RetailPViewDrawContext", source);
Assert.DoesNotContain("DrawLandscapeSlice =", source); Assert.DoesNotContain("DrawLandscapeSlice =", source);

View file

@ -152,6 +152,28 @@ public sealed class RuntimeResourceSlotTests
Assert.Equal(["update:2"], calls); Assert.Equal(["update:2"], calls);
} }
[Fact]
public void OwnedFramePublicationWithdrawsOnlyItsExactPair()
{
var calls = new List<string>();
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] [Fact]
public void OwnedSlotRejectsReentrantAcquireWithoutLosingOuterResource() public void OwnedSlotRejectsReentrantAcquireWithoutLosingOuterResource()
{ {

View file

@ -328,6 +328,12 @@ public sealed class WorldRenderFrameBuilderTests
public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition() public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition()
{ {
string gameWindow = GameWindowSource(); string gameWindow = GameWindowSource();
string frameRoot = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
string worldScene = File.ReadAllText(Path.Combine( string worldScene = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",
@ -357,11 +363,14 @@ public sealed class WorldRenderFrameBuilderTests
"_passes.DrawFlatTerrain(", "_passes.DrawFlatTerrain(",
"NormalWorldDrawn: true"); "NormalWorldDrawn: true");
AssertAppearsInOrder( AssertAppearsInOrder(
gameWindow, frameRoot,
"var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(", "var skyPesFrame = new SkyPesFrameController(",
"var worldRenderFrameBuilder =", "var worldRenderFrameBuilder =",
"new AcDream.App.Rendering.RenderFrameOrchestrator(", "new RenderFrameOrchestrator(",
"_frameGraphs.Withdraw();", "d.FrameGraphs.PublishOwned(");
AssertAppearsInOrder(
gameWindow,
"publication.Dispose();",
"new(\"equipped children\"", "new(\"equipped children\"",
"new(\"effect network state\"", "new(\"effect network state\"",
"new(\"audio\"", "new(\"audio\"",

View file

@ -279,11 +279,11 @@ public sealed class WorldSceneRendererTests
FindRepoRoot(), FindRepoRoot(),
"src", "src",
"AcDream.App", "AcDream.App",
"Rendering", "Composition",
"GameWindow.cs")); "FrameRootComposition.cs"));
Assert.Contains("new AcDream.App.Rendering.WorldSceneRenderer(", source); Assert.Contains("new WorldSceneRenderer(", source);
Assert.Contains("new AcDream.App.Rendering.RenderFrameOrchestrator(", source); Assert.Contains("new RenderFrameOrchestrator(", source);
Assert.DoesNotContain("_worldSceneRenderer", source); Assert.DoesNotContain("_worldSceneRenderer", source);
Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source); Assert.DoesNotContain("_retailPViewRenderer.DrawInside(", source);
Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source); Assert.DoesNotContain("_retailAlphaQueue.BeginFrame();", source);

View file

@ -276,10 +276,16 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Update", "Update",
"UpdateFrameRuntimeAdapters.cs")); "UpdateFrameRuntimeAdapters.cs"));
string frameRoot = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
Assert.Equal(0, CountOccurrences(source, "PublishTime(")); Assert.Equal(0, CountOccurrences(source, "PublishTime("));
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime(")); Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source, Assert.Contains("new PhysicsScriptClockPublisher(", frameRoot,
StringComparison.Ordinal); StringComparison.Ordinal);
} }
@ -349,10 +355,16 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Composition", "Composition",
"SessionPlayerComposition.cs")); "SessionPlayerComposition.cs"));
string frameRoot = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
Assert.Contains("new LiveObjectFrameController(", sessionPlayer); Assert.Contains("new LiveObjectFrameController(", sessionPlayer);
Assert.Contains("new LiveSpatialPresentationReconciler(", 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("AdvanceLiveObjectRuntime", source, StringComparison.Ordinal);
Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source, Assert.DoesNotContain("ReconcileLiveObjectSpatialPresentation", source,
StringComparison.Ordinal); StringComparison.Ordinal);
@ -375,7 +387,6 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Composition", "Composition",
"SessionPlayerComposition.cs")); "SessionPlayerComposition.cs"));
Assert.Contains("new StreamingFrameController(", sessionPlayer); Assert.Contains("new StreamingFrameController(", sessionPlayer);
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal); Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences( Assert.Equal(1, CountOccurrences(
@ -492,6 +503,12 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Composition", "Composition",
"SessionPlayerComposition.cs")); "SessionPlayerComposition.cs"));
string frameRoot = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
Assert.Contains( Assert.Contains(
"new LocalPlayerTeleportController(", "new LocalPlayerTeleportController(",
@ -513,10 +530,10 @@ public sealed class UpdateFrameOrchestratorTests
Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource, Assert.Contains("ILocalPlayerTeleportNetworkSink", networkSource,
StringComparison.Ordinal); StringComparison.Ordinal);
AssertAppearsInOrder( AssertAppearsInOrder(
source, frameRoot,
"new AcDream.App.Update.LiveEntityLivenessFramePhase(", "new LiveEntityLivenessFramePhase(",
"sessionPlayer.LocalTeleport,", "session.LocalTeleport,",
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(", "new PlayerModeAutoEntryFramePhase(",
"cameraFrame);"); "cameraFrame);");
} }