using Silk.NET.Maths; using Silk.NET.Windowing; namespace AcDream.App.Rendering; internal sealed class NativeWindowCallbackCleanupDeferredException() : InvalidOperationException( "Native window callback cleanup was requested reentrantly and remains pending."); /// Fixed application callbacks accepted from the native window. internal sealed class WindowCallbackTargets { public WindowCallbackTargets( Action load, Action update, Action render, Action closing, Action focusChanged, Action> framebufferResize) { Load = load ?? throw new ArgumentNullException(nameof(load)); Update = update ?? throw new ArgumentNullException(nameof(update)); Render = render ?? throw new ArgumentNullException(nameof(render)); Closing = closing ?? throw new ArgumentNullException(nameof(closing)); FocusChanged = focusChanged ?? throw new ArgumentNullException(nameof(focusChanged)); FramebufferResize = framebufferResize ?? throw new ArgumentNullException(nameof(framebufferResize)); } public Action Load { get; } public Action Update { get; } public Action Render { get; } public Action Closing { get; } public Action FocusChanged { get; } public Action> FramebufferResize { get; } } /// /// Narrow event surface used to test native callback acquisition without /// implementing Silk's complete contract. /// internal interface IWindowCallbackSurface { void AddLoad(Action callback); void RemoveLoad(Action callback); void AddUpdate(Action callback); void RemoveUpdate(Action callback); void AddRender(Action callback); void RemoveRender(Action callback); void AddClosing(Action callback); void RemoveClosing(Action callback); void AddFocusChanged(Action callback); void RemoveFocusChanged(Action callback); void AddMove(Action> callback); void RemoveMove(Action> callback); void AddStateChanged(Action callback); void RemoveStateChanged(Action callback); void AddFramebufferResize(Action> callback); void RemoveFramebufferResize(Action> callback); } /// Production adapter over Silk's native event surface. internal sealed class SilkWindowCallbackSurface(IWindow window) : IWindowCallbackSurface { private readonly IWindow _window = window ?? throw new ArgumentNullException(nameof(window)); public void AddLoad(Action callback) => _window.Load += callback; public void RemoveLoad(Action callback) => _window.Load -= callback; public void AddUpdate(Action callback) => _window.Update += callback; public void RemoveUpdate(Action callback) => _window.Update -= callback; public void AddRender(Action callback) => _window.Render += callback; public void RemoveRender(Action callback) => _window.Render -= callback; public void AddClosing(Action callback) => _window.Closing += callback; public void RemoveClosing(Action callback) => _window.Closing -= callback; public void AddFocusChanged(Action callback) => _window.FocusChanged += callback; public void RemoveFocusChanged(Action callback) => _window.FocusChanged -= callback; public void AddMove(Action> callback) => _window.Move += callback; public void RemoveMove(Action> callback) => _window.Move -= callback; public void AddStateChanged(Action callback) => _window.StateChanged += callback; public void RemoveStateChanged(Action callback) => _window.StateChanged -= callback; public void AddFramebufferResize(Action> callback) => _window.FramebufferResize += callback; public void RemoveFramebufferResize(Action> callback) => _window.FramebufferResize -= callback; } /// /// Owns the exact GameWindow-to-Silk callback edge. Registration is /// transactional, physical removal is retriable, and the shared logical gate /// is closed before any detach can re-enter a retiring owner. /// internal sealed class SilkWindowCallbackBinding : IDisposable { private enum LifecycleState { Created, Attaching, Attached, AttachFailed, Detaching, DetachPending, Detached, } private const int LoadIndex = 0; private const int UpdateIndex = 1; private const int MainRenderIndex = 2; private const int PacingRenderIndex = 3; private const int ClosingIndex = 4; private const int FocusChangedIndex = 5; private const int MoveIndex = 6; private const int StateChangedIndex = 7; private const int FramebufferResizeIndex = 8; private const int AttachmentCount = 9; private readonly IWindowCallbackSurface _surface; private readonly HostQuiescenceGate _quiescence; private readonly Action _load; private readonly Action _update; private readonly Action _mainRender; private readonly Action _pacingRender; private readonly Action _closing; private readonly Action _focusChanged; private readonly Action> _moved; private readonly Action _stateChanged; private readonly Action> _framebufferResize; private readonly ResourceShutdownTransaction _detach; private readonly bool[] _attached = new bool[AttachmentCount]; private readonly object _lifecycleSync = new(); private int _attachedCount; private int _detachRequested; private int _lifecycleOwnerThreadId; private LifecycleState _lifecycleState = LifecycleState.Created; private SilkWindowCallbackBinding( IWindowCallbackSurface surface, WindowCallbackTargets targets, DisplayFramePacingController pacing, HostQuiescenceGate quiescence) { _surface = surface ?? throw new ArgumentNullException(nameof(surface)); ArgumentNullException.ThrowIfNull(targets); ArgumentNullException.ThrowIfNull(pacing); _quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence)); _load = () => _quiescence.Invoke(targets.Load); _update = value => _quiescence.Invoke(targets.Update, value); _mainRender = value => _quiescence.Invoke(targets.Render, value); _pacingRender = value => _quiescence.Invoke(pacing.OnFrameRendered, value); _closing = () => _quiescence.Invoke(targets.Closing); _focusChanged = value => _quiescence.Invoke(targets.FocusChanged, value); _moved = value => _quiescence.Invoke(pacing.OnWindowMoved, value); _stateChanged = value => _quiescence.Invoke(pacing.OnWindowStateChanged, value); _framebufferResize = value => _quiescence.Invoke(targets.FramebufferResize, value); _detach = new ResourceShutdownTransaction( new ResourceShutdownStage("native window callbacks", [ new("framebuffer resize", () => Remove(FramebufferResizeIndex)), new("window state", () => Remove(StateChangedIndex)), new("window move", () => Remove(MoveIndex)), new("focus changed", () => Remove(FocusChangedIndex)), new("closing", () => Remove(ClosingIndex)), new("pacing render", () => Remove(PacingRenderIndex)), new("main render", () => Remove(MainRenderIndex)), new("update", () => Remove(UpdateIndex)), new("load", () => Remove(LoadIndex)), ])); } public bool IsDetached { get { lock (_lifecycleSync) return _attached.All(static attached => !attached); } } public bool IsDisposalComplete { get { lock (_lifecycleSync) return _lifecycleState == LifecycleState.Detached; } } public static SilkWindowCallbackBinding Create( IWindow window, WindowCallbackTargets targets, DisplayFramePacingController pacing, HostQuiescenceGate quiescence) => Create(new SilkWindowCallbackSurface(window), targets, pacing, quiescence); internal static SilkWindowCallbackBinding Create( IWindowCallbackSurface surface, WindowCallbackTargets targets, DisplayFramePacingController pacing, HostQuiescenceGate quiescence) => new(surface, targets, pacing, quiescence); public void Attach() { int threadId = Environment.CurrentManagedThreadId; lock (_lifecycleSync) { if (_lifecycleState != LifecycleState.Created) { throw _lifecycleState == LifecycleState.Detached ? new ObjectDisposedException(nameof(SilkWindowCallbackBinding)) : new InvalidOperationException( "Native window callback attachment has already started."); } if (Volatile.Read(ref _detachRequested) != 0 || _detach.IsComplete) throw new ObjectDisposedException(nameof(SilkWindowCallbackBinding)); _lifecycleState = LifecycleState.Attaching; _lifecycleOwnerThreadId = threadId; } try { Attach(LoadIndex); Attach(UpdateIndex); Attach(MainRenderIndex); Attach(PacingRenderIndex); Attach(ClosingIndex); Attach(FocusChangedIndex); Attach(MoveIndex); Attach(StateChangedIndex); Attach(FramebufferResizeIndex); lock (_lifecycleSync) { if (Volatile.Read(ref _detachRequested) != 0) { throw new ObjectDisposedException( nameof(SilkWindowCallbackBinding), "Shutdown was requested while native callbacks were attaching."); } _lifecycleState = LifecycleState.Attached; _lifecycleOwnerThreadId = 0; System.Threading.Monitor.PulseAll(_lifecycleSync); } } catch (Exception attachError) { Interlocked.Exchange(ref _detachRequested, 1); _quiescence.StopAccepting(); List? rollbackErrors = null; for (int index = _attachedCount - 1; index >= 0; index--) { try { Remove(index); } catch (Exception rollbackError) { (rollbackErrors ??= []).Add(new InvalidOperationException( $"Failed to roll back native window callback index {index}.", rollbackError)); } } lock (_lifecycleSync) { _lifecycleState = LifecycleState.AttachFailed; _lifecycleOwnerThreadId = 0; System.Threading.Monitor.PulseAll(_lifecycleSync); } if (rollbackErrors is not null) { rollbackErrors.Insert(0, new InvalidOperationException( "Native window callback registration failed.", attachError)); throw new AggregateException( "Native window callback registration and rollback both failed.", rollbackErrors); } throw new InvalidOperationException( "Native window callback registration failed and was rolled back.", attachError); } } private void Attach(int index) { // A custom event add accessor is allowed to perform its side effect // before throwing. Treat the edge as possibly acquired until a // matching remove succeeds; removing an unattached delegate is safe. lock (_lifecycleSync) { _attached[index] = true; _attachedCount = Math.Max(_attachedCount, index + 1); } switch (index) { case LoadIndex: _surface.AddLoad(_load); break; case UpdateIndex: _surface.AddUpdate(_update); break; case MainRenderIndex: _surface.AddRender(_mainRender); break; case PacingRenderIndex: _surface.AddRender(_pacingRender); break; case ClosingIndex: _surface.AddClosing(_closing); break; case FocusChangedIndex: _surface.AddFocusChanged(_focusChanged); break; case MoveIndex: _surface.AddMove(_moved); break; case StateChangedIndex: _surface.AddStateChanged(_stateChanged); break; case FramebufferResizeIndex: _surface.AddFramebufferResize(_framebufferResize); break; default: throw new ArgumentOutOfRangeException(nameof(index)); } if (Volatile.Read(ref _detachRequested) != 0) { throw new ObjectDisposedException( nameof(SilkWindowCallbackBinding), "Shutdown was requested while native callbacks were attaching."); } } private void Remove(int index) { lock (_lifecycleSync) { if (index >= _attachedCount || !_attached[index]) return; } switch (index) { case LoadIndex: _surface.RemoveLoad(_load); break; case UpdateIndex: _surface.RemoveUpdate(_update); break; case MainRenderIndex: _surface.RemoveRender(_mainRender); break; case PacingRenderIndex: _surface.RemoveRender(_pacingRender); break; case ClosingIndex: _surface.RemoveClosing(_closing); break; case FocusChangedIndex: _surface.RemoveFocusChanged(_focusChanged); break; case MoveIndex: _surface.RemoveMove(_moved); break; case StateChangedIndex: _surface.RemoveStateChanged(_stateChanged); break; case FramebufferResizeIndex: _surface.RemoveFramebufferResize(_framebufferResize); break; default: throw new ArgumentOutOfRangeException(nameof(index)); } lock (_lifecycleSync) _attached[index] = false; } public void Dispose() { Interlocked.Exchange(ref _detachRequested, 1); // Gate order is deliberate: never hold the physical-detach monitor // while waiting for an in-flight native callback to finish. Closing // itself holds the gate reentrantly and may enter this method. _quiescence.StopAccepting(); int threadId = Environment.CurrentManagedThreadId; bool ownsGate = _quiescence.IsEnteredByCurrentThread; while (true) { lock (_lifecycleSync) { if (_lifecycleState == LifecycleState.Detached) return; if (_lifecycleState == LifecycleState.Attaching) { if (_lifecycleOwnerThreadId == threadId || ownsGate) throw new NativeWindowCallbackCleanupDeferredException(); System.Threading.Monitor.Wait(_lifecycleSync); continue; } if (_lifecycleState == LifecycleState.Detaching) { if (_lifecycleOwnerThreadId == threadId) return; if (ownsGate) throw new NativeWindowCallbackCleanupDeferredException(); System.Threading.Monitor.Wait(_lifecycleSync); continue; } _lifecycleState = LifecycleState.Detaching; _lifecycleOwnerThreadId = threadId; break; } } try { _detach.CompleteOrThrow(); lock (_lifecycleSync) { _lifecycleState = LifecycleState.Detached; _lifecycleOwnerThreadId = 0; System.Threading.Monitor.PulseAll(_lifecycleSync); } } catch { lock (_lifecycleSync) { _lifecycleState = LifecycleState.DetachPending; _lifecycleOwnerThreadId = 0; System.Threading.Monitor.PulseAll(_lifecycleSync); } throw; } } }