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

@ -29,12 +29,28 @@ internal sealed class GameFrameGraphSlot
public bool IsPublished => Volatile.Read(ref _pair) is not null;
public void Publish(IGameUpdateFrameRoot update, IGameRenderFrameRoot render)
{
_ = PublishCore(update, render);
}
public IDisposable PublishOwned(
IGameUpdateFrameRoot update,
IGameRenderFrameRoot render)
{
FrameGraphPair pair = PublishCore(update, render);
return new Publication(this, pair);
}
private FrameGraphPair PublishCore(
IGameUpdateFrameRoot update,
IGameRenderFrameRoot render)
{
ArgumentNullException.ThrowIfNull(update);
ArgumentNullException.ThrowIfNull(render);
var pair = new FrameGraphPair(update, render);
if (Interlocked.CompareExchange(ref _pair, pair, null) is not null)
throw new InvalidOperationException("A game frame graph is already published.");
return pair;
}
public bool Tick(UpdateFrameInput input)
@ -64,4 +80,22 @@ internal sealed class GameFrameGraphSlot
{
Interlocked.Exchange(ref _pair, null);
}
private void Withdraw(FrameGraphPair expected) =>
Interlocked.CompareExchange(ref _pair, null, expected);
private sealed class Publication : IDisposable
{
private GameFrameGraphSlot? _owner;
private readonly FrameGraphPair _expected;
public Publication(GameFrameGraphSlot owner, FrameGraphPair expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Withdraw(_expected);
}
}