diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index e384be4..c5a7aa0 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -121,10 +121,13 @@ class Program { lastWidth = window.Width; lastHeight = window.Height; - renderContext.Resize(lastWidth, lastHeight); - ref var cam = ref cameraEntity.Ensure(); - cam.AspectRatio = (float)lastWidth / lastHeight; - cameraEntity.Set(cam); + if (lastWidth > 0 && lastHeight > 0) + { + renderContext.Resize(lastWidth, lastHeight); + ref var cam = ref cameraEntity.Ensure(); + cam.AspectRatio = (float)lastWidth / lastHeight; + cameraEntity.Set(cam); + } } cameraController.Update(input, (float)timing.DeltaTime); diff --git a/src/Engine.Graphics.Vulkan/VulkanContext.cs b/src/Engine.Graphics.Vulkan/VulkanContext.cs index 4f497ae..5480bef 100644 --- a/src/Engine.Graphics.Vulkan/VulkanContext.cs +++ b/src/Engine.Graphics.Vulkan/VulkanContext.cs @@ -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; diff --git a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs index ba32837..dbcbf9e 100644 --- a/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs +++ b/src/Engine.Graphics.Vulkan/VulkanFrameResources.cs @@ -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; } } diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs b/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs index aad2e3e..0b745e5 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs @@ -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() diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index c1fff7b..c842bc1 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -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();