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

@ -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;