acdream/src/AcDream.App/Rendering/GameFrameGraphSlot.cs
Erik 3628aeb520 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>
2026-07-22 19:02:08 +02:00

101 lines
2.7 KiB
C#

using AcDream.App.Update;
namespace AcDream.App.Rendering;
internal interface IGameUpdateFrameRoot
{
void Tick(UpdateFrameInput input);
}
internal interface IGameRenderFrameRoot
{
RenderFrameOutcome Render(RenderFrameInput input);
}
/// <summary>
/// Sole atomic publication point for the current update/render frame pair.
/// Native callbacks are deliberately inert before publication and after
/// withdrawal so a partial load or converging shutdown cannot enter half a
/// frame graph.
/// </summary>
internal sealed class GameFrameGraphSlot
{
private sealed record FrameGraphPair(
IGameUpdateFrameRoot Update,
IGameRenderFrameRoot Render);
private FrameGraphPair? _pair;
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)
{
FrameGraphPair? pair = Volatile.Read(ref _pair);
if (pair is null)
return false;
pair.Update.Tick(input);
return true;
}
public bool Render(RenderFrameInput input, out RenderFrameOutcome outcome)
{
FrameGraphPair? pair = Volatile.Read(ref _pair);
if (pair is null)
{
outcome = default;
return false;
}
outcome = pair.Render.Render(input);
return true;
}
public void Withdraw()
{
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);
}
}