using AcDream.App.Input; using Silk.NET.Maths; namespace AcDream.App.Rendering; internal interface IFramebufferViewportTarget { void ResizeViewport(int width, int height); } // Campaign V slice V11 deleted SilkFramebufferViewportTarget, the GL // implementation of IFramebufferViewportTarget, and left a null target on the // assumption that the driver's OUT_OF_DATE/SUBOPTIMAL results would drive // swapchain recreation on resize. That assumption is driver-dependent — a // conformant driver may present a stale-extent swapchain scaled to the new // window indefinitely (observed on Windows AMD; #387, the 2026-08-13 // "resolution pick just stretches" user gate report). The sole implementation // is now VulkanHostInputCameraCompositionFactory.SwapchainRecreateViewportTarget, // which arms the context's frame-boundary swapchain recreation on every // resize event. internal interface IFramebufferCameraTarget { void SetAspect(float aspect); } internal sealed class CameraFramebufferTarget(CameraController camera) : IFramebufferCameraTarget { private readonly CameraController _camera = camera ?? throw new ArgumentNullException(nameof(camera)); public void SetAspect(float aspect) => _camera.SetAspect(aspect); } internal interface IFramebufferDevToolsTarget { void ResetLayout(int width, int height); } /// Expected-owner lease for the optional Phase-3 resize edge. internal sealed class FramebufferDevToolsBinding : IDisposable { private readonly FramebufferResizeController _owner; private readonly IFramebufferDevToolsTarget _target; private bool _disposed; public FramebufferDevToolsBinding( FramebufferResizeController owner, IFramebufferDevToolsTarget target) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _target = target ?? throw new ArgumentNullException(nameof(target)); _owner.BindDevTools(_target); } public void Dispose() { if (_disposed) return; _owner.UnbindDevTools(_target); _disposed = true; } } /// /// The one framebuffer-size target. Logical gameplay/UI dimensions continue /// to come from Window.Size; this owner only applies physical framebuffer /// viewport/aspect changes in the frozen acdream host order. /// internal sealed class FramebufferResizeController { private readonly ViewportAspectState _viewportAspect; private IFramebufferViewportTarget? _viewport; private IFramebufferCameraTarget? _camera; private IFramebufferDevToolsTarget? _devTools; public FramebufferResizeController(ViewportAspectState viewportAspect) => _viewportAspect = viewportAspect ?? throw new ArgumentNullException(nameof(viewportAspect)); public void BindViewport(IFramebufferViewportTarget viewport) { ArgumentNullException.ThrowIfNull(viewport); BindOnce(ref _viewport, viewport, "viewport"); } public void BindCamera(IFramebufferCameraTarget camera) { ArgumentNullException.ThrowIfNull(camera); BindOnce(ref _camera, camera, "camera"); } public void BindDevTools(IFramebufferDevToolsTarget devTools) { ArgumentNullException.ThrowIfNull(devTools); BindOnce(ref _devTools, devTools, "developer tools"); } public void UnbindDevTools(IFramebufferDevToolsTarget devTools) { ArgumentNullException.ThrowIfNull(devTools); if (ReferenceEquals(_devTools, devTools)) _devTools = null; } public void Resize(Vector2D newSize) => Resize(newSize.X, newSize.Y); public void Resize(int width, int height) { if (width <= 0 || height <= 0) return; // #387 evidence line (permanent — resize events are rare and this is // the only trace of whether the native event fired at all). Lives here // rather than in GameWindow.OnFramebufferResize because the window // callback is contractually a one-line typed-owner handoff // (GameWindowSlice8BoundaryTests.FramebufferResize_IsOneTypedOwnerHandoff). Console.WriteLine($"window: framebuffer resize event {width}x{height}"); // Frozen order: GL viewport, shared aspect publication, camera, then // the optional developer-tools layout reset (unbound since Campaign V // slice V11 removed the ImGui frontend). Late binding never replays an // earlier resize transition. _viewport?.ResizeViewport(width, height); _viewportAspect.Update(width, height); _camera?.SetAspect(width / (float)height); _devTools?.ResetLayout(width, height); } private static void BindOnce(ref T? slot, T value, string name) where T : class { if (slot is not null && !ReferenceEquals(slot, value)) throw new InvalidOperationException( $"The framebuffer {name} target is already bound."); slot = value; } }