refactor(app): own native window callback lifetime

Publish the fixed Silk callback binding before acquisition, quiesce admitted callbacks before teardown, and retain failed physical detach ownership for retry. Preserve the frozen callback order while covering partial event accessors, concurrency, reentrancy, and shutdown completion.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 09:59:10 +02:00
parent 476c2e6de1
commit 18d4b999de
7 changed files with 1416 additions and 50 deletions

View file

@ -22,7 +22,9 @@ public sealed class GameWindow : IDisposable
private readonly WorldGameState _worldGameState;
private readonly WorldEvents _worldEvents;
private readonly AcDream.Core.Selection.SelectionState _selection;
private readonly HostQuiescenceGate _hostQuiescence = new();
private IWindow? _window;
private SilkWindowCallbackBinding? _windowCallbacks;
private GL? _gl;
private IInputContext? _input;
private TerrainModernRenderer? _terrain;
@ -667,23 +669,20 @@ public sealed class GameWindow : IDisposable
_window = Window.Create(options);
_displayFramePacing.BindSurface(
new SilkDisplayFramePacingSurface(_window));
_window.Load += OnLoad;
_window.Update += OnUpdate;
_window.Render += OnRender;
// Registered after OnRender so software pacing waits after all frame
// work and before Silk.NET performs its automatic buffer swap.
_window.Render += _displayFramePacing.OnFrameRendered;
_window.Closing += OnClosing;
_window.FocusChanged += OnFocusChanged;
_window.Move += _displayFramePacing.OnWindowMoved;
_window.StateChanged += _displayFramePacing.OnWindowStateChanged;
// L.0 Display tab: keep the GL viewport + camera aspect in sync
// with the window framebuffer. Without this handler, resizing
// the window (or applying a Display-tab Resolution change at
// startup) leaves the viewport pinned to the original size —
// user sees a small render in the corner of a big window.
_window.FramebufferResize += OnFramebufferResize;
// The fixed binding preserves main Render before post-render pacing,
// owns rollback/reverse-detach, and gates every native entry point.
_windowCallbacks = SilkWindowCallbackBinding.Create(
_window,
new WindowCallbackTargets(
OnLoad,
OnUpdate,
OnRender,
OnClosing,
OnFocusChanged,
OnFramebufferResize),
_displayFramePacing,
_hostQuiescence);
_windowCallbacks.Attach();
_window.Run();
}
@ -4375,10 +4374,14 @@ public sealed class GameWindow : IDisposable
}
private void OnClosing()
=> CompleteShutdown();
{
_hostQuiescence.StopAccepting();
CompleteShutdown();
}
private void CompleteShutdown()
{
_hostQuiescence.StopAccepting();
_shutdown ??= CreateShutdownTransaction();
try
{
@ -4574,6 +4577,18 @@ public sealed class GameWindow : IDisposable
]),
new ResourceShutdownStage("input",
[
new("native window callbacks", () =>
{
SilkWindowCallbackBinding? binding = _windowCallbacks;
if (binding is null)
return;
binding.Dispose();
if (!binding.IsDisposalComplete)
throw new NativeWindowCallbackCleanupDeferredException();
if (ReferenceEquals(_windowCallbacks, binding))
_windowCallbacks = null;
}),
new("combat input subscription", ()
=> Combat.CombatModeChanged -= SetInputCombatScope),
new("input context", () =>