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>
101 lines
2.7 KiB
C#
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);
|
|
}
|
|
}
|