using AcDream.App.Update; namespace AcDream.App.Rendering; internal interface IGameUpdateFrameRoot { void Tick(UpdateFrameInput input); } internal interface IGameRenderFrameRoot { RenderFrameOutcome Render(RenderFrameInput input); } /// /// 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. /// 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) { 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."); } 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); } }