User report: resolution picks (and window drags) stretched the image instead of changing the pixel count. Root cause: Campaign V slice V11 deleted the GL viewport target and left a null target, assuming the driver's OUT_OF_DATE/SUBOPTIMAL acquire/present results would drive swapchain recreation on resize. That is driver-dependent and spec-insufficient — this machine's Windows AMD driver keeps presenting the stale-extent swapchain scaled to the new window indefinitely, so OnFramebufferResize only ever updated the camera aspect while every pass (UI included) kept rendering at the old extent. Fix: SwapchainRecreateViewportTarget implements the existing IFramebufferViewportTarget seam for Vulkan and arms VulkanGraphicsContext.RequestRecreate() on every resize event; the next PrepareFrame rebuilds the swapchain at the live FramebufferSize (bursts collapse to one recreation, stale events cannot install a stale extent, minimised sizes stay gated by FramebufferResizeController). Tests: SwapchainRecreateViewportTargetTests (target contract, size- agnostic arming, null hook, controller-to-target end-to-end with the minimised gate). Full Debug App suite 4,941/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
3 KiB
C#
80 lines
3 KiB
C#
using AcDream.App.Composition;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
/// <summary>
|
|
/// #387: the V11 null viewport target assumed the driver's OUT_OF_DATE/
|
|
/// SUBOPTIMAL results would drive swapchain recreation on window resize; on
|
|
/// Windows AMD the driver instead presents the stale-extent swapchain scaled
|
|
/// to the new window (the "resolution pick just stretches" 2026-08-13 user
|
|
/// gate report). These pin the replacement target's contract: every resize
|
|
/// event reaching the seam arms the context's frame-boundary recreation.
|
|
/// </summary>
|
|
public sealed class SwapchainRecreateViewportTargetTests
|
|
{
|
|
[Fact]
|
|
public void ResizeViewport_ArmsRecreation_OncePerEvent()
|
|
{
|
|
int armed = 0;
|
|
IFramebufferViewportTarget target =
|
|
new VulkanHostInputCameraCompositionFactory.SwapchainRecreateViewportTarget(
|
|
() => armed++);
|
|
|
|
target.ResizeViewport(1024, 768);
|
|
Assert.Equal(1, armed);
|
|
|
|
// A second resize arms again — the context collapses bursts itself at
|
|
// the frame boundary (it re-reads the live FramebufferSize), so the
|
|
// target must never de-duplicate or latch.
|
|
target.ResizeViewport(800, 600);
|
|
Assert.Equal(2, armed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResizeViewport_IgnoresTheEventSize_ContextReadsLiveFramebuffer()
|
|
{
|
|
// The width/height are deliberately unused (doc on the target): the
|
|
// context reads the CURRENT framebuffer size at the frame boundary,
|
|
// so a stale event cannot install a stale extent. Pin that the arm
|
|
// fires regardless of the reported size values.
|
|
int armed = 0;
|
|
IFramebufferViewportTarget target =
|
|
new VulkanHostInputCameraCompositionFactory.SwapchainRecreateViewportTarget(
|
|
() => armed++);
|
|
|
|
target.ResizeViewport(int.MaxValue, 1);
|
|
Assert.Equal(1, armed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_RejectsNullRecreateHook()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new VulkanHostInputCameraCompositionFactory.SwapchainRecreateViewportTarget(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResizeController_DeliversResizeEventsToTheBoundTarget()
|
|
{
|
|
// The end-to-end seam FramebufferResizeController.Resize →
|
|
// ResizeViewport → arm: the same event flow GameWindow's
|
|
// OnFramebufferResize drives in production. Zero-area (minimised)
|
|
// events are gated by the controller and must NOT arm.
|
|
int armed = 0;
|
|
var controller = new FramebufferResizeController(new ViewportAspectState());
|
|
controller.BindViewport(
|
|
new VulkanHostInputCameraCompositionFactory.SwapchainRecreateViewportTarget(
|
|
() => armed++));
|
|
|
|
controller.Resize(1280, 720);
|
|
Assert.Equal(1, armed);
|
|
|
|
controller.Resize(0, 0); // minimised — gated, no arm
|
|
Assert.Equal(1, armed);
|
|
|
|
controller.Resize(1024, 768);
|
|
Assert.Equal(2, armed);
|
|
}
|
|
}
|