fix #387: window resize never recreated the Vulkan swapchain (stretch)
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>
This commit is contained in:
parent
2fd99c4265
commit
a7ae756b44
4 changed files with 147 additions and 22 deletions
|
|
@ -0,0 +1,80 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue