GameWindowSlice8BoundaryTests.FramebufferResize_IsOneTypedOwnerHandoff correctly rejected the log line added to GameWindow.OnFramebufferResize — the window callback is contractually a one-line handoff. The line now lives in FramebufferResizeController.Resize after its zero-size gate, which is also the better home (one owner, all callers covered). Full Debug App suite 4,941/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
139 lines
4.9 KiB
C#
139 lines
4.9 KiB
C#
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);
|
|
}
|
|
|
|
/// <summary>Expected-owner lease for the optional Phase-3 resize edge.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<int> 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<T>(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;
|
|
}
|
|
}
|