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;
}
}