refactor(app): close ordered startup composition

This commit is contained in:
Erik 2026-07-22 19:20:20 +02:00
parent 54244d31f1
commit 530b4bd8f5
16 changed files with 537 additions and 215 deletions

View file

@ -150,10 +150,14 @@ lifecycle/reconnect gate, and 395.2-second synchronized nine-stop soak pass.
Checkpoint H adds sole terrain/sky render-resource roots, one retained
Host/runtime lease, an atomic update/render frame-root pair, prepared portal
fallback/transfer, and checked retryable GL construction/state boundaries.
`GameWindow` is 3,689 raw lines / 162 fields / 37 methods, down 12,034 lines
(76.5%) from baseline. All three final corrected-diff reviews are clean. Slice
8 checkpoints IL—ordered composition, retryable shutdown, canonical soak
snapshots, and final connected gates—remain active.
Checkpoint I now routes production through the same executable nine-phase
pipeline as its failure oracle, carries exact platform/settings results through
typed phase inputs, atomically publishes frame roots, and makes session start
terminal. `GameWindow` is 1,945 raw lines, down 13,778 lines (87.6%) from
baseline. App Release passes 3,431 tests / 3 skips and the complete Release
suite passes 7,803 / 5 skips. Checkpoint I is complete; checkpoints JL—focused
retryable shutdown, canonical soak snapshots, and final connected gates—remain
active.
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
`docs/architecture/code-structure.md`. **Carried:** #232, #153,
#116, remaining

View file

@ -148,10 +148,14 @@ lifecycle/reconnect gate, and 395.2-second synchronized nine-stop soak pass.
Checkpoint H adds sole terrain/sky render-resource roots, one retained
Host/runtime lease, an atomic update/render frame-root pair, prepared portal
fallback/transfer, and checked retryable GL construction/state boundaries.
`GameWindow` is 3,689 raw lines / 162 fields / 37 methods, down 12,034 lines
(76.5%) from baseline. All three final corrected-diff reviews are clean. Slice
8 checkpoints IL—ordered composition, retryable shutdown, canonical soak
snapshots, and final connected gates—remain active.
Checkpoint I now routes production through the same executable nine-phase
pipeline as its failure oracle, carries exact platform/settings results through
typed phase inputs, atomically publishes frame roots, and makes session start
terminal. `GameWindow` is 1,945 raw lines, down 13,778 lines (87.6%) from
baseline. App Release passes 3,431 tests / 3 skips and the complete Release
suite passes 7,803 / 5 skips. Checkpoint I is complete; checkpoints JL—focused
retryable shutdown, canonical soak snapshots, and final connected gates—remain
active.
See `docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md` and
`docs/architecture/code-structure.md`. **Carried:** #232, #153,
#116, remaining

View file

@ -36,11 +36,11 @@ What does NOT go here:
command targets, and sole gameplay input subscriber before frame publication.
I.8b now owns the complete update/render construction, exact atomic frame
publication, and lifecycle resource snapshot source. I.8c makes session
start the terminal `OnLoad` operation. `GameWindow.OnUpdate` and
`GameWindow.OnRender` are typed orchestration handoffs and the class is
1,910 raw lines. I.9 plus the remaining
shutdown/closeout
cleanup is active in
start the terminal `OnLoad` operation. I.9 corrected the production root to
invoke the exact tested pipeline and made every consumed platform/settings
result explicit. `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed
orchestration handoffs and the class is 1,945 raw lines. Checkpoint I is
complete; the shutdown/lifetime cutover and closeout remain active in
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
This is the behavior-preserving prerequisite before new M4 feature bodies.
- **Connected-gate fidelity:** `#232` adds canonical managed/GPU/cache-owner

View file

@ -739,12 +739,14 @@ diagnostic command targets, and sole gameplay input subscriber into Phase 7
before frame publication. I.8b moves the complete update/render construction
body into `FrameRootCompositionPhase`, publishes the pair through an exact
lease, and gives lifecycle resource sampling a focused source. `GameWindow` is
1,910 raw lines after I.8c makes live-session start the terminal `OnLoad`
operation. I.9L remain active, with the complete Checkpoint-I audit next. The
App gate passes 3,430 tests / 3 intentional skips and the complete Release suite
passes 7,802 tests / 5 intentional skips. The clean solution build retains only
the 17 test-project warnings tracked by #228; the I.8c corrected-diff review is
clean.
1,945 raw lines after I.9 corrects production to invoke the same executable
pipeline as the failure oracle, carries exact platform/settings results through
the typed contract, and keeps live-session start terminal. Checkpoint I is
complete; JL remain active with lifetime/shutdown extraction next. The App gate
passes 3,431 tests / 3 intentional skips and the complete Release suite passes
7,803 tests / 5 intentional skips. The clean solution build retains only the 17
test-project warnings tracked by #228; the complete Checkpoint-I behavior,
architecture, and adversarial corrected-diff passes are clean.
### 4.4 Exit criteria

View file

@ -104,11 +104,12 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
resources have transactional phase ownership and exact-owner late bindings.
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
including explicit reversible ownership for every late edge. `GameWindow` is
now 1,910 raw lines after I.8c, which makes session start the terminal `OnLoad`
operation. I.9, shutdown, canonical soak snapshots, and closeout checkpoints
JL remain. The App gate passes 3,430 tests / 3 intentional skips, the complete
Release suite passes 7,802 tests / 5 skips, and the I.8c corrected-diff review
is clean.
now 1,945 raw lines after I.9 makes production invoke the same typed pipeline
as the failure oracle and keeps session start terminal. Checkpoint I is
complete; shutdown, canonical soak snapshots, and closeout checkpoints JL
remain. The App gate passes 3,431 tests / 3 intentional skips, the complete
Release suite passes 7,803 tests / 5 skips, and the complete Checkpoint-I
corrected-diff review is clean.
This is a behavior-preserving structural program. Severe regressions still get
root-cause fixes in separate commits; ordinary feature work resumes with M4

View file

@ -63,11 +63,13 @@ named reversible late edges. I.8a adds the focused live-session runtime factory
and attaches command/input owners before frame publication. I.8b composes and
atomically publishes the complete frame pair and extracts lifecycle resource
sampling. I.8c makes session start the terminal `OnLoad` operation.
`GameWindow` is 1,910 raw lines. I.9 and checkpoints JL remain active. Issue
#232 tracks
`GameWindow` is 1,945 raw lines after I.9 routes production through the exact
tested pipeline with explicit prior-result dependencies. Checkpoint I is
complete and checkpoints JL remain active. Issue #232 tracks
process-residency variance in the soak without loosening its leak threshold.
The App gate passes 3,430 tests / 3 skips, the complete Release suite passes
7,802 tests / 5 skips, and the I.8c corrected-diff review is clean.
The App gate passes 3,431 tests / 3 skips, the complete Release suite passes
7,803 tests / 5 skips, and the complete Checkpoint-I corrected-diff review is
clean.
Carried:
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus

View file

@ -1,6 +1,6 @@
# GameWindow Slice 8 Checkpoint I — ordered production composition
**Status:** Active 2026-07-22.
**Status:** Complete 2026-07-22.
**Parent:**
[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](2026-07-22-gamewindow-slice-8-composition-lifecycle.md),
Checkpoint I.
@ -299,9 +299,19 @@ ledger only after the complete pipeline is live.
the pair through an exact owned slot lease, and moves lifecycle resource
sampling out of the window. I.8c makes the exact session start and its
existing diagnostics the terminal `OnLoad` operation. `GameWindow.cs` is
1,910 raw lines. I.9 corrected-diff review and documentation closeout is the
active cut.
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
1,910 raw lines.
- I.9 found and corrected one structural defect: production manually repeated
the phase order instead of invoking the executable pipeline used by failure
tests, and several later phases received real platform/settings results only
through surrounding captures. Production now enters the same fixed pipeline;
platform and settings are explicit typed phase inputs and concrete phases
reject mismatched result instances. Session start remains the terminal
operation. Three corrected-diff passes covering behavior order, architecture/
ownership, and adversarial failure/reentrancy are clean. `GameWindow.cs` is
1,945 raw lines. App Release passes 3,431 tests / 3 intentional skips; the
complete Release suite passes 7,803 tests / 5 intentional skips with only the
17 pre-existing test-project warnings tracked by #228. No retail behavior or
divergence changed. Checkpoint I is complete; Checkpoint J is active.
## 6. Automated acceptance

View file

@ -28,9 +28,9 @@ audit.
- [x] G — extract two-phase persisted settings/display/quality ownership.
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
explicit single ownership and transfer seams.
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. I.1I.8 are complete; I.9
corrected-diff review and checkpoint closeout is active. Detailed plan:
- [x] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. Production and failure tests now
invoke the same pipeline and carry exact typed prior results. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
owner and prove all partial-load/reentrant/retry paths.
@ -627,6 +627,16 @@ corrected-diff reviews are clean. The complete Release suite passes 7,606 tests
teleport transfer, and frame-root publication. Each concrete phase also
proves transactional rollback of its internal acquisition prefix.
Result: all nine production phases now run through the same executable pipeline
as the prefix/failure oracle. Platform and settings results are explicit typed
inputs wherever consumed; phases reject mismatched instances, all long-lived
owners publish to focused lifetime slots, the frame pair publishes atomically,
and session start is terminal. `GameWindow.cs` is 1,945 raw lines. Three
corrected-diff passes are clean; App Release passes 3,431 tests / 3 skips and
the complete Release suite passes 7,803 tests / 5 skips with the existing 17
#228 warnings. No connected gate was required for the final structural
correction; Checkpoint L retains the connected lifecycle and soak gates.
### J — lifetime cutover and shutdown failure policy
- Move the teardown-only stage records and shutdown manifest out of GameWindow.

View file

@ -127,6 +127,7 @@ internal sealed class FrameRootRuntimeBindings : IDisposable
internal sealed class FrameRootCompositionPhase
: IFrameRootCompositionPhase<
GameWindowPlatformResult<GL, IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
@ -153,6 +154,7 @@ internal sealed class FrameRootCompositionPhase
}
public FrameRootResult Compose(
GameWindowPlatformResult<GL, IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
@ -161,6 +163,7 @@ internal sealed class FrameRootCompositionPhase
LivePresentationResult live,
SessionPlayerResult session)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
@ -168,6 +171,12 @@ internal sealed class FrameRootCompositionPhase
ArgumentNullException.ThrowIfNull(interaction);
ArgumentNullException.ThrowIfNull(live);
ArgumentNullException.ThrowIfNull(session);
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)
|| !ReferenceEquals(_dependencies.Input, platform.Input))
{
throw new InvalidOperationException(
"Frame-root dependencies do not match the ordered platform result.");
}
var scope = new CompositionAcquisitionScope();
FrameRootRuntimeBindings? bindings = null;

View file

@ -20,29 +20,37 @@ internal interface IWorldRenderCompositionPhase<in TPlatform, in TContent, in TS
TResult Compose(TPlatform platform, TContent content, TSettings settings);
}
internal interface IInteractionUiCompositionPhase<in THost, in TContent, in TSettings, in TWorld, out TResult>
internal interface IInteractionUiCompositionPhase<in TPlatform, in THost, in TContent, in TSettings, in TWorld, out TResult>
{
TResult Compose(THost host, TContent content, TSettings settings, TWorld world);
TResult Compose(TPlatform platform, THost host, TContent content, TSettings settings, TWorld world);
}
internal interface ILivePresentationCompositionPhase<in THost, in TContent, in TWorld, in TInteraction, out TResult>
internal interface ILivePresentationCompositionPhase<in TPlatform, in THost, in TContent, in TSettings, in TWorld, in TInteraction, out TResult>
{
TResult Compose(THost host, TContent content, TWorld world, TInteraction interaction);
TResult Compose(
TPlatform platform,
THost host,
TContent content,
TSettings settings,
TWorld world,
TInteraction interaction);
}
internal interface ISessionPlayerCompositionPhase<in THost, in TContent, in TWorld, in TInteraction, in TLive, out TResult>
internal interface ISessionPlayerCompositionPhase<in THost, in TContent, in TSettings, in TWorld, in TInteraction, in TLive, out TResult>
{
TResult Compose(
THost host,
TContent content,
TSettings settings,
TWorld world,
TInteraction interaction,
TLive live);
}
internal interface IFrameRootCompositionPhase<in THost, in TContent, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult>
internal interface IFrameRootCompositionPhase<in TPlatform, in THost, in TContent, in TSettings, in TWorld, in TInteraction, in TLive, in TSession, out TResult>
{
TResult Compose(
TPlatform platform,
THost host,
TContent content,
TSettings settings,
@ -57,6 +65,149 @@ internal interface ISessionStartCompositionPhase<in TFrame>
void Start(TFrame frame);
}
/// <summary>
/// Synchronous production entry point for the fixed composition topology.
/// The delegates exist only for the duration of <see cref="Run"/>; this lets
/// the native host remain the write-only publication shell without retaining
/// it in any phase owner.
/// </summary>
internal static class GameWindowCompositionPipeline
{
public static void Run<
TPlatform,
THost,
TContent,
TSettings,
TWorld,
TInteraction,
TLive,
TSession,
TFrame>(
TPlatform platform,
Func<TPlatform, THost> host,
Func<TPlatform, THost, TContent> content,
Func<TPlatform, THost, TContent, TSettings> settings,
Func<TPlatform, TContent, TSettings, TWorld> world,
Func<TPlatform, THost, TContent, TSettings, TWorld, TInteraction> interaction,
Func<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive> live,
Func<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession> session,
Func<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
Action<TFrame> start)
{
new GameWindowCompositionPipeline<
TPlatform,
THost,
TContent,
TSettings,
TWorld,
TInteraction,
TLive,
TSession,
TFrame>(
new HostPhase<TPlatform, THost>(host),
new ContentPhase<TPlatform, THost, TContent>(content),
new SettingsPhase<TPlatform, THost, TContent, TSettings>(settings),
new WorldPhase<TPlatform, TContent, TSettings, TWorld>(world),
new InteractionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction>(interaction),
new LivePhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive>(live),
new SessionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession>(session),
new FramePhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame>(frame),
new StartPhase<TFrame>(start))
.Run(platform);
}
private sealed class HostPhase<TPlatform, TResult>(
Func<TPlatform, TResult> compose)
: IHostInputCameraCompositionPhase<TPlatform, TResult>
{
public TResult Compose(TPlatform platform) => compose(platform);
}
private sealed class ContentPhase<TPlatform, THost, TResult>(
Func<TPlatform, THost, TResult> compose)
: IContentEffectsAudioCompositionPhase<TPlatform, THost, TResult>
{
public TResult Compose(TPlatform platform, THost host) => compose(platform, host);
}
private sealed class SettingsPhase<TPlatform, THost, TContent, TResult>(
Func<TPlatform, THost, TContent, TResult> compose)
: ISettingsDevToolsCompositionPhase<TPlatform, THost, TContent, TResult>
{
public TResult Compose(TPlatform platform, THost host, TContent content) =>
compose(platform, host, content);
}
private sealed class WorldPhase<TPlatform, TContent, TSettings, TResult>(
Func<TPlatform, TContent, TSettings, TResult> compose)
: IWorldRenderCompositionPhase<TPlatform, TContent, TSettings, TResult>
{
public TResult Compose(TPlatform platform, TContent content, TSettings settings) =>
compose(platform, content, settings);
}
private sealed class InteractionPhase<TPlatform, THost, TContent, TSettings, TWorld, TResult>(
Func<TPlatform, THost, TContent, TSettings, TWorld, TResult> compose)
: IInteractionUiCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TResult>
{
public TResult Compose(
TPlatform platform,
THost host,
TContent content,
TSettings settings,
TWorld world) => compose(platform, host, content, settings, world);
}
private sealed class LivePhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TResult>(
Func<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TResult> compose)
: ILivePresentationCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TResult>
{
public TResult Compose(
TPlatform platform,
THost host,
TContent content,
TSettings settings,
TWorld world,
TInteraction interaction) =>
compose(platform, host, content, settings, world, interaction);
}
private sealed class SessionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TResult>(
Func<THost, TContent, TSettings, TWorld, TInteraction, TLive, TResult> compose)
: ISessionPlayerCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TResult>
{
public TResult Compose(
THost host,
TContent content,
TSettings settings,
TWorld world,
TInteraction interaction,
TLive live) => compose(host, content, settings, world, interaction, live);
}
private sealed class FramePhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TResult>(
Func<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TResult> compose)
: IFrameRootCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TResult>
{
public TResult Compose(
TPlatform platform,
THost host,
TContent content,
TSettings settings,
TWorld world,
TInteraction interaction,
TLive live,
TSession session) =>
compose(platform, host, content, settings, world, interaction, live, session);
}
private sealed class StartPhase<TFrame>(Action<TFrame> start)
: ISessionStartCompositionPhase<TFrame>
{
public void Start(TFrame frame) => start(frame);
}
}
/// <summary>
/// Fixed startup topology. It intentionally retains no cumulative context or
/// final runtime graph; immutable phase results exist only as locals during
@ -77,10 +228,10 @@ internal sealed class GameWindowCompositionPipeline<
private readonly IContentEffectsAudioCompositionPhase<TPlatform, THost, TContent> _content;
private readonly ISettingsDevToolsCompositionPhase<TPlatform, THost, TContent, TSettings> _settings;
private readonly IWorldRenderCompositionPhase<TPlatform, TContent, TSettings, TWorld> _world;
private readonly IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> _interaction;
private readonly ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> _live;
private readonly ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> _session;
private readonly IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame;
private readonly IInteractionUiCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction> _interaction;
private readonly ILivePresentationCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive> _live;
private readonly ISessionPlayerCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession> _session;
private readonly IFrameRootCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> _frame;
private readonly ISessionStartCompositionPhase<TFrame> _start;
public GameWindowCompositionPipeline(
@ -88,10 +239,10 @@ internal sealed class GameWindowCompositionPipeline<
IContentEffectsAudioCompositionPhase<TPlatform, THost, TContent> content,
ISettingsDevToolsCompositionPhase<TPlatform, THost, TContent, TSettings> settings,
IWorldRenderCompositionPhase<TPlatform, TContent, TSettings, TWorld> world,
IInteractionUiCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction> interaction,
ILivePresentationCompositionPhase<THost, TContent, TWorld, TInteraction, TLive> live,
ISessionPlayerCompositionPhase<THost, TContent, TWorld, TInteraction, TLive, TSession> session,
IFrameRootCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
IInteractionUiCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction> interaction,
ILivePresentationCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive> live,
ISessionPlayerCompositionPhase<THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession> session,
IFrameRootCompositionPhase<TPlatform, THost, TContent, TSettings, TWorld, TInteraction, TLive, TSession, TFrame> frame,
ISessionStartCompositionPhase<TFrame> start)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
@ -111,10 +262,11 @@ internal sealed class GameWindowCompositionPipeline<
TContent content = _content.Compose(platform, host);
TSettings settings = _settings.Compose(platform, host, content);
TWorld world = _world.Compose(platform, content, settings);
TInteraction interaction = _interaction.Compose(host, content, settings, world);
TLive live = _live.Compose(host, content, world, interaction);
TSession session = _session.Compose(host, content, world, interaction, live);
TInteraction interaction = _interaction.Compose(platform, host, content, settings, world);
TLive live = _live.Compose(platform, host, content, settings, world, interaction);
TSession session = _session.Compose(host, content, settings, world, interaction, live);
TFrame frame = _frame.Compose(
platform,
host,
content,
settings,

View file

@ -693,6 +693,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
internal sealed class InteractionRetainedUiCompositionPhase
: IInteractionUiCompositionPhase<
GameWindowPlatformResult<GL, IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
@ -805,16 +806,20 @@ internal sealed class InteractionRetainedUiCompositionPhase
}
public InteractionRetainedUiResult Compose(
GameWindowPlatformResult<GL, IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
if (!ReferenceEquals(_dependencies.InputDispatcher, host.InputDispatcher)
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics)
|| !ReferenceEquals(_dependencies.Input, platform.Input)
|| !ReferenceEquals(_dependencies.InputDispatcher, host.InputDispatcher)
|| !ReferenceEquals(_dependencies.Dats, content.Dats)
|| !ReferenceEquals(_dependencies.MagicCatalog, content.MagicCatalog)
|| !ReferenceEquals(_dependencies.TextureCache, world.Foundation.TextureCache)

View file

@ -131,8 +131,10 @@ internal enum LivePresentationCompositionPoint
internal sealed class LivePresentationCompositionPhase
: ILivePresentationCompositionPhase<
GameWindowPlatformResult<GL, Silk.NET.Input.IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult>
@ -154,15 +156,24 @@ internal sealed class LivePresentationCompositionPhase
}
public LivePresentationResult Compose(
GameWindowPlatformResult<GL, Silk.NET.Input.IInputContext> platform,
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction)
{
ArgumentNullException.ThrowIfNull(platform);
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
if (!ReferenceEquals(_dependencies.Gl, platform.Graphics))
{
throw new InvalidOperationException(
"Live-presentation dependencies do not match the ordered platform result.");
}
return ComposeCore(host, content, world, interaction);
}

View file

@ -148,6 +148,7 @@ internal sealed class SessionPlayerCompositionPhase
: ISessionPlayerCompositionPhase<
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult,
@ -172,15 +173,22 @@ internal sealed class SessionPlayerCompositionPhase
public SessionPlayerResult Compose(
HostInputCameraResult host,
ContentEffectsAudioResult content,
SettingsDevToolsResult settings,
WorldRenderResult world,
InteractionRetainedUiResult interaction,
LivePresentationResult live)
{
ArgumentNullException.ThrowIfNull(host);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(interaction);
ArgumentNullException.ThrowIfNull(live);
if (!ReferenceEquals(_dependencies.SettingsDevTools, settings))
{
throw new InvalidOperationException(
"Session/player dependencies do not match the ordered settings result.");
}
var scope = new CompositionAcquisitionScope();
SessionPlayerRuntimeBindings? bindings = null;

View file

@ -1097,8 +1097,18 @@ public sealed class GameWindow :
GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();
HostInputCameraResult hostInputCamera =
new HostInputCameraCompositionPhase(
GameWindowCompositionPipeline.Run<
GameWindowPlatformResult<GL, IInputContext>,
HostInputCameraResult,
ContentEffectsAudioResult,
SettingsDevToolsResult,
WorldRenderResult,
InteractionRetainedUiResult,
LivePresentationResult,
SessionPlayerResult,
FrameRootResult>(
platform,
platformResult => new HostInputCameraCompositionPhase(
new HostInputCameraDependencies(
_framebufferResize,
_window!.FramebufferSize,
@ -1111,12 +1121,9 @@ public sealed class GameWindow :
_chaseCameraInput,
_pointerPosition,
_renderDiagnosticLog),
this).Compose(platform);
WorldRenderDiagnostics worldRenderDiagnostics =
hostInputCamera.WorldRenderDiagnostics;
ContentEffectsAudioResult contentEffectsAudio =
new ContentEffectsAudioCompositionPhase(
this).Compose(platformResult),
(platformResult, hostInputCamera) =>
new ContentEffectsAudioCompositionPhase(
new ContentEffectsAudioDependencies(
_datDir,
_physicsDataCache,
@ -1130,91 +1137,95 @@ public sealed class GameWindow :
_options.NoAudio,
Console.WriteLine,
Console.Error.WriteLine),
this).Compose(platform, hostInputCamera);
SettingsDevToolsOptionalDependencies? optionalDevTools = null;
if (DevToolsEnabled)
{
var devToolsWorldEntities =
new DeferredCanonicalWorldEntityCountSource();
var devToolsFrameDiagnostics =
new DeferredRenderFrameDiagnosticsSource();
var devToolsPlayerModeCommands =
new DeferredDevToolsPlayerModeCommands();
var devToolsFacts = new DevToolsRuntimeFacts(
_localPlayerMode,
_playerControllerSlot,
hostInputCamera.CameraController,
devToolsWorldEntities,
_animatedEntities,
_debugVmRenderFacts,
_physicsEngine,
_worldSceneDebugState,
_renderRange,
hostInputCamera.CameraPointerInput,
_worldEnvironment,
Lighting,
contentEffectsAudio.ParticleSystem,
devToolsFrameDiagnostics);
IRuntimeKeyBindingTarget? keyBindingTarget =
hostInputCamera.InputDispatcher is { } settingsDispatcher
? new RuntimeKeyBindingTarget(settingsDispatcher)
: null;
optionalDevTools = new SettingsDevToolsOptionalDependencies(
devToolsFacts,
keyBindingTarget,
devToolsWorldEntities,
devToolsFrameDiagnostics,
devToolsPlayerModeCommands);
}
SettingsDevToolsResult settingsDevTools =
new SettingsDevToolsCompositionPhase(
new SettingsDevToolsDependencies(
_window!,
_runtimeSettings,
new RuntimeSettingsStartupTargets(
new SilkRuntimeDisplayWindowTarget(_window!),
_displayFramePacing,
this).Compose(platformResult, hostInputCamera),
(platformResult, hostInputCamera, contentEffectsAudio) =>
{
SettingsDevToolsOptionalDependencies? optionalDevTools = null;
if (DevToolsEnabled)
{
var devToolsWorldEntities =
new DeferredCanonicalWorldEntityCountSource();
var devToolsFrameDiagnostics =
new DeferredRenderFrameDiagnosticsSource();
var devToolsPlayerModeCommands =
new DeferredDevToolsPlayerModeCommands();
var devToolsFacts = new DevToolsRuntimeFacts(
_localPlayerMode,
_playerControllerSlot,
hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine),
_hostQuiescence,
Chat,
Combat,
LocalPlayer,
optionalDevTools,
_runtimeDiagnosticCommands,
_combatFeedback,
_keyBindings,
_frameProfiler,
_framebufferResize,
Console.WriteLine),
this).Compose(platform, hostInputCamera, contentEffectsAudio);
Action<string>? compositionToast = settingsDevTools.DevTools is { } composedDevTools
? text => composedDevTools.Debug.AddToast(text)
: null;
devToolsWorldEntities,
_animatedEntities,
_debugVmRenderFacts,
_physicsEngine,
_worldSceneDebugState,
_renderRange,
hostInputCamera.CameraPointerInput,
_worldEnvironment,
Lighting,
contentEffectsAudio.ParticleSystem,
devToolsFrameDiagnostics);
IRuntimeKeyBindingTarget? keyBindingTarget =
hostInputCamera.InputDispatcher is { } settingsDispatcher
? new RuntimeKeyBindingTarget(settingsDispatcher)
: null;
optionalDevTools = new SettingsDevToolsOptionalDependencies(
devToolsFacts,
keyBindingTarget,
devToolsWorldEntities,
devToolsFrameDiagnostics,
devToolsPlayerModeCommands);
}
const uint initialCenterLandblockId = 0xA9B4FFFFu;
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
new WorldRenderDependencies(
_worldEnvironment,
_renderResourceLifetime,
_gpuFrameFlights!,
initialCenterLandblockId,
Console.WriteLine),
this).Compose(platform, contentEffectsAudio, settingsDevTools);
string shadersDir = worldRender.Foundation.ShadersDirectory;
Console.WriteLine(
$"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
InteractionRetainedUiResult interactionUi =
new InteractionRetainedUiCompositionPhase(
new InteractionRetainedUiDependencies(
return new SettingsDevToolsCompositionPhase(
new SettingsDevToolsDependencies(
_window!,
_runtimeSettings,
new RuntimeSettingsStartupTargets(
new SilkRuntimeDisplayWindowTarget(_window!),
_displayFramePacing,
hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine),
_hostQuiescence,
Chat,
Combat,
LocalPlayer,
optionalDevTools,
_runtimeDiagnosticCommands,
_combatFeedback,
_keyBindings,
_frameProfiler,
_framebufferResize,
Console.WriteLine),
this).Compose(platformResult, hostInputCamera, contentEffectsAudio);
},
(platformResult, contentEffectsAudio, settingsDevTools) =>
{
const uint initialCenterLandblockId = 0xA9B4FFFFu;
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
new WorldRenderDependencies(
_worldEnvironment,
_renderResourceLifetime,
_gpuFrameFlights!,
initialCenterLandblockId,
Console.WriteLine),
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
Console.WriteLine(
$"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
return worldRender;
},
(platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) =>
{
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
? text => devTools.Debug.AddToast(text)
: null;
return new InteractionRetainedUiCompositionPhase(
new InteractionRetainedUiDependencies(
_options,
platform.Graphics,
platformResult.Graphics,
_window!,
platform.Input,
shadersDir,
platformResult.Input,
worldRender.Foundation.ShadersDirectory,
contentEffectsAudio.Dats,
_datLock,
worldRender.Foundation.TextureCache,
@ -1252,16 +1263,26 @@ public sealed class GameWindow :
Console.WriteLine),
_retailUiLease,
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender);
LivePresentationResult livePresentation =
new LivePresentationCompositionPhase(
new LivePresentationDependencies(
},
(platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi) =>
{
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
? text => devTools.Debug.AddToast(text)
: null;
return new LivePresentationCompositionPhase(
new LivePresentationDependencies(
_options,
platform.Graphics,
platformResult.Graphics,
_window!,
_datLock,
_runtimeSettings,
@ -1299,13 +1320,20 @@ public sealed class GameWindow :
_uiFrameDiagnostics,
compositionToast),
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi);
SessionPlayerResult sessionPlayer =
new SessionPlayerCompositionPhase(
},
(hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation) =>
new SessionPlayerCompositionPhase(
new SessionPlayerDependencies(
_options,
_window!,
@ -1371,58 +1399,65 @@ public sealed class GameWindow :
this).Compose(
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation);
FrameRootResult frameRoots = new FrameRootCompositionPhase(
new FrameRootDependencies(
_options,
_gl!,
_window!,
_input!,
WorldTime,
Weather,
Lighting,
_worldEnvironment,
_physicsEngine,
_cellVisibility,
_localPlayerMode,
_localPlayerIdentity,
_chaseCameraInput,
_playerControllerSlot,
_liveWorldOrigin,
_particleVisibility,
_effectPoses,
_renderRange,
_runtimeSettings,
_displayFramePacing,
_worldSceneDebugState,
_retailAlphaQueue,
_frameProfiler,
_frameDiag,
_renderDiagnosticLog,
_debugVmRenderFacts,
_inputCapture,
_cameraInput,
_selection,
_animatedEntities,
_updateFrameClock,
Combat,
_frameGraphs,
Console.WriteLine),
this).Compose(
livePresentation),
(platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation,
sessionPlayer);
new SessionStartCompositionPhase(
new SessionStartDependencies(_options, Console.WriteLine))
.Start(frameRoots);
sessionPlayer) => new FrameRootCompositionPhase(
new FrameRootDependencies(
_options,
platformResult.Graphics,
_window!,
platformResult.Input,
WorldTime,
Weather,
Lighting,
_worldEnvironment,
_physicsEngine,
_cellVisibility,
_localPlayerMode,
_localPlayerIdentity,
_chaseCameraInput,
_playerControllerSlot,
_liveWorldOrigin,
_particleVisibility,
_effectPoses,
_renderRange,
_runtimeSettings,
_displayFramePacing,
_worldSceneDebugState,
_retailAlphaQueue,
_frameProfiler,
_frameDiag,
_renderDiagnosticLog,
_debugVmRenderFacts,
_inputCapture,
_cameraInput,
_selection,
_animatedEntities,
_updateFrameClock,
Combat,
_frameGraphs,
Console.WriteLine),
this).Compose(
platformResult,
hostInputCamera,
contentEffectsAudio,
settingsDevTools,
worldRender,
interactionUi,
livePresentation,
sessionPlayer),
frameRoots => new SessionStartCompositionPhase(
new SessionStartDependencies(_options, Console.WriteLine))
.Start(frameRoots));
}
private void OnUpdate(double dt)

View file

@ -42,6 +42,65 @@ public sealed class GameWindowCompositionPipelineTests
Assert.Equal("phase-9", phases.Calls[^1]);
}
[Fact]
public void ProductionDelegateEntryUsesTheSamePipelineAndExactResults()
{
var calls = new List<int>();
var values = Enumerable.Range(0, 9).Select(value => new Token(value)).ToArray();
GameWindowCompositionPipeline.Run<
Token,
Token,
Token,
Token,
Token,
Token,
Token,
Token,
Token>(
values[0],
platform => Pass(1, values[1], platform),
(platform, host) => Pass(2, values[2], platform, host),
(platform, host, content) => Pass(3, values[3], platform, host, content),
(platform, content, settings) => Pass(4, values[4], platform, content, settings),
(platform, host, content, settings, world) =>
Pass(5, values[5], platform, host, content, settings, world),
(platform, host, content, settings, world, interaction) =>
Pass(6, values[6], platform, host, content, settings, world, interaction),
(host, content, settings, world, interaction, live) =>
Pass(7, values[7], host, content, settings, world, interaction, live),
(platform, host, content, settings, world, interaction, live, session) =>
Pass(8, values[8], platform, host, content, settings, world, interaction, live, session),
frame =>
{
Assert.Same(values[8], frame);
calls.Add(9);
});
Assert.Equal(Enumerable.Range(1, 9), calls);
Token Pass(int phase, Token result, params Token[] actual)
{
Token[] expected = phase switch
{
1 => [values[0]],
2 => [values[0], values[1]],
3 => [values[0], values[1], values[2]],
4 => [values[0], values[2], values[3]],
5 => [values[0], values[1], values[2], values[3], values[4]],
6 => [values[0], values[1], values[2], values[3], values[4], values[5]],
7 => [values[1], values[2], values[3], values[4], values[5], values[6]],
8 => [values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7]],
_ => throw new ArgumentOutOfRangeException(nameof(phase)),
};
Assert.Equal(expected.Length, actual.Length);
for (int index = 0; index < expected.Length; index++)
Assert.Same(expected[index], actual[index]);
calls.Add(phase);
return result;
}
}
private sealed record Token(int Phase);
private sealed class RecordingPhases(int failingPhase) :
@ -49,10 +108,10 @@ public sealed class GameWindowCompositionPipelineTests
IContentEffectsAudioCompositionPhase<Token, Token, Token>,
ISettingsDevToolsCompositionPhase<Token, Token, Token, Token>,
IWorldRenderCompositionPhase<Token, Token, Token, Token>,
IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>,
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>,
ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token>,
IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>,
IInteractionUiCompositionPhase<Token, Token, Token, Token, Token, Token>,
ILivePresentationCompositionPhase<Token, Token, Token, Token, Token, Token, Token>,
ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token, Token>,
IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token, Token>,
ISessionStartCompositionPhase<Token>
{
public Token Platform { get; } = new(0);
@ -111,12 +170,14 @@ public sealed class GameWindowCompositionPipelineTests
return _world;
}
Token IInteractionUiCompositionPhase<Token, Token, Token, Token, Token>.Compose(
Token IInteractionUiCompositionPhase<Token, Token, Token, Token, Token, Token>.Compose(
Token platform,
Token host,
Token content,
Token settings,
Token world)
{
Assert.Same(Platform, platform);
Assert.Same(_host, host);
Assert.Same(_content, content);
Assert.Same(_settings, settings);
@ -125,29 +186,35 @@ public sealed class GameWindowCompositionPipelineTests
return _interaction;
}
Token ILivePresentationCompositionPhase<Token, Token, Token, Token, Token>.Compose(
Token ILivePresentationCompositionPhase<Token, Token, Token, Token, Token, Token, Token>.Compose(
Token platform,
Token host,
Token content,
Token settings,
Token world,
Token interaction)
{
Assert.Same(Platform, platform);
Assert.Same(_host, host);
Assert.Same(_content, content);
Assert.Same(_settings, settings);
Assert.Same(_world, world);
Assert.Same(_interaction, interaction);
Record(6);
return _live;
}
Token ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token>.Compose(
Token ISessionPlayerCompositionPhase<Token, Token, Token, Token, Token, Token, Token>.Compose(
Token host,
Token content,
Token settings,
Token world,
Token interaction,
Token live)
{
Assert.Same(_host, host);
Assert.Same(_content, content);
Assert.Same(_settings, settings);
Assert.Same(_world, world);
Assert.Same(_interaction, interaction);
Assert.Same(_live, live);
@ -155,7 +222,8 @@ public sealed class GameWindowCompositionPipelineTests
return _session;
}
Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token>.Compose(
Token IFrameRootCompositionPhase<Token, Token, Token, Token, Token, Token, Token, Token, Token>.Compose(
Token platform,
Token host,
Token content,
Token settings,
@ -164,6 +232,7 @@ public sealed class GameWindowCompositionPipelineTests
Token live,
Token session)
{
Assert.Same(Platform, platform);
Assert.Same(_host, host);
Assert.Same(_content, content);
Assert.Same(_settings, settings);

View file

@ -52,22 +52,21 @@ public sealed class GameWindowSlice8BoundaryTests
AssertAppearsInOrder(
body,
"GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();",
"GameWindowCompositionPipeline.Run<",
"new HostInputCameraCompositionPhase(",
"this).Compose(platform);",
"this).Compose(platformResult),",
"new ContentEffectsAudioCompositionPhase(",
"this).Compose(platform, hostInputCamera);",
"this).Compose(platformResult, hostInputCamera),",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"InteractionRetainedUiResult interactionUi =",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
"new InteractionRetainedUiCompositionPhase(",
"this).Compose(",
"LivePresentationResult livePresentation =",
"new LivePresentationCompositionPhase(",
"this).Compose(",
"new SessionPlayerCompositionPhase(",
"FrameRootResult frameRoots = new FrameRootCompositionPhase(",
"new FrameRootCompositionPhase(",
"new SessionStartCompositionPhase(",
".Start(frameRoots);");
".Start(frameRoots));");
string framePhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
@ -98,7 +97,7 @@ public sealed class GameWindowSlice8BoundaryTests
"_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences(
body,
".Start(frameRoots);"));
".Start(frameRoots));"));
string phaseOne = Slice(
File.ReadAllText(Path.Combine(
FindRepoRoot(),
@ -131,9 +130,9 @@ public sealed class GameWindowSlice8BoundaryTests
"case LiveSessionStartStatus.MissingCredentials:",
"case LiveSessionStartStatus.Failed:");
int start = body.IndexOf(
".Start(frameRoots);",
".Start(frameRoots));",
StringComparison.Ordinal);
string postStart = body[(start + ".Start(frameRoots);".Length)..];
string postStart = body[(start + ".Start(frameRoots));".Length)..];
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
postStart,
@"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)"));
@ -174,7 +173,7 @@ public sealed class GameWindowSlice8BoundaryTests
source,
StringComparison.Ordinal);
Assert.Contains(
"new WorldRenderDependencies(\n _worldEnvironment,",
"new WorldRenderDependencies(\n _worldEnvironment,",
load.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
string worldCompose = Slice(
@ -350,13 +349,14 @@ public sealed class GameWindowSlice8BoundaryTests
"Window.Create(options)");
AssertAppearsInOrder(
load,
"GameWindowCompositionPipeline.Run<",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
"new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(",
"new SessionStartCompositionPhase(",
".Start(frameRoots);");
".Start(frameRoots));");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",