fix: make Vulkan swapchain recreation resilient

This commit is contained in:
emil28092005
2026-07-19 01:38:02 +03:00
parent 96c32296d1
commit bfe5a7f44f
5 changed files with 87 additions and 20 deletions
+7 -4
View File
@@ -121,10 +121,13 @@ class Program
{
lastWidth = window.Width;
lastHeight = window.Height;
renderContext.Resize(lastWidth, lastHeight);
ref var cam = ref cameraEntity.Ensure<Camera>();
cam.AspectRatio = (float)lastWidth / lastHeight;
cameraEntity.Set(cam);
if (lastWidth > 0 && lastHeight > 0)
{
renderContext.Resize(lastWidth, lastHeight);
ref var cam = ref cameraEntity.Ensure<Camera>();
cam.AspectRatio = (float)lastWidth / lastHeight;
cameraEntity.Set(cam);
}
}
cameraController.Update(input, (float)timing.DeltaTime);
@@ -27,7 +27,6 @@ internal sealed unsafe class VulkanContext : IDisposable
public VkPhysicalDeviceMemoryProperties MemoryProperties;
public VkFormat SurfaceFormat;
public VkColorSpaceKHR SurfaceColorSpace;
public VkExtent2D SurfaceExtent;
public bool ValidationEnabled;
private VkDebugUtilsMessengerEXT _debugMessenger;
@@ -241,12 +241,35 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
Vk.vkUnmapMemory(_device, UboMemories[frameIndex]);
}
public void WaitFrame(int frameIndex)
public void WaitForFrame(int frameIndex)
{
fixed (VkFence* fencePtr = &FrameFences[frameIndex])
{
Vk.vkWaitForFences(_device, 1, fencePtr, VkBool32.True, ulong.MaxValue);
}
public void ResetFrameFence(int frameIndex)
{
fixed (VkFence* fencePtr = &FrameFences[frameIndex])
Vk.vkResetFences(_device, 1, fencePtr);
}
public void RecreateSubmitSemaphores(uint swapchainImageCount)
{
foreach (var semaphore in SubmitSemaphores)
{
if (semaphore.Handle != 0)
Vk.vkDestroySemaphore(_device, semaphore, 0);
}
SubmitSemaphores = new VkSemaphore[swapchainImageCount];
var semInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo };
for (int i = 0; i < SubmitSemaphores.Length; i++)
{
var semaphore = VkSemaphore.Null;
var result = Vk.vkCreateSemaphore(_device, &semInfo, 0, &semaphore);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateSemaphore (submit) failed: {result}");
SubmitSemaphores[i] = semaphore;
}
}
@@ -6,6 +6,7 @@ internal sealed class VulkanRenderContext : IRenderContext{
private readonly VulkanContext _ctx;
private readonly VulkanSwapchain _swapchain;
private readonly IWindow _window;
private VulkanRenderer? _renderer;
private bool _disposed;
public IWindow Window => _window;
@@ -27,12 +28,18 @@ internal sealed class VulkanRenderContext : IRenderContext{
public IRenderer CreateRenderer()
{
return new VulkanRenderer(_ctx, _swapchain);
return _renderer ??= new VulkanRenderer(_ctx, _swapchain, _window);
}
public void Resize(int width, int height)
{
_swapchain.Recreate(width, height);
if (width <= 0 || height <= 0)
return;
if (_renderer is not null)
_renderer.RecreateSwapchain(width, height);
else
_swapchain.Recreate(width, height);
}
public void Dispose()
+46 -11
View File
@@ -11,6 +11,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
{
private readonly VulkanContext _ctx;
private readonly VulkanSwapchain _swapchain;
private readonly IWindow _window;
private readonly VulkanPipeline _pipeline;
private readonly VulkanFrameResources _frameResources;
private readonly VulkanShadowMap _shadowMap;
@@ -40,10 +41,11 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
public bool IsScreenshotRequested => _screenshotRequested;
public IScreenshotProvider ScreenshotProvider => this;
internal VulkanRenderer(VulkanContext ctx, VulkanSwapchain swapchain)
internal VulkanRenderer(VulkanContext ctx, VulkanSwapchain swapchain, IWindow window)
{
_ctx = ctx;
_swapchain = swapchain;
_window = window;
var vertSpv = LoadShader("Shaders/triangle.vert.spv");
var fragSpv = LoadShader("Shaders/triangle.frag.spv");
@@ -63,6 +65,16 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
internal VulkanImGui? ImGuiLayer => _imGui;
internal void RecreateSwapchain(int width, int height)
{
if (width <= 0 || height <= 0)
return;
_swapchain.Recreate(width, height);
_frameResources.RecreateSubmitSemaphores(_swapchain.ImageCount);
DisposeScreenshotBuffer();
}
private void InitScreenshotBuffer()
{
if (_screenshotInitialized) return;
@@ -101,6 +113,20 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
Vk.vkBindBufferMemory(_ctx.Device, _screenshotBuffer, _screenshotMemory, 0);
}
private void DisposeScreenshotBuffer()
{
if (_screenshotBuffer.Handle != 0)
Vk.vkDestroyBuffer(_ctx.Device, _screenshotBuffer, 0);
if (_screenshotMemory.Handle != 0)
Vk.vkFreeMemory(_ctx.Device, _screenshotMemory, 0);
_screenshotBuffer = VkBuffer.Null;
_screenshotMemory = VkDeviceMemory.Null;
_screenshotBufferSize = 0;
_screenshotInitialized = false;
CapturedFrame = null;
}
public void CaptureFrame(VkCommandBuffer cmd, uint imageIndex)
{
InitScreenshotBuffer();
@@ -266,7 +292,12 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model, bool castShadow)> drawCalls, byte* uboData, List<(Vector3 pos, float intensity, Vector3 color, float range)> lights, Matrix4x4[] lightViewProjs, int numShadowLights)
{
_frameResources.WaitFrame(_frameIndex);
_frameResources.WaitForFrame(_frameIndex);
// A minimized window has no drawable surface. Keep the frame fence signaled and wait
// for the window event loop to provide a non-zero extent before trying to acquire.
if (_window.Width <= 0 || _window.Height <= 0)
return;
// Read captured frame from previous render (GPU has finished by now)
if (IsRecording)
@@ -288,17 +319,23 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain,
ulong.MaxValue, _frameResources.AcquireSemaphores[_frameIndex], VkFence.Null, &imageIndex);
if (acquireResult == VkResult.ErrorOutOfDateKHR || acquireResult == VkResult.SuboptimalKHR)
// An out-of-date acquire does not signal the semaphore. Do not reset the frame fence
// until an image has been acquired and a submission is guaranteed; otherwise returning
// from this frame would leave the next WaitForFrame blocked forever.
if (acquireResult == VkResult.ErrorOutOfDateKHR)
{
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
Render(vp, drawCalls, uboData, lights, lightViewProjs, numShadowLights);
RecreateSwapchain(_window.Width, _window.Height);
return;
}
if (acquireResult != VkResult.Success)
// SuboptimalKHR is a success code: the semaphore WAS signaled and imageIndex is valid.
// Render and present this frame normally; the post-present check below recreates the
// swapchain at a safe point, after this frame's submit has re-armed the frame fence.
if (acquireResult != VkResult.Success && acquireResult != VkResult.SuboptimalKHR)
throw new InvalidOperationException($"vkAcquireNextImageKHR failed: {acquireResult}");
_frameResources.ResetFrameFence(_frameIndex);
_totalTime += 0.016f;
_frameResources.UpdateUbo(_frameIndex, uboData, VulkanFrameResources.UboSize);
@@ -620,8 +657,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
{
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
RecreateSwapchain(_window.Width, _window.Height);
}
_frameIndex = (_frameIndex + 1) % VulkanFrameResources.MaxFramesInFlight;
@@ -742,8 +778,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
}
_meshCache.Clear();
if (_screenshotBuffer.Handle != 0) Vk.vkDestroyBuffer(_ctx.Device, _screenshotBuffer, 0);
if (_screenshotMemory.Handle != 0) Vk.vkFreeMemory(_ctx.Device, _screenshotMemory, 0);
DisposeScreenshotBuffer();
_shadowMap?.Dispose();
_imGui?.Dispose();
_frameResources?.Dispose();