From 896f100940ee582763fa61ec5d2525a665f17d99 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 20:07:25 +0300 Subject: [PATCH 01/36] fix: order vulkan forward and taa pass writes before their readers The forward pass discarded depth that TAA and SSR sample afterwards, and neither the forward nor the TAA render pass declared an outgoing dependency, so the post passes could read hdrScene, depth and the TAA result before those writes landed. Store forward depth and add 0->EXTERNAL dependencies to both passes. --- src/Graphics/Vulkan/VulkanDeferred.cpp | 35 +++++++++++++++++--------- src/Graphics/Vulkan/VulkanTAA.cpp | 28 ++++++++++++++------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index eca45981..2cc1da17 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -575,7 +575,7 @@ bool VulkanRenderer::CreateForwardRenderPass() { depthAtt.format = depthFormat; depthAtt.samples = VK_SAMPLE_COUNT_1_BIT; depthAtt.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAtt.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; @@ -595,15 +595,26 @@ bool VulkanRenderer::CreateForwardRenderPass() { subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | - VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + std::array deps{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + // Feeds the post passes + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; std::array attachments = {colorAtt, depthAtt}; @@ -613,8 +624,8 @@ bool VulkanRenderer::CreateForwardRenderPass() { rpInfo.pAttachments = attachments.data(); rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; - rpInfo.dependencyCount = 1; - rpInfo.pDependencies = &dep; + rpInfo.dependencyCount = static_cast(deps.size()); + rpInfo.pDependencies = deps.data(); return vkCreateRenderPass(device, &rpInfo, nullptr, &m_forwardRenderPass) == VK_SUCCESS; } diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 49e5c443..273a4a07 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -149,13 +149,23 @@ bool VulkanRenderer::CreateTAAResources() { sub.colorAttachmentCount = 1; sub.pColorAttachments = &ref; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + std::array deps{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + // Feeds copy-back and history + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_TRANSFER_BIT; + deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].dstAccessMask = + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT; VkRenderPassCreateInfo rpi{}; rpi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; @@ -163,8 +173,8 @@ bool VulkanRenderer::CreateTAAResources() { rpi.pAttachments = &att; rpi.subpassCount = 1; rpi.pSubpasses = ⊂ - rpi.dependencyCount = 1; - rpi.pDependencies = &dep; + rpi.dependencyCount = static_cast(deps.size()); + rpi.pDependencies = deps.data(); if (vkCreateRenderPass(device, &rpi, nullptr, &m_taaRenderPass) != VK_SUCCESS) return false; } From 1c9d12a8ba514a45f909fcc7f52dc482bb286a82 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 20:07:36 +0300 Subject: [PATCH 02/36] fix: clear the vulkan shadow map when it is created The map was only transitioned to a readable layout on creation and after a resolution change, so the lighting pass sampled undefined depth until the first shadow pass ran. Clear it to far depth through the shadow render pass instead. --- src/Graphics/Vulkan/VulkanRenderer.cpp | 31 +++++++++++-------------- src/Graphics/Vulkan/VulkanShadow.cpp | 32 +++++++++++--------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 3aaf87ef..367f2ca9 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -1619,24 +1619,19 @@ void VulkanRenderer::ApplyShadowResolutionChange() { cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &barrier); + // Clear to far depth + VkClearValue clear{}; + clear.depthStencil = {1.0f, 0}; + VkRenderPassBeginInfo clearPass{}; + clearPass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + clearPass.renderPass = m_shadowRenderPass; + clearPass.framebuffer = m_shadowFramebuffer; + clearPass.renderArea.extent = {m_shadowMapResolution, + m_shadowMapResolution}; + clearPass.clearValueCount = 1; + clearPass.pClearValues = &clear; + vkCmdBeginRenderPass(cmdBuf, &clearPass, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmdBuf); vkEndCommandBuffer(cmdBuf); diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 3ff9a4d6..bbfdd910 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -187,25 +187,19 @@ bool VulkanRenderer::CreateShadowResources() { cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); + // Clear to far depth + VkClearValue clear{}; + clear.depthStencil = {1.0f, 0}; + VkRenderPassBeginInfo clearPass{}; + clearPass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + clearPass.renderPass = m_shadowRenderPass; + clearPass.framebuffer = m_shadowFramebuffer; + clearPass.renderArea.extent = {m_shadowMapResolution, + m_shadowMapResolution}; + clearPass.clearValueCount = 1; + clearPass.pClearValues = &clear; + vkCmdBeginRenderPass(cmdBuf, &clearPass, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmdBuf); vkEndCommandBuffer(cmdBuf); From d4833fc7f8d03eff44c4b2d5f5a1b3ffde02cf46 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 20:07:40 +0300 Subject: [PATCH 03/36] fix: seed the first vulkan taa reprojection with the current matrix The previous view-projection starts zeroed, so frame 0 reprojected through a zero matrix and could write NaN into the history that the next frames then blend from. --- src/Graphics/Vulkan/VulkanTAA.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 273a4a07..209f5c7e 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -420,10 +420,12 @@ void VulkanRenderer::UpdateTAAUBO() { TAAParams p{}; memcpy(p.InvCurrentVP, invCurrentVP, sizeof(p.InvCurrentVP)); - memcpy(p.PrevVP, m_prevViewProj, sizeof(p.PrevVP)); + // First frame: identity reprojection + memcpy(p.PrevVP, m_taaFrameIdx == 0 ? currentVP : m_prevViewProj, + sizeof(p.PrevVP)); p.ScreenW = static_cast(scExtent.width); p.ScreenH = static_cast(scExtent.height); - // First two frames skip history (prev VP is zero-initialized) + // First two frames skip history p.BlendFactor = (m_taaFrameIdx <= 1) ? 1.0f : m_taaBlendFactor; p._pad = 0.0f; From 6bac5330b60b67d9ba778fb762c11eb4bf7c3f1d Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 20:07:44 +0300 Subject: [PATCH 04/36] feat: add vulkan gpu pass timing and a deterministic frame capture --gpuprofile brackets every recorded pass with timestamp queries, logs avg/max ms per pass every 2 s and adds GPU__ms columns to --bench CSVs. -capture -capture-frame runs on a fixed 1/60 s step with user input dropped, then writes frame n without UI as a PPM and exits, so renderer changes can be checked pixel for pixel. --- CMakeLists.txt | 2 + include/private/Core/Window.hpp | 1 + include/private/Graphics/Common/Renderer.hpp | 14 ++ .../Graphics/Vulkan/VulkanRenderer.hpp | 92 +++++++ src/Core/Application.cpp | 37 ++- src/Core/Window.cpp | 32 +++ src/Graphics/Vulkan/VulkanBloom.cpp | 13 +- src/Graphics/Vulkan/VulkanCapture.cpp | 215 ++++++++++++++++ src/Graphics/Vulkan/VulkanDeferred.cpp | 4 + src/Graphics/Vulkan/VulkanDevice.cpp | 2 + src/Graphics/Vulkan/VulkanProfiler.cpp | 235 ++++++++++++++++++ src/Graphics/Vulkan/VulkanRenderer.cpp | 38 ++- src/Graphics/Vulkan/VulkanSSAO.cpp | 2 + src/Graphics/Vulkan/VulkanSSR.cpp | 2 + src/Graphics/Vulkan/VulkanSwapchain.cpp | 28 +++ src/Graphics/Vulkan/VulkanTAA.cpp | 2 + src/Lighting/LightManager.cpp | 6 +- 17 files changed, 718 insertions(+), 7 deletions(-) create mode 100644 src/Graphics/Vulkan/VulkanCapture.cpp create mode 100644 src/Graphics/Vulkan/VulkanProfiler.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 26a243af..d31a1255 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,12 +155,14 @@ set(GRAPHICS_OPENGL_SOURCES set(GRAPHICS_VULKAN_SOURCES src/Graphics/Vulkan/VulkanBloom.cpp src/Graphics/Vulkan/VulkanBuffer.cpp + src/Graphics/Vulkan/VulkanCapture.cpp src/Graphics/Vulkan/VulkanCubemapTexture.cpp src/Graphics/Vulkan/VulkanDeferred.cpp src/Graphics/Vulkan/VulkanDescriptors.cpp src/Graphics/Vulkan/VulkanDevice.cpp src/Graphics/Vulkan/VulkanIBL.cpp src/Graphics/Vulkan/VulkanPipelines.cpp + src/Graphics/Vulkan/VulkanProfiler.cpp src/Graphics/Vulkan/VulkanRenderer.cpp src/Graphics/Vulkan/VulkanShader.cpp src/Graphics/Vulkan/VulkanShadow.cpp diff --git a/include/private/Core/Window.hpp b/include/private/Core/Window.hpp index e4b09e1e..33f87372 100644 --- a/include/private/Core/Window.hpp +++ b/include/private/Core/Window.hpp @@ -67,6 +67,7 @@ class ENGINE_API Window { bool bIsFullScreen = false; bool bShouldClose = false; bool m_imguiReady = false; + bool m_inputLocked = false; std::string WindowName; SDL_Window* SDLWindow = nullptr; diff --git a/include/private/Graphics/Common/Renderer.hpp b/include/private/Graphics/Common/Renderer.hpp index 33325a08..f522405b 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -7,6 +7,8 @@ namespace Sleak { +class Benchmark; + namespace RenderEngine { /// Selects which graphics API a Renderer instance targets. @@ -63,6 +65,18 @@ class ENGINE_API Renderer { /// Returns the backend's command-recording interface. virtual RenderContext* GetContext() = 0; + /// Adds the backend's GPU timing columns to a benchmark session. + virtual void RegisterBenchmarkMetrics(Benchmark& bench) { (void)bench; } + + /// Arms a readback of the next rendered frame, UI excluded, into a + /// binary PPM. Returns false when the backend cannot capture. + virtual bool RequestFrameCapture(const std::string& path) { + (void)path; + return false; + } + /// True while an armed frame capture has not been written yet. + virtual bool IsFrameCapturePending() const { return false; } + inline RendererType GetType() const { return Type; diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 46b8170a..5fc49551 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -219,6 +219,16 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Recreates the extent-dependent shadow map objects at the queued resolution. void ApplyShadowResolutionChange() override; + // Measurement + /// Adds per-pass GPU millisecond columns when --gpuprofile is on. + void RegisterBenchmarkMetrics(Benchmark& bench) override; + /// Arms a UI-free readback of the next presented frame into a binary PPM. + bool RequestFrameCapture(const std::string& path) override; + /// True while an armed capture has not been written yet. + bool IsFrameCapturePending() const override { + return !m_capturePath.empty(); + } + private: /// Compiles the skybox shaders and creates the skybox descriptor set and pipeline. bool CreateSkyboxPipeline(); @@ -931,6 +941,88 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Sets the dynamic viewport and scissor to fill the given extent. Defined in VulkanBloom.cpp (most call sites of the four post-effect TUs). static void FillFullscreenViewportScissor(VkCommandBuffer cmd, VkExtent2D ext); + + // ---- GPU timestamp profiler (--gpuprofile) ---- + /// Frame passes bracketed by timestamp pairs, in log-table order. + enum class GpuPass : uint8_t { + Shadow, + GBuffer, + SSAO, + Lighting, + Forward, + TAA, + SSR, + Bloom, + Tonemap, + UI, + Count + }; + static constexpr uint32_t GPU_PASS_COUNT = + static_cast(GpuPass::Count); + static constexpr uint32_t GPU_ROW_OTHER = GPU_PASS_COUNT; + static constexpr uint32_t GPU_ROW_TOTAL = GPU_PASS_COUNT + 1; + static constexpr uint32_t GPU_ROW_COUNT = GPU_PASS_COUNT + 2; + static constexpr uint32_t GPU_MAX_PAIRS = 16; + static constexpr uint32_t GPU_QUERIES_PER_FRAME = 2 + 2 * GPU_MAX_PAIRS; + + /// Timestamp pairs recorded into one frame-in-flight slot of the pool. + struct GpuProfileSlot { + std::array pairPass{}; + uint32_t pairCount = 0; + bool pairOpen = false; + bool recording = false; + bool submitted = false; + }; + + VkQueryPool m_gpuQueryPool = VK_NULL_HANDLE; + bool m_gpuProfilerEnabled = false; + double m_gpuTickNs = 0.0; + uint64_t m_gpuTickMask = 0; + std::array m_gpuSlots{}; + std::array m_gpuSumMs{}; + std::array m_gpuMaxMs{}; + std::array m_gpuRuns{}; + std::array m_gpuLastMs{}; + uint32_t m_gpuWindowFrames = 0; + Timer m_gpuWindowTimer; + + /// Creates the timestamp query pool; leaves profiling off when the + /// graphics queue has no timestamp support. + bool CreateGpuProfiler(); + /// Destroys the timestamp query pool. + void CleanupGpuProfiler(); + /// Folds this slot's finished timestamps into the running window and + /// logs the table once the window elapses. Call after the slot's fence. + void CollectGpuProfile(); + /// Resets this slot's queries and stamps the frame start. + void BeginGpuFrame(); + /// Closes any pass left open and stamps the frame end. + void EndGpuFrame(); + /// Flags this slot's queries for readback on its next fence wait. + void MarkGpuFrameSubmitted(); + /// Stamps the start of a pass. + void BeginGpuPass(GpuPass pass); + /// Stamps the end of the pass opened by the matching BeginGpuPass. + void EndGpuPass(GpuPass pass); + /// Logs the averaged per-pass table for the current window. + void LogGpuProfile(float seconds) const; + + // ---- Frame capture (-capture) ---- + bool m_captureMode = false; + bool m_swapchainTransferSrc = false; + std::string m_capturePath; + bool m_captureRecorded = false; + VkBuffer m_captureBuffer = VK_NULL_HANDLE; + VkDeviceMemory m_captureMemory = VK_NULL_HANDLE; + VkExtent2D m_captureExtent = {0, 0}; + VkFormat m_captureFormat = VK_FORMAT_UNDEFINED; + + /// Records the swapchain-image-to-buffer copy for an armed capture. + void RecordFrameCaptureCopy(); + /// Waits for the captured frame, writes the PPM, and disarms the capture. + void FinishFrameCapture(); + /// Frees the capture readback buffer. + void CleanupFrameCapture(); }; } // namespace RenderEngine diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 83c0eae5..6cfed951 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -141,6 +141,7 @@ namespace Sleak { m_benchmark = new Benchmark(); m_benchmark->Initialize(renderer); + renderer->RegisterBenchmarkMetrics(*m_benchmark); { auto& cfg = m_DebugOverlay->GetConfig(); cfg.ShowCameraPanel = false; @@ -154,6 +155,23 @@ namespace Sleak { float accumulator = 0.0f; const float fixedTimestep = 1.0f / 60.0f; + // Frame capture + const std::string capturePath = CommandLine::GetValue("-capture"); + const bool captureMode = !capturePath.empty(); + uint64_t captureFrame = 600; + try { + captureFrame = + std::stoull(CommandLine::GetValue("-capture-frame", "600")); + } catch (...) { + } + if (captureMode) + SLEAK_INFO( + "Frame capture: frame {} -> {} (fixed 1/60 s step, " + "resizes ignored)", + captureFrame, capturePath); + uint64_t frameIndex = 0; + bool captureArmed = false; + // Initialize and begin the game (and scene) if (Game) { if (!Game->Initialize()) { @@ -187,7 +205,8 @@ namespace Sleak { while(!CoreWindow->ShouldClose()) { float currentTime = FrameTimer.Elapsed(); - DeltaTime = currentTime - lastTime; + DeltaTime = + captureMode ? fixedTimestep : currentTime - lastTime; lastTime = currentTime; #if defined(_DEBUG) && defined(COUNT_FRAME) @@ -197,7 +216,7 @@ namespace Sleak { CoreWindow->Update(); // Apply any pending resize (deferred from event handler to avoid GPU hang) - if (m_pendingResize) { + if (m_pendingResize && !captureMode) { renderer->Resize(m_pendingResizeW, m_pendingResizeH); width = static_cast(m_pendingResizeW); height = static_cast(m_pendingResizeH); @@ -212,6 +231,16 @@ namespace Sleak { } } + if (captureMode && !captureArmed && + frameIndex >= captureFrame) { + captureArmed = true; + if (!renderer->RequestFrameCapture(capturePath)) { + SLEAK_ERROR("Frame capture unavailable on {}", + renderer->GetTypeStr()); + CoreWindow->Close(); + } + } + renderer->BeginRender(); // Update active scene if present @@ -247,6 +276,10 @@ namespace Sleak { queue->ExecuteCommands(context); renderer->EndRender(); + + if (captureArmed && !renderer->IsFrameCapturePending()) + CoreWindow->Close(); + ++frameIndex; } } else{ diff --git a/src/Core/Window.cpp b/src/Core/Window.cpp index e8c18cbf..79dbe2d8 100644 --- a/src/Core/Window.cpp +++ b/src/Core/Window.cpp @@ -3,6 +3,7 @@ #include "../../include/private/Graphics/Common/RendererFactory.hpp" #include #include +#include #include #include #include @@ -11,6 +12,32 @@ namespace Sleak { +namespace { + +/// True for keyboard and mouse events that carry user input. +bool IsUserInputEvent(Uint32 type) { + switch (type) { + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + case SDL_EVENT_TEXT_INPUT: + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: + return true; + default: + return false; + } +} + +/// SDL event filter that drops user input while a capture runs. +bool SDLCALL DropUserInput(void* userdata, SDL_Event* event) { + (void)userdata; + return !IsUserInputEvent(event->type); +} + +} // namespace + int Window::Width = 0; int Window::Height = 0; @@ -37,6 +64,8 @@ bool Window::InitializeWindow() { if(bIsInitialized) return false; + m_inputLocked = !CommandLine::GetValue("-capture").empty(); + auto type = RenderEngine::RendererFactory::GetRendererType(); int GraphicsAPI = 0; @@ -50,6 +79,7 @@ bool Window::InitializeWindow() { SLEAK_FATAL("Failed to initialize SDL: {0}", SDL_GetError()); return false; } + if (m_inputLocked) SDL_SetEventFilter(DropUserInput, nullptr); SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, GraphicsAPI | SDL_WINDOW_RESIZABLE); @@ -180,6 +210,8 @@ void Window::Update() { } } } + + if (m_inputLocked) SDL_GetRelativeMouseState(nullptr, nullptr); } void Window::SetFullScreen(bool isFullscreen) { diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index f1e50b57..d842ddc8 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -609,6 +609,7 @@ void VulkanRenderer::RenderBloomPass() { // [MIP..MIP*2-2] : upsamples (bloomMip[MIP-1-k] -> bloomMip[MIP-2-k]) // BLOOM_TRANSITION_COUNT = 1 + (MIP-1) + (MIP-1) = 11 for MIP=6. auto& setArray = m_bloomFilterSets[currentFrame]; + BeginGpuPass(GpuPass::Bloom); // ---------- 1. Threshold / prefilter pass ---------- // Input: sceneHDR (already SHADER_READ_ONLY_OPTIMAL via forward RP finalLayout) @@ -695,6 +696,7 @@ void VulkanRenderer::RenderBloomPass() { vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); } + EndGpuPass(GpuPass::Bloom); } /// Tonemaps and composites the HDR scene, bloom, and SSR into the swapchain image, then draws ImGui. @@ -748,6 +750,7 @@ void VulkanRenderer::RenderBloomCompositePass() { rp.renderArea.extent = scExtent; rp.clearValueCount = 0; + BeginGpuPass(GpuPass::Tonemap); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, scExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_bloomCompositePipeline); @@ -767,12 +770,20 @@ void VulkanRenderer::RenderBloomCompositePass() { vkCmdDraw(command, 3, 1, 0, 0); // ImGui draws inside the composite pass, AFTER the HDR → LDR tonemap. + // Captures skip UI + const bool drawUI = bImFrameActive && m_capturePath.empty(); + if (drawUI) { + EndGpuPass(GpuPass::Tonemap); + BeginGpuPass(GpuPass::UI); + } if (bImFrameActive) { ImGui::Render(); - ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); + if (drawUI) + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); } vkCmdEndRenderPass(command); + EndGpuPass(drawUI ? GpuPass::UI : GpuPass::Tonemap); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanCapture.cpp b/src/Graphics/Vulkan/VulkanCapture.cpp new file mode 100644 index 00000000..7bf5fe6a --- /dev/null +++ b/src/Graphics/Vulkan/VulkanCapture.cpp @@ -0,0 +1,215 @@ +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +/// Byte offsets of red, green, and blue inside one 4-byte texel; false when +/// the format is not an 8-bit RGBA or BGRA layout. +bool ChannelOffsets(VkFormat format, uint32_t& r, uint32_t& g, uint32_t& b) { + switch (format) { + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: + r = 2; + g = 1; + b = 0; + return true; + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: + r = 0; + g = 1; + b = 2; + return true; + default: + return false; + } +} + +/// Picks a host-visible, host-coherent memory type, preferring cached ones. +uint32_t FindReadbackMemoryType(VkPhysicalDevice gpu, uint32_t typeBits) { + VkPhysicalDeviceMemoryProperties props; + vkGetPhysicalDeviceMemoryProperties(gpu, &props); + const VkMemoryPropertyFlags coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + for (VkMemoryPropertyFlags wanted : + {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, coherent}) { + for (uint32_t i = 0; i < props.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && + (props.memoryTypes[i].propertyFlags & wanted) == wanted) + return i; + } + } + return UINT32_MAX; +} + +} // namespace + +/// Arms a UI-free readback of the next presented frame into a binary PPM. +bool VulkanRenderer::RequestFrameCapture(const std::string& path) { + if (path.empty()) return false; + if (!m_swapchainTransferSrc) { + SLEAK_ERROR( + "Frame capture: swapchain was created without TRANSFER_SRC usage"); + return false; + } + uint32_t r, g, b; + if (!ChannelOffsets(scImageFormat, r, g, b)) { + SLEAK_ERROR("Frame capture: unsupported swapchain format {}", + static_cast(scImageFormat)); + return false; + } + m_capturePath = path; + return true; +} + +/// Records the swapchain-image-to-buffer copy for an armed capture. Runs after +/// the final render pass, so the image is in PRESENT_SRC_KHR. +void VulkanRenderer::RecordFrameCaptureCopy() { + CleanupFrameCapture(); + + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = + static_cast(scExtent.width) * scExtent.height * 4; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &m_captureBuffer) != + VK_SUCCESS) + m_captureBuffer = VK_NULL_HANDLE; + bool ok = m_captureBuffer != VK_NULL_HANDLE; + if (ok) { + VkMemoryRequirements req; + vkGetBufferMemoryRequirements(device, m_captureBuffer, &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + alloc.memoryTypeIndex = + FindReadbackMemoryType(physicalDevice, req.memoryTypeBits); + if (alloc.memoryTypeIndex == UINT32_MAX || + vkAllocateMemory(device, &alloc, nullptr, &m_captureMemory) != + VK_SUCCESS) + m_captureMemory = VK_NULL_HANDLE; + ok = m_captureMemory != VK_NULL_HANDLE && + vkBindBufferMemory(device, m_captureBuffer, m_captureMemory, 0) == + VK_SUCCESS; + } + if (!ok) { + SLEAK_ERROR("Frame capture: readback buffer allocation failed"); + CleanupFrameCapture(); + m_capturePath.clear(); + return; + } + + const VkImage image = swapChainImages[CurrentFrameIndex]; + + VkImageMemoryBarrier toCopy{}; + toCopy.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + toCopy.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toCopy.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + toCopy.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toCopy.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toCopy.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toCopy.image = image; + toCopy.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &toCopy); + + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {scExtent.width, scExtent.height, 1}; + vkCmdCopyImageToBuffer(command, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_captureBuffer, 1, ®ion); + + VkImageMemoryBarrier toPresent = toCopy; + toPresent.srcAccessMask = 0; + toPresent.dstAccessMask = 0; + toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkMemoryBarrier toHost{}; + toHost.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier( + command, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 1, + &toHost, 0, nullptr, 1, &toPresent); + + m_captureExtent = scExtent; + m_captureFormat = scImageFormat; + m_captureRecorded = true; +} + +/// Waits for the captured frame, writes the PPM, and disarms the capture. +void VulkanRenderer::FinishFrameCapture() { + m_captureRecorded = false; + std::string path; + path.swap(m_capturePath); + + const VkResult wait = vkWaitForFences( + device, 1, &inFlightFences[currentFrame], VK_TRUE, 10'000'000'000ull); + void* mapped = nullptr; + if (wait != VK_SUCCESS || + vkMapMemory(device, m_captureMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != + VK_SUCCESS) { + SLEAK_ERROR("Frame capture: readback of {} failed ({})", path, + static_cast(wait)); + CleanupFrameCapture(); + return; + } + + uint32_t r = 0, g = 1, b = 2; + ChannelOffsets(m_captureFormat, r, g, b); + const size_t pixels = + static_cast(m_captureExtent.width) * m_captureExtent.height; + const auto* src = static_cast(mapped); + std::vector rgb(pixels * 3); + for (size_t i = 0; i < pixels; ++i) { + rgb[i * 3 + 0] = src[i * 4 + r]; + rgb[i * 3 + 1] = src[i * 4 + g]; + rgb[i * 3 + 2] = src[i * 4 + b]; + } + vkUnmapMemory(device, m_captureMemory); + CleanupFrameCapture(); + + std::ofstream out(path, std::ios::binary); + out << "P6\n" + << m_captureExtent.width << " " << m_captureExtent.height << "\n255\n"; + out.write(reinterpret_cast(rgb.data()), + static_cast(rgb.size())); + if (!out) { + SLEAK_ERROR("Frame capture: could not write {}", path); + return; + } + SLEAK_INFO("Frame capture: wrote {}x{} to {}", m_captureExtent.width, + m_captureExtent.height, path); +} + +/// Frees the capture readback buffer. +void VulkanRenderer::CleanupFrameCapture() { + if (m_captureBuffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, m_captureBuffer, nullptr); + m_captureBuffer = VK_NULL_HANDLE; + } + if (m_captureMemory != VK_NULL_HANDLE) { + vkFreeMemory(device, m_captureMemory, nullptr); + m_captureMemory = VK_NULL_HANDLE; + } +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index 2cc1da17..061eaa10 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -1124,6 +1124,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 1. End GBuffer render pass — transitions color RTs → SHADER_READ_ONLY, // depth → DEPTH_STENCIL_READ_ONLY via finalLayout in CreateGBufferRenderPass vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::GBuffer); m_inGeometryPass = false; m_activeCustomFormat = 0; // geometry pass is over; pipeline state doesn't survive across render passes @@ -1147,6 +1148,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { rpBegin.clearValueCount = 1; rpBegin.pClearValues = &clearVal; + BeginGpuPass(GpuPass::Lighting); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); // 4. Set viewport and scissor @@ -1187,6 +1189,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 8. End lighting render pass vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Lighting); // 9. Transition depth back to DEPTH_STENCIL_ATTACHMENT_OPTIMAL for forward pass VkImageMemoryBarrier depthBarrier{}; @@ -1219,6 +1222,7 @@ void VulkanRenderer::BeginForwardTransparentPass() { rpBegin.renderArea.extent = scExtent; rpBegin.clearValueCount = 0; // LOAD_OP — no clear needed + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{}; diff --git a/src/Graphics/Vulkan/VulkanDevice.cpp b/src/Graphics/Vulkan/VulkanDevice.cpp index 83ce61e4..5e65d6d2 100644 --- a/src/Graphics/Vulkan/VulkanDevice.cpp +++ b/src/Graphics/Vulkan/VulkanDevice.cpp @@ -263,6 +263,8 @@ bool VulkanRenderer::CreateDevice() { // Query max MSAA sample count VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); + SLEAK_INFO("Selected GPU: {} (driver {:#x})", deviceProperties.deviceName, + deviceProperties.driverVersion); VkSampleCountFlags counts = deviceProperties.limits.framebufferColorSampleCounts & deviceProperties.limits.framebufferDepthSampleCounts; m_maxMsaaSampleCount = 1; diff --git a/src/Graphics/Vulkan/VulkanProfiler.cpp b/src/Graphics/Vulkan/VulkanProfiler.cpp new file mode 100644 index 00000000..69948d2b --- /dev/null +++ b/src/Graphics/Vulkan/VulkanProfiler.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +constexpr float kGpuLogIntervalSeconds = 2.0f; + +constexpr std::array kGpuRowNames = { + "shadow", "gbuffer", "ssao", "lighting", "forward", "taa", + "ssr", "bloom", "tonemap", "ui", "other", "total"}; + +} // namespace + +/// Creates the timestamp query pool; leaves profiling off when the graphics +/// queue has no timestamp support. +bool VulkanRenderer::CreateGpuProfiler() { + static_assert(kGpuRowNames.size() == GPU_ROW_COUNT); + + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + + uint32_t familyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, + nullptr); + std::vector families(familyCount); + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, + families.data()); + + const uint32_t validBits = + QueueIDs.GraphicsIndex < familyCount + ? families[QueueIDs.GraphicsIndex].timestampValidBits + : 0; + if (validBits == 0 || props.limits.timestampPeriod <= 0.0f) { + SLEAK_WARN( + "GPU profiler: graphics queue has no timestamp support, " + "--gpuprofile ignored"); + return false; + } + + VkQueryPoolCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = MAX_FRAMES_IN_FLIGHT * GPU_QUERIES_PER_FRAME; + if (vkCreateQueryPool(device, &info, nullptr, &m_gpuQueryPool) != + VK_SUCCESS) { + SLEAK_WARN("GPU profiler: query pool creation failed"); + m_gpuQueryPool = VK_NULL_HANDLE; + return false; + } + + m_gpuTickNs = static_cast(props.limits.timestampPeriod); + m_gpuTickMask = validBits >= 64 ? ~0ull : ((1ull << validBits) - 1); + m_gpuSlots = {}; + m_gpuSumMs = {}; + m_gpuMaxMs = {}; + m_gpuRuns = {}; + m_gpuLastMs = {}; + m_gpuWindowFrames = 0; + m_gpuWindowTimer.Reset(); + m_gpuProfilerEnabled = true; + + SLEAK_INFO("GPU profiler: on ({} timestamp bits, {:.3f} ns/tick)", + validBits, m_gpuTickNs); + return true; +} + +/// Destroys the timestamp query pool. +void VulkanRenderer::CleanupGpuProfiler() { + m_gpuProfilerEnabled = false; + if (m_gpuQueryPool != VK_NULL_HANDLE) { + vkDestroyQueryPool(device, m_gpuQueryPool, nullptr); + m_gpuQueryPool = VK_NULL_HANDLE; + } +} + +/// Folds this slot's finished timestamps into the running window and logs the +/// table once the window elapses. Call after the slot's fence. +void VulkanRenderer::CollectGpuProfile() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.submitted) return; + slot.submitted = false; + + // value + availability per query + const uint32_t queryCount = 2 + 2 * slot.pairCount; + std::array data{}; + const VkResult res = vkGetQueryPoolResults( + device, m_gpuQueryPool, currentFrame * GPU_QUERIES_PER_FRAME, + queryCount, queryCount * 2 * sizeof(uint64_t), data.data(), + 2 * sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT); + if (res != VK_SUCCESS || data[1] == 0 || data[3] == 0) return; + + auto ticksToMs = [this](uint64_t begin, uint64_t end) { + const uint64_t ticks = (end - begin) & m_gpuTickMask; + return static_cast(static_cast(ticks) * m_gpuTickNs * + 1e-6); + }; + + std::array frameMs{}; + std::array ran{}; + float passSum = 0.0f; + for (uint32_t i = 0; i < slot.pairCount; ++i) { + const uint64_t* begin = &data[(2 + 2 * i) * 2]; + const uint64_t* end = &data[(3 + 2 * i) * 2]; + if (begin[1] == 0 || end[1] == 0) continue; + const float ms = ticksToMs(begin[0], end[0]); + const auto row = static_cast(slot.pairPass[i]); + frameMs[row] += ms; + ran[row] = true; + passSum += ms; + } + frameMs[GPU_ROW_TOTAL] = ticksToMs(data[0], data[2]); + frameMs[GPU_ROW_OTHER] = std::max(0.0f, frameMs[GPU_ROW_TOTAL] - passSum); + ran[GPU_ROW_TOTAL] = true; + ran[GPU_ROW_OTHER] = true; + + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + m_gpuLastMs[row] = ran[row] ? frameMs[row] : 0.0f; + if (!ran[row]) continue; + m_gpuSumMs[row] += frameMs[row]; + m_gpuMaxMs[row] = std::max(m_gpuMaxMs[row], frameMs[row]); + ++m_gpuRuns[row]; + } + ++m_gpuWindowFrames; + + const float elapsed = m_gpuWindowTimer.Elapsed(); + if (elapsed < kGpuLogIntervalSeconds) return; + LogGpuProfile(elapsed); + m_gpuSumMs = {}; + m_gpuMaxMs = {}; + m_gpuRuns = {}; + m_gpuWindowFrames = 0; + m_gpuWindowTimer.Reset(); +} + +/// Resets this slot's queries and stamps the frame start. +void VulkanRenderer::BeginGpuFrame() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + slot = {}; + slot.recording = true; + const uint32_t base = currentFrame * GPU_QUERIES_PER_FRAME; + vkCmdResetQueryPool(command, m_gpuQueryPool, base, GPU_QUERIES_PER_FRAME); + vkCmdWriteTimestamp(command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + m_gpuQueryPool, base); +} + +/// Closes any pass left open and stamps the frame end. +void VulkanRenderer::EndGpuFrame() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording) return; + if (slot.pairOpen) EndGpuPass(slot.pairPass[slot.pairCount]); + vkCmdWriteTimestamp(command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 1); + slot.recording = false; +} + +/// Flags this slot's queries for readback on its next fence wait. +void VulkanRenderer::MarkGpuFrameSubmitted() { + if (!m_gpuProfilerEnabled) return; + m_gpuSlots[currentFrame].submitted = true; +} + +/// Stamps the start of a pass. Timestamps use BOTTOM_OF_PIPE so each pair +/// measures the time the pass adds once earlier work has drained. +void VulkanRenderer::BeginGpuPass(GpuPass pass) { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording || slot.pairOpen || slot.pairCount >= GPU_MAX_PAIRS) + return; + vkCmdWriteTimestamp( + command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 2 + 2 * slot.pairCount); + slot.pairPass[slot.pairCount] = pass; + slot.pairOpen = true; +} + +/// Stamps the end of the pass opened by the matching BeginGpuPass. +void VulkanRenderer::EndGpuPass(GpuPass pass) { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording || !slot.pairOpen || + slot.pairPass[slot.pairCount] != pass) + return; + vkCmdWriteTimestamp( + command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 3 + 2 * slot.pairCount); + slot.pairOpen = false; + ++slot.pairCount; +} + +/// Logs the averaged per-pass table for the current window. +void VulkanRenderer::LogGpuProfile(float seconds) const { + SLEAK_INFO("GPU profile: {} frames in {:.1f} s, avg / max ms", + m_gpuWindowFrames, seconds); + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + if (m_gpuRuns[row] == 0) continue; + const double avg = m_gpuSumMs[row] / m_gpuRuns[row]; + const std::string runs = + m_gpuRuns[row] < m_gpuWindowFrames + ? std::format(" ({}/{} frames)", m_gpuRuns[row], + m_gpuWindowFrames) + : std::string(); + SLEAK_INFO(" {:<9}{:8.3f}{:8.3f}{}", kGpuRowNames[row], avg, + m_gpuMaxMs[row], runs); + } +} + +/// Adds per-pass GPU millisecond columns when --gpuprofile is on. Values lag +/// the CSV row by MAX_FRAMES_IN_FLIGHT frames. +void VulkanRenderer::RegisterBenchmarkMetrics(Benchmark& bench) { + if (!m_gpuProfilerEnabled) return; + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + if (row == GPU_ROW_OTHER) continue; + bench.RegisterMetric(std::format("GPU_{}_ms", kGpuRowNames[row]), + [this, row] { return m_gpuLastMs[row]; }); + } +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 367f2ca9..452818bd 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -78,6 +78,8 @@ VulkanRenderer::~VulkanRenderer() { /// Runs the full Vulkan bring-up sequence: instance, device, swapchain, /// pipelines, and sync objects. bool VulkanRenderer::Initialize() { + m_captureMode = !CommandLine::GetValue("-capture").empty(); + if (!InitVulkan()) SLEAK_RETURN_ERR("Failed to initialize Vulkan Instance!"); @@ -151,6 +153,8 @@ bool VulkanRenderer::Initialize() { if (!CreateSyncObjects()) SLEAK_RETURN_ERR("Failed to synchronization objects of renderer!"); + if (CommandLine::HasFlag("--gpuprofile")) CreateGpuProfiler(); + SetPerformanceCounter(true); SLEAK_INFO("Vulkan renderer has been initialized successfully!"); @@ -190,6 +194,7 @@ void VulkanRenderer::BeginRender() { vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); } + CollectGpuProfile(); // Clean up staging buffers from the previous use of this frame slot. // The fence wait above guarantees the GPU finished both the transfer @@ -259,6 +264,7 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame + BeginGpuFrame(); // Skip shadow pass if no cached draws — preserve previous frame's shadow map auto* shadowQueue = RenderCommandQueue::GetInstance(); @@ -277,6 +283,7 @@ void VulkanRenderer::BeginRender() { shadowPassInfo.clearValueCount = 1; shadowPassInfo.pClearValues = &shadowClear; + BeginGpuPass(GpuPass::Shadow); vkCmdBeginRenderPass(command, &shadowPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_shadowPipeline); @@ -313,6 +320,7 @@ void VulkanRenderer::BeginRender() { m_shadowPassActive = false; vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Shadow); } // ---- Deferred path: begin GBuffer render pass ---- @@ -333,6 +341,7 @@ void VulkanRenderer::BeginRender() { gbufferPassInfo.clearValueCount = GBUFFER_COUNT + 1; gbufferPassInfo.pClearValues = gbufferClears; + BeginGpuPass(GpuPass::GBuffer); vkCmdBeginRenderPass(command, &gbufferPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferPipeline); @@ -407,6 +416,7 @@ void VulkanRenderer::BeginRender() { passInfo.clearValueCount = clearValueCount; passInfo.pClearValues = clearValues; + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &passInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); @@ -471,6 +481,7 @@ void VulkanRenderer::EndRender() { // end it now so we don't have a dangling render pass. if (m_gbufferResourcesCreated && m_deferredEnabled && m_inGeometryPass) { vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::GBuffer); m_inGeometryPass = false; } @@ -486,6 +497,7 @@ void VulkanRenderer::EndRender() { rpBegin.renderArea.offset = {0, 0}; rpBegin.renderArea.extent = scExtent; rpBegin.clearValueCount = 0; + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); m_forwardPassOpen = true; } @@ -493,6 +505,7 @@ void VulkanRenderer::EndRender() { // Close the forward pass (HDR scene → SHADER_READ_ONLY_OPTIMAL via finalLayout). if (m_forwardPassOpen) { vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Forward); m_forwardPassOpen = false; } @@ -510,13 +523,23 @@ void VulkanRenderer::EndRender() { RenderBloomCompositePass(); } else { // Forward (non-deferred) path — ImGui inside main render pass. + const bool drawUI = bImFrameActive && m_capturePath.empty(); + if (drawUI) { + EndGpuPass(GpuPass::Forward); + BeginGpuPass(GpuPass::UI); + } if (bImFrameActive) { ImGui::Render(); - ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); + if (drawUI) + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); } vkCmdEndRenderPass(command); + EndGpuPass(drawUI ? GpuPass::UI : GpuPass::Forward); } + if (!m_capturePath.empty()) RecordFrameCaptureCopy(); + EndGpuFrame(); + if (vkEndCommandBuffer(command) != VK_SUCCESS) { SLEAK_ERROR("Failed to end command buffer!"); return; @@ -558,6 +581,8 @@ void VulkanRenderer::EndRender() { if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { SLEAK_ERROR("Failed to submit draw command buffer!"); + } else { + MarkGpuFrameSubmitted(); } // Present @@ -573,13 +598,17 @@ void VulkanRenderer::EndRender() { VkResult presentResult = vkQueuePresentKHR(presentQueue, &presentInfo); + // Capture pins the extent if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || - presentResult == VK_SUBOPTIMAL_KHR) { + (presentResult == VK_SUBOPTIMAL_KHR && !m_captureMode)) { RecreateSwapChain(); - } else if (presentResult != VK_SUCCESS) { + } else if (presentResult != VK_SUCCESS && + presentResult != VK_SUBOPTIMAL_KHR) { SLEAK_ERROR("Failed to present render!"); } + if (m_captureRecorded) FinishFrameCapture(); + // Reset per-frame deferred state flags m_forwardPassOpen = false; m_inForwardTransparentPass = false; @@ -1257,6 +1286,9 @@ void VulkanRenderer::Cleanup() { debugMessenger = VK_NULL_HANDLE; } + CleanupGpuProfiler(); + CleanupFrameCapture(); + // Drain any buffers freed during teardown, then destroy the VMA allocator // (it must outlive every vmaDestroyBuffer, and both precede vkDestroyDevice). VulkanBuffer::FlushAllDeferredDeletions(); diff --git a/src/Graphics/Vulkan/VulkanSSAO.cpp b/src/Graphics/Vulkan/VulkanSSAO.cpp index ac2d3d08..83599fb7 100644 --- a/src/Graphics/Vulkan/VulkanSSAO.cpp +++ b/src/Graphics/Vulkan/VulkanSSAO.cpp @@ -860,6 +860,7 @@ void VulkanRenderer::RenderSSAOPasses() { rp.renderArea.extent = m_ssaoExtent; rp.clearValueCount = 0; + BeginGpuPass(GpuPass::SSAO); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, m_ssaoExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssaoPipeline); @@ -879,6 +880,7 @@ void VulkanRenderer::RenderSSAOPasses() { &m_ssaoBlurSets[currentFrame], 0, nullptr); vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::SSAO); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanSSR.cpp b/src/Graphics/Vulkan/VulkanSSR.cpp index 2bddc6e1..8d146af2 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -554,6 +554,7 @@ void VulkanRenderer::RenderSSRPass() { rp.renderArea.extent = scExtent; rp.clearValueCount = 0; // DONT_CARE load — we write every pixel + BeginGpuPass(GpuPass::SSR); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, scExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssrPipeline); @@ -563,6 +564,7 @@ void VulkanRenderer::RenderSSRPass() { m_ssrPipelineLayout, 0, 2, sets, 0, nullptr); vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::SSR); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanSwapchain.cpp b/src/Graphics/Vulkan/VulkanSwapchain.cpp index 6d9ddd60..2d0e2954 100644 --- a/src/Graphics/Vulkan/VulkanSwapchain.cpp +++ b/src/Graphics/Vulkan/VulkanSwapchain.cpp @@ -12,6 +12,26 @@ namespace Sleak { namespace RenderEngine { +namespace { + +/// Short name of a present mode for logs. +const char* PresentModeName(VkPresentModeKHR mode) { + switch (mode) { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "IMMEDIATE"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "MAILBOX"; + case VK_PRESENT_MODE_FIFO_KHR: + return "FIFO"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "FIFO_RELAXED"; + default: + return "OTHER"; + } +} + +} // namespace + /// Creates the swapchain from the queried surface capabilities. bool VulkanRenderer::CreateSwapChain() { auto details = QuerySwapchain(); @@ -39,6 +59,11 @@ bool VulkanRenderer::CreateSwapChain() { info.imageArrayLayers = 1; info.presentMode = mode; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + m_swapchainTransferSrc = + m_captureMode && + (details->caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + if (m_swapchainTransferSrc) + info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; uint32_t indices[] = {QueueIDs.GraphicsIndex, QueueIDs.PresentIndex}; @@ -70,6 +95,9 @@ bool VulkanRenderer::CreateSwapChain() { scImageFormat = format.format; scExtent = extent; + SLEAK_INFO("Swapchain: {}x{}, {} images, format {}, present mode {}", + extent.width, extent.height, scImageCount, + static_cast(format.format), PresentModeName(mode)); return true; } diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 209f5c7e..9110d7a1 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -441,6 +441,7 @@ void VulkanRenderer::RenderTAAPass() { const uint32_t writeIdx = static_cast(m_taaFrameIdx % 2); const uint32_t readIdx = 1u - writeIdx; + BeginGpuPass(GpuPass::TAA); // ---- 1. Depth barrier: DEPTH_STENCIL_ATTACHMENT → READ_ONLY ---- // (SSR will skip its own depth barrier since TAA already handles it) @@ -578,6 +579,7 @@ void VulkanRenderer::RenderTAAPass() { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 2, toRead); } + EndGpuPass(GpuPass::TAA); // Advance ping-pong for next frame m_taaFrameIdx++; diff --git a/src/Lighting/LightManager.cpp b/src/Lighting/LightManager.cpp index 8b46086e..a50dbe77 100644 --- a/src/Lighting/LightManager.cpp +++ b/src/Lighting/LightManager.cpp @@ -340,10 +340,14 @@ void LightManager::UpdateShadowData() { // nowhere else to stash time). Using a static Timer keeps this // self-contained and independent of Application state. static Sleak::Timer s_sceneClock; + static const bool s_fixedStep = !CommandLine::GetValue("-capture").empty(); + static uint64_t s_sceneFrames = 0; ubo.CameraPos[0] = camPos.GetX(); ubo.CameraPos[1] = camPos.GetY(); ubo.CameraPos[2] = camPos.GetZ(); - ubo.CameraPos[3] = s_sceneClock.Elapsed(); + // Capture: frame-driven clock + ubo.CameraPos[3] = s_fixedStep ? static_cast(s_sceneFrames++) / 60.0f + : s_sceneClock.Elapsed(); // CURRENT lightVP — renderers stage SetLightVP and commit at BeginRender, // so this frame's shadow map IS rendered with this matrix. The old From 318b7463a884c84b09541b08e9de545ce7520cfd Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 00:42:13 +0300 Subject: [PATCH 05/36] perf: load the vulkan validation layer only in debug or with --validate The layer was enabled on every run whenever it was installed, Release included, so every Vulkan call paid validation overhead and --validate did nothing. It now loads in Debug builds or with --validate, and --syncval also turns on synchronization validation. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 6 +- src/Graphics/Vulkan/VulkanDevice.cpp | 62 ++++++++++++++++--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 5fc49551..17059cc7 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -285,7 +285,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool CreateShadowLightUBOResources(); /// Destroys the shadow map image, pipeline, render pass, and light UBO resources. void CleanupShadowResources(); - /// Creates the Vulkan instance with validation layers when available. + /// Creates the Vulkan instance. The validation layer is on in Debug builds + /// or with --validate, and --syncval adds synchronization validation. bool InitVulkan(); /// Creates the SDL-backed Vulkan presentation surface. bool CreateSurface(); @@ -322,7 +323,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool CreateDefaultTexture(); /// Allocates and writes a per-texture descriptor set for the given texture. void WriteTextureDescriptors(VulkanTexture* texture); - /// Registers the debug messenger callback for validation output. + /// Registers the debug messenger callback when the validation layer is on. bool SetupDebugMessenger(); /// Destroys the swapchain, its image views, and framebuffers. @@ -433,6 +434,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = nullptr; + bool m_validationEnabled = false; Window* sdlWindow; diff --git a/src/Graphics/Vulkan/VulkanDevice.cpp b/src/Graphics/Vulkan/VulkanDevice.cpp index 5e65d6d2..a028fb85 100644 --- a/src/Graphics/Vulkan/VulkanDevice.cpp +++ b/src/Graphics/Vulkan/VulkanDevice.cpp @@ -11,6 +11,7 @@ #include #include #include +#include "Core/CommandLine.hpp" #include "Core/Logger.hpp" #include "SDL3/SDL_error.h" #ifdef PLATFORM_LINUX @@ -22,7 +23,8 @@ namespace Sleak { namespace RenderEngine { -/// Creates the Vulkan instance with validation layers when available. +/// Creates the Vulkan instance. The validation layer is on in Debug builds +/// or with --validate, and --syncval adds synchronization validation. bool VulkanRenderer::InitVulkan() { try { instance = VK_NULL_HANDLE; @@ -57,19 +59,22 @@ bool VulkanRenderer::InitVulkan() { requiredExtensions.push_back(VK_MVK_MOLTENVK_EXTENSION_NAME); #endif - // Always attempt to enable validation layers so GPU errors are - // reported via the debug messenger as SLEAK_ERROR messages rather - // than silent VK_ERROR_DEVICE_LOST crashes. If the layer is not - // installed the instance still creates successfully (empty list). + // Validation layer gating + const bool syncValidation = CommandLine::HasFlag("--syncval"); + bool wantValidation = + syncValidation || CommandLine::HasFlag("--validate"); +#ifdef _DEBUG + wantValidation = true; +#endif + const char* desiredLayer = "VK_LAYER_KHRONOS_validation"; std::vector enabledLayers; - { + if (wantValidation) { uint32_t layerCount = 0; vkEnumerateInstanceLayerProperties(&layerCount, nullptr); std::vector availableLayers(layerCount); vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); - const char* desiredLayer = "VK_LAYER_KHRONOS_validation"; for (const auto& layer : availableLayers) { if (strcmp(layer.layerName, desiredLayer) == 0) { enabledLayers.push_back(desiredLayer); @@ -81,6 +86,7 @@ bool VulkanRenderer::InitVulkan() { SLEAK_WARN("VK_LAYER_KHRONOS_validation not available — GPU errors will not be reported"); } } + m_validationEnabled = !enabledLayers.empty(); if (enabledLayers.empty()) { auto it = std::find_if( requiredExtensions.begin(), requiredExtensions.end(), @@ -93,6 +99,41 @@ bool VulkanRenderer::InitVulkan() { } } + // Sync validation settings + const VkBool32 syncEnabled = VK_TRUE; + VkLayerSettingEXT syncSetting{desiredLayer, "validate_sync", + VK_LAYER_SETTING_TYPE_BOOL32_EXT, 1, + &syncEnabled}; + VkLayerSettingsCreateInfoEXT layerSettings{}; + layerSettings.sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT; + layerSettings.settingCount = 1; + layerSettings.pSettings = &syncSetting; + bool useLayerSettings = false; + if (m_validationEnabled && syncValidation) { + uint32_t layerExtCount = 0; + vkEnumerateInstanceExtensionProperties(desiredLayer, &layerExtCount, + nullptr); + std::vector layerExts(layerExtCount); + vkEnumerateInstanceExtensionProperties(desiredLayer, &layerExtCount, + layerExts.data()); + for (const auto& ext : layerExts) { + if (strcmp(ext.extensionName, + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME) == 0) { + useLayerSettings = true; + break; + } + } + if (useLayerSettings) { + requiredExtensions.push_back( + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME); + SLEAK_INFO("Vulkan synchronization validation enabled"); + } else { + SLEAK_WARN( + "Vulkan synchronization validation needs " + "VK_EXT_layer_settings, which this layer lacks"); + } + } + // Check and list vulkan extensions uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, @@ -120,8 +161,9 @@ bool VulkanRenderer::InitVulkan() { createInfo.ppEnabledLayerNames = enabledLayers.data(); VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - if (!enabledLayers.empty()) { + if (m_validationEnabled) { PopulateDebugMessengerCreateInfo(debugCreateInfo); + if (useLayerSettings) debugCreateInfo.pNext = &layerSettings; createInfo.pNext = &debugCreateInfo; } @@ -327,8 +369,10 @@ VulkanRenderer::GetUniqueQueueCreateInfos() { return result; } -/// Registers the debug messenger callback for validation output. +/// Registers the debug messenger callback when the validation layer is on. bool VulkanRenderer::SetupDebugMessenger() { + if (!m_validationEnabled) return true; + VkDebugUtilsMessengerCreateInfoEXT createInfo{}; PopulateDebugMessengerCreateInfo(createInfo); From acd276958f8834e535049d8dfa7387787b7b8625 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 00:42:14 +0300 Subject: [PATCH 06/36] perf: skip repeated vulkan shadow push constants Every voxel caster shares one transform, so the shadow pass pushed the same 128 bytes for each draw. Push once per shadow pass while the cached product is unchanged; all shadow pipelines share one layout, so the values persist across pipeline binds. --- .../private/Graphics/Vulkan/VulkanRenderer.hpp | 3 +++ src/Graphics/Vulkan/VulkanRenderer.cpp | 18 ++++++++++++++---- src/Graphics/Vulkan/VulkanShadow.cpp | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 17059cc7..7b0939c8 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -524,6 +524,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { float m_shadowWorldCache[16] = {}; float m_shadowPCCache[32] = {}; bool m_shadowPCCacheValid = false; + // Layout m_shadowPCCache was last pushed with in this shadow pass, or + // null once anything else overwrote the push constants. + VkPipelineLayout m_shadowPCPushedLayout = VK_NULL_HANDLE; // Light VP matrix (stored as raw floats for push constant computation) float m_lightVP[16] = {}; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 452818bd..7c3d091d 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -313,6 +313,7 @@ void VulkanRenderer::BeginRender() { m_shadowPassActive = true; m_shadowPCCacheValid = false; + m_shadowPCPushedLayout = VK_NULL_HANDLE; auto* queue = RenderCommandQueue::GetInstance(); if (queue) { queue->ExecuteShadowPass(this); @@ -750,8 +751,9 @@ void VulkanRenderer::BindIndexBuffer(RefPtr buffer, VK_INDEX_TYPE_UINT32); } -/// Pushes constant-buffer data via push constants, applying TAA jitter or -/// the shadow push-constant cache as needed. +/// Pushes constant-buffer data via push constants, applying TAA jitter or the +/// shadow push-constant cache. A shadow push that would repeat the bytes +/// already pushed in this pass is skipped. void VulkanRenderer::BindConstantBuffer(RefPtr buffer, uint32_t slot) { if (!bFrameStarted) return; @@ -787,6 +789,7 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, } vkCmdPushConstants(command, activeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, size, jdata); + m_shadowPCPushedLayout = VK_NULL_HANDLE; return; } @@ -813,13 +816,20 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, memcpy(&m_shadowPCCache[16], srcWorld, 64); memcpy(m_shadowWorldCache, srcWorld, 64); m_shadowPCCacheValid = true; + m_shadowPCPushedLayout = VK_NULL_HANDLE; } - vkCmdPushConstants(command, activeLayout, - VK_SHADER_STAGE_VERTEX_BIT, 0, 128, m_shadowPCCache); + // Pushes survive pipeline binds + if (m_shadowPCPushedLayout != activeLayout) { + vkCmdPushConstants(command, activeLayout, + VK_SHADER_STAGE_VERTEX_BIT, 0, 128, + m_shadowPCCache); + m_shadowPCPushedLayout = activeLayout; + } } else { vkCmdPushConstants(command, activeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, size, data); + m_shadowPCPushedLayout = VK_NULL_HANDLE; } } diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index bbfdd910..0b64c5ed 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -585,6 +585,7 @@ void VulkanRenderer::CleanupShadowResources() { void VulkanRenderer::BeginShadowPass() { m_shadowPassActive = true; m_shadowPCCacheValid = false; + m_shadowPCPushedLayout = VK_NULL_HANDLE; } /// Marks the shadow pass inactive. From 816542dd3187ede0286ba9586d4c70d5a5ef07fb Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 00:42:14 +0300 Subject: [PATCH 07/36] perf: remove vulkan upload stalls and slow constant buffer reads Destroying any buffer that had ever been batch uploaded forced a synchronous submit and fence wait while a batch was open, stalling the CPU on the previous frame thousands of times per minute while streaming. Deferred deletion already covers that lifetime, so the flush is gone and an unsubmitted batch is discarded at shutdown. Staging now comes from a resident VMA pool so longer-lived staging no longer churns 64 MB blocks. Constant buffers move from uncached BAR memory to cached host memory because the CPU reads them on every bind. A frame whose acquire fails no longer submits its upload batch or advances the deletion clock. --- .../private/Graphics/Vulkan/VulkanBuffer.hpp | 17 ++-- .../Graphics/Vulkan/VulkanRenderer.hpp | 7 +- src/Graphics/Vulkan/VulkanBuffer.cpp | 84 +++++++++++-------- src/Graphics/Vulkan/VulkanRenderer.cpp | 18 ++-- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanBuffer.hpp b/include/private/Graphics/Vulkan/VulkanBuffer.hpp index 02eddf31..89076004 100644 --- a/include/private/Graphics/Vulkan/VulkanBuffer.hpp +++ b/include/private/Graphics/Vulkan/VulkanBuffer.hpp @@ -22,6 +22,8 @@ class ENGINE_API VulkanBuffer : public BufferBase { void Update() override; /// Overwrites buffer contents, staging through a temporary buffer for device-local memory. void Update(void* data, size_t size) override; + /// Hands the GPU buffer to deferred deletion, which keeps it alive until + /// every frame and upload batch that may reference it has completed. void Cleanup() override; /// Maps host-visible memory for direct CPU writes. @@ -34,13 +36,17 @@ class ENGINE_API VulkanBuffer : public BufferBase { // VMA allocator lifecycle (owned by the renderer; created after the // logical device, destroyed after all buffers are drained). + /// Creates the VMA allocator plus a staging pool whose blocks stay + /// resident for the allocator's lifetime. static void InitAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device); + /// Destroys the staging pool and the allocator once every buffer is freed. static void DestroyAllocator(); static VmaAllocator GetAllocator() { return s_allocator; } - // Flush all pending buffer copies in a single batched submission (synchronous). - static void FlushPendingCopies(); + /// Frees an upload batch that was recorded but never submitted, along + /// with its staging buffers. Only valid while the device is idle. + static void DiscardPendingCopies(); // Async flush: submit pending copies signaling the given semaphore. // No CPU wait — the caller must wait on the semaphore before using data. @@ -67,6 +73,8 @@ class ENGINE_API VulkanBuffer : public BufferBase { private: /// Allocates a VMA-backed VkBuffer with the given usage/memory properties. + /// Constant buffers get CPU-cached memory since every bind reads them, and + /// staging buffers come from the resident staging pool. void CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VmaAllocation& memory, @@ -97,7 +105,6 @@ class ENGINE_API VulkanBuffer : public BufferBase { VmaAllocation m_stagingMemory = VK_NULL_HANDLE; void* m_mappedData = nullptr; - bool m_pendingInBatch = false; // --- Batched transfer state --- static bool s_batchingEnabled; @@ -156,8 +163,8 @@ class ENGINE_API VulkanBuffer : public BufferBase { static void EvictPoolOverBudget(); public: - // Called by the renderer each frame after fence wait to safely - // destroy buffers that are no longer referenced by the GPU. + // Called by the renderer once per submitted frame, after its fence wait, + // to safely destroy buffers that are no longer referenced by the GPU. static void ProcessDeferredDeletions(uint32_t maxFramesInFlight); static void FlushAllDeferredDeletions(); static void AdvanceDeletionFrame() { s_frameNumber++; } diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 7b0939c8..6cb75854 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -57,8 +57,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Runs the full Vulkan bring-up sequence: instance, device, swapchain, /// pipelines, and sync objects. virtual bool Initialize() override; - /// Prepares the command buffer and begins the shadow/GBuffer/forward - /// render pass; drawing happens via the RenderContext methods below. + /// Acquires a swapchain image and begins the shadow/GBuffer/forward + /// passes; drawing happens via the RenderContext methods below. virtual void BeginRender() override; /// Ends the active render pass, submits the command buffer, and presents. virtual void EndRender() override; @@ -66,7 +66,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { virtual void Cleanup() override; /// Blocks until the device finishes all submitted GPU work. virtual void WaitIdle() override; - /// Kicks off the current frame's async buffer upload batch. + /// Submits the open upload batch for this frame. A frame that failed to + /// start leaves it open, so it rides along with the next submitted frame. virtual void FlushPendingTransfers() override; // GPU memory tracking diff --git a/src/Graphics/Vulkan/VulkanBuffer.cpp b/src/Graphics/Vulkan/VulkanBuffer.cpp index 9b25c97e..638310af 100644 --- a/src/Graphics/Vulkan/VulkanBuffer.cpp +++ b/src/Graphics/Vulkan/VulkanBuffer.cpp @@ -15,6 +15,12 @@ namespace RenderEngine { VmaAllocator VulkanBuffer::s_allocator = VK_NULL_HANDLE; +// Staging pool whose blocks stay resident, so upload bursts never pay for +// fresh vkAllocateMemory calls. +static VmaPool g_stagingPool = VK_NULL_HANDLE; +static constexpr VkDeviceSize kStagingBlockSize = 64ull * 1024 * 1024; +static constexpr size_t kStagingMinBlocks = 2; + void VulkanBuffer::InitAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device) { @@ -27,11 +33,38 @@ void VulkanBuffer::InitAllocator(VkInstance instance, if (vmaCreateAllocator(&aci, &s_allocator) != VK_SUCCESS) { SLEAK_ERROR("Failed to create VMA allocator!"); s_allocator = VK_NULL_HANDLE; + return; + } + + // Staging pool + VkBufferCreateInfo stagingInfo{}; + stagingInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + stagingInfo.size = 64 * 1024; + stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo stagingAlloc{}; + stagingAlloc.usage = VMA_MEMORY_USAGE_AUTO; + stagingAlloc.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + stagingAlloc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VmaPoolCreateInfo poolInfo{}; + poolInfo.blockSize = kStagingBlockSize; + poolInfo.minBlockCount = kStagingMinBlocks; + if (vmaFindMemoryTypeIndexForBufferInfo( + s_allocator, &stagingInfo, &stagingAlloc, + &poolInfo.memoryTypeIndex) != VK_SUCCESS || + vmaCreatePool(s_allocator, &poolInfo, &g_stagingPool) != VK_SUCCESS) { + SLEAK_WARN("VMA staging pool unavailable, using the default pools"); + g_stagingPool = VK_NULL_HANDLE; } } void VulkanBuffer::DestroyAllocator() { if (s_allocator == VK_NULL_HANDLE) return; + if (g_stagingPool != VK_NULL_HANDLE) { + vmaDestroyPool(s_allocator, g_stagingPool); + g_stagingPool = VK_NULL_HANDLE; + } vmaDestroyAllocator(s_allocator); s_allocator = VK_NULL_HANDLE; } @@ -308,14 +341,6 @@ void VulkanBuffer::Update(void* data, size_t size) { void VulkanBuffer::Cleanup() { if (m_device == VK_NULL_HANDLE) return; - // Only flush if THIS buffer has a pending copy in the active batch. - // Normally old buffers are destroyed before the batch starts (two-pass - // column rebuild), so this only triggers during scene transitions. - if (m_pendingInBatch && s_batchActive) { - FlushPendingCopies(); - m_pendingInBatch = false; - } - if (Type == BufferType::Constant && m_mappedData) { vmaUnmapMemory(s_allocator, m_memory); m_mappedData = nullptr; @@ -399,8 +424,14 @@ void VulkanBuffer::CreateBuffer(VkDeviceSize size, VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_AUTO; allocCI.requiredFlags = properties; // preserve exact memory-property semantics - if (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) - allocCI.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + if (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) { + // Constants are read back + allocCI.flags |= + Type == BufferType::Constant + ? VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + : VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + } + if (usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT) allocCI.pool = g_stagingPool; VmaAllocationInfo info{}; VkResult r = vmaCreateBuffer(s_allocator, &bufferInfo, &allocCI, @@ -467,7 +498,6 @@ void VulkanBuffer::CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkBufferCopy copyRegion{}; copyRegion.size = size; vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, ©Region); - m_pendingInBatch = true; return; } @@ -543,28 +573,13 @@ void VulkanBuffer::EnsureBatchStarted(VkDevice device, VkCommandPool pool, s_batchActive = true; } -void VulkanBuffer::FlushPendingCopies() { - if (!s_batchActive) return; - - vkEndCommandBuffer(s_batchCommandBuffer); - - // Single submit + single fence for ALL batched copies - VkFenceCreateInfo fenceInfo{}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - VkFence batchFence; - vkCreateFence(s_batchDevice, &fenceInfo, nullptr, &batchFence); - - VkSubmitInfo submitInfo{}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &s_batchCommandBuffer; - - vkQueueSubmit(s_batchQueue, 1, &submitInfo, batchFence); - vkWaitForFences(s_batchDevice, 1, &batchFence, VK_TRUE, UINT64_MAX); - - // Cleanup - vkDestroyFence(s_batchDevice, batchFence, nullptr); - vkFreeCommandBuffers(s_batchDevice, s_batchCommandPool, 1, &s_batchCommandBuffer); +void VulkanBuffer::DiscardPendingCopies() { + if (s_batchActive) { + vkFreeCommandBuffers(s_batchDevice, s_batchCommandPool, 1, + &s_batchCommandBuffer); + s_batchCommandBuffer = VK_NULL_HANDLE; + s_batchActive = false; + } for (auto& pending : s_pendingCleanup) { vmaDestroyBuffer(s_allocator, pending.buffer, pending.memory); @@ -573,9 +588,6 @@ void VulkanBuffer::FlushPendingCopies() { s_perTypeBytes[pending.memoryTypeIndex] -= pending.allocSize; } s_pendingCleanup.clear(); - - s_batchCommandBuffer = VK_NULL_HANDLE; - s_batchActive = false; } VulkanBuffer::AsyncFlushResult diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 7c3d091d..5cf631fd 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -162,9 +162,9 @@ bool VulkanRenderer::Initialize() { return true; } -/// Prepares the command buffer and begins the shadow/GBuffer/forward render -/// pass. Does not end the command buffer; the RenderCommandQueue records -/// draw commands via the RenderContext interface after this returns. +/// Acquires a swapchain image and begins the shadow/GBuffer/forward passes +/// for the RenderCommandQueue to record into. A failed acquire returns early, +/// leaving deferred deletions unaged and the upload batch open. void VulkanRenderer::BeginRender() { bFrameStarted = false; m_inGeometryPass = false; @@ -215,9 +215,6 @@ void VulkanRenderer::BeginRender() { flush = {}; } - VulkanBuffer::ProcessDeferredDeletions(MAX_FRAMES_IN_FLIGHT); - VulkanBuffer::AdvanceDeletionFrame(); - // Enable batched buffer uploads for this frame (async, zero CPU blocking). // This is disabled during init/scene transitions where buffers may be // created and destroyed before a flush. @@ -235,6 +232,10 @@ void VulkanRenderer::BeginRender() { return; } + // Submitting frames age deletions + VulkanBuffer::ProcessDeferredDeletions(MAX_FRAMES_IN_FLIGHT); + VulkanBuffer::AdvanceDeletionFrame(); + // Wait if this swapchain image is still in use by a DIFFERENT frame slot if (CurrentFrameIndex < imagesInFlight.size() && imagesInFlight[CurrentFrameIndex] != VK_NULL_HANDLE && @@ -1059,8 +1060,10 @@ void VulkanRenderer::WaitIdle() { if (device) vkDeviceWaitIdle(device); } -/// Kicks off the current frame's async buffer upload batch. +/// Submits the open upload batch for this frame. A frame that failed to +/// start leaves it open, so it rides along with the next submitted frame. void VulkanRenderer::FlushPendingTransfers() { + if (!bFrameStarted) return; m_asyncFlush[currentFrame] = VulkanBuffer::FlushPendingCopiesAsync( m_transferSemaphores[currentFrame]); } @@ -1084,6 +1087,7 @@ void VulkanRenderer::Cleanup() { // Wait for the device to finish all work if (device) { vkDeviceWaitIdle(device); + VulkanBuffer::DiscardPendingCopies(); } RenderCommandQueue::Shutdown(); From 746702bcf384b20a52a7d4a3c4fdae60ac572e35 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 21:58:04 +0300 Subject: [PATCH 08/36] perf: persist the vulkan pipeline cache between runs Every pipeline was compiled without a VkPipelineCache. Create one at startup from cache/vulkan_pipelines.bin when its header matches this device and driver, pass it to every pipeline and to ImGui, and save it on shutdown through a temp file and a rename, skipping the write when nothing changed. With the driver shader cache off, pipeline creation at startup drops from about 450 ms to 3 ms once the cache is warm. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 9 + src/Graphics/Vulkan/VulkanBloom.cpp | 16 +- src/Graphics/Vulkan/VulkanDeferred.cpp | 9 +- src/Graphics/Vulkan/VulkanDescriptors.cpp | 1 + src/Graphics/Vulkan/VulkanPipelines.cpp | 159 +++++++++++++++++- src/Graphics/Vulkan/VulkanRenderer.cpp | 4 + src/Graphics/Vulkan/VulkanSSAO.cpp | 6 +- src/Graphics/Vulkan/VulkanSSR.cpp | 3 +- src/Graphics/Vulkan/VulkanShadow.cpp | 2 +- src/Graphics/Vulkan/VulkanTAA.cpp | 4 +- 10 files changed, 191 insertions(+), 22 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 6cb75854..21cf7991 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -301,6 +301,11 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool CreateImageViews(); /// Creates the main forward graphics pipeline and its pipeline layout. bool CreateGraphicsPipeline(); + /// Creates the pipeline cache, seeded from disk when the saved blob was + /// written by this device and driver. + void CreatePipelineCache(); + /// Saves the pipeline cache to disk when it changed, then destroys it. + void DestroyPipelineCache(); /// Creates the main forward render pass with optional MSAA color and resolve attachments. bool CreateRenderPass(); /// Creates one framebuffer per swapchain image for the main render pass. @@ -402,6 +407,10 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { std::vector GPUs; VkPipeline pipeline = VK_NULL_HANDLE; VkPipelineLayout pipelineLay = VK_NULL_HANDLE; + VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; + // Loaded blob size and hash, so an unchanged cache is not rewritten + size_t m_pipelineCacheLoadedSize = 0; + uint64_t m_pipelineCacheLoadedHash = 0; std::vector imageAvailableSemaphores; std::vector renderFinishedSemaphores; std::vector inFlightFences; diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index d842ddc8..59d71744 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -433,7 +433,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.layout = m_bloomFilterPipelineLayout; gpi.renderPass = m_bloomRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomThresholdPipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomThresholdPipeline) != VK_SUCCESS) + return false; // ---- Downsample pipeline ---- VkPipelineShaderStageCreateInfo downStages[] = { @@ -441,7 +443,9 @@ bool VulkanRenderer::CreateBloomPipelines() { m_bloomDownsampleShader->GetFragInfo() }; gpi.pStages = downStages; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomDownsamplePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomDownsamplePipeline) != VK_SUCCESS) + return false; // ---- Upsample pipeline (additive blend, LOAD render pass) ---- VkPipelineShaderStageCreateInfo upStages[] = { @@ -451,7 +455,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.pStages = upStages; gpi.pColorBlendState = &cbAdd; gpi.renderPass = m_bloomAddRenderPass; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomUpsamplePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomUpsamplePipeline) != VK_SUCCESS) + return false; // ---- Composite pipeline ---- VkPipelineShaderStageCreateInfo compStages[] = { @@ -462,7 +468,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.pColorBlendState = &cbOpaque; gpi.layout = m_bloomCompositePipelineLayout; gpi.renderPass = m_bloomCompositeRenderPass; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomCompositePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomCompositePipeline) != VK_SUCCESS) + return false; return true; } diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index 061eaa10..fe474214 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -237,7 +237,7 @@ bool VulkanRenderer::CreateGBufferPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_gbufferPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_gbufferPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create GBuffer pipeline!"); return false; @@ -349,7 +349,7 @@ bool VulkanRenderer::CreateSkinnedGbufferPipeline() { pi.basePipelineIndex = -1; VkResult res = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pi, nullptr, &m_skinnedGbufferPipeline); + device, m_pipelineCache, 1, &pi, nullptr, &m_skinnedGbufferPipeline); delete sh; if (res != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create skinned GBuffer pipeline!"); @@ -548,8 +548,9 @@ bool VulkanRenderer::CreateLightingPipeline() { pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineIndex = -1; - VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_lightingPipeline); + VkResult result = + vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, + nullptr, &m_lightingPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create lighting pipeline!"); return false; diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 153ae44a..ec879b0e 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -285,6 +285,7 @@ bool VulkanRenderer::CreateImGUI() { initInfo.Device = device; initInfo.QueueFamily = QueueIDs.GraphicsIndex; initInfo.Queue = graphicsQueue; + initInfo.PipelineCache = m_pipelineCache; initInfo.DescriptorPool = imguiDescriptorPool; initInfo.MinImageCount = 2; initInfo.ImageCount = diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 01c3e353..bb0b5f05 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -3,13 +3,156 @@ #include #include #include +#include +#include +#include #include +#include #include #include "Core/Logger.hpp" namespace Sleak { namespace RenderEngine { +namespace { + +// Relative to the working directory, which is the executable's directory +constexpr const char* kPipelineCacheDir = "cache"; +constexpr const char* kPipelineCachePath = "cache/vulkan_pipelines.bin"; +constexpr const char* kPipelineCacheTempPath = "cache/vulkan_pipelines.bin.tmp"; +// VkPipelineCacheHeaderVersionOne: 4 x uint32 + UUID +constexpr size_t kPipelineCacheHeaderSize = 16 + VK_UUID_SIZE; + +/// FNV-1a hash, used to skip rewriting an unchanged pipeline cache. +uint64_t HashBytes(const std::vector& data) { + uint64_t hash = 14695981039346656037ull; + for (char c : data) { + hash ^= static_cast(c); + hash *= 1099511628211ull; + } + return hash; +} + +/// Reads one little-endian field of a pipeline cache header. +uint32_t ReadLE32(const char* bytes) { + const auto* b = reinterpret_cast(bytes); + return static_cast(b[0]) | static_cast(b[1]) << 8 | + static_cast(b[2]) << 16 | + static_cast(b[3]) << 24; +} + +} // namespace + +/// Creates the pipeline cache, seeded from disk when the saved blob was +/// written by this device and driver. +void VulkanRenderer::CreatePipelineCache() { + std::vector blob; + std::ifstream file(kPipelineCachePath, std::ios::binary | std::ios::ate); + if (file) { + const std::streamoff size = file.tellg(); + if (size > 0) { + blob.resize(static_cast(size)); + file.seekg(0); + if (!file.read(blob.data(), size)) blob.clear(); + } + } + file.close(); + + // Header check + if (!blob.empty()) { + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + const bool valid = + blob.size() >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) <= blob.size() && + ReadLE32(blob.data() + 4) == + static_cast(VK_PIPELINE_CACHE_HEADER_VERSION_ONE) && + ReadLE32(blob.data() + 8) == props.vendorID && + ReadLE32(blob.data() + 12) == props.deviceID && + std::memcmp(blob.data() + 16, props.pipelineCacheUUID, + VK_UUID_SIZE) == 0; + if (!valid) { + SLEAK_INFO( + "Pipeline cache: ignoring blob from another device or " + "driver"); + blob.clear(); + } + } + + VkPipelineCacheCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + info.initialDataSize = blob.size(); + info.pInitialData = blob.empty() ? nullptr : blob.data(); + VkResult result = + vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); + if (result != VK_SUCCESS && !blob.empty()) { + blob.clear(); + info.initialDataSize = 0; + info.pInitialData = nullptr; + result = + vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); + } + if (result != VK_SUCCESS) { + m_pipelineCache = VK_NULL_HANDLE; + SLEAK_WARN( + "Pipeline cache: creation failed, pipelines compile uncached"); + return; + } + + m_pipelineCacheLoadedSize = blob.size(); + m_pipelineCacheLoadedHash = blob.empty() ? 0 : HashBytes(blob); + SLEAK_INFO("Pipeline cache: {} ({} bytes)", + blob.empty() ? "empty" : "loaded", blob.size()); +} + +/// Saves the pipeline cache to disk when it changed, then destroys it. Writes +/// a temp file and renames it so a failed write never leaves a torn cache. +void VulkanRenderer::DestroyPipelineCache() { + if (m_pipelineCache == VK_NULL_HANDLE) return; + + std::vector data; + size_t size = 0; + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, nullptr) == + VK_SUCCESS && + size > 0) { + data.resize(size); + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, + data.data()) == VK_SUCCESS) + data.resize(size); + else + data.clear(); + } + vkDestroyPipelineCache(device, m_pipelineCache, nullptr); + m_pipelineCache = VK_NULL_HANDLE; + + if (data.empty()) return; + if (data.size() == m_pipelineCacheLoadedSize && + HashBytes(data) == m_pipelineCacheLoadedHash) + return; + + std::error_code ec; + std::filesystem::create_directories(kPipelineCacheDir, ec); + std::ofstream out(kPipelineCacheTempPath, + std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(data.size())); + out.close(); + if (!out) { + SLEAK_WARN("Pipeline cache: could not write {}", + kPipelineCacheTempPath); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + std::filesystem::rename(kPipelineCacheTempPath, kPipelineCachePath, ec); + if (ec) { + SLEAK_WARN("Pipeline cache: could not replace {}: {}", + kPipelineCachePath, ec.message()); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + SLEAK_INFO("Pipeline cache: saved {} bytes", data.size()); +} + /// Binds the skybox pipeline and its descriptor set for the current frame. void VulkanRenderer::BeginSkyboxPass() { if (!bFrameStarted) return; @@ -249,7 +392,7 @@ bool VulkanRenderer::CreateGraphicsPipeline() { pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineIndex = -1; - result = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, + result = vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipeline); if (result != VK_SUCCESS) SLEAK_ERROR("Failed to create graphics pipeline!!"); @@ -575,7 +718,7 @@ bool VulkanRenderer::CreateSkyboxPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &skyboxPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &skyboxPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline!"); return false; @@ -705,7 +848,7 @@ bool VulkanRenderer::CreateDebugLinePipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &debugLinePipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &debugLinePipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create debug line pipeline!"); return false; @@ -867,7 +1010,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipes.main); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipes.main); delete shader; if (result != VK_SUCCESS) { @@ -957,7 +1100,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipes.shadow); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipes.shadow); delete shadowShader; if (result != VK_SUCCESS) { @@ -1046,7 +1189,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { gbPipeInfo.subpass = 0; VkResult gbResult = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &gbPipeInfo, nullptr, &pipes.gbuffer); + device, m_pipelineCache, 1, &gbPipeInfo, nullptr, &pipes.gbuffer); delete gbufShader; if (gbResult != VK_SUCCESS) { @@ -1156,7 +1299,7 @@ bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { trPipeInfo.basePipelineIndex = -1; VkResult trResult = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &trPipeInfo, nullptr, &pipes.transparent); + device, m_pipelineCache, 1, &trPipeInfo, nullptr, &pipes.transparent); delete transparentShader; if (trResult != VK_SUCCESS) { @@ -1443,7 +1586,7 @@ bool VulkanRenderer::CreateSkinnedPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &skinnedPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &skinnedPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create skinned pipeline!"); return false; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 5cf631fd..30a73530 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -93,6 +93,8 @@ bool VulkanRenderer::Initialize() { if (!CreateDevice()) SLEAK_RETURN_ERR("Failed to initialize devices!"); + CreatePipelineCache(); + if (!CreateSwapChain()) SLEAK_RETURN_ERR("Failed to create swap chain!"); @@ -1107,6 +1109,8 @@ void VulkanRenderer::Cleanup() { imguiDescriptorPool = VK_NULL_HANDLE; } + DestroyPipelineCache(); + // Destroy descriptor pool (frees descriptor sets too) if (descriptorPool) { vkDestroyDescriptorPool(device, descriptorPool, nullptr); diff --git a/src/Graphics/Vulkan/VulkanSSAO.cpp b/src/Graphics/Vulkan/VulkanSSAO.cpp index 83599fb7..f1e1aeeb 100644 --- a/src/Graphics/Vulkan/VulkanSSAO.cpp +++ b/src/Graphics/Vulkan/VulkanSSAO.cpp @@ -519,7 +519,8 @@ bool VulkanRenderer::CreateSSAOPipelines() { gpi.renderPass = m_ssaoRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssaoPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSAO: failed to create ssao pipeline"); return false; } @@ -547,7 +548,8 @@ bool VulkanRenderer::CreateSSAOPipelines() { }; gpi.pStages = blurStages; gpi.layout = m_ssaoBlurPipelineLayout; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoBlurPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssaoBlurPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSAO: failed to create ssao blur pipeline"); return false; } diff --git a/src/Graphics/Vulkan/VulkanSSR.cpp b/src/Graphics/Vulkan/VulkanSSR.cpp index 8d146af2..1c4911a5 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -350,7 +350,8 @@ bool VulkanRenderer::CreateSSRResources() { gpi.renderPass = m_ssrRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssrPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssrPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSR: failed to create pipeline"); return false; } diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 0b64c5ed..76aa782b 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -369,7 +369,7 @@ bool VulkanRenderer::CreateShadowPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_shadowPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_shadowPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create shadow pipeline!"); return false; diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 9110d7a1..bb88deab 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -356,8 +356,8 @@ bool VulkanRenderer::CreateTAAResources() { gpi.layout = m_taaPipelineLayout; gpi.renderPass = m_taaRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, - &m_taaPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_taaPipeline) != VK_SUCCESS) { SLEAK_ERROR("TAA: failed to create pipeline"); return false; } From 6d3abef119036f8770bd1161b7df213c52aba26d Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 21:58:51 +0300 Subject: [PATCH 09/36] perf: build vulkan custom-format pipelines before their first draw Custom-format variants were compiled mid-frame on first use, including the forward variant the deferred path never binds, and every format switch re-ran the builder under the registry lock. Look variants up per handle, build the ones the active path needs when a format is registered or the GBuffer is rebuilt, and keep lazy creation as a fallback. Startup no longer compiles the forward and skinned GBuffer pipelines twice, and the shared pipeline layout is created once instead of leaking on every rebuild. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 56 +- src/Graphics/Vulkan/VulkanDeferred.cpp | 2 + src/Graphics/Vulkan/VulkanPipelines.cpp | 743 +++++++----------- src/Graphics/Vulkan/VulkanRenderer.cpp | 13 +- 4 files changed, 318 insertions(+), 496 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 21cf7991..5b6e0f48 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -299,7 +298,11 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool RecreateSwapChain(); /// Creates an image view for each swapchain image. bool CreateImageViews(); - /// Creates the main forward graphics pipeline and its pipeline layout. + /// Creates the layout shared by the forward, skybox, debug line, skinned, + /// and shadow pipelines. + bool CreateMainPipelineLayout(); + /// Creates the main forward graphics pipeline, creating the shared + /// layout on first use. bool CreateGraphicsPipeline(); /// Creates the pipeline cache, seeded from disk when the saved blob was /// written by this device and driver. @@ -471,30 +474,47 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Compiles the debug line shaders and creates the line-list pipeline. bool CreateDebugLinePipeline(); - // Custom vertex format pipelines (built lazily per registered VertexFormatHandle) - /// The four pipeline variants a registered vertex layout can drive. - /// A failed flag marks a variant as permanently absent so draws skip it - /// instead of retrying compilation every frame. + // Custom vertex format pipelines (one per registered VertexFormatHandle) + /// Render pass a custom-format pipeline variant is built for. + enum class CustomPass : uint8_t { + Main, + Shadow, + GBuffer, + Transparent, + Count + }; + static constexpr size_t CUSTOM_PASS_COUNT = + static_cast(CustomPass::Count); + /// The pipeline variants a registered vertex layout can drive, indexed by + /// CustomPass. A failed flag marks a variant as permanently absent so + /// draws skip it instead of retrying compilation every frame. struct CustomFormatPipelines { - VkPipeline main = VK_NULL_HANDLE; - VkPipeline shadow = VK_NULL_HANDLE; - VkPipeline gbuffer = VK_NULL_HANDLE; - VkPipeline transparent = VK_NULL_HANDLE; - bool mainFailed = false; - bool shadowFailed = false; - bool gbufferFailed = false; - bool transparentFailed = false; + std::array pipelines{}; + std::array failed{}; }; - std::unordered_map - m_customFormatPipelines; + // Indexed by handle - 1 (handles are dense and start at 1) + std::vector m_customFormatPipelines; + // Registered formats whose active-path variants were prebuilt + VertexFormatHandle m_customFormatsPrebuilt = 0; VertexFormatHandle m_activeCustomFormat = 0; /// Set while the active custom format has no pipeline for the current pass; /// draws are dropped rather than issued against a mismatched vertex layout. bool m_customFormatUnbound = false; /// True while the bound vertex buffer's format has no pipeline for this pass. bool CustomFormatDrawsSuppressed() const; - /// Creates any missing pipeline variant for a registered format; returns false when the main variant is unusable. - bool CreateCustomFormatPipelines(VertexFormatHandle format); + /// True when the render pass a custom-format variant targets exists. + bool CustomPassAvailable(CustomPass pass) const; + /// Returns a format's pipeline for a pass, compiling it on first request. + VkPipeline GetCustomFormatPipeline(VertexFormatHandle format, + CustomPass pass); + /// Compiles one pipeline variant of a registered layout; returns + /// VK_NULL_HANDLE when its stem is empty or compilation fails. + VkPipeline BuildCustomFormatPipeline(VertexFormatHandle format, + const VertexLayoutDesc& desc, + CustomPass pass); + /// Compiles the variants the active render path draws with for every + /// registered format not prebuilt yet. + void PrebuildCustomFormatPipelines(); /// Destroys every cached custom-format pipeline (all variants, all formats). void DestroyCustomFormatPipelines(); diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index fe474214..d3def6f0 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -249,6 +249,8 @@ bool VulkanRenderer::CreateGBufferPipeline() { /// Compiles the skinned GBuffer shaders so skinned meshes write into the GBuffer. bool VulkanRenderer::CreateSkinnedGbufferPipeline() { + if (m_skinnedGbufferPipeline != VK_NULL_HANDLE) return true; + // Load skinned vert + gbuffer frag (SPIR-V already on disk) VulkanShader* sh = new VulkanShader(device); if (!sh->compile("assets/shaders/skinned_shader.vert.spv", diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index bb0b5f05..6bddbdf5 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "Core/Logger.hpp" @@ -197,9 +198,41 @@ void VulkanRenderer::EndSkyboxPass() { } } -/// Creates the main forward graphics pipeline and its pipeline layout. +/// Creates the layout shared by the forward, skybox, debug line, skinned, and +/// shadow pipelines. +bool VulkanRenderer::CreateMainPipelineLayout() { + if (pipelineLay != VK_NULL_HANDLE) return true; + + // Push constants + VkPushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushConstantRange.offset = 0; + pushConstantRange.size = 128; // sizeof(mat4) * 2 = 128 bytes (WVP + World) + + // Descriptor set layouts + std::array setLayouts = { + descriptorSetLayout, boneDescriptorSetLayout, + m_lightUBODescriptorSetLayout, m_shadowSamplerDescriptorSetLayout}; + + VkPipelineLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + layoutInfo.setLayoutCount = static_cast(setLayouts.size()); + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &pushConstantRange; + + if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLay) != + VK_SUCCESS) + SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); + + return true; +} + +/// Creates the main forward graphics pipeline, creating the shared layout +/// on first use. bool VulkanRenderer::CreateGraphicsPipeline() { - VkResult result; + if (pipeline != VK_NULL_HANDLE) return true; + if (!CreateMainPipelineLayout()) return false; simpleShader = new VulkanShader(device); bool isShader = @@ -345,33 +378,6 @@ bool VulkanRenderer::CreateGraphicsPipeline() { colorBlendInfo.attachmentCount = 1; colorBlendInfo.pAttachments = &colorBlendAttachment; - // Pipeline layout — push constants for WVP matrix + descriptor set - // for texture sampler - VkPushConstantRange pushConstantRange{}; - pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - pushConstantRange.offset = 0; - pushConstantRange.size = 128; // sizeof(mat4) * 2 = 128 bytes (WVP + World) - - // Four descriptor set layouts: - // set 0 = texture sampler, set 1 = bone UBO, - // set 2 = light/shadow UBO, set 3 = shadow map sampler - std::array setLayouts = { - descriptorSetLayout, boneDescriptorSetLayout, - m_lightUBODescriptorSetLayout, m_shadowSamplerDescriptorSetLayout - }; - - VkPipelineLayoutCreateInfo layoutInfo{}; - layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - layoutInfo.setLayoutCount = static_cast(setLayouts.size()); - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = 1; - layoutInfo.pPushConstantRanges = &pushConstantRange; - - result = vkCreatePipelineLayout(device, &layoutInfo, nullptr, - &pipelineLay); - if (result != VK_SUCCESS) - SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); - // Create pipeline VkGraphicsPipelineCreateInfo pipelineInfo{}; pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; @@ -392,8 +398,8 @@ bool VulkanRenderer::CreateGraphicsPipeline() { pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineIndex = -1; - result = vkCreateGraphicsPipelines(device, m_pipelineCache, 1, - &pipelineInfo, nullptr, &pipeline); + VkResult result = vkCreateGraphicsPipelines( + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipeline); if (result != VK_SUCCESS) SLEAK_ERROR("Failed to create graphics pipeline!!"); @@ -877,443 +883,244 @@ static VkFormat ToVkVertexFormat(VertexAttribFormat format) { return VK_FORMAT_R32G32B32_SFLOAT; } -/// Creates any missing pipeline variant (main, shadow, GBuffer, transparent) -/// for a registered vertex layout. Variants whose shader fails to load are -/// marked absent so draws skip them without retrying every frame. -bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { +/// True when the render pass a custom-format variant targets exists. +bool VulkanRenderer::CustomPassAvailable(CustomPass pass) const { + switch (pass) { + case CustomPass::Shadow: + return m_shadowRenderPass != VK_NULL_HANDLE && + m_shadowShader != nullptr; + case CustomPass::GBuffer: + return m_gbufferResourcesCreated && + m_gbufferRenderPass != VK_NULL_HANDLE; + default: + return true; + } +} + +/// Returns a format's pipeline for a pass, compiling it on first request. +/// A variant that failed once stays absent instead of recompiling per frame. +VkPipeline VulkanRenderer::GetCustomFormatPipeline(VertexFormatHandle format, + CustomPass pass) { + if (format == 0) return VK_NULL_HANDLE; + if (format > m_customFormatPipelines.size()) { + if (!VertexFormatRegistry::Get(format)) return VK_NULL_HANDLE; + m_customFormatPipelines.resize(format); + } + CustomFormatPipelines& pipes = m_customFormatPipelines[format - 1]; + const size_t slot = static_cast(pass); + if (pipes.pipelines[slot] != VK_NULL_HANDLE || pipes.failed[slot]) + return pipes.pipelines[slot]; + + // Lazy fallback + if (!CustomPassAvailable(pass)) return VK_NULL_HANDLE; const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); - if (!desc || desc->stride == 0 || desc->attributes.empty()) return false; + if (!desc) return VK_NULL_HANDLE; + pipes.pipelines[slot] = BuildCustomFormatPipeline(format, *desc, pass); + pipes.failed[slot] = pipes.pipelines[slot] == VK_NULL_HANDLE; + return pipes.pipelines[slot]; +} - CustomFormatPipelines& pipes = m_customFormatPipelines[format]; +/// Compiles one pipeline variant of a registered layout; returns +/// VK_NULL_HANDLE when its stem is empty or compilation fails. +VkPipeline VulkanRenderer::BuildCustomFormatPipeline( + VertexFormatHandle format, const VertexLayoutDesc& desc, CustomPass pass) { + static constexpr const char* kPassNames[CUSTOM_PASS_COUNT] = { + "main", "shadow", "GBuffer", "transparent"}; + const char* passName = kPassNames[static_cast(pass)]; + + if (desc.stride == 0 || desc.attributes.empty()) { + SLEAK_ERROR("VulkanRenderer: Vertex format {} has an empty layout", + format); + return VK_NULL_HANDLE; + } - // Vertex input state shared by all four variants - VkVertexInputBindingDescription bindingDescription{}; - bindingDescription.binding = 0; - bindingDescription.stride = desc->stride; - bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + const std::string* stem = &desc.shaderStem; + if (pass == CustomPass::Shadow) stem = &desc.shadowShaderStem; + if (pass == CustomPass::GBuffer) stem = &desc.gbufferShaderStem; + if (pass == CustomPass::Transparent) stem = &desc.transparentShaderStem; + if (stem->empty()) { + if (pass == CustomPass::Main) + SLEAK_ERROR( + "VulkanRenderer: Vertex format {} has no main shader stem", + format); + return VK_NULL_HANDLE; + } - std::vector attributeDescs; - attributeDescs.reserve(desc->attributes.size()); - for (const auto& attr : desc->attributes) { - attributeDescs.push_back({attr.location, 0, - ToVkVertexFormat(attr.format), attr.offset}); + // Shader stages + const std::string base = "assets/shaders/" + *stem; + std::string shaderPath = base; + VulkanShader shader(device); + bool compiled = false; + if (pass == CustomPass::Shadow) { + shaderPath = base + ".vert.spv"; + compiled = shader.compileVertexOnly(shaderPath); + } else if (pass == CustomPass::GBuffer) { + shaderPath = base + ".vert.spv"; + compiled = + shader.compile(shaderPath, "assets/shaders/gbuffer.frag.spv"); + } else { + compiled = shader.compile(base); + } + if (!compiled) { + SLEAK_ERROR( + "VulkanRenderer: Failed to compile '{}' for vertex format {}", + shaderPath, format); + return VK_NULL_HANDLE; + } + VkPipelineShaderStageCreateInfo stages[] = {shader.GetVertexInfo(), + shader.GetFragInfo()}; + + // Vertex input + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = desc.stride; + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attributes; + attributes.reserve(desc.attributes.size()); + for (const auto& attr : desc.attributes) { + attributes.push_back( + {attr.location, 0, ToVkVertexFormat(attr.format), attr.offset}); } - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; - vertexInputInfo.sType = + VkPipelineVertexInputStateCreateInfo vertexInput{}; + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; - vertexInputInfo.vertexBindingDescriptionCount = 1; - vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; - vertexInputInfo.vertexAttributeDescriptionCount = - static_cast(attributeDescs.size()); - vertexInputInfo.pVertexAttributeDescriptions = attributeDescs.data(); + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = + static_cast(attributes.size()); + vertexInput.pVertexAttributeDescriptions = attributes.data(); + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = + VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + inputAssembly.primitiveRestartEnable = VK_FALSE; - std::vector dynamicStates = { - VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + const VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; - dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); - dynamicState.pDynamicStates = dynamicStates.data(); + dynamicState.dynamicStateCount = 2; + dynamicState.pDynamicStates = dynamicStates; - // Forward/main variant — opaque geometry in the forward pass - if (pipes.main == VK_NULL_HANDLE && !pipes.mainFailed) { - if (desc->shaderStem.empty()) { - SLEAK_ERROR("VulkanRenderer: Vertex format {} has no main shader stem", format); - pipes.mainFailed = true; - } else { - const std::string stemPath = "assets/shaders/" + desc->shaderStem; - auto* shader = new VulkanShader(device); - if (!shader->compile(stemPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - stemPath, format); - pipes.mainFailed = true; - delete shader; - } else { - VkPipelineShaderStageCreateInfo shaderStages[] = { - shader->GetVertexInfo(), shader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo{}; - inputAssemblyInfo.sType = - VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - inputAssemblyInfo.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo viewportInfo{}; - viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - viewportInfo.viewportCount = 1; - viewportInfo.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.sType = - VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rasterizer.depthClampEnable = VK_FALSE; - rasterizer.rasterizerDiscardEnable = VK_FALSE; - rasterizer.polygonMode = VK_POLYGON_MODE_FILL; - rasterizer.lineWidth = 1.0f; - rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; - rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_FALSE; - - VkPipelineMultisampleStateCreateInfo msaa{}; - msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - msaa.sampleShadingEnable = VK_FALSE; - msaa.rasterizationSamples = m_msaaSamples; - - VkPipelineDepthStencilStateCreateInfo depthStencil{}; - depthStencil.sType = - VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - depthStencil.depthTestEnable = VK_TRUE; - depthStencil.depthWriteEnable = VK_TRUE; - depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; - depthStencil.depthBoundsTestEnable = VK_FALSE; - depthStencil.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendAttachmentState colorBlendAttachment{}; - colorBlendAttachment.blendEnable = VK_TRUE; - colorBlendAttachment.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - colorBlendAttachment.dstColorBlendFactor = - VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; - colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; - colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; - - VkPipelineColorBlendStateCreateInfo colorBlendInfo{}; - colorBlendInfo.sType = - VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - colorBlendInfo.logicOpEnable = VK_FALSE; - colorBlendInfo.attachmentCount = 1; - colorBlendInfo.pAttachments = &colorBlendAttachment; - - VkGraphicsPipelineCreateInfo pipelineInfo{}; - pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - pipelineInfo.stageCount = 2; - pipelineInfo.pStages = shaderStages; - pipelineInfo.pVertexInputState = &vertexInputInfo; - pipelineInfo.pInputAssemblyState = &inputAssemblyInfo; - pipelineInfo.pViewportState = &viewportInfo; - pipelineInfo.pRasterizationState = &rasterizer; - pipelineInfo.pMultisampleState = &msaa; - pipelineInfo.pDepthStencilState = &depthStencil; - pipelineInfo.pColorBlendState = &colorBlendInfo; - pipelineInfo.pDynamicState = &dynamicState; - pipelineInfo.layout = pipelineLay; // reuse main layout (same descriptor sets) - pipelineInfo.subpass = 0; - pipelineInfo.renderPass = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? m_forwardRenderPass : renderPass; - pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; - pipelineInfo.basePipelineIndex = -1; - - VkResult result = vkCreateGraphicsPipelines( - device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipes.main); - delete shader; - - if (result != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create main pipeline for vertex format {}", - format); - pipes.main = VK_NULL_HANDLE; - pipes.mainFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} main pipeline created", format); - } - } - } + // Rasterization + VkPipelineRasterizationStateCreateInfo rasterizer{}; + rasterizer.sType = + VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = pass == CustomPass::Transparent + ? VK_CULL_MODE_NONE + : VK_CULL_MODE_BACK_BIT; + rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + if (pass == CustomPass::Shadow) { + rasterizer.depthBiasEnable = VK_TRUE; + rasterizer.depthBiasConstantFactor = 1.25f; + rasterizer.depthBiasSlopeFactor = 1.75f; } - // Shadow variant — depth-only, rendered into the shadow map - if (pipes.shadow == VK_NULL_HANDLE && !pipes.shadowFailed && - !desc->shadowShaderStem.empty() && - m_shadowRenderPass != VK_NULL_HANDLE && m_shadowShader) { - const std::string shadowPath = - "assets/shaders/" + desc->shadowShaderStem + ".vert.spv"; - auto* shadowShader = new VulkanShader(device); - if (!shadowShader->compileVertexOnly(shadowPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - shadowPath, format); - pipes.shadowFailed = true; - delete shadowShader; - } else { - VkPipelineShaderStageCreateInfo shaderStage = shadowShader->GetVertexInfo(); - - VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; - inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - inputAssembly.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo viewportState{}; - viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - viewportState.viewportCount = 1; - viewportState.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rasterizer.depthClampEnable = VK_FALSE; - rasterizer.rasterizerDiscardEnable = VK_FALSE; - rasterizer.polygonMode = VK_POLYGON_MODE_FILL; - rasterizer.lineWidth = 1.0f; - rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; - rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; - rasterizer.depthBiasClamp = 0.0f; - - VkPipelineMultisampleStateCreateInfo msaa{}; - msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - msaa.sampleShadingEnable = VK_FALSE; - msaa.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - - VkPipelineDepthStencilStateCreateInfo depthStencil{}; - depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - depthStencil.depthTestEnable = VK_TRUE; - depthStencil.depthWriteEnable = VK_TRUE; - depthStencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; - depthStencil.depthBoundsTestEnable = VK_FALSE; - depthStencil.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendStateCreateInfo colorBlendInfo{}; - colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - colorBlendInfo.logicOpEnable = VK_FALSE; - colorBlendInfo.attachmentCount = 0; - - VkGraphicsPipelineCreateInfo pipelineInfo{}; - pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - pipelineInfo.stageCount = 1; - pipelineInfo.pStages = &shaderStage; - pipelineInfo.pVertexInputState = &vertexInputInfo; - pipelineInfo.pInputAssemblyState = &inputAssembly; - pipelineInfo.pViewportState = &viewportState; - pipelineInfo.pRasterizationState = &rasterizer; - pipelineInfo.pMultisampleState = &msaa; - pipelineInfo.pDepthStencilState = &depthStencil; - pipelineInfo.pColorBlendState = &colorBlendInfo; - pipelineInfo.pDynamicState = &dynamicState; - pipelineInfo.layout = pipelineLay; - pipelineInfo.renderPass = m_shadowRenderPass; - pipelineInfo.subpass = 0; - pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; - pipelineInfo.basePipelineIndex = -1; - - VkResult result = vkCreateGraphicsPipelines( - device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipes.shadow); - delete shadowShader; - - if (result != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create shadow pipeline for vertex format {}", - format); - pipes.shadow = VK_NULL_HANDLE; - pipes.shadowFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} shadow pipeline created", format); - } - } - } + // Multisampling + const bool forwardTarget = + m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE; + VkPipelineMultisampleStateCreateInfo multisample{}; + multisample.sType = + VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + if (pass == CustomPass::Main || + (pass == CustomPass::Transparent && !forwardTarget)) + multisample.rasterizationSamples = m_msaaSamples; - // GBuffer variant — deferred geometry pass, writes the GBuffer attachments - if (pipes.gbuffer == VK_NULL_HANDLE && !pipes.gbufferFailed && - !desc->gbufferShaderStem.empty() && m_gbufferResourcesCreated && - m_gbufferRenderPass != VK_NULL_HANDLE) { - const std::string gbufVertPath = - "assets/shaders/" + desc->gbufferShaderStem + ".vert.spv"; - auto* gbufShader = new VulkanShader(device); - if (!gbufShader->compile(gbufVertPath, "assets/shaders/gbuffer.frag.spv")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - gbufVertPath, format); - pipes.gbufferFailed = true; - delete gbufShader; - } else { - VkPipelineShaderStageCreateInfo gbufStages[] = { - gbufShader->GetVertexInfo(), gbufShader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo ia{}; - ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - ia.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo vp{}; - vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - vp.viewportCount = 1; - vp.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rs{}; - rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rs.polygonMode = VK_POLYGON_MODE_FILL; - rs.lineWidth = 1.0f; - rs.cullMode = VK_CULL_MODE_BACK_BIT; - rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - - VkPipelineMultisampleStateCreateInfo ms{}; - ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - - VkPipelineDepthStencilStateCreateInfo ds{}; - ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - ds.depthTestEnable = VK_TRUE; - ds.depthWriteEnable = VK_TRUE; - ds.depthCompareOp = VK_COMPARE_OP_LESS; - - VkPipelineColorBlendAttachmentState opaqueBlend{}; - opaqueBlend.blendEnable = VK_FALSE; - opaqueBlend.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - std::array gbAtts; - gbAtts.fill(opaqueBlend); - - VkPipelineColorBlendStateCreateInfo cb{}; - cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - cb.attachmentCount = static_cast(gbAtts.size()); - cb.pAttachments = gbAtts.data(); - - VkGraphicsPipelineCreateInfo gbPipeInfo{}; - gbPipeInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - gbPipeInfo.stageCount = 2; - gbPipeInfo.pStages = gbufStages; - gbPipeInfo.pVertexInputState = &vertexInputInfo; - gbPipeInfo.pInputAssemblyState = &ia; - gbPipeInfo.pViewportState = &vp; - gbPipeInfo.pRasterizationState = &rs; - gbPipeInfo.pMultisampleState = &ms; - gbPipeInfo.pDepthStencilState = &ds; - gbPipeInfo.pColorBlendState = &cb; - gbPipeInfo.pDynamicState = &dynamicState; - // gbuffer.frag reads set 0 as m_pbrMaterialDSL, so the GBuffer - // variant must use m_gbufferGeomLayout, never pipelineLay. - gbPipeInfo.layout = m_gbufferGeomLayout; - gbPipeInfo.renderPass = m_gbufferRenderPass; - gbPipeInfo.subpass = 0; - - VkResult gbResult = vkCreateGraphicsPipelines( - device, m_pipelineCache, 1, &gbPipeInfo, nullptr, &pipes.gbuffer); - delete gbufShader; - - if (gbResult != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create GBuffer pipeline for vertex format {}", - format); - pipes.gbuffer = VK_NULL_HANDLE; - pipes.gbufferFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} GBuffer pipeline created", format); - } - } + VkPipelineDepthStencilStateCreateInfo depthStencil{}; + depthStencil.sType = + VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depthStencil.depthTestEnable = VK_TRUE; + depthStencil.depthWriteEnable = VK_TRUE; + depthStencil.depthCompareOp = pass == CustomPass::Shadow + ? VK_COMPARE_OP_LESS_OR_EQUAL + : VK_COMPARE_OP_LESS; + + // Color blending + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + if (pass == CustomPass::Main || pass == CustomPass::Transparent) { + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorBlendOp = VK_BLEND_OP_ADD; + blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = pass == CustomPass::Transparent + ? VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA + : VK_BLEND_FACTOR_ZERO; + blend.alphaBlendOp = VK_BLEND_OP_ADD; } + std::array blends; + blends.fill(blend); + + VkPipelineColorBlendStateCreateInfo colorBlend{}; + colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + colorBlend.attachmentCount = 1; + if (pass == CustomPass::Shadow) colorBlend.attachmentCount = 0; + if (pass == CustomPass::GBuffer) colorBlend.attachmentCount = GBUFFER_COUNT; + colorBlend.pAttachments = + colorBlend.attachmentCount > 0 ? blends.data() : nullptr; + + // Pipeline + VkGraphicsPipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipelineInfo.stageCount = pass == CustomPass::Shadow ? 1 : 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pDepthStencilState = &depthStencil; + pipelineInfo.pColorBlendState = &colorBlend; + pipelineInfo.pDynamicState = &dynamicState; + pipelineInfo.layout = + pass == CustomPass::GBuffer ? m_gbufferGeomLayout : pipelineLay; + pipelineInfo.renderPass = forwardTarget ? m_forwardRenderPass : renderPass; + if (pass == CustomPass::Shadow) + pipelineInfo.renderPass = m_shadowRenderPass; + if (pass == CustomPass::GBuffer) + pipelineInfo.renderPass = m_gbufferRenderPass; + pipelineInfo.subpass = 0; + pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; + pipelineInfo.basePipelineIndex = -1; - // Transparent variant — alpha-blended, two-sided, runs in the forward - // transparent pass over the deferred scene image. - if (pipes.transparent == VK_NULL_HANDLE && !pipes.transparentFailed && - !desc->transparentShaderStem.empty()) { - const std::string transparentPath = - "assets/shaders/" + desc->transparentShaderStem; - auto* transparentShader = new VulkanShader(device); - if (!transparentShader->compile(transparentPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - transparentPath, format); - pipes.transparentFailed = true; - delete transparentShader; - } else { - VkPipelineShaderStageCreateInfo trStages[] = { - transparentShader->GetVertexInfo(), - transparentShader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo ia{}; - ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - ia.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo vp{}; - vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - vp.viewportCount = 1; - vp.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rs{}; - rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rs.depthClampEnable = VK_FALSE; - rs.rasterizerDiscardEnable = VK_FALSE; - rs.polygonMode = VK_POLYGON_MODE_FILL; - rs.lineWidth = 1.0f; - // Transparent surfaces are viewed from both sides - rs.cullMode = VK_CULL_MODE_NONE; - rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rs.depthBiasEnable = VK_FALSE; - - VkPipelineMultisampleStateCreateInfo ms{}; - ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - ms.sampleShadingEnable = VK_FALSE; - // The forward transparent pass runs on m_forwardRenderPass (1 sample) - // when deferred is enabled; use the main render pass samples otherwise. - ms.rasterizationSamples = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? VK_SAMPLE_COUNT_1_BIT - : m_msaaSamples; - - VkPipelineDepthStencilStateCreateInfo ds{}; - ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - ds.depthTestEnable = VK_TRUE; - ds.depthWriteEnable = VK_TRUE; - ds.depthCompareOp = VK_COMPARE_OP_LESS; - ds.depthBoundsTestEnable = VK_FALSE; - ds.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendAttachmentState blendAtt{}; - blendAtt.blendEnable = VK_TRUE; - blendAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - blendAtt.colorBlendOp = VK_BLEND_OP_ADD; - blendAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; - blendAtt.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - - VkPipelineColorBlendStateCreateInfo cb{}; - cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - cb.logicOpEnable = VK_FALSE; - cb.attachmentCount = 1; - cb.pAttachments = &blendAtt; - - VkGraphicsPipelineCreateInfo trPipeInfo{}; - trPipeInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - trPipeInfo.stageCount = 2; - trPipeInfo.pStages = trStages; - trPipeInfo.pVertexInputState = &vertexInputInfo; - trPipeInfo.pInputAssemblyState = &ia; - trPipeInfo.pViewportState = &vp; - trPipeInfo.pRasterizationState = &rs; - trPipeInfo.pMultisampleState = &ms; - trPipeInfo.pDepthStencilState = &ds; - trPipeInfo.pColorBlendState = &cb; - trPipeInfo.pDynamicState = &dynamicState; - trPipeInfo.layout = pipelineLay; // reuse main layout (same descriptor sets) - trPipeInfo.subpass = 0; - trPipeInfo.renderPass = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? m_forwardRenderPass - : renderPass; - trPipeInfo.basePipelineHandle = VK_NULL_HANDLE; - trPipeInfo.basePipelineIndex = -1; - - VkResult trResult = vkCreateGraphicsPipelines( - device, m_pipelineCache, 1, &trPipeInfo, nullptr, &pipes.transparent); - delete transparentShader; - - if (trResult != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create transparent pipeline for vertex format {}", - format); - pipes.transparent = VK_NULL_HANDLE; - pipes.transparentFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} transparent pipeline created", format); - } - } + VkPipeline created = VK_NULL_HANDLE; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, + nullptr, &created) != VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to create {} pipeline for vertex format {}", + passName, format); + return VK_NULL_HANDLE; } + SLEAK_INFO("VulkanRenderer: Custom format {} {} pipeline created", format, + passName); + return created; +} - return pipes.main != VK_NULL_HANDLE; +/// Compiles the variants the active render path draws with for every +/// registered format not prebuilt yet; the forward main variant is skipped +/// while deferred is active and only built if a draw asks for it. +void VulkanRenderer::PrebuildCustomFormatPipelines() { + while (VertexFormatRegistry::Get(m_customFormatsPrebuilt + 1)) { + const VertexFormatHandle format = ++m_customFormatsPrebuilt; + GetCustomFormatPipeline(format, CustomPass::Shadow); + GetCustomFormatPipeline(format, IsDeferredEnabled() + ? CustomPass::GBuffer + : CustomPass::Main); + GetCustomFormatPipeline(format, CustomPass::Transparent); + } } @@ -1323,31 +1130,21 @@ void VulkanRenderer::BeginCustomFormatPass(VertexFormatHandle format) { if (format == 0) return; if (m_activeCustomFormat == format) return; m_activeCustomFormat = format; - m_customFormatUnbound = false; - if (!CreateCustomFormatPipelines(format)) { - m_customFormatUnbound = true; - return; - } - const CustomFormatPipelines& pipes = m_customFormatPipelines[format]; - - VkPipeline target; + CustomPass pass = CustomPass::Main; if (m_shadowPassActive) { - target = pipes.shadow; + pass = CustomPass::Shadow; } else if (m_inGeometryPass) { - target = pipes.gbuffer; + pass = CustomPass::GBuffer; } else if (m_inForwardTransparentPass) { - target = pipes.transparent; - } else { - target = pipes.main; + pass = CustomPass::Transparent; } // No variant for this pass: drop the draws rather than rasterize this // layout's vertices through a pipeline built for another one. - if (target == VK_NULL_HANDLE) { - m_customFormatUnbound = true; - return; - } + const VkPipeline target = GetCustomFormatPipeline(format, pass); + m_customFormatUnbound = target == VK_NULL_HANDLE; + if (m_customFormatUnbound) return; vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, target); } @@ -1383,17 +1180,15 @@ void VulkanRenderer::EndCustomFormatPass() { } -/// Destroys every cached custom-format pipeline and clears the map. +/// Destroys every cached custom-format pipeline and clears the table. void VulkanRenderer::DestroyCustomFormatPipelines() { - for (auto& entry : m_customFormatPipelines) { - CustomFormatPipelines& pipes = entry.second; - if (pipes.main) vkDestroyPipeline(device, pipes.main, nullptr); - if (pipes.shadow) vkDestroyPipeline(device, pipes.shadow, nullptr); - if (pipes.gbuffer) vkDestroyPipeline(device, pipes.gbuffer, nullptr); - if (pipes.transparent) - vkDestroyPipeline(device, pipes.transparent, nullptr); + for (const CustomFormatPipelines& pipes : m_customFormatPipelines) { + for (VkPipeline pipe : pipes.pipelines) { + if (pipe) vkDestroyPipeline(device, pipe, nullptr); + } } m_customFormatPipelines.clear(); + m_customFormatsPrebuilt = 0; m_activeCustomFormat = 0; } diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 30a73530..35505be3 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -128,8 +128,8 @@ bool VulkanRenderer::Initialize() { if (!CreateDefaultTexture()) SLEAK_WARN("Failed to create default white texture for Vulkan"); - if (!CreateGraphicsPipeline()) - SLEAK_RETURN_ERR("Failed to create graphics pipeline!"); + if (!CreateMainPipelineLayout()) + SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); if (!CreateShadowLightUBOResources()) SLEAK_WARN("Failed to create light UBO resources — dynamic lighting disabled"); @@ -146,6 +146,10 @@ bool VulkanRenderer::Initialize() { SLEAK_WARN("Failed to create GBuffer resources — deferred rendering disabled"); } + // Forward path + if (pipeline == VK_NULL_HANDLE && !CreateGraphicsPipeline()) + SLEAK_RETURN_ERR("Failed to create graphics pipeline!"); + // Eagerly create bone UBO resources so set 1 is always bound at pass start. // Must happen after CreateDescriptorSetLayout() (boneDescriptorSetLayout is ready) // and after GBuffer init (m_deferredEnabled is known). @@ -189,6 +193,7 @@ void VulkanRenderer::BeginRender() { ApplyMSAAChange(); if (m_shadowResChangeRequested) ApplyShadowResolutionChange(); + PrebuildCustomFormatPipelines(); VkResult result; @@ -1845,7 +1850,6 @@ bool VulkanRenderer::CreateGBufferResources() { if (skyboxPipeline) { vkDestroyPipeline(device, skyboxPipeline, nullptr); skyboxPipeline = VK_NULL_HANDLE; } if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; } if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; } - if (m_skinnedGbufferPipeline) { vkDestroyPipeline(device, m_skinnedGbufferPipeline, nullptr); m_skinnedGbufferPipeline = VK_NULL_HANDLE; } DestroyCustomFormatPipelines(); // Destroy old skybox descriptor pool (CreateSkyboxPipeline allocates new ones) if (skyboxDescriptorPool) { @@ -1860,7 +1864,6 @@ bool VulkanRenderer::CreateGBufferResources() { CreateSkyboxPipeline(); CreateDebugLinePipeline(); CreateSkinnedPipeline(); - CreateSkinnedGbufferPipeline(); m_gbufferResourcesCreated = true; @@ -1885,6 +1888,8 @@ bool VulkanRenderer::CreateGBufferResources() { } m_skyboxDescriptorsWritten = true; } + + PrebuildCustomFormatPipelines(); } SLEAK_INFO("VulkanRenderer: Deferred GBuffer resources created ({}x{})", From 0b3c401d5d9eeb16f97a3b43ac0a81ab4d13aba1 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 21:59:04 +0300 Subject: [PATCH 10/36] perf: enable only the vulkan device features the engine uses The device enabled every supported feature, robustBufferAccess included, which bounds-checks buffer and vertex fetches. Enable sampler anisotropy and full 32-bit indices, and robustBufferAccess only when --validate is passed. --- src/Graphics/Vulkan/VulkanDevice.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanDevice.cpp b/src/Graphics/Vulkan/VulkanDevice.cpp index a028fb85..ef31490b 100644 --- a/src/Graphics/Vulkan/VulkanDevice.cpp +++ b/src/Graphics/Vulkan/VulkanDevice.cpp @@ -298,9 +298,14 @@ bool VulkanRenderer::CreateDevice() { // Build unique queue create infos (no duplicates!) auto queueCreateInfos = GetUniqueQueueCreateInfos(); - // Get supported features + // Enable only used features + VkPhysicalDeviceFeatures supported{}; + vkGetPhysicalDeviceFeatures(physicalDevice, &supported); VkPhysicalDeviceFeatures features{}; - vkGetPhysicalDeviceFeatures(physicalDevice, &features); + features.samplerAnisotropy = supported.samplerAnisotropy; + features.fullDrawIndexUint32 = supported.fullDrawIndexUint32; + if (CommandLine::HasFlag("--validate")) + features.robustBufferAccess = supported.robustBufferAccess; // Query max MSAA sample count VkPhysicalDeviceProperties deviceProperties; From 4c90687be1b9eab936323c62c9ed7769ce7c2072 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 21:59:21 +0300 Subject: [PATCH 11/36] fix: declare only the vertex attributes vulkan forward shaders read The forward, skybox and debug line pipelines described the bone ID and weight attributes their shaders never read, which raised eight validation warnings at startup. Declare locations 0 to 4 only. --- src/Graphics/Vulkan/VulkanPipelines.cpp | 34 ++++--------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 6bddbdf5..7fb1b78a 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -255,13 +255,13 @@ bool VulkanRenderer::CreateGraphicsPipeline() { static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); - // Vertex input — matches Sleak::Vertex (64 bytes) + // Vertex input (locations 0-4) VkVertexInputBindingDescription bindingDescription{}; bindingDescription.binding = 0; bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; // Position: float3 at offset 0 attributeDescs[0].binding = 0; @@ -293,18 +293,6 @@ bool VulkanRenderer::CreateGraphicsPipeline() { attributeDescs[4].format = VK_FORMAT_R32G32_SFLOAT; attributeDescs[4].offset = offsetof(Vertex, u); - // BoneIDs: int4 - attributeDescs[5].binding = 0; - attributeDescs[5].location = 5; - attributeDescs[5].format = VK_FORMAT_R32G32B32A32_SINT; - attributeDescs[5].offset = offsetof(Vertex, boneIDs); - - // BoneWeights: float4 - attributeDescs[6].binding = 0; - attributeDescs[6].location = 6; - attributeDescs[6].format = VK_FORMAT_R32G32B32A32_SFLOAT; - attributeDescs[6].offset = offsetof(Vertex, boneWeights); - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; @@ -601,13 +589,13 @@ bool VulkanRenderer::CreateSkyboxPipeline() { static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); - // Same vertex layout as main pipeline + // Same vertex layout as main pipeline (locations 0-4) VkVertexInputBindingDescription bindingDescription{}; bindingDescription.binding = 0; bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; attributeDescs[0].binding = 0; attributeDescs[0].location = 0; attributeDescs[0].format = VK_FORMAT_R32G32B32_SFLOAT; @@ -633,16 +621,6 @@ bool VulkanRenderer::CreateSkyboxPipeline() { attributeDescs[4].format = VK_FORMAT_R32G32_SFLOAT; attributeDescs[4].offset = offsetof(Vertex, u); - attributeDescs[5].binding = 0; - attributeDescs[5].location = 5; - attributeDescs[5].format = VK_FORMAT_R32G32B32A32_SINT; - attributeDescs[5].offset = offsetof(Vertex, boneIDs); - - attributeDescs[6].binding = 0; - attributeDescs[6].location = 6; - attributeDescs[6].format = VK_FORMAT_R32G32B32A32_SFLOAT; - attributeDescs[6].offset = offsetof(Vertex, boneWeights); - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; @@ -765,14 +743,12 @@ bool VulkanRenderer::CreateDebugLinePipeline() { bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; attributeDescs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, px)}; attributeDescs[1] = {1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, nx)}; attributeDescs[2] = {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, tx)}; attributeDescs[3] = {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, r)}; attributeDescs[4] = {4, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, u)}; - attributeDescs[5] = {5, 0, VK_FORMAT_R32G32B32A32_SINT, offsetof(Vertex, boneIDs)}; - attributeDescs[6] = {6, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, boneWeights)}; VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = From 6926cacc1cc6d1553ebfe29a663d23b91a088a88 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 22:00:04 +0300 Subject: [PATCH 12/36] fix: stop leaking vulkan shaders, pools and texture descriptor sets Rebuilding swapchain resources recompiled the forward, skinned, debug line and skybox shaders without freeing the old modules, and an MSAA change also leaked the skybox and ImGui descriptor pools. Reuse them instead. Texture descriptor sets were never freed, so texture churn exhausted their pool; a destroyed texture now hands its sets and GPU objects to a queue that frees them once the last submission that could use them has finished. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 13 +- .../private/Graphics/Vulkan/VulkanTexture.hpp | 17 ++- src/Graphics/Vulkan/VulkanDescriptors.cpp | 3 +- src/Graphics/Vulkan/VulkanPipelines.cpp | 131 ++++++++++-------- src/Graphics/Vulkan/VulkanRenderer.cpp | 28 ++-- src/Graphics/Vulkan/VulkanTexture.cpp | 116 +++++++++++++--- 6 files changed, 217 insertions(+), 91 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 5b6e0f48..29312096 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -230,9 +230,11 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { } private: - /// Compiles the skybox shaders and creates the skybox descriptor set and pipeline. + /// Compiles the skybox shaders on first use, sizes the skybox descriptor + /// sets to the swapchain image count, and creates the skybox pipeline. bool CreateSkyboxPipeline(); - /// Compiles the skinned shaders and creates the forward skinned pipeline. + /// Compiles the skinned shaders on first use and creates the forward + /// skinned pipeline. bool CreateSkinnedPipeline(); // Deferred rendering @@ -301,8 +303,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Creates the layout shared by the forward, skybox, debug line, skinned, /// and shadow pipelines. bool CreateMainPipelineLayout(); - /// Creates the main forward graphics pipeline, creating the shared - /// layout on first use. + /// Creates the main forward graphics pipeline, plus its layout and shader + /// on first use. bool CreateGraphicsPipeline(); /// Creates the pipeline cache, seeded from disk when the saved blob was /// written by this device and driver. @@ -418,6 +420,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { std::vector renderFinishedSemaphores; std::vector inFlightFences; std::vector imagesInFlight; + // Submission serial of the last frame recorded in each frame slot + std::array m_slotSerial{}; + uint64_t m_submitSerial = 0; VulkanShader* simpleShader = nullptr; VkClearValue clearColor; diff --git a/include/private/Graphics/Vulkan/VulkanTexture.hpp b/include/private/Graphics/Vulkan/VulkanTexture.hpp index eea2a970..6096fdaf 100644 --- a/include/private/Graphics/Vulkan/VulkanTexture.hpp +++ b/include/private/Graphics/Vulkan/VulkanTexture.hpp @@ -43,12 +43,24 @@ class ENGINE_API VulkanTexture : public Texture { VkImageView GetImageView() const { return m_imageView; } VkSampler GetSampler() const { return m_sampler; } - // Per-texture descriptor sets (one per swapchain image) - void SetDescriptorSets(std::vector sets) { m_descriptorSets = std::move(sets); } + /// Takes ownership of per-swapchain-image descriptor sets allocated from + /// pool, releasing any sets the texture held before. + void SetDescriptorSets(std::vector sets, + VkDescriptorPool pool); const std::vector& GetDescriptorSets() const { return m_descriptorSets; } bool HasDescriptorSets() const { return !m_descriptorSets.empty(); } + /// Stamps textures released from now on with the serial of the next + /// frame submission, the last one that may still reference them. + static void SetReleaseSerial(uint64_t serial); + /// Destroys released textures whose stamped submission has completed. + static void DestroyReleased(uint64_t completedSerial); + /// Destroys every released texture; the caller guarantees the GPU is idle. + static void DestroyAllReleased(); + private: + /// Hands the descriptor sets and GPU objects to the release queue so they + /// outlive any frame still in flight. void Cleanup(); /// Writes this texture's image view/sampler into each of its per-frame descriptor sets. void UpdateDescriptorSets(); @@ -78,6 +90,7 @@ class ENGINE_API VulkanTexture : public Texture { VkSampler m_sampler = VK_NULL_HANDLE; std::vector m_descriptorSets; + VkDescriptorPool m_descriptorPool = VK_NULL_HANDLE; mutable VkDescriptorSet m_imguiDescriptorSet = VK_NULL_HANDLE; uint32_t m_width = 0; diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index ec879b0e..eee77b8e 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -109,6 +109,7 @@ bool VulkanRenderer::CreateDescriptorPool() { VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; poolInfo.maxSets = totalSets; @@ -229,7 +230,7 @@ void VulkanRenderer::WriteTextureDescriptors(VulkanTexture* texture) { vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); } - texture->SetDescriptorSets(std::move(sets)); + texture->SetDescriptorSets(std::move(sets), descriptorPool); } /// Initializes ImGui and its Vulkan backend against the active render pass. diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 7fb1b78a..9596fe78 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "Core/Logger.hpp" @@ -228,18 +229,20 @@ bool VulkanRenderer::CreateMainPipelineLayout() { return true; } -/// Creates the main forward graphics pipeline, creating the shared layout -/// on first use. +/// Creates the main forward graphics pipeline, plus its layout and shader on +/// first use. bool VulkanRenderer::CreateGraphicsPipeline() { if (pipeline != VK_NULL_HANDLE) return true; if (!CreateMainPipelineLayout()) return false; - simpleShader = new VulkanShader(device); - bool isShader = - simpleShader->compile("assets/shaders/default_shader"); - - if (!isShader) - SLEAK_RETURN_ERR("Cannot compile shaders!") + if (!simpleShader) { + simpleShader = new VulkanShader(device); + if (!simpleShader->compile("assets/shaders/default_shader")) { + delete simpleShader; + simpleShader = nullptr; + SLEAK_RETURN_ERR("Cannot compile shaders!") + } + } VkPipelineShaderStageCreateInfo shaderStages[] = { simpleShader->GetVertexInfo(), simpleShader->GetFragInfo()}; @@ -526,52 +529,65 @@ bool VulkanRenderer::CreateFrameBuffer() { return true; } -/// Compiles the skybox shaders and creates the skybox descriptor set and pipeline. +/// Compiles the skybox shaders on first use, sizes the skybox descriptor sets +/// to the swapchain image count, and creates the skybox pipeline. bool VulkanRenderer::CreateSkyboxPipeline() { // 1. Compile skybox shaders - skyboxShader = new VulkanShader(device); - if (!skyboxShader->compile("assets/shaders/skybox")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile skybox shaders"); - delete skyboxShader; - skyboxShader = nullptr; - return false; + if (!skyboxShader) { + skyboxShader = new VulkanShader(device); + if (!skyboxShader->compile("assets/shaders/skybox")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile skybox shaders"); + delete skyboxShader; + skyboxShader = nullptr; + return false; + } } // 2. Create skybox descriptor pool and sets (same layout as main) uint32_t imageCount = static_cast(swapChainImages.size()); - VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = imageCount; + if (skyboxDescriptorSets.size() != imageCount) { + if (skyboxDescriptorPool) { + vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr); + skyboxDescriptorPool = VK_NULL_HANDLE; + } + skyboxDescriptorSets.clear(); - VkDescriptorPoolCreateInfo poolInfo{}; - poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - poolInfo.maxSets = imageCount; + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = imageCount; - if (vkCreateDescriptorPool(device, &poolInfo, nullptr, - &skyboxDescriptorPool) != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create skybox descriptor pool"); - return false; - } + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + poolInfo.maxSets = imageCount; - std::vector layouts(imageCount, - descriptorSetLayout); + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, + &skyboxDescriptorPool) != VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to create skybox descriptor pool"); + return false; + } - VkDescriptorSetAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - allocInfo.descriptorPool = skyboxDescriptorPool; - allocInfo.descriptorSetCount = imageCount; - allocInfo.pSetLayouts = layouts.data(); + std::vector layouts(imageCount, + descriptorSetLayout); - skyboxDescriptorSets.resize(imageCount); - if (vkAllocateDescriptorSets(device, &allocInfo, - skyboxDescriptorSets.data()) != - VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to allocate skybox descriptor sets"); - return false; + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = skyboxDescriptorPool; + allocInfo.descriptorSetCount = imageCount; + allocInfo.pSetLayouts = layouts.data(); + + std::vector sets(imageCount); + if (vkAllocateDescriptorSets(device, &allocInfo, sets.data()) != + VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to allocate skybox descriptor sets"); + return false; + } + skyboxDescriptorSets = std::move(sets); } // 3. Create skybox pipeline (same as main but with skybox shaders @@ -717,12 +733,14 @@ bool VulkanRenderer::CreateSkyboxPipeline() { bool VulkanRenderer::CreateDebugLinePipeline() { if (debugLinePipeline != VK_NULL_HANDLE) return true; - debugLineShader = new VulkanShader(device); - if (!debugLineShader->compile("assets/shaders/debug_line")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile debug line shaders"); - delete debugLineShader; - debugLineShader = nullptr; - return false; + if (!debugLineShader) { + debugLineShader = new VulkanShader(device); + if (!debugLineShader->compile("assets/shaders/debug_line")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile debug line shaders"); + delete debugLineShader; + debugLineShader = nullptr; + return false; + } } VkPipelineShaderStageCreateInfo shaderStages[] = { @@ -1205,15 +1223,20 @@ void VulkanRenderer::EndDebugLinePass() { } } -/// Compiles the skinned shaders and creates the forward skinned pipeline. +/// Compiles the skinned shaders on first use and creates the forward skinned +/// pipeline. bool VulkanRenderer::CreateSkinnedPipeline() { + if (skinnedPipeline != VK_NULL_HANDLE) return true; + // 1. Compile skinned shaders - skinnedShader = new VulkanShader(device); - if (!skinnedShader->compile("assets/shaders/skinned_shader")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile skinned shaders"); - delete skinnedShader; - skinnedShader = nullptr; - return false; + if (!skinnedShader) { + skinnedShader = new VulkanShader(device); + if (!skinnedShader->compile("assets/shaders/skinned_shader")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile skinned shaders"); + delete skinnedShader; + skinnedShader = nullptr; + return false; + } } // 2. Create pipeline (same as main but with skinned shaders) diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 35505be3..fe7ba59b 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -200,6 +200,7 @@ void VulkanRenderer::BeginRender() { if (device && !inFlightFences.empty()) { vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + VulkanTexture::DestroyReleased(m_slotSerial[currentFrame]); } CollectGpuProfile(); @@ -592,6 +593,8 @@ void VulkanRenderer::EndRender() { SLEAK_ERROR("Failed to submit draw command buffer!"); } else { MarkGpuFrameSubmitted(); + m_slotSerial[currentFrame] = ++m_submitSerial; + VulkanTexture::SetReleaseSerial(m_submitSerial + 1); } // Present @@ -1116,6 +1119,13 @@ void VulkanRenderer::Cleanup() { DestroyPipelineCache(); + // Drain released textures + if (device) { + delete m_defaultTexture; + m_defaultTexture = nullptr; + VulkanTexture::DestroyAllReleased(); + } + // Destroy descriptor pool (frees descriptor sets too) if (descriptorPool) { vkDestroyDescriptorPool(device, descriptorPool, nullptr); @@ -1285,12 +1295,6 @@ void VulkanRenderer::Cleanup() { renderPass = VK_NULL_HANDLE; } - // Destroy default texture - if (m_defaultTexture) { - delete m_defaultTexture; - m_defaultTexture = nullptr; - } - // Destroy command pool (this will also free command buffers) if (commands) { vkDestroyCommandPool(device, commands, nullptr); @@ -1443,6 +1447,10 @@ void VulkanRenderer::ApplyMSAAChange() { ImGui::DestroyContext(); bImInitialized = false; } + if (imguiDescriptorPool) { + vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); + imguiDescriptorPool = VK_NULL_HANDLE; + } // Cleanup swapchain-related resources CleanupMSAAColorResources(); @@ -1851,14 +1859,6 @@ bool VulkanRenderer::CreateGBufferResources() { if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; } if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; } DestroyCustomFormatPipelines(); - // Destroy old skybox descriptor pool (CreateSkyboxPipeline allocates new ones) - if (skyboxDescriptorPool) { - vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr); - skyboxDescriptorPool = VK_NULL_HANDLE; - } - skyboxDescriptorSets.clear(); - delete skyboxShader; - skyboxShader = nullptr; CreateGraphicsPipeline(); CreateSkyboxPipeline(); diff --git a/src/Graphics/Vulkan/VulkanTexture.cpp b/src/Graphics/Vulkan/VulkanTexture.cpp index 92d09cc7..b961fb79 100644 --- a/src/Graphics/Vulkan/VulkanTexture.cpp +++ b/src/Graphics/Vulkan/VulkanTexture.cpp @@ -5,10 +5,72 @@ #include #include #include +#include +#include +#include namespace Sleak { namespace RenderEngine { +namespace { + +/// GPU objects of a destroyed texture, kept until no submitted frame can +/// still reference them. +struct ReleasedTexture { + VkDevice device = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + std::vector sets; + VkSampler sampler = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + uint64_t serial = 0; +}; + +std::mutex g_releaseMutex; +std::vector g_released; +uint64_t g_releaseSerial = 1; + +/// Frees a released texture's descriptor sets and destroys its objects. +void DestroyReleasedTexture(ReleasedTexture& entry) { + if (entry.pool != VK_NULL_HANDLE && !entry.sets.empty()) { + vkFreeDescriptorSets(entry.device, entry.pool, + static_cast(entry.sets.size()), + entry.sets.data()); + } + if (entry.sampler) vkDestroySampler(entry.device, entry.sampler, nullptr); + if (entry.view) vkDestroyImageView(entry.device, entry.view, nullptr); + if (entry.image) vkDestroyImage(entry.device, entry.image, nullptr); + if (entry.memory) vkFreeMemory(entry.device, entry.memory, nullptr); +} + +} // namespace + +/// Stamps textures released from now on with the serial of the next frame +/// submission, the last one that may still reference them. +void VulkanTexture::SetReleaseSerial(uint64_t serial) { + std::lock_guard lock(g_releaseMutex); + g_releaseSerial = serial; +} + +/// Destroys released textures whose stamped submission has completed. +void VulkanTexture::DestroyReleased(uint64_t completedSerial) { + std::lock_guard lock(g_releaseMutex); + auto done = std::stable_partition( + g_released.begin(), g_released.end(), + [&](const ReleasedTexture& e) { return e.serial > completedSerial; }); + for (auto it = done; it != g_released.end(); ++it) + DestroyReleasedTexture(*it); + g_released.erase(done, g_released.end()); +} + +/// Destroys every released texture; the caller guarantees the GPU is idle. +void VulkanTexture::DestroyAllReleased() { + std::lock_guard lock(g_releaseMutex); + for (ReleasedTexture& entry : g_released) DestroyReleasedTexture(entry); + g_released.clear(); +} + VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, @@ -274,6 +336,25 @@ uint64_t VulkanTexture::GetImGuiTextureID() const { return reinterpret_cast(m_imguiDescriptorSet); } +/// Takes ownership of per-swapchain-image descriptor sets allocated from +/// pool, releasing any sets the texture held before. +void VulkanTexture::SetDescriptorSets(std::vector sets, + VkDescriptorPool pool) { + if (!m_descriptorSets.empty()) { + ReleasedTexture entry; + entry.device = m_device; + entry.pool = m_descriptorPool; + entry.sets = std::move(m_descriptorSets); + std::lock_guard lock(g_releaseMutex); + entry.serial = g_releaseSerial; + g_released.push_back(std::move(entry)); + } + m_descriptorSets = std::move(sets); + m_descriptorPool = pool; +} + +/// Hands the descriptor sets and GPU objects to the release queue so they +/// outlive any frame still in flight. void VulkanTexture::Cleanup() { if (m_device == VK_NULL_HANDLE) return; @@ -281,22 +362,25 @@ void VulkanTexture::Cleanup() { ImGui_ImplVulkan_RemoveTexture(m_imguiDescriptorSet); m_imguiDescriptorSet = VK_NULL_HANDLE; } - if (m_sampler != VK_NULL_HANDLE) { - vkDestroySampler(m_device, m_sampler, nullptr); - m_sampler = VK_NULL_HANDLE; - } - if (m_imageView != VK_NULL_HANDLE) { - vkDestroyImageView(m_device, m_imageView, nullptr); - m_imageView = VK_NULL_HANDLE; - } - if (m_image != VK_NULL_HANDLE) { - vkDestroyImage(m_device, m_image, nullptr); - m_image = VK_NULL_HANDLE; - } - if (m_imageMemory != VK_NULL_HANDLE) { - vkFreeMemory(m_device, m_imageMemory, nullptr); - m_imageMemory = VK_NULL_HANDLE; - } + if (m_descriptorSets.empty() && m_sampler == VK_NULL_HANDLE && + m_imageView == VK_NULL_HANDLE && m_image == VK_NULL_HANDLE && + m_imageMemory == VK_NULL_HANDLE) + return; + + ReleasedTexture entry; + entry.device = m_device; + entry.pool = m_descriptorPool; + entry.sets = std::move(m_descriptorSets); + entry.sampler = std::exchange(m_sampler, VK_NULL_HANDLE); + entry.view = std::exchange(m_imageView, VK_NULL_HANDLE); + entry.image = std::exchange(m_image, VK_NULL_HANDLE); + entry.memory = std::exchange(m_imageMemory, VK_NULL_HANDLE); + m_descriptorSets.clear(); + m_descriptorPool = VK_NULL_HANDLE; + + std::lock_guard lock(g_releaseMutex); + entry.serial = g_releaseSerial; + g_released.push_back(std::move(entry)); } uint32_t VulkanTexture::FindMemoryType( From 36d6f3b49ff6913cbec9033804725a6112849e6c Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 22:00:40 +0300 Subject: [PATCH 13/36] fix: grow vulkan per-image semaphores when the swapchain is recreated RecreateSwapChain resized the image fence table but not the acquire and render-finished semaphores, so a swapchain with more images indexed past their end. Grow both lists to the new image count and clear the fence table, also after an MSAA change. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 3 ++ src/Graphics/Vulkan/VulkanRenderer.cpp | 51 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 29312096..185799fa 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -322,6 +322,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Creates the per-swapchain-image semaphores and per-frame fences and /// transfer semaphores. bool CreateSyncObjects(); + /// Grows the per-image semaphores to the swapchain image count and + /// clears the image-to-fence table. + bool CreateImageSyncObjects(); /// Creates the depth image, memory, and image view. bool CreateDepthResources(); /// Creates the texture, bone UBO, light UBO, and shadow sampler descriptor set layouts. diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index fe7ba59b..a94be8c8 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -1375,8 +1375,10 @@ bool VulkanRenderer::RecreateSwapChain() { return false; } - // Resize imagesInFlight in case swapchain image count changed - imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE); + if (!CreateImageSyncObjects()) { + SLEAK_ERROR("Failed to recreate swapchain image semaphores!"); + return false; + } // Recreate GBuffer resources if they were previously created if (hadGBuffer && m_deferredEnabled) { @@ -1458,6 +1460,7 @@ void VulkanRenderer::ApplyMSAAChange() { // Recreate everything CreateSwapChain(); + CreateImageSyncObjects(); CreateImageViews(); CreateDepthResources(); CreateMSAAColorResources(); @@ -1528,17 +1531,37 @@ bool VulkanRenderer::CreateCommandPool() { return true; } +/// Grows the per-image semaphores to the swapchain image count and clears +/// the image-to-fence table. Existing semaphores are kept: a present may +/// still reference them, and indices beyond the image count are harmless. +bool VulkanRenderer::CreateImageSyncObjects() { + const size_t imageCount = swapChainImages.size(); + imagesInFlight.assign(imageCount, VK_NULL_HANDLE); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + while (imageAvailableSemaphores.size() < imageCount) { + VkSemaphore available = VK_NULL_HANDLE; + VkSemaphore finished = VK_NULL_HANDLE; + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &available) != + VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &finished) != + VK_SUCCESS) { + if (available) vkDestroySemaphore(device, available, nullptr); + SLEAK_RETURN_ERR("Failed to create synchronization objects!"); + } + imageAvailableSemaphores.push_back(available); + renderFinishedSemaphores.push_back(finished); + } + return true; +} + /// Creates the per-swapchain-image semaphores and per-frame fences and /// transfer semaphores. bool VulkanRenderer::CreateSyncObjects() { - uint32_t imageCount = static_cast(swapChainImages.size()); - - // Semaphores sized to swapchain image count to prevent reuse - // while the presentation engine still holds a reference. - imageAvailableSemaphores.resize(imageCount); - renderFinishedSemaphores.resize(imageCount); + if (!CreateImageSyncObjects()) return false; inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); - imagesInFlight.resize(imageCount, VK_NULL_HANDLE); VkSemaphoreCreateInfo semaphoreInfo{}; semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; @@ -1547,16 +1570,6 @@ bool VulkanRenderer::CreateSyncObjects() { fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - // Create per-swapchain-image semaphores - for (uint32_t i = 0; i < imageCount; i++) { - if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, - &imageAvailableSemaphores[i]) != VK_SUCCESS || - vkCreateSemaphore(device, &semaphoreInfo, nullptr, - &renderFinishedSemaphores[i]) != VK_SUCCESS) { - SLEAK_RETURN_ERR("Failed to create synchronization objects!"); - } - } - // Create per-frame-in-flight fences and transfer semaphores for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, From 85ac56f58a8ebfe6fa9411e3a56333525459c67f Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 22:01:21 +0300 Subject: [PATCH 14/36] perf: recreate the vulkan swapchain on suboptimal only when it resized A suboptimal present rebuilt the swapchain and every size-dependent resource behind a device wait, even when the surface size had not changed. Check the surface extent first. Out-of-date presents and capture mode behave as before, and resize events that keep the same extent are skipped too. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 4 ++- src/Graphics/Vulkan/VulkanRenderer.cpp | 7 +++-- src/Graphics/Vulkan/VulkanSwapchain.cpp | 30 ++++++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 185799fa..7b14f8b3 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -298,6 +298,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool CreateSwapChain(); /// Rebuilds the swapchain and its dependents after resize or resolution change. bool RecreateSwapChain(); + /// True when the surface wants a different extent than the swapchain. + bool SurfaceExtentChanged(); /// Creates an image view for each swapchain image. bool CreateImageViews(); /// Creates the layout shared by the forward, skybox, debug line, skinned, @@ -372,7 +374,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkPresentModeKHR ChoosePresentMode( const std::vector& modes); /// Clamps the window size to the surface's supported extent. - VkExtent2D ChooseExtend(SwapchainDetails details); + VkExtent2D ChooseExtend(const VkSurfaceCapabilitiesKHR& caps); /// Picks the first supported depth-stencil format from the candidate list. VkFormat FindDepthFormat(); diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index a94be8c8..f6bc6096 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -612,7 +612,8 @@ void VulkanRenderer::EndRender() { // Capture pins the extent if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || - (presentResult == VK_SUBOPTIMAL_KHR && !m_captureMode)) { + (presentResult == VK_SUBOPTIMAL_KHR && !m_captureMode && + SurfaceExtentChanged())) { RecreateSwapChain(); } else if (presentResult != VK_SUCCESS && presentResult != VK_SUBOPTIMAL_KHR) { @@ -1334,9 +1335,9 @@ void VulkanRenderer::Cleanup() { } } -/// Recreates the swapchain for the new window dimensions. +/// Recreates the swapchain when the surface extent no longer matches it. void VulkanRenderer::Resize(uint32_t width, uint32_t height) { - if (device) { + if (device && (swapChain == VK_NULL_HANDLE || SurfaceExtentChanged())) { RecreateSwapChain(); } } diff --git a/src/Graphics/Vulkan/VulkanSwapchain.cpp b/src/Graphics/Vulkan/VulkanSwapchain.cpp index 2d0e2954..c6134b2e 100644 --- a/src/Graphics/Vulkan/VulkanSwapchain.cpp +++ b/src/Graphics/Vulkan/VulkanSwapchain.cpp @@ -43,7 +43,7 @@ bool VulkanRenderer::CreateSwapChain() { auto format = ChooseFormat(details->formats); auto mode = ChoosePresentMode(details->presentModes); - auto extent = ChooseExtend(details.value()); + auto extent = ChooseExtend(details->caps); uint32_t imageCount = details->caps.minImageCount + 1; if (details->caps.maxImageCount > 0 && @@ -439,10 +439,9 @@ VkPresentModeKHR VulkanRenderer::ChoosePresentMode( /// Clamps the window size to the surface's supported extent. // Fixed: clamp height using height, not width -VkExtent2D VulkanRenderer::ChooseExtend(SwapchainDetails details) { - if (details.caps.currentExtent.width != - std::numeric_limits::max()) - return details.caps.currentExtent; +VkExtent2D VulkanRenderer::ChooseExtend(const VkSurfaceCapabilitiesKHR& caps) { + if (caps.currentExtent.width != std::numeric_limits::max()) + return caps.currentExtent; int width, height; SDL_GetWindowSizeInPixels(sdlWindow->GetSDLWindow(), &width, &height); @@ -451,17 +450,26 @@ VkExtent2D VulkanRenderer::ChooseExtend(SwapchainDetails details) { static_cast(height)}; actualExtent.width = - std::clamp(actualExtent.width, - details.caps.minImageExtent.width, - details.caps.maxImageExtent.width); + std::clamp(actualExtent.width, caps.minImageExtent.width, + caps.maxImageExtent.width); actualExtent.height = - std::clamp(actualExtent.height, - details.caps.minImageExtent.height, - details.caps.maxImageExtent.height); + std::clamp(actualExtent.height, caps.minImageExtent.height, + caps.maxImageExtent.height); return actualExtent; } +/// True when the surface now wants a different extent than the swapchain +/// has. A failed query keeps the current swapchain. +bool VulkanRenderer::SurfaceExtentChanged() { + VkSurfaceCapabilitiesKHR caps{}; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, + &caps) != VK_SUCCESS) + return false; + const VkExtent2D extent = ChooseExtend(caps); + return extent.width != scExtent.width || extent.height != scExtent.height; +} + } } From 67db5f6e84501c2dfee9fe58ec674b48a0081e60 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:24:14 +0300 Subject: [PATCH 15/36] perf: read the vulkan taa resolve directly instead of copying it back TAA copied its resolved image back into the HDR scene every frame, a full screen RGBA16F copy with four layout transitions. SSR, bloom and the composite now sample the TAA target written this frame and fall back to the HDR scene when TAA is off. The capture is bit-identical. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 6 +- src/Graphics/Vulkan/VulkanBloom.cpp | 9 ++- src/Graphics/Vulkan/VulkanRenderer.cpp | 8 +-- src/Graphics/Vulkan/VulkanSSR.cpp | 16 ++++- src/Graphics/Vulkan/VulkanTAA.cpp | 67 ++++--------------- 5 files changed, 40 insertions(+), 66 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 7b14f8b3..87ec1117 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -870,6 +870,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkFramebuffer m_taaFramebufs[2] = {}; VkSampler m_taaSampler = VK_NULL_HANDLE; + VkImageView m_taaResolvedView = VK_NULL_HANDLE; // null if TAA is off + VkDescriptorSetLayout m_taaInputDSL = VK_NULL_HANDLE; VkDescriptorSetLayout m_taaUboDSL = VK_NULL_HANDLE; VkDescriptorPool m_taaPool = VK_NULL_HANDLE; @@ -893,8 +895,10 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { void CleanupTAAResources(); /// Computes the inverse current view-projection and reprojection blend factor into the TAA UBO. void UpdateTAAUBO(); - /// Resolves the current frame against TAA history and copies the result back into the HDR scene image. + /// Resolves the frame against TAA history into the ping-pong target. void RenderTAAPass(); + /// HDR scene color for the post passes: the TAA resolve when TAA ran. + VkImageView PostSceneView() const; // Per-image "fallback content is valid" flags. Set true once the image is // primed (static black/white in SHADER_READ_ONLY) by either diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index 59d71744..4502e285 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -45,11 +45,9 @@ bool VulkanRenderer::CreateHDRSceneResources() { info.format = m_hdrSceneFormat; info.tiling = VK_IMAGE_TILING_OPTIMAL; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - // TRANSFER_DST_BIT: TAA copies its resolved result back into hdrScene. // TRANSFER_SRC_BIT: MSAA resolve from the forward render pass. info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; info.samples = VK_SAMPLE_COUNT_1_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -620,9 +618,10 @@ void VulkanRenderer::RenderBloomPass() { BeginGpuPass(GpuPass::Bloom); // ---------- 1. Threshold / prefilter pass ---------- - // Input: sceneHDR (already SHADER_READ_ONLY_OPTIMAL via forward RP finalLayout) + // Input: scene color (TAA resolve or HDR scene, SHADER_READ_ONLY_OPTIMAL) // Output: bloomMip[0] - WriteSingleImageSampler(device, setArray[0], m_hdrSceneView, m_bloomSampler, + WriteSingleImageSampler(device, setArray[0], PostSceneView(), + m_bloomSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); { @@ -718,7 +717,7 @@ void VulkanRenderer::RenderBloomCompositePass() { VkDescriptorSet compSet = m_bloomCompositeSets[currentFrame]; std::array infos{}; infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - infos[0].imageView = m_hdrSceneView; + infos[0].imageView = PostSceneView(); infos[0].sampler = m_bloomSampler; infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index f6bc6096..1b871f3c 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -520,11 +520,11 @@ void VulkanRenderer::EndRender() { } if (deferredPath) { - // TAA: accumulate current HDR frame with history, write resolved result - // back into hdrScene. Also handles the depth barrier (ATTACHMENT → READ_ONLY) - // so SSR can skip its own barrier when TAA is enabled. + // TAA: accumulate current HDR frame with history into a ping-pong + // target that SSR, bloom and the composite read. Also handles the depth + // barrier (ATTACHMENT → READ_ONLY) so SSR can skip its own barrier. RenderTAAPass(); - // Screen-space reflections — reads TAA-resolved hdrScene + GBuffer. + // Screen-space reflections — reads the TAA-resolved scene + GBuffer. RenderSSRPass(); // Bloom pyramid generates the bloom result from the HDR scene. RenderBloomPass(); diff --git a/src/Graphics/Vulkan/VulkanSSR.cpp b/src/Graphics/Vulkan/VulkanSSR.cpp index 1c4911a5..6728e67d 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -427,7 +427,7 @@ void VulkanRenderer::UpdateSSRDescriptors() { infos[3].imageView = m_gbufferViews[0]; infos[3].sampler = m_gbufferSampler ? m_gbufferSampler : m_ssrSampler; - // sceneHDR — forward pass finalLayout is SHADER_READ_ONLY_OPTIMAL. + // sceneHDR — RenderSSRPass retargets it to the TAA resolve. infos[4].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; infos[4].imageView = m_hdrSceneView; infos[4].sampler = m_ssrSampler; @@ -547,6 +547,20 @@ void VulkanRenderer::RenderSSRPass() { UpdateSSRUBO(); + // Scene input for this frame + VkDescriptorImageInfo sceneInfo{}; + sceneInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + sceneInfo.imageView = PostSceneView(); + sceneInfo.sampler = m_ssrSampler; + VkWriteDescriptorSet sceneWrite{}; + sceneWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + sceneWrite.dstSet = m_ssrInputSets[currentFrame]; + sceneWrite.dstBinding = 4; + sceneWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + sceneWrite.descriptorCount = 1; + sceneWrite.pImageInfo = &sceneInfo; + vkUpdateDescriptorSets(device, 1, &sceneWrite, 0, nullptr); + VkRenderPassBeginInfo rp{}; rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rp.renderPass = m_ssrRenderPass; diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index bb88deab..719a56a7 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -32,7 +32,6 @@ bool VulkanRenderer::CreateTAAResources() { ic.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; ic.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; ic.samples = VK_SAMPLE_COUNT_1_BIT; ic.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -157,15 +156,13 @@ bool VulkanRenderer::CreateTAAResources() { deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - // Feeds copy-back and history + // Feeds history and the post passes deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | - VK_PIPELINE_STAGE_TRANSFER_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - deps[1].dstAccessMask = - VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT; + deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; VkRenderPassCreateInfo rpi{}; rpi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; @@ -401,6 +398,7 @@ void VulkanRenderer::CleanupTAAResources() { if (m_taaMemory[i]) { vkFreeMemory(device, m_taaMemory[i], nullptr); m_taaMemory[i] = VK_NULL_HANDLE; } } + m_taaResolvedView = VK_NULL_HANDLE; m_taaResourcesCreated = false; } @@ -435,8 +433,9 @@ void VulkanRenderer::UpdateTAAUBO() { memcpy(m_prevViewProj, currentVP, sizeof(m_prevViewProj)); } -/// Resolves the current frame against TAA history and copies the result back into the HDR scene image. +/// Resolves the frame against TAA history into the ping-pong target. void VulkanRenderer::RenderTAAPass() { + m_taaResolvedView = VK_NULL_HANDLE; if (!m_taaResourcesCreated || !m_taaEnabled) return; const uint32_t writeIdx = static_cast(m_taaFrameIdx % 2); @@ -531,59 +530,17 @@ void VulkanRenderer::RenderTAAPass() { // taaImages[writeIdx] is now SHADER_READ_ONLY_OPTIMAL (render pass finalLayout) } - // ---- 6. Copy TAA resolve → hdrScene ---- - // Both images are SHADER_READ_ONLY; transition for transfer then restore. - { - VkImageMemoryBarrier toTransfer[2]{}; - toTransfer[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - toTransfer[0].oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - toTransfer[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toTransfer[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransfer[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransfer[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - toTransfer[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toTransfer[0].image = m_taaImages[writeIdx]; - toTransfer[0].subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - - toTransfer[1] = toTransfer[0]; - toTransfer[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - toTransfer[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toTransfer[1].image = m_hdrSceneImage; - - vkCmdPipelineBarrier(command, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 2, toTransfer); - - VkImageCopy region{}; - region.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.extent = { scExtent.width, scExtent.height, 1 }; - vkCmdCopyImage(command, - m_taaImages[writeIdx], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - m_hdrSceneImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - 1, ®ion); - - VkImageMemoryBarrier toRead[2]{}; - toRead[0] = toTransfer[0]; - toRead[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toRead[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - toRead[0].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toRead[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - toRead[1] = toRead[0]; - toRead[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - toRead[1].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toRead[1].image = m_hdrSceneImage; - - vkCmdPipelineBarrier(command, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 2, toRead); - } + m_taaResolvedView = m_taaViews[writeIdx]; EndGpuPass(GpuPass::TAA); // Advance ping-pong for next frame m_taaFrameIdx++; } +/// HDR scene color for the post passes: the TAA resolve when TAA ran. +VkImageView VulkanRenderer::PostSceneView() const { + return m_taaResolvedView ? m_taaResolvedView : m_hdrSceneView; +} + } // namespace RenderEngine } // namespace Sleak From aa03f789be09a3dd331156e0bc607c18e8426c01 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:24:21 +0300 Subject: [PATCH 16/36] perf: skip dead ssr and bloom fetches in the vulkan composite The composite sampled the SSR and bloom images on every pixel even when both were disabled and bound to black fallbacks. A push constant now tells it whether SSR ran, and bloom is only sampled with a non-zero strength. The output is unchanged. --- assets/shaders/bloom_composite.frag | 17 ++++++++++------- assets/shaders/bloom_composite.frag.spv | Bin 3340 -> 3576 bytes src/Graphics/Vulkan/VulkanBloom.cpp | 8 +++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/assets/shaders/bloom_composite.frag b/assets/shaders/bloom_composite.frag index 2489c725..86ba1213 100644 --- a/assets/shaders/bloom_composite.frag +++ b/assets/shaders/bloom_composite.frag @@ -16,7 +16,7 @@ layout(push_constant) uniform BloomCompositePC { float bloomStrength; // bloom mix amount (typical 0.04..0.08 UE-style) float exposure; // exposure multiplier for HDR scene float gamma; // 1.0 unless tonemapping is enabled - float _pad1; + uint ssrEnabled; // 0 skips the SSR fetch }; // ACES Filmic tonemap (Narkowicz 2015) @@ -31,23 +31,26 @@ vec3 ACESFilm(vec3 x) { } void main() { - vec3 hdr = texture(sceneHDR, fragUV).rgb; - vec3 bloom = texture(bloomTex, fragUV).rgb; + vec3 hdr = texture(sceneHDR, fragUV).rgb; // SSR: ssr.rgb = reflectedColor * Fresnel * fades (premultiplied), ssr.a = fades only. // Unpremultiply to get reflectedColor * Fresnel, then lerp over the base scene using // ssr.a as coverage. This replaces (rather than adds to) the IBL-approximated specular, // so reflections are visible even on bright surfaces. - vec4 ssr = texture(ssrTex, fragUV); - if (ssr.a > 0.001) { - hdr = mix(hdr, ssr.rgb / max(ssr.a, 1e-4), min(ssr.a, 1.0)); + if (ssrEnabled != 0u) { + vec4 ssr = texture(ssrTex, fragUV); + if (ssr.a > 0.001) { + hdr = mix(hdr, ssr.rgb / max(ssr.a, 1e-4), min(ssr.a, 1.0)); + } } // Add bloom on top of the HDR scene. Bloom is a soft glow derived // from the bright parts of the scene; adding it preserves the base // luminance and only brightens hot spots (unlike mix, which dims // the scene by the bloom factor in non-glowing regions). - vec3 combined = hdr + bloom * clamp(bloomStrength, 0.0, 1.0); + vec3 combined = hdr; + if (bloomStrength > 0.0) + combined += texture(bloomTex, fragUV).rgb * clamp(bloomStrength, 0.0, 1.0); // Apply exposure before tone mapping. combined *= exposure; diff --git a/assets/shaders/bloom_composite.frag.spv b/assets/shaders/bloom_composite.frag.spv index c6baf0b34cdee2d7eea2bf40e5d3a73058367496..52c3671e32f99d68b2cb5b02922ef423a1b2f1c5 100644 GIT binary patch literal 3576 zcmZ9NX>(Ln5QZ;ggAn#5A_5bFRUnASz6S^)sL=$%;)2FWGKA5YOq`h@C@zRm6b1ba ze)I=W28V|ShX%$bW@g4}^;@OFSgAN&7@I6s3lsI?je>EDGI7P}iPG$3HlU4&Y~D8N zT|$mi{R5+Ci>30m8`t(8#cm^aC-P>hZ$n<6tirb?JJD+sh3Ue%GZ#={S{wCWtLCRJ zUDobM4#1x-RVwBFN_nPIE7l7m{l>PZKJ{_^qxEWGda8cC;cIW~vqy!S?3%4|Blom5 zczZFGFPHNTt{H1rYqjdYbbh>4m~7U42+rjl&LHtOY8+_l7Ykg0PUD?fWwzd5DOIYd$soP3KRp_(u(veLhV863J+)h& z-t8PU**8~jQzXIJ6FFxWoc)knl;P}$To*96ceWN|!_L;?ef4|3e(kK^`$es*ImL5& z8ZWbX<~sCtxE$zazwcJtI&#?7CMSBZ7pJ=X4s2tWfhWJ@PKLb_`{Cn1j)lDrd-L3e zr~dWW$2+_G!oLxF`_<(q!uCF{ot$o7?;{t8IQx_&U-m`ZRxG)|daB)yZH)^+c7?rx zo4$a@|Jo{+;OGwX>H(uNKA!qy^r18#uEr0hTbHKF;kMBi) zpy_W-4#L@M{hq7scOmCFhmkp89{Ztv1jO9@4KUX8^eayQ*BYwtLt1}7(jHr9+}Ahf zihZx3$GNYf8*jZahu6_7tL`2N`!aU-qu&o{TUQ>q%r}m-F84=1->fU*Yv|c~-Nv@x zasC{7=ab%Jp7%YvwMG5fzInL=e2?z4_b$1=VA_!8_6=_X-bcG1c_w4)_ctc|+WwYg z?OUmRoc+F;7lH3Z+c)tt@I721t2OQg#>fv-foJUl^1|iat@{KOTc7?@?02TefbYOr zo5Q&t2lBrX<6KVyIp_T+XPid5{vrM(@)VG>7wS>_S#)O~wGX0aYd?o=jJ&nkD>-ZP zUAzY5d@r8ijE8{yZ)t6Bq?~WY+D4ErYtuIj%0$81aJ^1@{g z7J{g^j6T2KX*hX%=WHv$eLH|Y>$c|)(mBqco5#55(MRa^=1v+@MVB)+`X*;yqsu?yj6UQSK+fJc*E>j;bJh1HkaMo;QP)@K=8If+(dXy-8csgGfp5^AiSJO~ z9PqnxUf%+1Febi>dnvC^cuoiMJK#Cb0_!j??yt}JM9s>m`3H1sKA-0N5nav|+yitu z&kycl%J~LIs9+EBC*XeL^;^3&+(+vBmlF;F|0h|qb8;;P2Z>vR>;(3;3mG*oP5H2w zr*^!T71(miK=f%9`pOLA{jP>HUjJNL#~O5V=!+a{(XA!y?$nN)&tS`~1Ci6-ug@TI zZh$jhf8=})-5mO2|0eW}8N~k0aK`J8-`pI!b?b}YMt?`0k>7U?Yyn%rfAmwIz5gFp CFc*RV literal 3340 zcmZ9NX>(Ln5QZ-#6Ci|riHO34U=;|WvhQ1fL5(I578hg~GD8@h$&51-1XmEL@{|4u ze*{&2P>gQ-KV=xpEEao3r80v$%15QvMhO&%&%3+LYM?@ zAeE)@sqtOiUUk>reQz1DJn2gVjaiW_O8SwaS*g$JFz9=*k~nz^p!Jb8NuIE&KN(C) zQrVfUzxY} z!QX7OTFum7NcM+)sb$`EVzr{$T}YZaLpUaORU+oa3B>+z_yq=Zjv~(1CkKn2B-f z>(K^~MKDafZ&+J@5!>8yVg!3hrpxcZHg-99`fKrI*sHLAdGg26u-9X6ezftKe*^Zh z!Qm6(e-(TCwH3$1c2=&PWVf!fk_$wf=S-6OCn9bumRw*1-EPOW#|0q2!rsV9U&mpb z=iQM7$sNgT--FBf>_VE)*%|*P(p>V!Yx_RrjNgqk-g}FDz7LoC{oQ&-i+N0JzLB}+czw?pWlc3 zoTW?dFPH+-UVW3>fb$63Z(7*S+V5A`#{136+kPwZw%-ZuW90Wuya9X<+TOX}8}Iun zUD)#;V2u0(9l3WekQXjz;W>_T0{hh;`46D`J|h1?^nCt9*v805{=?|d=a7Gr<6WfdAL8}B z1LV9rb@Mo<_ki=zc9!G7T5-2FFd>fT1T?hJ^!b#(dY{WEm;MV-&l<&2A*auGj^ZoIxGQu|QWPYZo1 zkk{{TS}ywe0^NSvAo}T`%ReSZ3E2g5p4~S*hjjUd_4R<9Z&*G0x`S@LsP!fKi?!~; z$;UhS3f-A|clz!D-=6pJHLwR`;_ZBs@%n^$yzg&;d0qzgVO*SFpYJAmRz}bF(e3#{ zR`UV6oGZ8=(C-8D2lp`J79uCx=J)-x}{|2PlFq NU@Q2KdFpfa{{td~`F#KY diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index 4502e285..ff3b0731 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -729,7 +729,8 @@ void VulkanRenderer::RenderBloomCompositePass() { // is disabled). Fall back to the bloom mip view only when SSR resources // haven't been created yet (should never happen once init completes). infos[2].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - if (m_ssrResourcesCreated && m_ssrView != VK_NULL_HANDLE) { + const bool ssrReady = m_ssrResourcesCreated && m_ssrView != VK_NULL_HANDLE; + if (ssrReady) { infos[2].imageView = m_ssrView; infos[2].sampler = m_ssrSampler ? m_ssrSampler : m_bloomSampler; } else { @@ -765,13 +766,14 @@ void VulkanRenderer::RenderBloomCompositePass() { m_bloomCompositePipelineLayout, 0, 1, &compSet, 0, nullptr); struct { - float bloomStrength, exposure, gamma, p1; + float bloomStrength, exposure, gamma; + uint32_t ssrEnabled; } pc; // bloom disabled -> 0 so the (black) mip contributes nothing pc.bloomStrength = m_bloomEnabled ? 0.06f : 0.0f; // UE4-style soft bloom pc.exposure = m_exposure; pc.gamma = m_tonemapEnabled ? m_gamma : 1.0f; - pc.p1 = 0.0f; + pc.ssrEnabled = (ssrReady && m_ssrEnabled) ? 1u : 0u; vkCmdPushConstants(command, m_bloomCompositePipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); vkCmdDraw(command, 3, 1, 0, 0); From a9cbcb24c290f2d030761b44f3bd9c57be8add22 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:24:21 +0300 Subject: [PATCH 17/36] perf: gather the taa depth neighborhood and fetch its color taps The 3x3 closest-depth search did ten texel fetches; four depth gathers cover the same texels, visited in the original order with the same strict compare. The current color and its 3x3 clamp neighborhood are read with texelFetch, and the history taps use an explicit LOD after the non-uniform early outs. The capture is bit-identical. --- assets/shaders/taa.frag | 45 ++++++++++++++++++++---------------- assets/shaders/taa.frag.spv | Bin 12072 -> 13380 bytes 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/assets/shaders/taa.frag b/assets/shaders/taa.frag index 5f83b7b4..d68b3e0e 100644 --- a/assets/shaders/taa.frag +++ b/assets/shaders/taa.frag @@ -48,11 +48,11 @@ vec3 SampleHistoryCatmullRom(vec2 uv) { vec2 tc12 = (texPos1 + w2 / w12) / ScreenSize; vec3 result = - texture(historyTex, vec2(tc12.x, tc0.y )).rgb * (w12.x * w0.y ) + - texture(historyTex, vec2(tc0.x, tc12.y)).rgb * (w0.x * w12.y) + - texture(historyTex, vec2(tc12.x, tc12.y)).rgb * (w12.x * w12.y) + - texture(historyTex, vec2(tc3.x, tc12.y)).rgb * (w3.x * w12.y) + - texture(historyTex, vec2(tc12.x, tc3.y )).rgb * (w12.x * w3.y ); + textureLod(historyTex, vec2(tc12.x, tc0.y ), 0.0).rgb * (w12.x * w0.y ) + + textureLod(historyTex, vec2(tc0.x, tc12.y), 0.0).rgb * (w0.x * w12.y) + + textureLod(historyTex, vec2(tc12.x, tc12.y), 0.0).rgb * (w12.x * w12.y) + + textureLod(historyTex, vec2(tc3.x, tc12.y), 0.0).rgb * (w3.x * w12.y) + + textureLod(historyTex, vec2(tc12.x, tc3.y ), 0.0).rgb * (w12.x * w3.y ); float wsum = (w12.x * w0.y) + (w0.x * w12.y) + (w12.x * w12.y) + (w3.x * w12.y) + (w12.x * w3.y); return max(result / wsum, vec3(0.0)); @@ -61,32 +61,36 @@ vec3 SampleHistoryCatmullRom(vec2 uv) { void main() { vec2 texelSize = 1.0 / ScreenSize; - vec3 current = texture(currentTex, fragUV).rgb; - ivec2 ip = ivec2(fragUV * ScreenSize); ivec2 maxIp = ivec2(ScreenSize) - 1; - float depth = texelFetch(gDepth, clamp(ip, ivec2(0), maxIp), 0).r; + vec3 current = texelFetch(currentTex, clamp(ip, ivec2(0), maxIp), 0).rgb; + + // 3x3 depth via gathers + vec2 cuv = vec2(ip) * texelSize; + vec4 g00 = textureGather(gDepth, cuv); + float depth = g00.y; if (depth >= 0.9999) { outColor = vec4(current, 1.0); return; } + vec4 g10 = textureGatherOffset(gDepth, cuv, ivec2(1, 0)); + vec4 g01 = textureGatherOffset(gDepth, cuv, ivec2(0, 1)); + vec4 g11 = textureGatherOffset(gDepth, cuv, ivec2(1, 1)); // Closest-depth dilation with UNFILTERED depth reads. Bilinear-filtered // depth at silhouettes yields positions on neither surface — the source // of edge shimmer at distance where every texel is a silhouette. vec2 closestOff = vec2(0.0); float closestDepth = depth; - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - float d = texelFetch(gDepth, - clamp(ip + ivec2(x, y), ivec2(0), maxIp), - 0).r; - if (d < closestDepth) { - closestDepth = d; - closestOff = vec2(x, y); - } - } - } + // x-major order, strict < + if (g00.w < closestDepth) { closestDepth = g00.w; closestOff = vec2(-1.0, -1.0); } + if (g00.x < closestDepth) { closestDepth = g00.x; closestOff = vec2(-1.0, 0.0); } + if (g01.x < closestDepth) { closestDepth = g01.x; closestOff = vec2(-1.0, 1.0); } + if (g00.z < closestDepth) { closestDepth = g00.z; closestOff = vec2( 0.0, -1.0); } + if (g01.y < closestDepth) { closestDepth = g01.y; closestOff = vec2( 0.0, 1.0); } + if (g10.z < closestDepth) { closestDepth = g10.z; closestOff = vec2( 1.0, -1.0); } + if (g10.y < closestDepth) { closestDepth = g10.y; closestOff = vec2( 1.0, 0.0); } + if (g11.y < closestDepth) { closestDepth = g11.y; closestOff = vec2( 1.0, 1.0); } // Reproject the closest surface in the neighborhood and apply its motion // to this pixel — edges follow the foreground surface. @@ -120,7 +124,8 @@ void main() { vec3 nMax = vec3(-1e10); for (int x = -1; x <= 1; ++x) { for (int y = -1; y <= 1; ++y) { - vec3 s = texture(currentTex, fragUV + vec2(x, y) * texelSize).rgb; + vec3 s = texelFetch(currentTex, + clamp(ip + ivec2(x, y), ivec2(0), maxIp), 0).rgb; nMin = min(nMin, s); nMax = max(nMax, s); } diff --git a/assets/shaders/taa.frag.spv b/assets/shaders/taa.frag.spv index 6ed9d260842589c967702e261527c076afe2f6e6..c7ec5c95c37e31070a2112521a86352205381b47 100644 GIT binary patch literal 13380 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 Date: Fri, 9 Oct 2026 01:24:39 +0300 Subject: [PATCH 18/36] perf: fetch the gbuffer and skip back-face pcf in the vulkan lighting pass The lighting pass reads depth and the GBuffer targets with texelFetch at the fragment's pixel, takes the shadow PCF taps at an explicit LOD, and skips the 16-tap PCF where the back-face smoothstep zeroes the shadow term anyway. The capture is bit-identical. --- assets/shaders/lighting_pass.frag | 16 +++++++++------- assets/shaders/lighting_pass.frag.spv | Bin 26696 -> 27080 bytes 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/assets/shaders/lighting_pass.frag b/assets/shaders/lighting_pass.frag index 06da48bb..118fabb9 100644 --- a/assets/shaders/lighting_pass.frag +++ b/assets/shaders/lighting_pass.frag @@ -96,7 +96,7 @@ float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) { float shadow = 0.0; for (int i = 0; i < PCF_SAMPLES; ++i) { vec2 off = VogelDisk(i, PCF_SAMPLES, phi) * filterRadius; - shadow += texture(gShadow, vec3(uv + off, zRef)); + shadow += textureLod(gShadow, vec3(uv + off, zRef), 0.0); } return shadow / float(PCF_SAMPLES); } @@ -209,16 +209,17 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { // ================================================================== void main() { // Early-out for sky pixels (depth == 1.0 means nothing was drawn) - float depth = texture(gDepth, fragUV).r; + ivec2 px = ivec2(gl_FragCoord.xy); + float depth = texelFetch(gDepth, px, 0).r; if (depth >= 1.0) { outColor = vec4(0.0, 0.0, 0.0, 1.0); return; } // Reconstruct world position from depth instead of a GBuffer RT. vec3 worldPos = ReconstructWorldPos(fragUV, depth); vec4 ndcPos = vec4(fragUV.x * 2.0 - 1.0, 1.0 - 2.0 * fragUV.y, depth, 1.0); - vec4 albedoAO = texture(gAlbedoAO, fragUV); - vec4 normalRg = texture(gNormalRough, fragUV); - vec4 metalEmit = texture(gMetalEmit, fragUV); // R16G16B16A16: .r=metallic, .gba=emissive + vec4 albedoAO = texelFetch(gAlbedoAO, px, 0); + vec4 normalRg = texelFetch(gNormalRough, px, 0); + vec4 metalEmit = texelFetch(gMetalEmit, px, 0); // R16G16B16A16: .r=metallic, .gba=emissive vec3 albedo = albedoAO.rgb; float bakedAO = albedoAO.a; // vertex AO baked into GBuffer — always [0.4, 1.0] @@ -308,8 +309,9 @@ void main() { vec3 Ldir = normalize(-uLightDir.xyz); float rawNdL = dot(N, Ldir); float wrapNdL = clamp(rawNdL * 0.85 + 0.15, 0.0, 1.0); - float shad = CalcShadow(worldPos, N); - shad *= smoothstep(-0.15, 0.0, rawNdL); + float shad = 0.0; + if (rawNdL > -0.15) + shad = CalcShadow(worldPos, N) * smoothstep(-0.15, 0.0, rawNdL); vec3 direct = uLightColor.rgb * uLightColor.a * wrapNdL * shad; for (uint li = 0u; li < uNumExtraLights; ++li) { vec3 Lx = normalize(-uExtraDir[li].xyz); diff --git a/assets/shaders/lighting_pass.frag.spv b/assets/shaders/lighting_pass.frag.spv index 8ed6ac244fa9337dd9f2b35ff29b90258c2f0bc6..f11ad18d1b7b469d531ba6bffe13968376b6c27b 100644 GIT binary patch literal 27080 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

#^Y6+w;Nllv@DK-qZ<}yJtEd3&97G8_#mujsHG! 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 literal 26696 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 From 140430d9407aee2d30091458da2535189bb18612 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:24:53 +0300 Subject: [PATCH 19/36] fix: apply ssao in the vulkan lighting pass when ibl is off The SSAO term was only used in the IBL branch, and IBL is never enabled on Vulkan, so SSAO had no visible effect. The hemisphere ambient now takes the baked AO times SSAO remapped to [0.5, 1], the same way the GL lighting pass applies it. With SSAO off the bound fallback is white and the output is unchanged. --- assets/shaders/lighting_pass.frag | 4 +++- assets/shaders/lighting_pass.frag.spv | Bin 27080 -> 27216 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/shaders/lighting_pass.frag b/assets/shaders/lighting_pass.frag index 118fabb9..71ff1a80 100644 --- a/assets/shaders/lighting_pass.frag +++ b/assets/shaders/lighting_pass.frag @@ -318,7 +318,9 @@ void main() { float NdLx = max(dot(N, Lx), 0.0); direct += uExtraColor[li].rgb * uExtraColor[li].a * NdLx; } - ambient = albedo * mix(groundAmbient, skyAmbient, hemisphere) * bakedAO; + // SSAO remapped like GL + float aoGL = bakedAO * (0.5 + 0.5 * ssao); + ambient = albedo * mix(groundAmbient, skyAmbient, hemisphere) * aoGL; Lo = albedo * direct; } diff --git a/assets/shaders/lighting_pass.frag.spv b/assets/shaders/lighting_pass.frag.spv index f11ad18d1b7b469d531ba6bffe13968376b6c27b..d49ee59335fb3472fc0579778bc67c9d2b9aa7ee 100644 GIT binary patch delta 919 zcmY*XJxfAS7=G1j-cnO5D>F5}O7o*Rq9BkAib`lNX=tc09D@*t))uFxhK6V>E))&z zjYSbOL`1(M=y~)UvI~d%J|E|K-+S(Pe6c;f+Pn^X(j{UOw|Hgd#(UvkG!2nzt-LCD zoMOlr@J?;NR#$ie_^?{99w?mnhn-TizWkp?pa?qNwuqDwQ?#Cg-)43aw8&8@FFg`L zH3dvguQ^ylc7gpPdgpZdU@==~saSw*F{#iH#XDi$r`F?*9+bV1D%dj*iMS7V6Zi!4 z6T5-872i~xM{(4&&x@K_)E2}cJPC5l<*fav*Hj;3pCJvocKjLqFkC6j#sT14;0^V$ z1Hk0;@`%|E9t3BDzt{;C=FrQ**k}kfo71O1GKktVwnR>qX&3~J6gd4k8k5u0kD%_J zeiZl}9^E7aOb>bPZ{kZst)HcEBMgRVI%yo3D$kPRMS$rcuRT%JdtIIwFn#2i&Ra!J z`{Ss|QRA;BR$4=n02zc)z^7#D4)# CXGX>V delta 811 zcmY+CKTE?<6vc1S#8%sAtF2n?A7fihQ3OX3J5|I*?MEn%DwU2dp@WNqiyy$jLAogc zo!lJ-Md&2r?BJ$@;5p$vS_6;V^SkHX`;zybUW~_Q!-<%ssE8qPain_dTqRf2mdJkV zu+pSR8_9Ld8nZC$(81x%vkmL|OU=o@|9krn;XnM%CXBzRrfF}!0 zA32Be%*nKW7BN}PN0JwSKqvEvJv3t>F9Y?O`qc%*b^ImPFsGZ7>-r*MHrK~6kC?}> zgvHz|-n=HP^=YI*7ncx|HSo1Ojxu60KAKe&V^hr*6vMu8FQRgd_aYeU>1X}{j6*9h NG50G*5Z~T({{ZQ6K(YV; From 156d108f43c8248067df4179097fedacc8f70572 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:24:53 +0300 Subject: [PATCH 20/36] fix: warn once when ibl is requested on vulkan Vulkan binds 1x1 stub IBL maps and never bakes them, so the lighting pass always takes the hemisphere ambient path. Requesting IBL was silently ignored; it now logs a one-time warning instead. --- src/Graphics/Vulkan/VulkanDeferred.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index d3def6f0..bad3b2b9 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -1172,6 +1172,13 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 5. Bind lighting pipeline vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_lightingPipeline); + // IBL is not baked here + static bool iblWarned = false; + if (m_iblEnabled && !iblWarned) { + SLEAK_WARN("Vulkan: IBL is not implemented, using hemisphere ambient"); + iblWarned = true; + } + // 6. Bind descriptor sets: // set 0: GBuffer samplers (RTs + shadow maps) // set 1: DeferredCB (InvViewProj + screen size) From 0964165b3a7a3a88d528462351cc72ceee79fd6c Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:28:39 +0300 Subject: [PATCH 21/36] fix: keep the imgui frame open on vulkan frames that fail to acquire When vkAcquireNextImageKHR failed, BeginRender returned before ImGui::NewFrame while the game still issued UI calls, which tripped the WithinFrameScope assert in Debug. Those frames now start a UI frame and EndRender closes it without drawing. Normal frames are unchanged. --- src/Graphics/Vulkan/VulkanRenderer.cpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 1b871f3c..58c268c2 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -169,8 +169,8 @@ bool VulkanRenderer::Initialize() { } /// Acquires a swapchain image and begins the shadow/GBuffer/forward passes -/// for the RenderCommandQueue to record into. A failed acquire returns early, -/// leaving deferred deletions unaged and the upload batch open. +/// for the RenderCommandQueue to record into. A failed acquire returns early +/// with only a UI frame open, leaving deletions unaged and uploads batched. void VulkanRenderer::BeginRender() { bFrameStarted = false; m_inGeometryPass = false; @@ -228,15 +228,27 @@ void VulkanRenderer::BeginRender() { // created and destroyed before a flush. VulkanBuffer::SetBatchingEnabled(true); + // UI frame for skipped frames + auto beginSkippedUIFrame = [this]() { + bImFrameActive = false; + if (!bImInitialized) return; + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + bImFrameActive = true; + }; + // Acquire the next image from the swapchain result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[m_semaphoreIndex], VK_NULL_HANDLE, &CurrentFrameIndex); if (result == VK_ERROR_OUT_OF_DATE_KHR) { RecreateSwapChain(); + beginSkippedUIFrame(); return; } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { SLEAK_ERROR("Failed to acquire swapchain image!"); + beginSkippedUIFrame(); return; } @@ -480,9 +492,14 @@ void VulkanRenderer::BeginRender() { } /// Ends the active render pass, submits the command buffer, and presents. +/// A skipped frame only closes its UI frame. void VulkanRenderer::EndRender() { - if (!bRender || !bFrameStarted) + if (!bRender || !bFrameStarted) { + // Skipped frame, no UI draw + if (bImFrameActive && bImInitialized) ImGui::EndFrame(); + bImFrameActive = false; return; + } const bool deferredPath = m_gbufferResourcesCreated && m_deferredEnabled && m_bloomResourcesCreated; From 67a3969741a456eabbce68b12c00a7b7ea04141f Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:35:46 +0300 Subject: [PATCH 22/36] perf: use a precomputed vogel disk for the vulkan shadow pcf Each of the 16 PCF taps evaluated sqrt, sin and cos of its own Vogel angle. The unrotated disk is now a constant table and the per-pixel rotation is a single sin/cos pair applied to every tap. The capture is bit-identical. --- assets/shaders/lighting_pass.frag | 20 +++++++++++++------- assets/shaders/lighting_pass.frag.spv | Bin 27216 -> 28016 bytes 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/assets/shaders/lighting_pass.frag b/assets/shaders/lighting_pass.frag index 71ff1a80..a8b99749 100644 --- a/assets/shaders/lighting_pass.frag +++ b/assets/shaders/lighting_pass.frag @@ -80,12 +80,16 @@ float InterleavedGradientNoise(vec2 screenPos) { return fract(magic.z * fract(dot(screenPos, magic.xy))); } -vec2 VogelDisk(int i, int count, float phi) { - const float goldenAngle = 2.39996323; - float r = sqrt((float(i) + 0.5) / float(count)); - float theta = float(i) * goldenAngle + phi; - return vec2(r * cos(theta), r * sin(theta)); -} +// r_i * (cos, sin)(i * 2.39996323), r_i = sqrt((i + 0.5) / 16) +const vec2 kVogel16[16] = vec2[]( + vec2( 0.176776692, 0.000000000), vec2(-0.225772178, 0.206825846), + vec2( 0.034557982, -0.393771189), vec2( 0.284571416, 0.371172631), + vec2(-0.522223197, -0.092373738), vec2( 0.494695337, -0.314684824), + vec2(-0.165465304, 0.615525170), vec2(-0.315562004, -0.607594155), + vec2( 0.684642316, 0.250029722), vec2(-0.712255912, 0.294009357), + vec2( 0.343354186, -0.733728746), vec2( 0.253730458, 0.808931929), + vec2(-0.764746789, -0.443184328), vec2( 0.897133617, -0.197234073), + vec2(-0.547505567, 0.778773187), vec2(-0.126488309, -0.976089476)); // Fixed-radius rotated PCF — crisp shadows like a classic forward renderer, // at a fraction of PCSS cost (no blocker search). Hardware comparison sampler @@ -93,9 +97,11 @@ vec2 VogelDisk(int i, int count, float phi) { const int PCF_SAMPLES = 16; float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) { + vec2 cs = vec2(cos(phi), sin(phi)) * filterRadius; float shadow = 0.0; for (int i = 0; i < PCF_SAMPLES; ++i) { - vec2 off = VogelDisk(i, PCF_SAMPLES, phi) * filterRadius; + vec2 d = kVogel16[i]; + vec2 off = vec2(d.x * cs.x - d.y * cs.y, d.x * cs.y + d.y * cs.x); shadow += textureLod(gShadow, vec3(uv + off, zRef), 0.0); } return shadow / float(PCF_SAMPLES); diff --git a/assets/shaders/lighting_pass.frag.spv b/assets/shaders/lighting_pass.frag.spv index d49ee59335fb3472fc0579778bc67c9d2b9aa7ee..dbe8ef1f5ae0ab7a8f0ad954cd9f3fd602d6e738 100644 GIT binary patch literal 28016 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 tHWIF8^1?Kl9pIn1$p&4JhYe4QA_ Date: Fri, 9 Oct 2026 01:43:27 +0300 Subject: [PATCH 23/36] fix: sample vulkan shadows with the matrix the map was rendered with The shadow pass is recorded in BeginRender with the light matrix staged during the previous update, but the light UBO was then filled with the newly computed matrix, so lighting and water sampled the map one snapped frustum step off while the camera moved. The UBO's LightVP now carries the matrix of the last recorded shadow pass. --- include/private/Graphics/Vulkan/VulkanRenderer.hpp | 3 +++ src/Graphics/Vulkan/VulkanRenderer.cpp | 3 +++ src/Graphics/Vulkan/VulkanShadow.cpp | 13 ++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 87ec1117..63979e1b 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -575,6 +575,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { // entire frame so shadow pass and main pass agree on the transform. float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Matrix the shadow map was last rendered with + float m_shadowMapLightVP[16] = {}; + bool m_shadowMapRendered = false; // Light/Shadow UBO (set 2, binding 0) VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 58c268c2..dc6405a4 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -335,6 +335,8 @@ void VulkanRenderer::BeginRender() { m_shadowPassActive = true; m_shadowPCCacheValid = false; m_shadowPCPushedLayout = VK_NULL_HANDLE; + memcpy(m_shadowMapLightVP, m_lightVP, sizeof(m_shadowMapLightVP)); + m_shadowMapRendered = true; auto* queue = RenderCommandQueue::GetInstance(); if (queue) { queue->ExecuteShadowPass(this); @@ -1624,6 +1626,7 @@ void VulkanRenderer::ApplyShadowResolutionChange() { m_shadowImageMemory = VK_NULL_HANDLE; m_shadowMapResolution = m_pendingShadowMapResolution; + m_shadowMapRendered = false; VkImageCreateInfo imageInfo{}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 76aa782b..0f4d6a86 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "Core/Logger.hpp" @@ -593,11 +594,21 @@ void VulkanRenderer::EndShadowPass() { m_shadowPassActive = false; } -/// Copies light and shadow data into the current frame's mapped UBO. +/// Copies light and shadow data into the current frame's mapped UBO. LightVP +/// is replaced by the matrix the shadow map was rendered with, since the map +/// is recorded in BeginRender before the caller stages a newer one. void VulkanRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { if (!m_lightUBOCreated || !data) return; uint32_t copySize = std::min(size, static_cast(sizeof(ShadowLightUBO))); memcpy(m_lightUBOMapped[currentFrame], data, copySize); + + constexpr size_t kLightVPOffset = offsetof(ShadowLightUBO, LightVP); + if (m_shadowMapRendered && + copySize >= kLightVPOffset + sizeof(m_shadowMapLightVP)) { + char* dst = static_cast(m_lightUBOMapped[currentFrame]); + memcpy(dst + kLightVPOffset, m_shadowMapLightVP, + sizeof(m_shadowMapLightVP)); + } } /// Stages the light view-projection matrix for commit at the next BeginRender. From 3f9928b40c1d0d0f3c6d9ea899f2a05dd5e25a30 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:59:38 +0300 Subject: [PATCH 24/36] feat: expose the light matrix the next shadow pass renders with LightManager::GetShadowLightVP returns the view-projection last staged on the renderer, which the next BeginRender commits for its shadow pass, so games can cull shadow casters against the light's box. --- include/public/Lighting/LightManager.hpp | 10 ++++++++++ src/Lighting/LightManager.cpp | 3 +++ 2 files changed, 13 insertions(+) diff --git a/include/public/Lighting/LightManager.hpp b/include/public/Lighting/LightManager.hpp index 02887700..cffded7e 100644 --- a/include/public/Lighting/LightManager.hpp +++ b/include/public/Lighting/LightManager.hpp @@ -72,6 +72,12 @@ namespace Sleak { /// Refreshes the deferred-pass constant buffer (fog, ambient) independent of the per-light data. void UpdateDeferredCB(); + /// Light view-projection the next shadow pass renders with (row-major, + /// row vectors), or nullptr when no light casts shadows. + const float* GetShadowLightVP() const { + return m_hasShadowLightVP ? m_shadowLightVP : nullptr; + } + void SetAmbientColor(float r, float g, float b); void SetAmbientIntensity(float intensity) { m_ambientIntensity = intensity; @@ -124,6 +130,10 @@ namespace Sleak { float m_ambientB = 0.1f; float m_ambientIntensity = 1.0f; + // Last light VP staged on the renderer + float m_shadowLightVP[16] = {}; + bool m_hasShadowLightVP = false; + // Fog bool m_fogEnabled = true; float m_fogR = 0.62f, m_fogG = 0.74f, m_fogB = 0.88f; // horizon (warm-cool sky tint) diff --git a/src/Lighting/LightManager.cpp b/src/Lighting/LightManager.cpp index a50dbe77..6d377630 100644 --- a/src/Lighting/LightManager.cpp +++ b/src/Lighting/LightManager.cpp @@ -203,6 +203,7 @@ void LightManager::UpdateShadowData() { DirectionalLight* activeLight = shadowLight ? shadowLight : anyDirLight; if (!activeLight) { + m_hasShadowLightVP = false; static bool warned = false; if (!warned) { SLEAK_WARN("UpdateShadowData: No directional light found!"); warned = true; } return; @@ -314,6 +315,8 @@ void LightManager::UpdateShadowData() { // Set the light VP matrix on the renderer renderer->SetLightVP(&lightVP(0, 0)); + std::memcpy(m_shadowLightVP, &lightVP(0, 0), sizeof(m_shadowLightVP)); + m_hasShadowLightVP = shadowLight != nullptr; // Build shadow light UBO const auto& camPos = Camera::GetMainCameraPosition(); From c9aea73eb2b03f7b97da1b41d061ca2f4b86e0c4 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 02:39:45 +0300 Subject: [PATCH 25/36] fix: give every vulkan bone palette bind its own uniform slot Each BindBoneBuffer copied into the same per-frame bone UBO at offset 0, so every skinned draw of a frame, including the skinned shadow casters recorded earlier in BeginRender, rendered with the last palette written. Set 1 is now a dynamic uniform buffer backed by a per-frame ring of 16 KB slots that grows in 32-slot chunks with their own buffer and set; each bind claims a slot and binds it by dynamic offset, and pass starts bind slot 0, which holds an identity palette. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 29 ++- src/Graphics/Vulkan/VulkanDeferred.cpp | 6 +- src/Graphics/Vulkan/VulkanDescriptors.cpp | 214 ++++++++++-------- src/Graphics/Vulkan/VulkanRenderer.cpp | 17 +- 4 files changed, 144 insertions(+), 122 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 63979e1b..89353b24 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -165,7 +165,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { virtual void BeginSkyboxPass() override; /// Restores the previous pipeline and descriptor set after the skybox draw. virtual void EndSkyboxPass() override; - /// Copies bone matrices into the current frame's UBO and binds its descriptor set. + /// Copies bone matrices into their own bone ring slot and binds it. virtual void BindBoneBuffer(RefPtr buffer) override; /// Binds the skinned pipeline matching the currently active render pass. virtual void BeginSkinnedPass() override; @@ -528,18 +528,29 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Destroys every cached custom-format pipeline (all variants, all formats). void DestroyCustomFormatPipelines(); - // Bone UBO (for skeletal animation — set 1, binding 0) + // Bone palette ring (set 1, binding 0, dynamic offset per bind) + struct BoneUBOChunk { + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + void* mapped = nullptr; + }; + static constexpr uint32_t BONE_PALETTE_BYTES = 256 * 64; + static constexpr uint32_t BONE_SLOTS_PER_CHUNK = 32; VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool boneDescriptorPool = VK_NULL_HANDLE; - std::array boneUBOBuffers = {}; - std::array boneUBOMemory = {}; - std::array boneUBOMapped = {}; - std::array boneDescriptorSets = {}; + std::array, MAX_FRAMES_IN_FLIGHT> m_boneChunks; + VkDeviceSize m_boneSlotStride = BONE_PALETTE_BYTES; + uint32_t m_boneSlot[MAX_FRAMES_IN_FLIGHT] = {}; bool m_boneUBOCreated = false; - /// Creates the per-frame bone UBO buffers and their descriptor sets. + /// Creates each frame's first bone ring chunk; slot 0 holds identity. bool CreateBoneUBOResources(); - /// Destroys the bone UBO buffers, memory, and descriptor pool. + /// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring. + bool AddBoneUBOChunk(uint32_t frame); + /// Destroys every bone ring chunk of every frame. void CleanupBoneUBOResources(); + /// Binds the identity palette (ring slot 0) at set 1. + void BindIdentityBonePalette(VkPipelineLayout layout); // ImGUI VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index bad3b2b9..bb6fdcef 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -1259,11 +1259,7 @@ void VulkanRenderer::BeginForwardTransparentPass() { pipelineLay, 0, 1, &descriptorSets[CurrentFrameIndex], 0, nullptr); } - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLay, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(pipelineLay); if (m_lightUBOCreated) { vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 2, 1, diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index eee77b8e..8d652c9d 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -34,7 +34,7 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { // Set 1: bone UBO (for skeletal animation) VkDescriptorSetLayoutBinding boneUBOBinding{}; boneUBOBinding.binding = 0; - boneUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + boneUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; boneUBOBinding.descriptorCount = 1; boneUBOBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; boneUBOBinding.pImmutableSamplers = nullptr; @@ -303,134 +303,151 @@ bool VulkanRenderer::CreateImGUI() { return true; } -/// Creates the per-frame bone UBO buffers and their descriptor sets. +/// Creates each frame's first bone ring chunk; slot 0 holds identity. bool VulkanRenderer::CreateBoneUBOResources() { if (m_boneUBOCreated) return true; - static constexpr uint32_t MAX_BONES = 256; - static constexpr VkDeviceSize boneUBOSize = MAX_BONES * 64; // 256 mat4 = 16384 bytes + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + const VkDeviceSize align = props.limits.minUniformBufferOffsetAlignment; + m_boneSlotStride = BONE_PALETTE_BYTES; + if (align > 0) + m_boneSlotStride = (m_boneSlotStride + align - 1) / align * align; - // Create per-frame UBO buffers (host-visible, coherent for fast CPU writes) for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - VkBufferCreateInfo bufferInfo{}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.size = boneUBOSize; - bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateBuffer(device, &bufferInfo, nullptr, &boneUBOBuffers[i]) != VK_SUCCESS) { - SLEAK_ERROR("Failed to create bone UBO buffer!"); - return false; - } - - VkMemoryRequirements memReqs; - vkGetBufferMemoryRequirements(device, boneUBOBuffers[i], &memReqs); - - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memReqs.size; - allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - - if (vkAllocateMemory(device, &allocInfo, nullptr, &boneUBOMemory[i]) != VK_SUCCESS) { - SLEAK_ERROR("Failed to allocate bone UBO memory!"); + if (!AddBoneUBOChunk(i)) { + CleanupBoneUBOResources(); return false; } - vkBindBufferMemory(device, boneUBOBuffers[i], boneUBOMemory[i], 0); - vkMapMemory(device, boneUBOMemory[i], 0, boneUBOSize, 0, &boneUBOMapped[i]); - - // Initialize with identity matrices - auto* matrices = static_cast(boneUBOMapped[i]); - for (uint32_t b = 0; b < MAX_BONES; ++b) { - // Identity matrix in column-major order + // Identity palette in slot 0 + auto* matrices = static_cast(m_boneChunks[i][0].mapped); + for (uint32_t b = 0; b < BONE_PALETTE_BYTES / 64; ++b) { for (int c = 0; c < 16; ++c) matrices[b * 16 + c] = (c % 5 == 0) ? 1.0f : 0.0f; } + m_boneSlot[i] = 1; } - // Create descriptor pool for bone UBO + m_boneUBOCreated = true; + SLEAK_INFO( + "VulkanRenderer: Bone UBO ring created ({} x {} bytes per frame)", + BONE_SLOTS_PER_CHUNK, m_boneSlotStride); + return true; +} + +/// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring: its +/// own buffer and dynamic-UBO set, so sets bound earlier stay valid. +bool VulkanRenderer::AddBoneUBOChunk(uint32_t frame) { + BoneUBOChunk chunk{}; + VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSize.descriptorCount = MAX_FRAMES_IN_FLIGHT; + poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + poolSize.descriptorCount = 1; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; - poolInfo.maxSets = MAX_FRAMES_IN_FLIGHT; - - if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &boneDescriptorPool) != VK_SUCCESS) { + poolInfo.maxSets = 1; + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &chunk.pool) != + VK_SUCCESS) { SLEAK_ERROR("Failed to create bone descriptor pool!"); return false; } - // Allocate descriptor sets - std::array layouts; - layouts.fill(boneDescriptorSetLayout); - VkDescriptorSetAllocateInfo dsAllocInfo{}; dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - dsAllocInfo.descriptorPool = boneDescriptorPool; - dsAllocInfo.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; - dsAllocInfo.pSetLayouts = layouts.data(); - - if (vkAllocateDescriptorSets(device, &dsAllocInfo, boneDescriptorSets.data()) != VK_SUCCESS) { - SLEAK_ERROR("Failed to allocate bone descriptor sets!"); + dsAllocInfo.descriptorPool = chunk.pool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &boneDescriptorSetLayout; + if (vkAllocateDescriptorSets(device, &dsAllocInfo, &chunk.set) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to allocate bone descriptor set!"); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); return false; } - // Write descriptor sets pointing to UBO buffers - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - VkDescriptorBufferInfo bufInfo{}; - bufInfo.buffer = boneUBOBuffers[i]; - bufInfo.offset = 0; - bufInfo.range = boneUBOSize; - - VkWriteDescriptorSet descriptorWrite{}; - descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - descriptorWrite.dstSet = boneDescriptorSets[i]; - descriptorWrite.dstBinding = 0; - descriptorWrite.dstArrayElement = 0; - descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - descriptorWrite.descriptorCount = 1; - descriptorWrite.pBufferInfo = &bufInfo; + // Host-visible, coherent for fast CPU writes + const VkDeviceSize size = m_boneSlotStride * BONE_SLOTS_PER_CHUNK; + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &bufferInfo, nullptr, &chunk.buffer) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to create bone UBO buffer!"); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); + return false; + } - vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); + VkMemoryRequirements memReqs; + vkGetBufferMemoryRequirements(device, chunk.buffer, &memReqs); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = FindMemoryType( + memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (vkAllocateMemory(device, &allocInfo, nullptr, &chunk.memory) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to allocate bone UBO memory!"); + vkDestroyBuffer(device, chunk.buffer, nullptr); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); + return false; } + vkBindBufferMemory(device, chunk.buffer, chunk.memory, 0); + vkMapMemory(device, chunk.memory, 0, size, 0, &chunk.mapped); - m_boneUBOCreated = true; - SLEAK_INFO("VulkanRenderer: Bone UBO resources created ({} bytes per frame)", boneUBOSize); + // One palette range; binds select the slot by dynamic offset + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = chunk.buffer; + bufInfo.offset = 0; + bufInfo.range = BONE_PALETTE_BYTES; + + VkWriteDescriptorSet descriptorWrite{}; + descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrite.dstSet = chunk.set; + descriptorWrite.dstBinding = 0; + descriptorWrite.dstArrayElement = 0; + descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + descriptorWrite.descriptorCount = 1; + descriptorWrite.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); + + m_boneChunks[frame].push_back(chunk); + if (m_boneChunks[frame].size() > 1) + SLEAK_INFO("VulkanRenderer: frame {} bone ring grew to {} slots", frame, + m_boneChunks[frame].size() * BONE_SLOTS_PER_CHUNK); return true; } - -/// Destroys the bone UBO buffers, memory, and descriptor pool. +/// Destroys every bone ring chunk of every frame. void VulkanRenderer::CleanupBoneUBOResources() { - if (!m_boneUBOCreated) return; - - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - if (boneUBOMapped[i]) { - vkUnmapMemory(device, boneUBOMemory[i]); - boneUBOMapped[i] = nullptr; - } - if (boneUBOBuffers[i]) { - vkDestroyBuffer(device, boneUBOBuffers[i], nullptr); - boneUBOBuffers[i] = VK_NULL_HANDLE; - } - if (boneUBOMemory[i]) { - vkFreeMemory(device, boneUBOMemory[i], nullptr); - boneUBOMemory[i] = VK_NULL_HANDLE; + for (auto& chunks : m_boneChunks) { + for (BoneUBOChunk& chunk : chunks) { + if (chunk.mapped) vkUnmapMemory(device, chunk.memory); + if (chunk.buffer) vkDestroyBuffer(device, chunk.buffer, nullptr); + if (chunk.memory) vkFreeMemory(device, chunk.memory, nullptr); + if (chunk.pool) + vkDestroyDescriptorPool(device, chunk.pool, nullptr); } - } - if (boneDescriptorPool) { - vkDestroyDescriptorPool(device, boneDescriptorPool, nullptr); - boneDescriptorPool = VK_NULL_HANDLE; + chunks.clear(); } m_boneUBOCreated = false; } -/// Copies bone matrices into the current frame's UBO and binds its descriptor set. +/// Binds the identity palette (ring slot 0) at set 1. +void VulkanRenderer::BindIdentityBonePalette(VkPipelineLayout layout) { + if (!m_boneUBOCreated || m_boneChunks[currentFrame].empty()) return; + const uint32_t offset = 0; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 1, + 1, &m_boneChunks[currentFrame][0].set, 1, &offset); +} + +/// Copies bone matrices into this bind's own slot of the frame's bone ring +/// and binds set 1 at that slot, so every skinned draw keeps its palette. void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { if (!bFrameStarted) return; if (!buffer) return; @@ -447,12 +464,19 @@ void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { if (!data) return; uint32_t size = static_cast(vkBuf->GetSize()); - static constexpr uint32_t MAX_BONE_UBO_SIZE = 256 * 64; // MAX_BONES * sizeof(mat4) - if (size > MAX_BONE_UBO_SIZE) size = MAX_BONE_UBO_SIZE; + if (size > BONE_PALETTE_BYTES) size = BONE_PALETTE_BYTES; - // Copy bone matrices to mapped UBO (use currentFrame, not CurrentFrameIndex + // Claim the next ring slot (use currentFrame, not CurrentFrameIndex // which is the swapchain image index and can exceed MAX_FRAMES_IN_FLIGHT) - memcpy(boneUBOMapped[currentFrame], data, size); + const uint32_t slot = m_boneSlot[currentFrame]; + auto& chunks = m_boneChunks[currentFrame]; + const uint32_t chunkIdx = slot / BONE_SLOTS_PER_CHUNK; + if (chunkIdx >= chunks.size() && !AddBoneUBOChunk(currentFrame)) return; + const BoneUBOChunk& chunk = chunks[chunkIdx]; + const uint32_t offset = + static_cast((slot % BONE_SLOTS_PER_CHUNK) * m_boneSlotStride); + memcpy(static_cast(chunk.mapped) + offset, data, size); + m_boneSlot[currentFrame] = slot + 1; // Bind bone descriptor set at set index 1. // In the GBuffer geometry pass the active layout is m_gbufferGeomLayout; @@ -462,9 +486,7 @@ void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { VkPipelineLayout boneBindLayout = (m_inGeometryPass && m_gbufferGeomLayout != VK_NULL_HANDLE) ? m_gbufferGeomLayout : pipelineLay; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - boneBindLayout, 1, 1, - &boneDescriptorSets[currentFrame], - 0, nullptr); + boneBindLayout, 1, 1, &chunk.set, 1, &offset); } /// Creates the per-frame PBR material descriptor set (set 0 in GBuffer pass): diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index dc6405a4..27992685 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -285,6 +285,7 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame + m_boneSlot[currentFrame] = 1; // slot 0 is the identity palette BeginGpuFrame(); // Skip shadow pass if no cached draws — preserve previous frame's shadow map @@ -312,11 +313,7 @@ void VulkanRenderer::BeginRender() { // (skinning conditioned on boneWeights). The shader must have set 1 bound even for // non-skinned casters, otherwise vkCmdDrawIndexed fires VUID-vkCmdDrawIndexed-None-08600. // Bind the bone UBO once at pass start so all shadow draws (skinned or static) are legal. - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLay, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(pipelineLay); VkViewport shadowViewport{}; shadowViewport.x = 0.0f; @@ -386,15 +383,11 @@ void VulkanRenderer::BeginRender() { // Bind descriptor sets for GBuffer geometry pass. // Set 0 (PBR material) is bound per-material by BindPBRMaterial(). - // Set 1 (bone matrices) is frame-constant: bound here so m_skinnedGbufferPipeline - // can always find a valid set 1, even for frames where no skinned draw fires. + // Set 1 (bone matrices) starts at the identity palette so + // m_skinnedGbufferPipeline always finds a valid set 1. // Sets 2-3 are frame-constant: light/shadow UBO and shadow samplers. if (m_gbufferGeomLayout != VK_NULL_HANDLE && m_lightUBOCreated) { - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - m_gbufferGeomLayout, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(m_gbufferGeomLayout); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferGeomLayout, 2, 1, &m_lightUBODescriptorSets[currentFrame], 0, nullptr); From e92204c75ff706b0fb56881566a3a5064a9dbf8f Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 11:16:02 +0300 Subject: [PATCH 26/36] fix: sample opengl shadows with the matrix the map was rendered with The shadow map is rendered in BeginRender with the light matrix staged during the previous update, but the shadow UBO then received the newer matrix, so shadows slid by one snapped frustum step while moving. Keep the matrix used for the last shadow pass and upload that one instead, matching the Vulkan fix. --- include/private/Graphics/OpenGL/OpenGLRenderer.hpp | 5 +++++ src/Graphics/OpenGL/OpenGLRenderer.cpp | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index eec29d7d..b7f6b4a0 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -123,9 +123,14 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { float m_lightVP[16] = {}; float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Matrix the shadow map was last rendered with + float m_shadowMapLightVP[16] = {}; + bool m_shadowMapRendered = false; bool m_inShadowPass = false; GLuint m_shadowTransformUBO = 0; void SetLightVP(const float* mat) override; + /// Uploads light and shadow data, sampling with the matrix the shadow + /// map was rendered with. void UpdateShadowLightUBO(const void* data, uint32_t size) override; /// Allocates the shadow-pass depth FBO and texture at the configured resolution. bool CreateShadowMapResources(); diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 9f9dfdac..89773c80 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -195,6 +195,7 @@ void OpenGLRenderer::Cleanup() { if (m_shadowDepthTex) { glDeleteTextures(1, &m_shadowDepthTex); m_shadowDepthTex = 0; } if (m_shadowFBO) { glDeleteFramebuffers(1, &m_shadowFBO); m_shadowFBO = 0; } m_shadowMapCreated = false; + m_shadowMapRendered = false; m_shadowUBOCreated = false; CleanupMSAAFramebuffer(); if (bImInitialized) { @@ -688,7 +689,10 @@ void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { const auto* ubo = static_cast(data); RenderEngine::PCSSShadowGPUData shadowData{}; - memcpy(shadowData.LightVP, ubo->LightVP, sizeof(float) * 16); + // Sample with the map's own matrix + memcpy(shadowData.LightVP, + m_shadowMapRendered ? m_shadowMapLightVP : ubo->LightVP, + sizeof(float) * 16); memcpy(shadowData.NdcToShadow, ubo->NdcToShadow, sizeof(float) * 16); shadowData.ShadowBias = ubo->ShadowBias; shadowData.ShadowStrength = ubo->ShadowStrength; @@ -780,6 +784,8 @@ void OpenGLRenderer::RenderShadowPass() { queue->ExecuteShadowPass(this); } m_inShadowPass = false; + memcpy(m_shadowMapLightVP, m_lightVP, sizeof(m_shadowMapLightVP)); + m_shadowMapRendered = true; // Restore state glDisable(GL_POLYGON_OFFSET_FILL); From 1919fd8e998a426d5ba0e789d0defe2bd8ea16ee Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 18 Oct 2026 23:11:01 +0300 Subject: [PATCH 27/36] refactor: replace bare literals with named constants Shadow depth bias, push constant size, bone palette size, the capture default frame, the staging probe size and the FNV hash constants are now named, and the pipeline cache header is read through the offsets of VkPipelineCacheHeaderVersionOne instead of hand-counted byte offsets. --- .../Graphics/Vulkan/VulkanRenderer.hpp | 11 ++++++- src/Core/Application.cpp | 15 ++++++--- src/Graphics/OpenGL/OpenGLRenderer.cpp | 2 +- src/Graphics/Vulkan/VulkanBuffer.cpp | 3 +- src/Graphics/Vulkan/VulkanDescriptors.cpp | 2 +- src/Graphics/Vulkan/VulkanPipelines.cpp | 33 ++++++++++++------- src/Graphics/Vulkan/VulkanProfiler.cpp | 5 +-- src/Graphics/Vulkan/VulkanRenderer.cpp | 6 ++-- src/Graphics/Vulkan/VulkanShadow.cpp | 4 +-- 9 files changed, 54 insertions(+), 27 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 89353b24..39daf049 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -9,6 +9,7 @@ #include #include "Graphics/Vulkan/VulkanBuffer.hpp" #include +#include #include #include #include @@ -16,6 +17,12 @@ #include #include +// Shadow caster depth bias +#define SHADOW_DEPTH_BIAS_CONSTANT 1.25f +#define SHADOW_DEPTH_BIAS_SLOPE 1.75f +// WVP + World matrices +#define PUSH_CONSTANT_BYTES (sizeof(float) * 16 * 2) + namespace Sleak { class ENGINE_API Window; namespace RenderEngine { @@ -536,7 +543,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkDeviceMemory memory = VK_NULL_HANDLE; void* mapped = nullptr; }; - static constexpr uint32_t BONE_PALETTE_BYTES = 256 * 64; + static constexpr uint32_t BONE_MATRIX_BYTES = sizeof(float) * 16; + static constexpr uint32_t BONE_PALETTE_BYTES = + MAX_BONES * BONE_MATRIX_BYTES; static constexpr uint32_t BONE_SLOTS_PER_CHUNK = 32; VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; std::array, MAX_FRAMES_IN_FLIGHT> m_boneChunks; diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 6cfed951..478e352d 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -34,6 +34,8 @@ #include #include +#define DEFAULT_CAPTURE_FRAME 600 + using namespace Sleak; using namespace Sleak::Math; @@ -158,11 +160,14 @@ namespace Sleak { // Frame capture const std::string capturePath = CommandLine::GetValue("-capture"); const bool captureMode = !capturePath.empty(); - uint64_t captureFrame = 600; - try { - captureFrame = - std::stoull(CommandLine::GetValue("-capture-frame", "600")); - } catch (...) { + uint64_t captureFrame = DEFAULT_CAPTURE_FRAME; + const std::string frameArg = + CommandLine::GetValue("-capture-frame"); + if (!frameArg.empty()) { + try { + captureFrame = std::stoull(frameArg); + } catch (...) { + } } if (captureMode) SLEAK_INFO( diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 89773c80..07344e60 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -692,7 +692,7 @@ void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { // Sample with the map's own matrix memcpy(shadowData.LightVP, m_shadowMapRendered ? m_shadowMapLightVP : ubo->LightVP, - sizeof(float) * 16); + sizeof(shadowData.LightVP)); memcpy(shadowData.NdcToShadow, ubo->NdcToShadow, sizeof(float) * 16); shadowData.ShadowBias = ubo->ShadowBias; shadowData.ShadowStrength = ubo->ShadowStrength; diff --git a/src/Graphics/Vulkan/VulkanBuffer.cpp b/src/Graphics/Vulkan/VulkanBuffer.cpp index 638310af..b2a89813 100644 --- a/src/Graphics/Vulkan/VulkanBuffer.cpp +++ b/src/Graphics/Vulkan/VulkanBuffer.cpp @@ -18,6 +18,7 @@ VmaAllocator VulkanBuffer::s_allocator = VK_NULL_HANDLE; // Staging pool whose blocks stay resident, so upload bursts never pay for // fresh vkAllocateMemory calls. static VmaPool g_stagingPool = VK_NULL_HANDLE; +#define STAGING_PROBE_BUFFER_BYTES (64 * 1024) static constexpr VkDeviceSize kStagingBlockSize = 64ull * 1024 * 1024; static constexpr size_t kStagingMinBlocks = 2; @@ -39,7 +40,7 @@ void VulkanBuffer::InitAllocator(VkInstance instance, // Staging pool VkBufferCreateInfo stagingInfo{}; stagingInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - stagingInfo.size = 64 * 1024; + stagingInfo.size = STAGING_PROBE_BUFFER_BYTES; stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VmaAllocationCreateInfo stagingAlloc{}; diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 8d652c9d..fcc8b37c 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -322,7 +322,7 @@ bool VulkanRenderer::CreateBoneUBOResources() { // Identity palette in slot 0 auto* matrices = static_cast(m_boneChunks[i][0].mapped); - for (uint32_t b = 0; b < BONE_PALETTE_BYTES / 64; ++b) { + for (uint32_t b = 0; b < BONE_PALETTE_BYTES / BONE_MATRIX_BYTES; ++b) { for (int c = 0; c < 16; ++c) matrices[b * 16 + c] = (c % 5 == 0) ? 1.0f : 0.0f; } diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 9596fe78..2f868dec 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -22,15 +22,17 @@ namespace { constexpr const char* kPipelineCacheDir = "cache"; constexpr const char* kPipelineCachePath = "cache/vulkan_pipelines.bin"; constexpr const char* kPipelineCacheTempPath = "cache/vulkan_pipelines.bin.tmp"; -// VkPipelineCacheHeaderVersionOne: 4 x uint32 + UUID -constexpr size_t kPipelineCacheHeaderSize = 16 + VK_UUID_SIZE; +constexpr size_t kPipelineCacheHeaderSize = + sizeof(VkPipelineCacheHeaderVersionOne); +#define FNV1A_OFFSET_BASIS 14695981039346656037ull +#define FNV1A_PRIME 1099511628211ull /// FNV-1a hash, used to skip rewriting an unchanged pipeline cache. uint64_t HashBytes(const std::vector& data) { - uint64_t hash = 14695981039346656037ull; + uint64_t hash = FNV1A_OFFSET_BASIS; for (char c : data) { hash ^= static_cast(c); - hash *= 1099511628211ull; + hash *= FNV1A_PRIME; } return hash; } @@ -68,11 +70,20 @@ void VulkanRenderer::CreatePipelineCache() { blob.size() >= kPipelineCacheHeaderSize && ReadLE32(blob.data()) >= kPipelineCacheHeaderSize && ReadLE32(blob.data()) <= blob.size() && - ReadLE32(blob.data() + 4) == + ReadLE32(blob.data() + + offsetof(VkPipelineCacheHeaderVersionOne, + headerVersion)) == static_cast(VK_PIPELINE_CACHE_HEADER_VERSION_ONE) && - ReadLE32(blob.data() + 8) == props.vendorID && - ReadLE32(blob.data() + 12) == props.deviceID && - std::memcmp(blob.data() + 16, props.pipelineCacheUUID, + ReadLE32(blob.data() + + offsetof(VkPipelineCacheHeaderVersionOne, vendorID)) == + props.vendorID && + ReadLE32(blob.data() + + offsetof(VkPipelineCacheHeaderVersionOne, deviceID)) == + props.deviceID && + std::memcmp(blob.data() + + offsetof(VkPipelineCacheHeaderVersionOne, + pipelineCacheUUID), + props.pipelineCacheUUID, VK_UUID_SIZE) == 0; if (!valid) { SLEAK_INFO( @@ -208,7 +219,7 @@ bool VulkanRenderer::CreateMainPipelineLayout() { VkPushConstantRange pushConstantRange{}; pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pushConstantRange.offset = 0; - pushConstantRange.size = 128; // sizeof(mat4) * 2 = 128 bytes (WVP + World) + pushConstantRange.size = PUSH_CONSTANT_BYTES; // Descriptor set layouts std::array setLayouts = { @@ -1016,8 +1027,8 @@ VkPipeline VulkanRenderer::BuildCustomFormatPipeline( rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; if (pass == CustomPass::Shadow) { rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; + rasterizer.depthBiasConstantFactor = SHADOW_DEPTH_BIAS_CONSTANT; + rasterizer.depthBiasSlopeFactor = SHADOW_DEPTH_BIAS_SLOPE; } // Multisampling diff --git a/src/Graphics/Vulkan/VulkanProfiler.cpp b/src/Graphics/Vulkan/VulkanProfiler.cpp index 69948d2b..df876763 100644 --- a/src/Graphics/Vulkan/VulkanProfiler.cpp +++ b/src/Graphics/Vulkan/VulkanProfiler.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -16,7 +17,7 @@ namespace { constexpr float kGpuLogIntervalSeconds = 2.0f; -constexpr std::array kGpuRowNames = { +constexpr std::array kGpuRowNames = { "shadow", "gbuffer", "ssao", "lighting", "forward", "taa", "ssr", "bloom", "tonemap", "ui", "other", "total"}; @@ -60,7 +61,7 @@ bool VulkanRenderer::CreateGpuProfiler() { } m_gpuTickNs = static_cast(props.limits.timestampPeriod); - m_gpuTickMask = validBits >= 64 ? ~0ull : ((1ull << validBits) - 1); + m_gpuTickMask = validBits >= std::numeric_limits::digits ? ~0ull : ((1ull << validBits) - 1); m_gpuSlots = {}; m_gpuSumMs = {}; m_gpuMaxMs = {}; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 27992685..a9c2b12f 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -789,7 +789,7 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, if (!data) return; uint32_t size = static_cast(vkBuf->GetSize()); - if (size > 128) size = 128; // Vulkan guarantees at least 128 bytes + if (size > PUSH_CONSTANT_BYTES) size = PUSH_CONSTANT_BYTES; // Choose the pipeline layout that owns the currently bound pipeline. // GBuffer geometry pass uses m_gbufferGeomLayout; all other passes use pipelineLay. @@ -817,7 +817,7 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, return; } - if (m_shadowPassActive && slot == 0 && size >= 128) { + if (m_shadowPassActive && slot == 0 && size >= PUSH_CONSTANT_BYTES) { // Shadow mode: push [LightVP*World (64)][World (64)]. // Buffer layout: [WVP (64 bytes)][World (64 bytes)]. // LightVP is frame-constant, so memoize on World and reuse the result @@ -846,7 +846,7 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, // Pushes survive pipeline binds if (m_shadowPCPushedLayout != activeLayout) { vkCmdPushConstants(command, activeLayout, - VK_SHADER_STAGE_VERTEX_BIT, 0, 128, + VK_SHADER_STAGE_VERTEX_BIT, 0, PUSH_CONSTANT_BYTES, m_shadowPCCache); m_shadowPCPushedLayout = activeLayout; } diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 0f4d6a86..79894589 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -328,8 +328,8 @@ bool VulkanRenderer::CreateShadowPipeline() { rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; // same winding as main pass rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; + rasterizer.depthBiasConstantFactor = SHADOW_DEPTH_BIAS_CONSTANT; + rasterizer.depthBiasSlopeFactor = SHADOW_DEPTH_BIAS_SLOPE; rasterizer.depthBiasClamp = 0.0f; VkPipelineMultisampleStateCreateInfo msaa{}; From 114447be9ad0da3ef2886c6bb217571b0e352585 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 19 Oct 2026 00:06:57 +0300 Subject: [PATCH 28/36] feat: let transparent vertex formats read the scene behind them VertexLayoutDesc gains transparentReadsScene. A format that sets it gets a snapshot of the scene color, depth, sky cubemap and a SceneReadUBO camera block in its transparent pass, so water and glass can refract and reflect. --- .../Graphics/Common/ConstantBuffer.hpp | 15 +++++++++++ include/public/Runtime/VertexLayout.hpp | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/include/private/Graphics/Common/ConstantBuffer.hpp b/include/private/Graphics/Common/ConstantBuffer.hpp index d96c6ebb..41a8af16 100644 --- a/include/private/Graphics/Common/ConstantBuffer.hpp +++ b/include/private/Graphics/Common/ConstantBuffer.hpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace Sleak { @@ -327,6 +328,20 @@ namespace Sleak { DeferredCBData gpuData; }; + + /// Camera block for transparent formats that read the scene behind + /// them (VertexLayoutDesc::transparentReadsScene), std140 layout. + struct alignas(16) SceneReadUBO { + float ViewProj[16]; + float InvViewProj[16]; + float ScreenWidth, ScreenHeight, InvScreenWidth, InvScreenHeight; + float JitterX, JitterY, NearPlane, FarPlane; // jitter in NDC + float SceneValid, SkyValid, _pad0, _pad1; + }; + static_assert( + offsetof(SceneReadUBO, _pad1) + sizeof(float) == + sizeof(SceneReadUBO), + "SceneReadUBO must match the std140 block without padding"); } } diff --git a/include/public/Runtime/VertexLayout.hpp b/include/public/Runtime/VertexLayout.hpp index a37a3a98..bc2db9a9 100644 --- a/include/public/Runtime/VertexLayout.hpp +++ b/include/public/Runtime/VertexLayout.hpp @@ -69,6 +69,29 @@ struct VertexAttribute { /// Sleak::VertexFormatRegistry::Register(desc); /// @endcode /// +/// Set `transparentReadsScene` for transparent geometry that refracts or +/// reflects what lies behind it, such as water. The first transparent draw +/// of the format in a frame snapshots the scene drawn so far, and its +/// shaders can then read it: +/// - Vulkan, descriptor set 3: binding 2 scene color (linear HDR), +/// binding 3 scene depth, binding 4 sky cubemap, binding 5 the +/// SceneReadUBO block below. +/// - OpenGL: texture unit 4 scene color (tonemapped, as shown), unit 5 +/// scene depth, unit 6 sky cubemap, uniform block binding 4 SceneReadUBO. +/// +/// @code{.glsl} +/// uniform SceneReadUBO { +/// mat4 ViewProj; // unjittered, row-major engine matrix +/// mat4 InvViewProj; +/// vec4 ScreenSize; // width, height, 1/width, 1/height +/// vec4 Camera; // TAA jitter in NDC (xy), near, far +/// vec4 Flags; // x: scene textures valid, y: sky valid +/// }; +/// @endcode +/// +/// Add `Camera.xy * gl_Position.w` to the clip position after the Vulkan Y +/// flip so the draw lines up with the jittered opaque geometry. +/// /// @see VertexFormatRegistry, VertexAttribute, VertexAttribFormat, MeshBatch /// @ingroup vertexformats struct VertexLayoutDesc { @@ -81,6 +104,8 @@ struct VertexLayoutDesc { std::string gbufferShaderStem; // Forward transparent variant; empty string skips the transparent pass. std::string transparentShaderStem; + // Transparent draws sample the opaque scene; see above. + bool transparentReadsScene = false; }; /// 0 is reserved for the engine's default vertex layout; registered From 5ee11a982dbefd07f572edaca77f7c18101bc9f3 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 19 Oct 2026 00:07:06 +0300 Subject: [PATCH 29/36] feat: snapshot the scene for scene-reading transparent draws on vulkan The first scene-reading transparent draw of a frame ends the forward pass, copies the HDR scene and depth into snapshot images and resumes the pass, so frames without such draws pay nothing. Set 3 gains the snapshot, sky cubemap and SceneReadUBO bindings, and the UBO carries the TAA jitter so the forward draw lines up with the jittered GBuffer depth. --- CMakeLists.txt | 1 + .../Graphics/Vulkan/VulkanRenderer.hpp | 64 +++ src/Graphics/Vulkan/VulkanDeferred.cpp | 6 + src/Graphics/Vulkan/VulkanDescriptors.cpp | 27 +- src/Graphics/Vulkan/VulkanPipelines.cpp | 10 + src/Graphics/Vulkan/VulkanRenderer.cpp | 13 + src/Graphics/Vulkan/VulkanSceneRead.cpp | 521 ++++++++++++++++++ src/Graphics/Vulkan/VulkanShadow.cpp | 6 +- src/Graphics/Vulkan/VulkanSwapchain.cpp | 6 +- 9 files changed, 638 insertions(+), 16 deletions(-) create mode 100644 src/Graphics/Vulkan/VulkanSceneRead.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d31a1255..ab76a230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,7 @@ set(GRAPHICS_VULKAN_SOURCES src/Graphics/Vulkan/VulkanPipelines.cpp src/Graphics/Vulkan/VulkanProfiler.cpp src/Graphics/Vulkan/VulkanRenderer.cpp + src/Graphics/Vulkan/VulkanSceneRead.cpp src/Graphics/Vulkan/VulkanShader.cpp src/Graphics/Vulkan/VulkanShadow.cpp src/Graphics/Vulkan/VulkanSSAO.cpp diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 39daf049..a25c9857 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -480,6 +480,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool m_skyboxDescriptorsWritten = false; VkImageView m_skyboxCubemapView = VK_NULL_HANDLE; // cached for MSAA re-bind VkSampler m_skyboxCubemapSampler = VK_NULL_HANDLE; // cached for MSAA re-bind + uint32_t m_skyboxViewSerial = 0; // bumped whenever the cached view changes // Skinned pipeline (uses skinned shaders with bone UBO) VkPipeline skinnedPipeline = VK_NULL_HANDLE; @@ -1095,6 +1096,69 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { void FinishFrameCapture(); /// Frees the capture readback buffer. void CleanupFrameCapture(); + + // ---- Scene read for transparent formats (set 3, bindings 2-5) ---- + // Set 3 holds the shadow map samplers first, then the scene-read inputs + static constexpr uint32_t SCENE_COLOR_BINDING = 2; + static constexpr uint32_t SCENE_DEPTH_BINDING = 3; + static constexpr uint32_t SCENE_SKY_BINDING = 4; + static constexpr uint32_t SCENE_UBO_BINDING = 5; + static constexpr uint32_t SET3_BINDING_COUNT = SCENE_UBO_BINDING + 1; + // Every set 3 binding but the UBO is a combined image sampler + static constexpr uint32_t SET3_SAMPLER_COUNT = SET3_BINDING_COUNT - 1; + static constexpr uint32_t SCENE_READ_IMAGE_COUNT = + SCENE_UBO_BINDING - SCENE_COLOR_BINDING; + // Light pool uniform buffers per frame: set 2 light UBO, set 3 SceneReadUBO + static constexpr uint32_t LIGHT_POOL_UBOS_PER_FRAME = 2; + // Snapshots of the HDR scene and depth taken mid forward pass + VkImage m_sceneColorCopyImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneColorCopyMemory = VK_NULL_HANDLE; + VkImageView m_sceneColorCopyView = VK_NULL_HANDLE; + VkImage m_sceneDepthCopyImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneDepthCopyMemory = VK_NULL_HANDLE; + VkImageView m_sceneDepthCopyView = VK_NULL_HANDLE; + // 1x1 stand-ins bound while there is no snapshot or sky + VkImage m_sceneFallbackImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneFallbackMemory = VK_NULL_HANDLE; + VkImageView m_sceneFallbackView = VK_NULL_HANDLE; + VkImage m_sceneFallbackCubeImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneFallbackCubeMemory = VK_NULL_HANDLE; + VkImageView m_sceneFallbackCubeView = VK_NULL_HANDLE; + VkSampler m_sceneLinearSampler = VK_NULL_HANDLE; + VkSampler m_scenePointSampler = VK_NULL_HANDLE; + std::array m_sceneReadUBOBuffers{}; + std::array m_sceneReadUBOMemory{}; + std::array m_sceneReadUBOMapped{}; + /// What one frame's set 3 bindings 2-4 last pointed at; a cleared entry + /// forces a rewrite. + struct SceneReadViews { + VkImageView color = VK_NULL_HANDLE; + VkImageView depth = VK_NULL_HANDLE; + VkImageView sky = VK_NULL_HANDLE; + uint32_t skySerial = 0; + }; + std::array m_sceneReadWritten{}; + bool m_sceneReadCreated = false; + bool m_sceneReadPrepared = false; // UBO filled (and copy taken) this frame + float m_cachedNearPlane = 0.0f; + float m_cachedFarPlane = 0.0f; + + /// Creates the scene-read samplers, stand-in images, and per-frame UBOs + /// and writes them into both frames' set 3. + bool CreateSceneReadResources(); + /// Destroys everything CreateSceneReadResources made plus the snapshots. + void CleanupSceneReadResources(); + /// Creates the extent-sized scene color and depth snapshot images. + bool CreateSceneCopyImages(); + /// Destroys the snapshot images and forces a set 3 rewrite. + void CleanupSceneCopyImages(); + /// Points a frame's set 3 bindings 2-4 at the current snapshot and sky. + void UpdateSceneReadDescriptors(uint32_t frame); + /// Fills this frame's SceneReadUBO and, inside the forward pass, splits it + /// to copy the scene. Runs once per frame on the first scene-reading draw. + void PrepareSceneRead(); + /// Records the scene color and depth copies between two forward passes. + void RecordSceneCopy(); }; } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index bb6fdcef..80b4ec38 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -1102,6 +1102,7 @@ void VulkanRenderer::CleanupGBufferResources() { CleanupSSAOResources(); CleanupSSRResources(); CleanupBloomResources(); + CleanupSceneCopyImages(); m_gbufferResourcesCreated = false; m_inGeometryPass = false; @@ -1296,6 +1297,11 @@ void VulkanRenderer::UpdateDeferredCB(const void* data, uint32_t size) { // exact inverse the lighting pass uses to reconstruct world from depth. if (copySize >= sizeof(m_cachedInvViewProj)) memcpy(m_cachedInvViewProj, data, sizeof(m_cachedInvViewProj)); + if (copySize >= sizeof(DeferredCBData)) { + const auto* cb = static_cast(data); + m_cachedNearPlane = cb->NearP; + m_cachedFarPlane = cb->FarP; + } // Compute sub-pixel Halton jitter for this frame (UV space). // Applied to WVP push constants in BindConstantBuffer during the geometry pass diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index fcc8b37c..94a3f34c 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -68,18 +68,21 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { // Set 3: shadow map samplers — binding 0 = compare sampler (PCF), // binding 1 = raw sampler (PCSS blocker search) - std::array shadowSamplerBindings{}; - shadowSamplerBindings[0].binding = 0; - shadowSamplerBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[0].descriptorCount = 1; - shadowSamplerBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[0].pImmutableSamplers = nullptr; - - shadowSamplerBindings[1].binding = 1; - shadowSamplerBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[1].descriptorCount = 1; - shadowSamplerBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[1].pImmutableSamplers = nullptr; + // Bindings 2-5: scene color, depth, sky, SceneReadUBO (scene-read formats) + std::array + shadowSamplerBindings{}; + for (uint32_t b = 0; b < shadowSamplerBindings.size(); ++b) { + shadowSamplerBindings[b].binding = b; + shadowSamplerBindings[b].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + shadowSamplerBindings[b].descriptorCount = 1; + shadowSamplerBindings[b].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + shadowSamplerBindings[b].pImmutableSamplers = nullptr; + } + shadowSamplerBindings[SCENE_UBO_BINDING].descriptorType = + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + shadowSamplerBindings[SCENE_UBO_BINDING].stageFlags = + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo shadowSamplerLayoutInfo{}; shadowSamplerLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 2f868dec..64795ac5 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -1151,6 +1151,16 @@ void VulkanRenderer::BeginCustomFormatPass(VertexFormatHandle format) { m_customFormatUnbound = target == VK_NULL_HANDLE; if (m_customFormatUnbound) return; + // Scene-reading shaders need set 3 bindings 2-5 written + if (pass == CustomPass::Transparent) { + const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); + if (desc && desc->transparentReadsScene) { + m_customFormatUnbound = !m_sceneReadCreated; + if (m_customFormatUnbound) return; + PrepareSceneRead(); + } + } + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, target); } diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index a9c2b12f..aa3f5c1f 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -134,6 +134,10 @@ bool VulkanRenderer::Initialize() { if (!CreateShadowLightUBOResources()) SLEAK_WARN("Failed to create light UBO resources — dynamic lighting disabled"); + if (!CreateSceneReadResources()) + SLEAK_WARN( + "Failed to create scene-read resources for transparent formats"); + if (!CreateShadowResources()) SLEAK_WARN("Failed to create shadow mapping resources — shadows disabled"); @@ -177,6 +181,7 @@ void VulkanRenderer::BeginRender() { m_inForwardTransparentPass = false; m_forwardPassOpen = false; m_activeCustomFormat = 0; + m_sceneReadPrepared = false; if (!bRender) return; @@ -203,6 +208,7 @@ void VulkanRenderer::BeginRender() { VulkanTexture::DestroyReleased(m_slotSerial[currentFrame]); } CollectGpuProfile(); + UpdateSceneReadDescriptors(currentFrame); // Clean up staging buffers from the previous use of this frame slot. // The fence wait above guarantees the GPU finished both the transfer @@ -923,6 +929,7 @@ ::Sleak::Texture* VulkanRenderer::CreateCubemapTexture( // Write cubemap to skybox descriptor sets m_skyboxCubemapView = texture->GetImageView(); m_skyboxCubemapSampler = texture->GetSampler(); + ++m_skyboxViewSerial; for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) { VkDescriptorImageInfo imageInfo{}; imageInfo.imageLayout = @@ -971,6 +978,7 @@ ::Sleak::Texture* VulkanRenderer::CreateCubemapTextureFromPanorama( // Write cubemap to skybox descriptor sets m_skyboxCubemapView = texture->GetImageView(); m_skyboxCubemapSampler = texture->GetSampler(); + ++m_skyboxViewSerial; for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) { VkDescriptorImageInfo imageInfo{}; imageInfo.imageLayout = @@ -1186,6 +1194,7 @@ void VulkanRenderer::Cleanup() { // Destroy shadow mapping resources CleanupShadowResources(); + CleanupSceneReadResources(); // Backstop: free any shader modules whose resource-guarded cleanup was // skipped (guard false while shader non-null). Cleanups null after delete, @@ -1849,6 +1858,10 @@ bool VulkanRenderer::CreateGBufferResources() { // GBuffer sampler set 0 binding 7 samples the SSAO blur result. if (!CreateSSAOResources()) { SLEAK_ERROR("GBuffer: SSAO resources failed!"); return false; } if (!CreateBloomResources()) { SLEAK_ERROR("GBuffer: bloom/HDR resources failed!"); return false; } + if (!CreateSceneCopyImages()) + SLEAK_WARN( + "GBuffer: scene snapshot images failed, transparent formats cannot " + "read the scene"); // SSR needs the HDR scene view (created by CreateBloomResources), so it // must come after that. The bloom composite pass later samples SSR. if (!CreateSSRResources()) { SLEAK_ERROR("GBuffer: SSR resources failed!"); return false; } diff --git a/src/Graphics/Vulkan/VulkanSceneRead.cpp b/src/Graphics/Vulkan/VulkanSceneRead.cpp new file mode 100644 index 00000000..afcb3a17 --- /dev/null +++ b/src/Graphics/Vulkan/VulkanSceneRead.cpp @@ -0,0 +1,521 @@ +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" + +namespace Sleak { +namespace RenderEngine { + +// ================================================================== +// Scene read for transparent formats (VertexLayoutDesc::transparentReadsScene) +// ================================================================== + +namespace { + +constexpr uint32_t SINGLE_LAYER = 1; +constexpr uint32_t CUBE_FACE_COUNT = 6; +constexpr VkExtent2D STAND_IN_EXTENT = {1, 1}; +constexpr float FAR_DEPTH = 1.0f; +// UV offsets span half the NDC range +constexpr float UV_TO_NDC = 2.0f; + +/// True when the depth format carries a stencil aspect. +bool HasStencil(VkFormat format) { + return format == VK_FORMAT_D32_SFLOAT_S8_UINT || + format == VK_FORMAT_D24_UNORM_S8_UINT || + format == VK_FORMAT_D16_UNORM_S8_UINT; +} + +/// Aspect mask covering every aspect of a depth format. +VkImageAspectFlags DepthAspects(VkFormat format) { + return VK_IMAGE_ASPECT_DEPTH_BIT | + (HasStencil(format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0); +} + +/// Picks a memory type matching the filter and property flags. +bool FindMemory(VkPhysicalDevice gpu, uint32_t typeBits, + VkMemoryPropertyFlags flags, uint32_t& index) { + VkPhysicalDeviceMemoryProperties props; + vkGetPhysicalDeviceMemoryProperties(gpu, &props); + for (uint32_t i = 0; i < props.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && + (props.memoryTypes[i].propertyFlags & flags) == flags) { + index = i; + return true; + } + } + return false; +} + +/// Creates a device-local 2D image, or a cube when given six layers, and +/// its view. +bool CreateSampledImage(VkDevice device, VkPhysicalDevice gpu, VkFormat format, + VkExtent2D extent, uint32_t layers, + VkImageUsageFlags usage, VkImageAspectFlags aspect, + VkImage& image, VkDeviceMemory& memory, + VkImageView& view) { + const bool cube = layers == CUBE_FACE_COUNT; + + VkImageCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + info.flags = cube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0; + info.imageType = VK_IMAGE_TYPE_2D; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = layers; + info.format = format; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + info.usage = usage; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateImage(device, &info, nullptr, &image) != VK_SUCCESS) + return false; + + VkMemoryRequirements req; + vkGetImageMemoryRequirements(device, image, &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + if (!FindMemory(gpu, req.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + alloc.memoryTypeIndex) || + vkAllocateMemory(device, &alloc, nullptr, &memory) != VK_SUCCESS) + return false; + vkBindImageMemory(device, image, memory, 0); + + VkImageViewCreateInfo vinfo{}; + vinfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + vinfo.image = image; + vinfo.viewType = cube ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_2D; + vinfo.format = format; + vinfo.subresourceRange = {aspect, 0, 1, 0, layers}; + return vkCreateImageView(device, &vinfo, nullptr, &view) == VK_SUCCESS; +} + +/// Destroys an image, its view, and its memory, nulling the handles. +void DestroyImage(VkDevice device, VkImage& image, VkDeviceMemory& memory, + VkImageView& view) { + if (view) vkDestroyImageView(device, view, nullptr); + if (image) vkDestroyImage(device, image, nullptr); + if (memory) vkFreeMemory(device, memory, nullptr); + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; +} + +/// Image barrier over every layer of a single-mip image. +VkImageMemoryBarrier MakeBarrier(VkImage image, VkImageAspectFlags aspect, + uint32_t layers, VkImageLayout from, + VkImageLayout to, VkAccessFlags src, + VkAccessFlags dst) { + VkImageMemoryBarrier b{}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = from; + b.newLayout = to; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.srcAccessMask = src; + b.dstAccessMask = dst; + b.image = image; + b.subresourceRange = {aspect, 0, 1, 0, layers}; + return b; +} + +/// One image to clear and leave in SHADER_READ_ONLY_OPTIMAL. +struct ClearTarget { + VkImage image; + VkImageAspectFlags aspect; + uint32_t layers; +}; + +/// Clears images to zero (depth to 1) and leaves them shader readable. +void ClearToShaderRead(VkDevice device, VkCommandPool pool, VkQueue queue, + const ClearTarget* targets, uint32_t count) { + VkCommandBufferAllocateInfo ca{}; + ca.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ca.commandPool = pool; + ca.commandBufferCount = 1; + VkCommandBuffer cmd = VK_NULL_HANDLE; + if (vkAllocateCommandBuffers(device, &ca, &cmd) != VK_SUCCESS) return; + + VkCommandBufferBeginInfo bi{}; + bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + + for (uint32_t i = 0; i < count; ++i) { + const ClearTarget& t = targets[i]; + const VkImageMemoryBarrier toDst = + MakeBarrier(t.image, t.aspect, t.layers, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &toDst); + + const VkImageSubresourceRange range = {t.aspect, 0, 1, 0, t.layers}; + if (t.aspect & VK_IMAGE_ASPECT_DEPTH_BIT) { + const VkClearDepthStencilValue far = {FAR_DEPTH, 0}; + vkCmdClearDepthStencilImage(cmd, t.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &far, 1, &range); + } else { + const VkClearColorValue zero = {}; + vkCmdClearColorImage(cmd, t.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, + &range); + } + + const VkImageMemoryBarrier toRead = MakeBarrier( + t.image, t.aspect, t.layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toRead); + } + vkEndCommandBuffer(cmd); + + VkSubmitInfo submit{}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE); + vkQueueWaitIdle(queue); + vkFreeCommandBuffers(device, pool, 1, &cmd); +} + +} // namespace + +/// Creates the scene-read samplers, stand-in images, and per-frame UBOs and +/// writes them into both frames' set 3 so the bindings are always valid. +bool VulkanRenderer::CreateSceneReadResources() { + if (m_sceneReadCreated) return true; + if (!m_lightUBOCreated) return false; + + // Samplers + VkSamplerCreateInfo sinfo{}; + sinfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sinfo.magFilter = VK_FILTER_LINEAR; + sinfo.minFilter = VK_FILTER_LINEAR; + sinfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sinfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.maxLod = 0.0f; + if (vkCreateSampler(device, &sinfo, nullptr, &m_sceneLinearSampler) != + VK_SUCCESS) + return false; + sinfo.magFilter = VK_FILTER_NEAREST; + sinfo.minFilter = VK_FILTER_NEAREST; + if (vkCreateSampler(device, &sinfo, nullptr, &m_scenePointSampler) != + VK_SUCCESS) + return false; + + // Stand-ins + const VkImageUsageFlags standInUsage = + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (!CreateSampledImage(device, physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, + STAND_IN_EXTENT, SINGLE_LAYER, standInUsage, + VK_IMAGE_ASPECT_COLOR_BIT, m_sceneFallbackImage, + m_sceneFallbackMemory, m_sceneFallbackView) || + !CreateSampledImage(device, physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, + STAND_IN_EXTENT, CUBE_FACE_COUNT, standInUsage, + VK_IMAGE_ASPECT_COLOR_BIT, m_sceneFallbackCubeImage, + m_sceneFallbackCubeMemory, + m_sceneFallbackCubeView)) { + SLEAK_ERROR("VulkanRenderer: scene-read stand-in images failed"); + return false; + } + const ClearTarget standIns[] = { + {m_sceneFallbackImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER}, + {m_sceneFallbackCubeImage, VK_IMAGE_ASPECT_COLOR_BIT, CUBE_FACE_COUNT}}; + ClearToShaderRead(device, commands, graphicsQueue, standIns, + static_cast(std::size(standIns))); + + // Per-frame UBOs + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + VkBufferCreateInfo binfo{}; + binfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + binfo.size = sizeof(SceneReadUBO); + binfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + binfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &binfo, nullptr, + &m_sceneReadUBOBuffers[i]) != VK_SUCCESS) + return false; + + VkMemoryRequirements req; + vkGetBufferMemoryRequirements(device, m_sceneReadUBOBuffers[i], &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + if (!FindMemory(physicalDevice, req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + alloc.memoryTypeIndex) || + vkAllocateMemory(device, &alloc, nullptr, + &m_sceneReadUBOMemory[i]) != VK_SUCCESS) + return false; + vkBindBufferMemory(device, m_sceneReadUBOBuffers[i], + m_sceneReadUBOMemory[i], 0); + vkMapMemory(device, m_sceneReadUBOMemory[i], 0, sizeof(SceneReadUBO), 0, + &m_sceneReadUBOMapped[i]); + std::memset(m_sceneReadUBOMapped[i], 0, sizeof(SceneReadUBO)); + + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = m_sceneReadUBOBuffers[i]; + bufInfo.range = sizeof(SceneReadUBO); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = m_shadowSamplerDescriptorSets[i]; + write.dstBinding = SCENE_UBO_BINDING; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + + m_sceneReadCreated = true; + m_sceneReadWritten = {}; + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + UpdateSceneReadDescriptors(i); + return true; +} + +/// Destroys everything CreateSceneReadResources made plus the snapshots. +void VulkanRenderer::CleanupSceneReadResources() { + CleanupSceneCopyImages(); + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + if (m_sceneReadUBOMapped[i]) + vkUnmapMemory(device, m_sceneReadUBOMemory[i]); + if (m_sceneReadUBOBuffers[i]) + vkDestroyBuffer(device, m_sceneReadUBOBuffers[i], nullptr); + if (m_sceneReadUBOMemory[i]) + vkFreeMemory(device, m_sceneReadUBOMemory[i], nullptr); + m_sceneReadUBOMapped[i] = nullptr; + m_sceneReadUBOBuffers[i] = VK_NULL_HANDLE; + m_sceneReadUBOMemory[i] = VK_NULL_HANDLE; + } + DestroyImage(device, m_sceneFallbackImage, m_sceneFallbackMemory, + m_sceneFallbackView); + DestroyImage(device, m_sceneFallbackCubeImage, m_sceneFallbackCubeMemory, + m_sceneFallbackCubeView); + if (m_sceneLinearSampler) + vkDestroySampler(device, m_sceneLinearSampler, nullptr); + if (m_scenePointSampler) + vkDestroySampler(device, m_scenePointSampler, nullptr); + m_sceneLinearSampler = VK_NULL_HANDLE; + m_scenePointSampler = VK_NULL_HANDLE; + m_sceneReadWritten = {}; + m_sceneReadCreated = false; +} + +/// Creates the extent-sized scene color and depth snapshot images, cleared +/// and shader readable until the first copy lands. +bool VulkanRenderer::CreateSceneCopyImages() { + CleanupSceneCopyImages(); + const VkImageUsageFlags usage = + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (!CreateSampledImage(device, physicalDevice, m_hdrSceneFormat, scExtent, + SINGLE_LAYER, usage, VK_IMAGE_ASPECT_COLOR_BIT, + m_sceneColorCopyImage, m_sceneColorCopyMemory, + m_sceneColorCopyView) || + !CreateSampledImage(device, physicalDevice, depthFormat, scExtent, + SINGLE_LAYER, usage, VK_IMAGE_ASPECT_DEPTH_BIT, + m_sceneDepthCopyImage, m_sceneDepthCopyMemory, + m_sceneDepthCopyView)) { + CleanupSceneCopyImages(); + return false; + } + const ClearTarget targets[] = { + {m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER}, + {m_sceneDepthCopyImage, DepthAspects(depthFormat), SINGLE_LAYER}}; + ClearToShaderRead(device, commands, graphicsQueue, targets, + static_cast(std::size(targets))); + return true; +} + +/// Destroys the snapshot images and forces a set 3 rewrite on both frames. +void VulkanRenderer::CleanupSceneCopyImages() { + DestroyImage(device, m_sceneColorCopyImage, m_sceneColorCopyMemory, + m_sceneColorCopyView); + DestroyImage(device, m_sceneDepthCopyImage, m_sceneDepthCopyMemory, + m_sceneDepthCopyView); + m_sceneReadWritten = {}; +} + +/// Points a frame's set 3 bindings 2-4 at the current snapshot and sky. +/// Call only while that frame's command buffer is not pending. +void VulkanRenderer::UpdateSceneReadDescriptors(uint32_t frame) { + if (!m_sceneReadCreated) return; + + SceneReadViews want; + want.color = + m_sceneColorCopyView ? m_sceneColorCopyView : m_sceneFallbackView; + want.depth = + m_sceneDepthCopyView ? m_sceneDepthCopyView : m_sceneFallbackView; + want.sky = + m_skyboxCubemapView ? m_skyboxCubemapView : m_sceneFallbackCubeView; + want.skySerial = m_skyboxViewSerial; + + SceneReadViews& have = m_sceneReadWritten[frame]; + std::array infos{}; + std::array writes{}; + uint32_t count = 0; + auto add = [&](uint32_t binding, VkImageView view, VkSampler sampler) { + infos[count].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + infos[count].imageView = view; + infos[count].sampler = sampler; + writes[count].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[count].dstSet = m_shadowSamplerDescriptorSets[frame]; + writes[count].dstBinding = binding; + writes[count].descriptorCount = 1; + writes[count].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[count].pImageInfo = &infos[count]; + ++count; + }; + if (have.color != want.color) + add(SCENE_COLOR_BINDING, want.color, m_sceneLinearSampler); + if (have.depth != want.depth) + add(SCENE_DEPTH_BINDING, want.depth, m_scenePointSampler); + if (have.sky != want.sky || have.skySerial != want.skySerial) + add(SCENE_SKY_BINDING, want.sky, + m_skyboxCubemapView ? m_skyboxCubemapSampler + : m_sceneLinearSampler); + if (count > 0) + vkUpdateDescriptorSets(device, count, writes.data(), 0, nullptr); + have = want; +} + +/// Fills this frame's SceneReadUBO and, inside the deferred forward pass, +/// snapshots the scene. Runs once per frame on the first scene-reading draw. +void VulkanRenderer::PrepareSceneRead() { + if (m_sceneReadPrepared || !m_sceneReadCreated) return; + m_sceneReadPrepared = true; + + const SceneReadViews& bound = m_sceneReadWritten[currentFrame]; + const bool sceneValid = m_forwardPassOpen && m_inForwardTransparentPass && + m_sceneColorCopyImage != VK_NULL_HANDLE && + bound.color == m_sceneColorCopyView && + bound.depth == m_sceneDepthCopyView; + if (sceneValid) RecordSceneCopy(); + + SceneReadUBO ubo{}; + Math::Matrix4 view, projection; + std::memcpy(&view(0, 0), m_cachedView, sizeof(m_cachedView)); + std::memcpy(&projection(0, 0), m_cachedProjection, + sizeof(m_cachedProjection)); + const Math::Matrix4 viewProj = view * projection; + std::memcpy(ubo.ViewProj, &viewProj(0, 0), sizeof(ubo.ViewProj)); + std::memcpy(ubo.InvViewProj, m_cachedInvViewProj, sizeof(ubo.InvViewProj)); + ubo.ScreenWidth = static_cast(std::max(scExtent.width, 1u)); + ubo.ScreenHeight = static_cast(std::max(scExtent.height, 1u)); + ubo.InvScreenWidth = 1.0f / ubo.ScreenWidth; + ubo.InvScreenHeight = 1.0f / ubo.ScreenHeight; + // Same UV-to-NDC scale the GBuffer push constants use + const bool jitter = + m_taaEnabled && m_taaResourcesCreated && m_inForwardTransparentPass; + ubo.JitterX = jitter ? m_taaJitter[0] * UV_TO_NDC : 0.0f; + ubo.JitterY = jitter ? m_taaJitter[1] * UV_TO_NDC : 0.0f; + ubo.NearPlane = m_cachedNearPlane; + ubo.FarPlane = m_cachedFarPlane; + ubo.SceneValid = static_cast(sceneValid); + ubo.SkyValid = static_cast(bound.sky != m_sceneFallbackCubeView); + std::memcpy(m_sceneReadUBOMapped[currentFrame], &ubo, sizeof(ubo)); +} + +/// Ends the forward pass, copies the HDR scene and depth into the +/// snapshots, and reopens the pass with its contents loaded. +void VulkanRenderer::RecordSceneCopy() { + vkCmdEndRenderPass(command); + + const VkImageAspectFlags depthAspects = DepthAspects(depthFormat); + const VkImageMemoryBarrier before[] = { + MakeBarrier(m_hdrSceneImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT), + MakeBarrier(depthImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT), + MakeBarrier(m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, + SINGLE_LAYER, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT), + MakeBarrier(m_sceneDepthCopyImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT)}; + vkCmdPipelineBarrier(command, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, static_cast(std::size(before)), + before); + + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {scExtent.width, scExtent.height, 1}; + vkCmdCopyImage(command, m_hdrSceneImage, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_sceneColorCopyImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + vkCmdCopyImage(command, depthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_sceneDepthCopyImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, ®ion); + + const VkImageMemoryBarrier after[] = { + MakeBarrier(m_hdrSceneImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT), + MakeBarrier(depthImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 0, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), + MakeBarrier(m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, + SINGLE_LAYER, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT), + MakeBarrier(m_sceneDepthCopyImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT)}; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, + static_cast(std::size(after)), after); + + VkRenderPassBeginInfo rpBegin{}; + rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBegin.renderPass = m_forwardRenderPass; + rpBegin.framebuffer = m_forwardFramebuffers[CurrentFrameIndex]; + rpBegin.renderArea.extent = scExtent; + vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + FillFullscreenViewportScissor(command, scExtent); +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 79894589..ffdbc3fe 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -418,11 +418,13 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { // Create descriptor pool for light UBO + shadow samplers. // Two shadow samplers per frame now (compare + raw for PCSS blocker search). + // Set 3 also carries the scene-read color, depth, sky, and UBO bindings. std::array poolSizes{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSizes[0].descriptorCount = MAX_FRAMES_IN_FLIGHT; + poolSizes[0].descriptorCount = + MAX_FRAMES_IN_FLIGHT * LIGHT_POOL_UBOS_PER_FRAME; poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * 2; + poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * SET3_SAMPLER_COUNT; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; diff --git a/src/Graphics/Vulkan/VulkanSwapchain.cpp b/src/Graphics/Vulkan/VulkanSwapchain.cpp index c6134b2e..1cba8a20 100644 --- a/src/Graphics/Vulkan/VulkanSwapchain.cpp +++ b/src/Graphics/Vulkan/VulkanSwapchain.cpp @@ -254,10 +254,12 @@ bool VulkanRenderer::CreateDepthResources() { imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Always add SAMPLED_BIT so deferred lighting pass can read depth. // When deferred is enabled, force 1x samples (GBuffer is non-MSAA). + // TRANSFER_SRC_BIT: scene-read snapshot for transparent formats. VkSampleCountFlagBits depthSamples = m_deferredEnabled ? VK_SAMPLE_COUNT_1_BIT : m_msaaSamples; - imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT; imageInfo.samples = depthSamples; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; From f0c7d2474a229c7fdee3db511f8ce96d8e474d81 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 19 Oct 2026 00:07:06 +0300 Subject: [PATCH 30/36] feat: snapshot the scene for scene-reading transparent draws on opengl The first scene-reading transparent draw of a frame copies the default framebuffer into a texture and binds it with the GBuffer depth, the sky cubemap and a SceneReadUBO. Rendering stays in the default framebuffer, so no offscreen HDR target is introduced. --- CMakeLists.txt | 1 + .../Graphics/OpenGL/OpenGLRenderer.hpp | 23 +++ src/Graphics/OpenGL/OpenGLRenderer.cpp | 28 ++-- src/Graphics/OpenGL/OpenGLSceneRead.cpp | 137 ++++++++++++++++++ 4 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/Graphics/OpenGL/OpenGLSceneRead.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ab76a230..cb569903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,7 @@ set(GRAPHICS_OPENGL_SOURCES src/Graphics/OpenGL/OpenGLCubemapTexture.cpp src/Graphics/OpenGL/OpenGLIBL.cpp src/Graphics/OpenGL/OpenGLRenderer.cpp + src/Graphics/OpenGL/OpenGLSceneRead.cpp src/Graphics/OpenGL/OpenGLShader.cpp src/Graphics/OpenGL/OpenGLTexture.cpp ) diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index b7f6b4a0..566a1156 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -206,6 +206,29 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { /// Lazily (re)bakes IBL data when the bound skybox cubemap changes. void EnsureIBL(); void CleanupIBL(); + + // ---- Scene read for transparent formats ---- + static constexpr GLuint SCENE_COLOR_UNIT = 4; + static constexpr GLuint SCENE_DEPTH_UNIT = 5; + static constexpr GLuint SCENE_SKY_UNIT = 6; + static constexpr GLuint SCENE_READ_UBO_BINDING = 4; + GLuint m_sceneCopyTex = 0; // RGBA8 snapshot of the default framebuffer + int m_sceneCopyWidth = 0; + int m_sceneCopyHeight = 0; + GLuint m_sceneReadUBO = 0; + GLuint m_sceneFallbackCube = 0; + GLuint m_sceneSkyCube = 0; // skybox cubemap found this frame, 0 if none + bool m_sceneReadPrepared = false; // snapshot taken this frame + float m_cachedInvViewProj[16] = {}; + float m_cachedNearPlane = 0.0f; + float m_cachedFarPlane = 0.0f; + /// Returns the active scene's skybox cubemap, or 0 when there is none. + GLuint FindSkyboxCubemap() const; + /// Snapshots the scene once per frame on the first scene-reading draw + /// and binds the snapshot, depth, sky, and SceneReadUBO. + void PrepareSceneRead(); + /// Deletes the snapshot texture, stand-in cubemap, and UBO. + void CleanupSceneRead(); }; } // namespace RenderEngine diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 07344e60..48c316d3 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -117,6 +117,7 @@ void OpenGLRenderer::SetupVertexLayout() { } void OpenGLRenderer::BeginRender() { + m_sceneReadPrepared = false; if (m_vsyncChangeRequested) ApplyVSyncChange(); if (m_msaaChangeRequested) @@ -189,6 +190,7 @@ void OpenGLRenderer::EndRender() { void OpenGLRenderer::Cleanup() { if (m_Initialized) { + CleanupSceneRead(); CleanupGBufferResources(); if (m_shadowTransformUBO) { glDeleteBuffers(1, &m_shadowTransformUBO); m_shadowTransformUBO = 0; } if (m_shadowUBO) { glDeleteBuffers(1, &m_shadowUBO); m_shadowUBO = 0; } @@ -370,6 +372,10 @@ void OpenGLRenderer::BindVertexBuffer(RefPtr buffer, const VertexLayoutDesc* customDesc = customFormat != 0 ? VertexFormatRegistry::Get(customFormat) : nullptr; + if (customDesc && customDesc->transparentReadsScene && + m_inForwardTransparentPass) + PrepareSceneRead(); + if (customDesc) { // One attribute pointer per registry entry, tracking which slots stay live const GLsizei stride = static_cast(customDesc->stride); @@ -939,20 +945,7 @@ void OpenGLRenderer::CleanupGBufferResources() { // IBL — lazy bake from active scene's skybox cubemap. // First call (or when the skybox cubemap changes) reruns the precompute. void OpenGLRenderer::EnsureIBL() { - auto* app = Application::GetInstance(); - if (!app) return; - auto* game = app->GetGame(); - if (!game) return; - auto* scene = game->GetActiveScene(); - if (!scene) return; - auto* skybox = scene->GetSkybox(); - if (!skybox || !skybox->IsInitialized()) return; - auto* cube = skybox->GetCubemapTexture(); - if (!cube) return; - auto* glCube = dynamic_cast(cube); - if (!glCube) return; - - GLuint cubeHandle = glCube->GetGLTexture(); + GLuint cubeHandle = FindSkyboxCubemap(); if (cubeHandle == 0) return; if (!m_ibl) m_ibl = new OpenGLIBL(); @@ -1054,6 +1047,13 @@ void OpenGLRenderer::BindPBRMaterial(Sleak::Material* material) { void OpenGLRenderer::UpdateDeferredCB(const void* data, uint32_t size) { if (!m_deferredCBCreated) return; + if (data && size >= sizeof(DeferredCBData)) { + const auto* cb = static_cast(data); + memcpy(m_cachedInvViewProj, cb->InvViewProj, + sizeof(m_cachedInvViewProj)); + m_cachedNearPlane = cb->NearPlane; + m_cachedFarPlane = cb->FarPlane; + } glBindBuffer(GL_UNIFORM_BUFFER, m_deferredCBUBO); glBufferSubData(GL_UNIFORM_BUFFER, 0, size, data); glBindBuffer(GL_UNIFORM_BUFFER, 0); diff --git a/src/Graphics/OpenGL/OpenGLSceneRead.cpp b/src/Graphics/OpenGL/OpenGLSceneRead.cpp new file mode 100644 index 00000000..6df289b9 --- /dev/null +++ b/src/Graphics/OpenGL/OpenGLSceneRead.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLRenderer.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +constexpr GLenum CUBE_FACE_COUNT = 6; +constexpr GLsizei STAND_IN_SIZE = 1; + +} // namespace + +// ================================================================== +// Scene read for transparent formats (VertexLayoutDesc::transparentReadsScene) +// ================================================================== + +/// Returns the active scene's skybox cubemap, or 0 when there is none. +GLuint OpenGLRenderer::FindSkyboxCubemap() const { + auto* app = Application::GetInstance(); + auto* game = app ? app->GetGame() : nullptr; + auto* scene = game ? game->GetActiveScene() : nullptr; + auto* skybox = scene ? scene->GetSkybox() : nullptr; + if (!skybox || !skybox->IsInitialized()) return 0; + auto* cube = + dynamic_cast(skybox->GetCubemapTexture()); + return cube ? cube->GetGLTexture() : 0; +} + +/// Snapshots the scene once per frame on the first scene-reading draw and +/// binds the snapshot, depth, sky, and SceneReadUBO for every such draw. +void OpenGLRenderer::PrepareSceneRead() { + // Creation binds go to the units rebound below + if (m_sceneFallbackCube == 0) { + glGenTextures(1, &m_sceneFallbackCube); + glActiveTexture(GL_TEXTURE0 + SCENE_SKY_UNIT); + glBindTexture(GL_TEXTURE_CUBE_MAP, m_sceneFallbackCube); + const uint32_t black = 0; + for (GLenum face = 0; face < CUBE_FACE_COUNT; ++face) + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, + STAND_IN_SIZE, STAND_IN_SIZE, 0, GL_RGBA, + GL_UNSIGNED_BYTE, &black); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0); + } + if (m_sceneCopyTex == 0) { + glGenTextures(1, &m_sceneCopyTex); + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + } + if (m_sceneReadUBO == 0) { + glGenBuffers(1, &m_sceneReadUBO); + glBindBuffer(GL_UNIFORM_BUFFER, m_sceneReadUBO); + glBufferData(GL_UNIFORM_BUFFER, sizeof(SceneReadUBO), nullptr, + GL_DYNAMIC_DRAW); + } + + if (!m_sceneReadPrepared) { + m_sceneReadPrepared = true; + m_sceneSkyCube = FindSkyboxCubemap(); + const int w = m_gbufferWidth; + const int h = m_gbufferHeight; + const bool valid = m_gbufferCreated && IsDeferredEnabled() && + m_gbufferDepth != 0 && w > 0 && h > 0; + + // Copy of the default framebuffer as drawn so far + if (valid) { + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + if (w != m_sceneCopyWidth || h != m_sceneCopyHeight) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + m_sceneCopyWidth = w; + m_sceneCopyHeight = h; + } + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); + } + + // Camera block + const Math::Matrix4 viewProj = + Camera::GetMainViewMatrix() * Camera::GetMainProjectionMatrix(); + SceneReadUBO ubo{}; + std::memcpy(ubo.ViewProj, &viewProj(0, 0), sizeof(ubo.ViewProj)); + std::memcpy(ubo.InvViewProj, m_cachedInvViewProj, + sizeof(ubo.InvViewProj)); + ubo.ScreenWidth = static_cast(std::max(w, 1)); + ubo.ScreenHeight = static_cast(std::max(h, 1)); + ubo.InvScreenWidth = 1.0f / ubo.ScreenWidth; + ubo.InvScreenHeight = 1.0f / ubo.ScreenHeight; + ubo.NearPlane = m_cachedNearPlane; + ubo.FarPlane = m_cachedFarPlane; + ubo.SceneValid = static_cast(valid); + ubo.SkyValid = static_cast(m_sceneSkyCube != 0); + glBindBuffer(GL_UNIFORM_BUFFER, m_sceneReadUBO); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(ubo), &ubo); + } + + glBindBufferBase(GL_UNIFORM_BUFFER, SCENE_READ_UBO_BINDING, m_sceneReadUBO); + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + glActiveTexture(GL_TEXTURE0 + SCENE_DEPTH_UNIT); + glBindTexture(GL_TEXTURE_2D, m_gbufferDepth); + glActiveTexture(GL_TEXTURE0 + SCENE_SKY_UNIT); + glBindTexture(GL_TEXTURE_CUBE_MAP, + m_sceneSkyCube != 0 ? m_sceneSkyCube : m_sceneFallbackCube); + glActiveTexture(GL_TEXTURE0); +} + +/// Deletes the snapshot texture, stand-in cubemap, and UBO. +void OpenGLRenderer::CleanupSceneRead() { + if (m_sceneCopyTex) glDeleteTextures(1, &m_sceneCopyTex); + if (m_sceneFallbackCube) glDeleteTextures(1, &m_sceneFallbackCube); + if (m_sceneReadUBO) glDeleteBuffers(1, &m_sceneReadUBO); + m_sceneCopyTex = 0; + m_sceneFallbackCube = 0; + m_sceneReadUBO = 0; + m_sceneCopyWidth = 0; + m_sceneCopyHeight = 0; +} + +} // namespace RenderEngine +} // namespace Sleak From 5d3dd88f0fe8ca53b58adc9c7be4d4b539c56c6f Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sat, 24 Oct 2026 16:26:33 +0300 Subject: [PATCH 31/36] feat(Render): add deferred rendering enabled --- include/public/Core/Application.hpp | 4 ++++ src/Core/Application.cpp | 3 +++ 2 files changed, 7 insertions(+) diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index 896cafaa..76df7df8 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -154,6 +154,10 @@ class ENGINE_API Application { bool GetVSync() const; void SetVSync(bool enabled); + /// Deferred rendering + const bool GetIsDeferredEnabled() const; + void SetDeferredEnabled(bool enabled); + // Screen-space ambient occlusion (deferred path only) bool IsSSAOEnabled() const; void SetSSAOEnabled(bool enabled); diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 478e352d..cd51b675 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -425,6 +425,9 @@ namespace Sleak { bool Application::GetVSync() const { return renderer->GetVSync(); } void Application::SetVSync(bool enabled) { renderer->SetVSync(enabled); } + const bool Application::GetIsDeferredEnabled() const { return renderer->GetDeferredEnabled(); } + void Application::SetDeferredEnabled(bool enable) { renderer->SetDeferredEnabled(enable); } + bool Application::IsSSAOEnabled() const { return renderer->IsSSAOEnabled(); } void Application::SetSSAOEnabled(bool e) { renderer->SetSSAOEnabled(e); } float Application::GetSSAORadius() const { return renderer->GetSSAORadius(); } From 2088c94a3b8689ad079aafd26078b466487ca5da Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 25 Oct 2026 14:42:42 +0300 Subject: [PATCH 32/36] feat: mark transparent draws as taa-reactive in the scene alpha --- include/public/Runtime/VertexLayout.hpp | 4 ++++ src/Graphics/Vulkan/VulkanPipelines.cpp | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/public/Runtime/VertexLayout.hpp b/include/public/Runtime/VertexLayout.hpp index bc2db9a9..5e97d5be 100644 --- a/include/public/Runtime/VertexLayout.hpp +++ b/include/public/Runtime/VertexLayout.hpp @@ -92,6 +92,10 @@ struct VertexAttribute { /// Add `Camera.xy * gl_Position.w` to the clip position after the Vulkan Y /// flip so the draw lines up with the jittered opaque geometry. /// +/// On Vulkan, transparent draws set the HDR scene alpha to 0. TAA reads that +/// as a reactive mask and trusts history less there, since animated or +/// refracted content is not described by the depth buffer. +/// /// @see VertexFormatRegistry, VertexAttribute, VertexAttribFormat, MeshBatch /// @ingroup vertexformats struct VertexLayoutDesc { diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 64795ac5..8f015598 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -1060,10 +1060,11 @@ VkPipeline VulkanRenderer::BuildCustomFormatPipeline( blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend.colorBlendOp = VK_BLEND_OP_ADD; - blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - blend.dstAlphaBlendFactor = pass == CustomPass::Transparent - ? VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA - : VK_BLEND_FACTOR_ZERO; + // Transparent draws zero the HDR scene alpha: TAA's reactive mask + const bool transparent = pass == CustomPass::Transparent; + blend.srcAlphaBlendFactor = transparent ? VK_BLEND_FACTOR_ZERO + : VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; blend.alphaBlendOp = VK_BLEND_OP_ADD; } std::array blends; From 095c4bcb8cbe7b62246d7e3cc78817915fe71c26 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 25 Oct 2026 14:42:42 +0300 Subject: [PATCH 33/36] fix: reproject vulkan taa in double precision and tighten its history rejection --- assets/shaders/taa.frag | 186 +++++++++++------- assets/shaders/taa.frag.spv | Bin 13380 -> 17948 bytes .../Graphics/Vulkan/VulkanInternal.hpp | 23 ++- .../Graphics/Vulkan/VulkanRenderer.hpp | 8 +- src/Graphics/Vulkan/VulkanTAA.cpp | 41 ++-- 5 files changed, 162 insertions(+), 96 deletions(-) diff --git a/assets/shaders/taa.frag b/assets/shaders/taa.frag index d68b3e0e..4dd2b4d2 100644 --- a/assets/shaders/taa.frag +++ b/assets/shaders/taa.frag @@ -22,16 +22,65 @@ layout(set = 0, binding = 1) uniform sampler2D historyTex; layout(set = 0, binding = 2) uniform sampler2D gDepth; layout(set = 1, binding = 0) uniform TAAParams { - mat4 InvCurrentVP; // inverse of unjittered current view-projection - mat4 PrevVP; // previous frame unjittered view-projection + mat4 CurrentToPrevClip; // inverse(current VP) * previous VP, unjittered vec2 ScreenSize; - float BlendFactor; // 0.1 = 10% current, 90% history (1.0 = disable history) + float BlendFactor; // current-frame weight when still (1.0 = history reset) float _pad; }; +// NOTE: Do not change this. Global constant for standard sRGB/BT.709 human perception weights +const vec3 LUMA_WEIGHTS = vec3(0.2126, 0.7152, 0.0722); + +const float SKY_DEPTH = 0.9999; +// Variance box half-size in standard deviations: lower = sharper, more flicker +const float VARIANCE_GAMMA = 1.0; +// Current-frame weight once a pixel moves MOTION_PX_FOR_MOVING pixels per frame +const float BLEND_MOVING = 0.2; +// Current-frame weight on fast-moving reactive pixels (transparent draws zero +// the scene alpha). Refraction/reflection parallax grows with camera motion, +// so a still or slow view keeps full history and steady glints. +const float BLEND_REACTIVE = 0.5; +const float MOTION_PX_FOR_MOVING = 2.0; +const float NEIGHBOURHOOD_SIZE = 9.0; +const float TONEMAP_INVERSE_FLOOR = 1e-3; +const float CLIP_EPSILON = 1e-5; + +float Luma(vec3 color) { return dot(color, LUMA_WEIGHTS); } + +// Karis weighting: blend in a compressed range so bright sky cannot dominate +// the average at leaf/sky edges. +vec3 Tonemap(vec3 color) { return color / (1.0 + Luma(color)); } +vec3 TonemapInverse(vec3 color) { + return color / max(1.0 - Luma(color), TONEMAP_INVERSE_FLOOR); +} + +vec3 RGBToYCoCg(vec3 c) { + return vec3( 0.25 * c.r + 0.5 * c.g + 0.25 * c.b, + 0.5 * c.r - 0.5 * c.b, + -0.25 * c.r + 0.5 * c.g - 0.25 * c.b); +} +vec3 YCoCgToRGB(vec3 c) { + return vec3(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z); +} + +vec3 FetchTonemappedYCoCg(ivec2 p, ivec2 maxIp) { + vec3 c = texelFetch(currentTex, clamp(p, ivec2(0), maxIp), 0).rgb; + return RGBToYCoCg(Tonemap(c)); +} + +// Pulls hist toward the box center until it touches the box. Unlike a +// per-channel clamp this never invents a colour that was not there. +vec3 ClipToAABB(vec3 hist, vec3 boxMin, vec3 boxMax) { + vec3 center = 0.5 * (boxMax + boxMin); + vec3 extents = max(0.5 * (boxMax - boxMin), vec3(CLIP_EPSILON)); + vec3 d = hist - center; + vec3 u = abs(d / extents); + float m = max(u.x, max(u.y, u.z)); + return m > 1.0 ? center + d / m : hist; +} + // Catmull-Rom history resample (Karis 5-tap): repeated bilinear resampling -// of the history buffer low-passes the whole image — this is what made the -// frame progressively blurry under camera motion. +// of the history buffer low-passes the whole image under camera motion. vec3 SampleHistoryCatmullRom(vec2 uv) { vec2 samplePos = uv * ScreenSize; vec2 texPos1 = floor(samplePos - 0.5) + 0.5; @@ -43,9 +92,9 @@ vec3 SampleHistoryCatmullRom(vec2 uv) { vec2 w3 = f * f * (-0.5 + 0.5 * f); vec2 w12 = w1 + w2; - vec2 tc0 = (texPos1 - 1.0) / ScreenSize; - vec2 tc3 = (texPos1 + 2.0) / ScreenSize; - vec2 tc12 = (texPos1 + w2 / w12) / ScreenSize; + vec2 tc0 = (texPos1 - 1.0) / ScreenSize; + vec2 tc3 = (texPos1 + 2.0) / ScreenSize; + vec2 tc12 = (texPos1 + w2 / w12) / ScreenSize; vec3 result = textureLod(historyTex, vec2(tc12.x, tc0.y ), 0.0).rgb * (w12.x * w0.y ) + @@ -59,30 +108,24 @@ vec3 SampleHistoryCatmullRom(vec2 uv) { } void main() { - vec2 texelSize = 1.0 / ScreenSize; - - ivec2 ip = ivec2(fragUV * ScreenSize); - ivec2 maxIp = ivec2(ScreenSize) - 1; - vec3 current = texelFetch(currentTex, clamp(ip, ivec2(0), maxIp), 0).rgb; - - // 3x3 depth via gathers - vec2 cuv = vec2(ip) * texelSize; - vec4 g00 = textureGather(gDepth, cuv); - float depth = g00.y; - if (depth >= 0.9999) { - outColor = vec4(current, 1.0); - return; - } + vec2 texelSize = 1.0 / ScreenSize; + ivec2 ip = ivec2(fragUV * ScreenSize); + ivec2 maxIp = ivec2(ScreenSize) - 1; + vec4 currentA = texelFetch(currentTex, clamp(ip, ivec2(0), maxIp), 0); + vec3 current = currentA.rgb; + float reactive = 1.0 - clamp(currentA.a, 0.0, 1.0); + + // 3x3 depth via gathers around the corner shared with the up-left texel + vec2 cuv = vec2(ip) * texelSize; + vec4 g00 = textureGather(gDepth, cuv); vec4 g10 = textureGatherOffset(gDepth, cuv, ivec2(1, 0)); vec4 g01 = textureGatherOffset(gDepth, cuv, ivec2(0, 1)); vec4 g11 = textureGatherOffset(gDepth, cuv, ivec2(1, 1)); - // Closest-depth dilation with UNFILTERED depth reads. Bilinear-filtered - // depth at silhouettes yields positions on neither surface — the source - // of edge shimmer at distance where every texel is a silhouette. + // Closest-depth dilation with unfiltered depth: edges follow the + // foreground surface, including sky pixels that border a leaf. vec2 closestOff = vec2(0.0); - float closestDepth = depth; - // x-major order, strict < + float closestDepth = g00.y; if (g00.w < closestDepth) { closestDepth = g00.w; closestOff = vec2(-1.0, -1.0); } if (g00.x < closestDepth) { closestDepth = g00.x; closestOff = vec2(-1.0, 0.0); } if (g01.x < closestDepth) { closestDepth = g01.x; closestOff = vec2(-1.0, 1.0); } @@ -92,59 +135,62 @@ void main() { if (g10.y < closestDepth) { closestDepth = g10.y; closestOff = vec2( 1.0, 0.0); } if (g11.y < closestDepth) { closestDepth = g11.y; closestOff = vec2( 1.0, 1.0); } - // Reproject the closest surface in the neighborhood and apply its motion - // to this pixel — edges follow the foreground surface. - // GBuffer stores UV.y=0 at screen top, so NDC.y = 1 - 2*uv.y - vec2 dilatedUV = fragUV + closestOff * texelSize; - vec4 clipPos = vec4(dilatedUV.x * 2.0 - 1.0, 1.0 - 2.0 * dilatedUV.y, - closestDepth, 1.0); - vec4 world4 = InvCurrentVP * clipPos; - vec3 worldPos = world4.xyz / world4.w; - - vec4 prevClip = PrevVP * vec4(worldPos, 1.0); - vec3 prevNDC = prevClip.xyz / prevClip.w; + // Whole neighbourhood is sky: nothing to reproject + if (closestDepth >= SKY_DEPTH) { + outColor = vec4(current, 1.0); + return; + } - vec2 prevUVd; - prevUVd.x = prevNDC.x * 0.5 + 0.5; - prevUVd.y = -prevNDC.y * 0.5 + 0.5; + // Reproject the closest surface with the previous frame's camera + vec2 dilatedUV = fragUV + closestOff * texelSize; + vec4 clipPos = vec4(dilatedUV.x * 2.0 - 1.0, 1.0 - 2.0 * dilatedUV.y, + closestDepth, 1.0); + vec4 prevClip = CurrentToPrevClip * clipPos; + vec3 prevNDC = prevClip.xyz / prevClip.w; + vec2 prevUVd = vec2(prevNDC.x * 0.5 + 0.5, -prevNDC.y * 0.5 + 0.5); vec2 velocity = dilatedUV - prevUVd; vec2 prevUV = fragUV - velocity; - // Disocclusion / off-screen — use current frame only - if (prevUV.x < 0.0 || prevUV.x > 1.0 || - prevUV.y < 0.0 || prevUV.y > 1.0) { + // Came from off-screen: no history exists + if (any(lessThan(prevUV, vec2(0.0))) || any(greaterThan(prevUV, vec2(1.0)))) { outColor = vec4(current, 1.0); return; } - // 3x3 neighborhood AABB to prevent ghosting. - // History samples outside this range are clamped — they indicate - // a surface that changed between frames (disocclusion, fast motion). - vec3 nMin = vec3(1e10); - vec3 nMax = vec3(-1e10); - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - vec3 s = texelFetch(currentTex, - clamp(ip + ivec2(x, y), ivec2(0), maxIp), 0).rgb; + // Neighbourhood statistics in tonemapped YCoCg + vec3 currentYC = vec3(0.0); + vec3 m1 = vec3(0.0); + vec3 m2 = vec3(0.0); + vec3 nMin = vec3(1e10); + vec3 nMax = vec3(-1e10); + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + vec3 s = FetchTonemappedYCoCg(ip + ivec2(x, y), maxIp); + if (x == 0 && y == 0) currentYC = s; + m1 += s; + m2 += s * s; nMin = min(nMin, s); nMax = max(nMax, s); } } - - vec3 rawHistory = SampleHistoryCatmullRom(prevUV); - vec3 history = clamp(rawHistory, nMin, nMax); - - // Adaptive blend: when history was clamped hard (disocclusion / fast motion), - // trust the current frame more. clampFactor approaches 1 when the raw history - // was far outside the neighborhood AABB. 0.5 floor: pure 100% current would - // let sub-pixel jitter shimmer through on hard-clamped pixels every frame. - // (Was 0.35 to hide shadow-edge shimmer — that is now handled by the wider - // PCF, and 0.35 smeared distant sub-pixel detail that clamps every frame.) - float aabbRange = max(length(nMax - nMin), 0.001); - float clampDist = length(rawHistory - history); - float clampFactor = clamp(clampDist / aabbRange * 2.0, 0.0, 1.0); - float adaptBlend = mix(BlendFactor, 0.5, clampFactor); - - vec3 resolved = mix(history, current, adaptBlend); - outColor = vec4(resolved, 1.0); + vec3 mean = m1 / NEIGHBOURHOOD_SIZE; + vec3 sigma = sqrt(max(m2 / NEIGHBOURHOOD_SIZE - mean * mean, vec3(0.0))); + + // Variance box, never wider than the real min/max of the neighbourhood + vec3 boxMin = max(nMin, mean - VARIANCE_GAMMA * sigma); + vec3 boxMax = min(nMax, mean + VARIANCE_GAMMA * sigma); + + vec3 historyYC = RGBToYCoCg(Tonemap(SampleHistoryCatmullRom(prevUV))); + historyYC = ClipToAABB(historyYC, boxMin, boxMax); + + // Still: lean on history for a converged, stable image. + // Moving: take more of the current frame so reprojection blur cannot build up. + float motionPx = length(velocity * ScreenSize); + float motion = clamp(motionPx / MOTION_PX_FOR_MOVING, 0.0, 1.0); + float motionBlend = mix(BlendFactor, max(BlendFactor, BLEND_MOVING), motion); + motionBlend = max(motionBlend, reactive * motion * BLEND_REACTIVE); + + vec3 resolvedYC = mix(historyYC, currentYC, motionBlend); + vec3 resolved = max(YCoCgToRGB(resolvedYC), vec3(0.0)); + outColor = vec4(TonemapInverse(resolved), 1.0); } diff --git a/assets/shaders/taa.frag.spv b/assets/shaders/taa.frag.spv index c7ec5c95c37e31070a2112521a86352205381b47..21e2bbb32199106252097e3598eaff7f5c10bbb2 100644 GIT binary patch literal 17948 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 AxBvhE literal 13380 zcmZ9S2Y_BxwT1s=GNFYM2)!ktgc?#P2{j>sfCdr>AYci@%mhXzGs9$(0E%QliV7%J zupkx`8x}+nm8M`h#*peBJca|e-|F_eeZDAT6>?p_ucp0l7H+NyW0k>)|l2N ztxa1C$F{0tVrwi)3vGNgFIv28@$^+AJ<|_7Xoe1(wK^(4eKv1x&>F|=SlQJ#py3I| zRE%5Tr%nP;2T@zC`_RX=Hf}9iynNA;C5sjxv%Gs~X!)v<3;TPQ_xBC-F7N3Z?(H7w zThpuGhJ|0>KzIM@p3QH=&3-H$?p>ogp8r$xoR@VE_x27f>$`|^if#vw?(ZGwIku~tGM|s(`79sm z>S3ztoFw4zTz6z1>Aip=XI8e|;43OyM{64MTCR@gcA$7>8_yTdYU2~cv)lNz%4b&P zerzgs?{r* zDxZg$skyhmefnq}t;f;&hT8t_l@-HX=bTDG=J*$UW!DAAW3KpEn%doNb``JVzr4=b z(b}}q)>N^#0-rO(a~ao~BwpA%G{T9`E7Rw&sAl`v+I`t{Pdgf(y{e)jRaRTAfk7vvnnUPhWr6NNs-8@>SRfXm~uiw>-MbtWpc?!cLd@9%+a_>9sCD-nu-Fs@6inY1-jQ$?l z<)y#(iFWI)@-^($&77@Z*SzsQxKYM!s${o&#Hdhbq_a9EGLdKfUf{vzh|F!qbl%$NE{!5;fj znRWh!p_bt_dkk)G)5m{+jjL{+e=?7$HS3f=QETRL?Q=EHdALtKVjo}g{kR*Ay8&Fy zVU8`C&FNlA4EMH2&SfX~@cK)Rnna~LgIza;*}a%`a`(Jia_tAVK66Ez36`rzJGkc2 ztYnUiw{JYp##q6}6_X{ks8fzKM*S&;OyVAG`I^?3us9)iUDux3=Gd@p}ZW zmSOzA!|jQC#C6V(p*_cuID1gQ;Fgyi8vAUx{dN6;VC(a~I+*^7eU^CW^E8U@1^0~4 zAir76CwCt|-r#jgj=8AAK}LH z+ap}N-yGq_zoxc96O-pDa5K--XzG{OG1r4# zr#?U}k1;>PP&bErSDu*v1~+3qi>99S{{gFcF2-wdJ$#-yF0HjOX!>b0kN1R{=Q08w z2ln|bH%2GeTEaJkyJiA+oc4|2{C{hIhQ2=A_32@~@4Ah_##~OfeNJxzR&yPfr+N-A zA(ro_3253K9!~mLr}wJ%)Ug@Zx(3PPb=(}Trd_@T*z1&jPlW5EZanXGHT^yBt--!? zvVI$|K4fb`l$QvFow?ub+3{8b$hUy?_uNk+}Z)IuD{O@d2Bm^)$U@w@3dVQ zYQF29!hbTe$1}vzwkt!;=bqR&_H8$?{W*y^=G+~u<`Hd=T2qd;XRX=erSrhVPLPDHlN}0 z?13Y|?g76O;(w&L;75U7cSn8Q=YjQ6_qrd=?BR9SHlLwp-^Izb0Nl*A5KTR`$#V|J zg4Yuxv78fY5!hIMW2Wxo;OgPWgUh;K0M|#|x)(EhShuzpGSsYFoLnb>o4HOzQ%~LU z)V&03-HGL#SWCfP_pehA&L(RIy$#ojInd?Pp>Zx0v zx=#mNcVamw)|p^?a0VlFz8I_?{t|Fm_eO9*D5si)Gbfl zBVg-JEa${p4K`NU?=^7s)V&s5)_nn7A9d@#h&gp@yO5z~-QwhW1-O~(m1ydzTb{Zv z23vPxIVaYuz_S@;-Iu`CQ};UXp^Q8SN5T52TlZzmsaxBn3^nT(C)caN&0LqGsi$sv z>b?SO-HGLFO@23A1@>Dm{59}7h5fbQ^1I@7aDCKAd7c~NYOry%ne$3!wbXkpSgm|! zy&kUa_lobBH-Odr&Ec|IyAH1IJ2|o52sc*Q=QqLC6YI@jdMDg<*Re+aF0el8_UL+MwfMgq ztd?)Xo4{%g4=2A{(uemT=zlmP=ks2$`cgXTchdX7Y8igttG^%3_}Z=IMrO5~-v_{+ zpWo3rzYn6R%Z+gh*me1~`Vd&nBie^+O*z^}YOVYR`zV_0_1A9S)3=Yo&8?5M-ps6) zzI`05mcD%gtmg1=D*N_H1pUjteG0CgzTE~^%Shiojb?o9)^aPeTCT}wz-r|_|14ZR zd-D#k+RoH9%Jp#Xe-5s$mRNVfZ)X_GTyp(l|2)`S<$dr4xcVhrBjbJ%td`t&fz`^~ zcf-}o-1orEZESPP^|R;R@ArYd2gAPv*57+g`iQ|z+}5hUIa1fpz}l_r7t9{krT%k=m!8-2G`h=fv9@U$0-{ zZ39i%`D{`wf(-(s~V))a8r|EX|e`;Ic!Zg73nC-AoQeY-o@_(#`vua)|q zweGdj$G-uXb278Gq^dj!1|~s=f2?N)NWjVH&!>U`S%B_g--{Y+wU6f z2Y~fa&$q%1u)5#T_D{c=@bpjnC|aI{{x+@M_ke%LIEdjlq<`N?-h*pB{1CY75`PZZ z_|qBs`c0yb=j3nQ+Ffh^4+Yzs@Wa49?-ToQxNGuFcLZ21BYWaVwB$6Var8B(zo%t9eA5Uu(vUc66=jnty97fVf_N?dhZM6m7m!<}my_kmu`t;Njn(51}@H zQ{05%{kSP}{-)#qM#%dre8&dgt-+@?_<;>Rzrjyv@Y5Rn%m(jj@N)}(K6tp`{-*pu z&F#78u{p!@bYJ;5jqMojtMYFe+t<2#*EKsZT&M1Jv?pqbxf56|e*^JYtdF|uy>4pp z_gSJAzE6YiSMzJ@di;BZnsuh`>0tMrBY6)1tLvYB&Hx)OKfup2xv8&-z2)`lx5kp|wxW?J&67Tt@0U96YBUsqYB5y8elOB-k9< zvi2x&`jWNt(A4$!JiJflF;dgf;PN~cH25(!PtC`|)ykR|!F}F3ay^a%tLvYdj|UrH zTWWp*xVZ;kh^DT8)-DEHt2WPLKC@cR<3w!oz4L3qwad2PipahG1&VoeR~Nw*E&6a zDVl!T=GV`imw_iS%ID5dKTEV>gGO^`7B0qpA9Z^cf)hNQ-2Sde%g||7u?Lf z0!=-+&jGtu-P~QwYRTOPHh1{T!S*fuJg~h=yne7g>Zxrd*f`o!+W@#(+aQ{HVhw>^ zt8Q)QGOL+;gn2Dv4a1lhF~5SL&xH(q#xuW?p&p-Cf$i_bjQCsvR?oFv2X>wM={&)= zV7`=LzIBYTj8R6u^DhHycX*6p=#!kU2HT^uuFK)-@woy#jZxNhC0yM-?zvo5VSYnl z_MX{@`85plYfE2V3oiTeI=JUt=D!-Qp8VH^k-I>I2}L7~0aS4}$HNw)E;2aM`O5!Mhk`O&^A2j9oBQgc zXzJJw-uFv_|<30F^Dp90&@vRAjk)zhm_gI%Yd zUfm8hzqa)1Ghq9rExr0Ixa`#(@aDey9GZG+x)W?a%bGq9S5L3L0Ct^vdi6!H`Ls2A zbr+g;hexwlcca;dvRC)O)l=8KVEb9tbstDTqN&H{Yv5^&vYxNQ)l<(mz^+rzx^IGASDx#);Og=DHrV`S?C-$U6Z=81 z>(ocW)RXTQVArW9-!H-D)RtJk0yod^*J$eT`3< +void MatMul4(const T A[16], const T B[16], T C[16]) { for (int r = 0; r < 4; ++r) for (int c = 0; c < 4; ++c) { - float s = 0.0f; + T s = T(0); for (int k = 0; k < 4; ++k) s += A[r * 4 + k] * B[k * 4 + c]; C[r * 4 + c] = s; } } /// Row-major 4x4 inverse via Gauss-Jordan elimination. -inline bool InvertMat4(const float M[16], float out[16]) { - float m[4][8]; +template +bool InvertMat4(const T M[16], T out[16]) { + constexpr T kSingularEpsilon = T(1e-7); + T m[4][8]; for (int r = 0; r < 4; ++r) { for (int c = 0; c < 4; ++c) m[r][c] = M[r * 4 + c]; - for (int c = 0; c < 4; ++c) m[r][4 + c] = (r == c) ? 1.0f : 0.0f; + for (int c = 0; c < 4; ++c) m[r][4 + c] = (r == c) ? T(1) : T(0); } for (int col = 0; col < 4; ++col) { - int pivot = -1; float maxv = 0.0f; + int pivot = -1; T maxv = T(0); for (int row = col; row < 4; ++row) { - float v = std::abs(m[row][col]); + T v = std::abs(m[row][col]); if (v > maxv) { maxv = v; pivot = row; } } - if (pivot < 0 || maxv < 1e-7f) return false; + if (pivot < 0 || maxv < kSingularEpsilon) return false; if (pivot != col) std::swap(m[col], m[pivot]); - float inv = 1.0f / m[col][col]; + T inv = T(1) / m[col][col]; for (int c = 0; c < 8; ++c) m[col][c] *= inv; for (int row = 0; row < 4; ++row) { if (row == col) continue; - float f = m[row][col]; + T f = m[row][col]; for (int c = 0; c < 8; ++c) m[row][c] -= f * m[col][c]; } } diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index a25c9857..93591a90 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -874,10 +874,9 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { // ---- TAA (Temporal Anti-Aliasing) resources ---- // Ping-pong history accumulation with depth-based reprojection - // and 3x3 neighborhood AABB clamping. + // and 3x3 YCoCg variance clipping. struct alignas(16) TAAParams { - float InvCurrentVP[16]; // inverse of unjittered current VP - float PrevVP[16]; // previous frame unjittered VP + float CurrentToPrevClip[16]; // inverse(current VP) * previous VP, unjittered float ScreenW, ScreenH; float BlendFactor; float _pad; @@ -886,6 +885,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool m_taaResourcesCreated = false; // m_taaEnabled is inherited from RenderEngine::Renderer (base class). uint64_t m_taaFrameIdx = 0; // global counter; ping-pong = idx % 2 + bool m_taaRanLastFrame = false; // false restarts accumulation VkImage m_taaImages[2] = {}; VkDeviceMemory m_taaMemory[2] = {}; @@ -910,7 +910,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkPipeline m_taaPipeline = VK_NULL_HANDLE; VulkanShader* m_taaShader = nullptr; - float m_prevViewProj[16] = {}; // previous frame unjittered VP (row-major) + double m_prevViewProj[16] = {}; // previous frame unjittered VP (row-major) float m_taaJitter[2] = {}; // current frame jitter in UV space /// Creates the ping-pong TAA history images, render pass, framebuffers, descriptors, and pipeline. diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 719a56a7..4a2a31bc 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -402,25 +402,36 @@ void VulkanRenderer::CleanupTAAResources() { m_taaResourcesCreated = false; } -/// Computes the inverse current view-projection and reprojection blend factor into the TAA UBO. +/// Computes the current-to-previous clip reprojection and blend factor into the TAA UBO. +/// Uses double: in float, inverse(VP) * VP drifts from identity far from the origin. void VulkanRenderer::UpdateTAAUBO() { if (!m_taaResourcesCreated || !m_taaUboMapped[currentFrame]) return; + double view[16], proj[16]; + for (int i = 0; i < 16; ++i) { + view[i] = static_cast(m_cachedView[i]); + proj[i] = static_cast(m_cachedProjection[i]); + } + // VP = View * Projection (row-major) - float currentVP[16]; - MatMul4(m_cachedView, m_cachedProjection, currentVP); + double currentVP[16]; + MatMul4(view, proj, currentVP); - float invCurrentVP[16]; - if (!InvertMat4(currentVP, invCurrentVP)) { - memset(invCurrentVP, 0, sizeof(invCurrentVP)); - for (int i = 0; i < 4; ++i) invCurrentVP[i * 4 + i] = 1.0f; // identity fallback + // First frame: identity reprojection + if (m_taaFrameIdx == 0) + memcpy(m_prevViewProj, currentVP, sizeof(m_prevViewProj)); + + double invCurrentVP[16]; + double currentToPrev[16]; + if (InvertMat4(currentVP, invCurrentVP)) { + MatMul4(invCurrentVP, m_prevViewProj, currentToPrev); + } else { + for (int i = 0; i < 16; ++i) currentToPrev[i] = (i / 4 == i % 4) ? 1.0 : 0.0; } TAAParams p{}; - memcpy(p.InvCurrentVP, invCurrentVP, sizeof(p.InvCurrentVP)); - // First frame: identity reprojection - memcpy(p.PrevVP, m_taaFrameIdx == 0 ? currentVP : m_prevViewProj, - sizeof(p.PrevVP)); + for (int i = 0; i < 16; ++i) + p.CurrentToPrevClip[i] = static_cast(currentToPrev[i]); p.ScreenW = static_cast(scExtent.width); p.ScreenH = static_cast(scExtent.height); // First two frames skip history @@ -436,7 +447,13 @@ void VulkanRenderer::UpdateTAAUBO() { /// Resolves the frame against TAA history into the ping-pong target. void VulkanRenderer::RenderTAAPass() { m_taaResolvedView = VK_NULL_HANDLE; - if (!m_taaResourcesCreated || !m_taaEnabled) return; + if (!m_taaResourcesCreated || !m_taaEnabled) { + m_taaRanLastFrame = false; + return; + } + // History and the previous VP are stale after TAA was off: restart accumulation + if (!m_taaRanLastFrame) m_taaFrameIdx = 0; + m_taaRanLastFrame = true; const uint32_t writeIdx = static_cast(m_taaFrameIdx % 2); const uint32_t readIdx = 1u - writeIdx; From 1f643ac5e0448d9a695c663270dee1684f4bc304 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 25 Oct 2026 14:42:42 +0300 Subject: [PATCH 34/36] feat: add rcas sharpening after taa on vulkan --- assets/shaders/bloom_composite.frag | 34 +++++++++++++++++++ assets/shaders/bloom_composite.frag.spv | Bin 3576 -> 7548 bytes include/private/Graphics/Common/Renderer.hpp | 5 +++ include/public/Core/Application.hpp | 5 +++ src/Core/Application.cpp | 4 +++ src/Graphics/Vulkan/VulkanBloom.cpp | 8 +++-- 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/assets/shaders/bloom_composite.frag b/assets/shaders/bloom_composite.frag index 86ba1213..2209626b 100644 --- a/assets/shaders/bloom_composite.frag +++ b/assets/shaders/bloom_composite.frag @@ -17,8 +17,41 @@ layout(push_constant) uniform BloomCompositePC { float exposure; // exposure multiplier for HDR scene float gamma; // 1.0 unless tonemapping is enabled uint ssrEnabled; // 0 skips the SSR fetch + float sharpenStrength; // post-TAA RCAS strength, 0 = off, 1 = strongest + float _pad0; + float _pad1; + float _pad2; }; +// AMD FidelityFX FSR1 RCAS: caps the negative lobe so sharpening never rings +const float RCAS_LIMIT = 0.25 - 1.0 / 16.0; +const float RCAS_EPSILON = 1e-4; +const float PERCEPTUAL_MAX = 1.0 - 1e-4; + +// Reversible per-channel Reinhard: RCAS expects values in [0, 1] +vec3 ToPerceptual(vec3 c) { return c / (1.0 + c); } +vec3 FromPerceptual(vec3 c) { return c / (1.0 - min(c, vec3(PERCEPTUAL_MAX))); } + +/// Robust contrast-adaptive sharpening of the centre texel e over its 4 neighbours. +vec3 SharpenRCAS(vec3 e, vec2 uv) { + vec3 b = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 0, -1)).rgb); + vec3 d = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2(-1, 0)).rgb); + vec3 f = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 1, 0)).rgb); + vec3 h = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 0, 1)).rgb); + vec3 c = ToPerceptual(e); + + vec3 mn4 = min(min(b, d), min(f, h)); + vec3 mx4 = max(max(b, d), max(f, h)); + vec3 hitMin = min(mn4, c) / max(4.0 * mx4, vec3(RCAS_EPSILON)); + vec3 hitMax = (1.0 - max(mx4, c)) / min(4.0 * mn4 - 4.0, vec3(-RCAS_EPSILON)); + vec3 lobeRGB = max(-hitMin, hitMax); + float lobe = max(-RCAS_LIMIT, min(max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)), 0.0)) + * sharpenStrength; + + vec3 sharp = (lobe * (b + d + f + h) + c) / (4.0 * lobe + 1.0); + return FromPerceptual(clamp(sharp, vec3(0.0), vec3(PERCEPTUAL_MAX))); +} + // ACES Filmic tonemap (Narkowicz 2015) vec3 ACESFilm(vec3 x) { const float a = 2.51; @@ -32,6 +65,7 @@ vec3 ACESFilm(vec3 x) { void main() { vec3 hdr = texture(sceneHDR, fragUV).rgb; + if (sharpenStrength > 0.0) hdr = SharpenRCAS(hdr, fragUV); // SSR: ssr.rgb = reflectedColor * Fresnel * fades (premultiplied), ssr.a = fades only. // Unpremultiply to get reflectedColor * Fresnel, then lerp over the base scene using diff --git a/assets/shaders/bloom_composite.frag.spv b/assets/shaders/bloom_composite.frag.spv index 52c3671e32f99d68b2cb5b02922ef423a1b2f1c5..94fa1b67c6844205a4fa34e5b8fcce96c799a785 100644 GIT binary patch literal 7548 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 literal 3576 zcmZ9NX>(Ln5QZ;ggAn#5A_5bFRUnASz6S^)sL=$%;)2FWGKA5YOq`h@C@zRm6b1ba ze)I=W28V|ShX%$bW@g4}^;@OFSgAN&7@I6s3lsI?je>EDGI7P}iPG$3HlU4&Y~D8N zT|$mi{R5+Ci>30m8`t(8#cm^aC-P>hZ$n<6tirb?JJD+sh3Ue%GZ#={S{wCWtLCRJ zUDobM4#1x-RVwBFN_nPIE7l7m{l>PZKJ{_^qxEWGda8cC;cIW~vqy!S?3%4|Blom5 zczZFGFPHNTt{H1rYqjdYbbh>4m~7U42+rjl&LHtOY8+_l7Ykg0PUD?fWwzd5DOIYd$soP3KRp_(u(veLhV863J+)h& z-t8PU**8~jQzXIJ6FFxWoc)knl;P}$To*96ceWN|!_L;?ef4|3e(kK^`$es*ImL5& z8ZWbX<~sCtxE$zazwcJtI&#?7CMSBZ7pJ=X4s2tWfhWJ@PKLb_`{Cn1j)lDrd-L3e zr~dWW$2+_G!oLxF`_<(q!uCF{ot$o7?;{t8IQx_&U-m`ZRxG)|daB)yZH)^+c7?rx zo4$a@|Jo{+;OGwX>H(uNKA!qy^r18#uEr0hTbHKF;kMBi) zpy_W-4#L@M{hq7scOmCFhmkp89{Ztv1jO9@4KUX8^eayQ*BYwtLt1}7(jHr9+}Ahf zihZx3$GNYf8*jZahu6_7tL`2N`!aU-qu&o{TUQ>q%r}m-F84=1->fU*Yv|c~-Nv@x zasC{7=ab%Jp7%YvwMG5fzInL=e2?z4_b$1=VA_!8_6=_X-bcG1c_w4)_ctc|+WwYg z?OUmRoc+F;7lH3Z+c)tt@I721t2OQg#>fv-foJUl^1|iat@{KOTc7?@?02TefbYOr zo5Q&t2lBrX<6KVyIp_T+XPid5{vrM(@)VG>7wS>_S#)O~wGX0aYd?o=jJ&nkD>-ZP zUAzY5d@r8ijE8{yZ)t6Bq?~WY+D4ErYtuIj%0$81aJ^1@{g z7J{g^j6T2KX*hX%=WHv$eLH|Y>$c|)(mBqco5#55(MRa^=1v+@MVB)+`X*;yqsu?yj6UQSK+fJc*E>j;bJh1HkaMo;QP)@K=8If+(dXy-8csgGfp5^AiSJO~ z9PqnxUf%+1Febi>dnvC^cuoiMJK#Cb0_!j??yt}JM9s>m`3H1sKA-0N5nav|+yitu z&kycl%J~LIs9+EBC*XeL^;^3&+(+vBmlF;F|0h|qb8;;P2Z>vR>;(3;3mG*oP5H2w zr*^!T71(miK=f%9`pOLA{jP>HUjJNL#~O5V=!+a{(XA!y?$nN)&tS`~1Ci6-ug@TI zZh$jhf8=})-5mO2|0eW}8N~k0aK`J8-`pI!b?b}YMt?`0k>7U?Yyn%rfAmwIz5gFp CFc*RV diff --git a/include/private/Graphics/Common/Renderer.hpp b/include/private/Graphics/Common/Renderer.hpp index f522405b..13203f02 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -192,6 +192,10 @@ class ENGINE_API Renderer { void SetTAAEnabled(bool enabled) { m_taaEnabled = enabled; } bool IsTAAEnabled() const { return m_taaEnabled; } + /// Contrast-adaptive sharpening applied after TAA, 0 = off, 1 = strongest. + void SetSharpenStrength(float strength) { m_sharpenStrength = strength; } + float GetSharpenStrength() const { return m_sharpenStrength; } + // Post-process: Bloom void SetBloomEnabled(bool enabled) { m_bloomEnabled = enabled; } bool IsBloomEnabled() const { return m_bloomEnabled; } @@ -324,6 +328,7 @@ class ENGINE_API Renderer { int m_ssrBinarySteps = 6; float m_ssrRoughnessThreshold = 0.9f; float m_taaBlendFactor = 0.1f; + float m_sharpenStrength = 0.0f; float m_bloomRadius = 1.0f; float m_bloomIntensity = 1.0f; diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index 76df7df8..7b038b07 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -182,6 +182,11 @@ class ENGINE_API Application { bool IsTAAEnabled() const; void SetTAAEnabled(bool enabled); + /// Post-TAA contrast-adaptive sharpening strength, 0 = off, 1 = strongest. + float GetSharpenStrength() const; + /// Sets post-TAA sharpening strength; only applied on frames TAA resolves. + void SetSharpenStrength(float strength); + // Bloom (deferred path only) bool IsBloomEnabled() const; void SetBloomEnabled(bool enabled); diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index cd51b675..63b5c150 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -448,6 +448,9 @@ namespace Sleak { bool Application::IsTAAEnabled() const { return renderer->IsTAAEnabled(); } void Application::SetTAAEnabled(bool e) { renderer->SetTAAEnabled(e); } + float Application::GetSharpenStrength() const { return renderer->GetSharpenStrength(); } + void Application::SetSharpenStrength(float s) { renderer->SetSharpenStrength(s); } + bool Application::IsBloomEnabled() const { return renderer->IsBloomEnabled(); } void Application::SetBloomEnabled(bool e) { renderer->SetBloomEnabled(e); } @@ -462,6 +465,7 @@ namespace Sleak { renderer->SetIBLEnabled(cfg.iblEnabled); renderer->SetIBLIntensity(cfg.iblIntensity); renderer->SetTAAEnabled(cfg.taaEnabled); + renderer->SetSharpenStrength(cfg.sharpenStrength); renderer->SetShadowMapResolution(cfg.shadowMapResolution); renderer->SetMSAASampleCount(cfg.msaaSamples); renderer->SetTonemappingEnabled(cfg.tonemapEnabled); diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index ff3b0731..30d6b945 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -330,7 +330,7 @@ bool VulkanRenderer::CreateBloomPipelines() { VkPushConstantRange compPushRange{}; compPushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; compPushRange.offset = 0; - compPushRange.size = 16; + compPushRange.size = 32; VkPipelineLayoutCreateInfo cliInfo{}; cliInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; @@ -768,12 +768,16 @@ void VulkanRenderer::RenderBloomCompositePass() { struct { float bloomStrength, exposure, gamma; uint32_t ssrEnabled; - } pc; + float sharpenStrength; + float _pad[3]; + } pc{}; // bloom disabled -> 0 so the (black) mip contributes nothing pc.bloomStrength = m_bloomEnabled ? 0.06f : 0.0f; // UE4-style soft bloom pc.exposure = m_exposure; pc.gamma = m_tonemapEnabled ? m_gamma : 1.0f; pc.ssrEnabled = (ssrReady && m_ssrEnabled) ? 1u : 0u; + // Sharpening an aliased (non-TAA) frame only amplifies the stair-steps + pc.sharpenStrength = (m_taaResolvedView != VK_NULL_HANDLE) ? m_sharpenStrength : 0.0f; vkCmdPushConstants(command, m_bloomCompositePipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); vkCmdDraw(command, 3, 1, 0, 0); From 92180f84d9bdbd30549d3fe635e422fb2f41058a Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 25 Oct 2026 18:23:26 +0300 Subject: [PATCH 35/36] style: apply clang-format to changed lines --- .../Graphics/Common/ConstantBuffer.hpp | 2 +- .../private/Graphics/Vulkan/VulkanBuffer.hpp | 144 +-- .../Graphics/Vulkan/VulkanInternal.hpp | 3 +- .../Graphics/Vulkan/VulkanRenderer.hpp | 889 +++++++++--------- .../private/Graphics/Vulkan/VulkanTexture.hpp | 2 +- include/public/Core/Application.hpp | 4 +- src/Core/Application.cpp | 18 +- src/Core/Window.cpp | 19 +- src/Graphics/Vulkan/VulkanBloom.cpp | 10 +- src/Graphics/Vulkan/VulkanBuffer.cpp | 3 +- src/Graphics/Vulkan/VulkanDeferred.cpp | 26 +- src/Graphics/Vulkan/VulkanDescriptors.cpp | 3 +- src/Graphics/Vulkan/VulkanDevice.cpp | 288 +++--- src/Graphics/Vulkan/VulkanPipelines.cpp | 277 +++--- src/Graphics/Vulkan/VulkanProfiler.cpp | 10 +- src/Graphics/Vulkan/VulkanRenderer.cpp | 9 +- src/Graphics/Vulkan/VulkanShadow.cpp | 11 +- src/Graphics/Vulkan/VulkanSwapchain.cpp | 35 +- src/Graphics/Vulkan/VulkanTAA.cpp | 32 +- src/Graphics/Vulkan/VulkanTexture.cpp | 6 +- 20 files changed, 914 insertions(+), 877 deletions(-) diff --git a/include/private/Graphics/Common/ConstantBuffer.hpp b/include/private/Graphics/Common/ConstantBuffer.hpp index 41a8af16..b68ddd3a 100644 --- a/include/private/Graphics/Common/ConstantBuffer.hpp +++ b/include/private/Graphics/Common/ConstantBuffer.hpp @@ -1,8 +1,8 @@ #ifndef _CONSTANTBUFFER_H_ #define _CONSTANTBUFFER_H_ -#include #include +#include #include #include diff --git a/include/private/Graphics/Vulkan/VulkanBuffer.hpp b/include/private/Graphics/Vulkan/VulkanBuffer.hpp index 89076004..5d5d4a01 100644 --- a/include/private/Graphics/Vulkan/VulkanBuffer.hpp +++ b/include/private/Graphics/Vulkan/VulkanBuffer.hpp @@ -72,65 +72,65 @@ class ENGINE_API VulkanBuffer : public BufferBase { static void SetBatchingEnabled(bool enabled) { s_batchingEnabled = enabled; } private: - /// Allocates a VMA-backed VkBuffer with the given usage/memory properties. - /// Constant buffers get CPU-cached memory since every bind reads them, and - /// staging buffers come from the resident staging pool. - void CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage, - VkMemoryPropertyFlags properties, - VkBuffer& buffer, VmaAllocation& memory, - VkDeviceSize* outAllocSize = nullptr, - uint32_t* outMemTypeIdx = nullptr); - - /// Records a GPU-side copy between buffers, batched or submitted immediately per SetBatchingEnabled. - void CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, - VkDeviceSize size); - - /// Finds a physical device memory type matching the filter and required properties. - uint32_t FindMemoryType(uint32_t typeFilter, - VkMemoryPropertyFlags properties); - - VkDevice m_device = VK_NULL_HANDLE; - VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; - VkCommandPool m_commandPool = VK_NULL_HANDLE; - VkQueue m_graphicsQueue = VK_NULL_HANDLE; - - VkBuffer m_buffer = VK_NULL_HANDLE; - VmaAllocation m_memory = VK_NULL_HANDLE; - VkDeviceSize m_allocSize = 0; // backing memory size (memReq) - VkDeviceSize m_bufferSize = 0; // logical VkBuffer size (bucketed) - VkBufferUsageFlags m_usage = 0; - uint32_t m_memoryTypeIndex = 0; - - VkBuffer m_stagingBuffer = VK_NULL_HANDLE; - VmaAllocation m_stagingMemory = VK_NULL_HANDLE; - - void* m_mappedData = nullptr; - - // --- Batched transfer state --- - static bool s_batchingEnabled; - static bool s_batchActive; - static VkCommandBuffer s_batchCommandBuffer; - static VkDevice s_batchDevice; - static VkCommandPool s_batchCommandPool; - static VkQueue s_batchQueue; - static std::vector s_pendingCleanup; - - /// Opens the shared batch command buffer if one isn't already recording. - static void EnsureBatchStarted(VkDevice device, VkCommandPool pool, - VkQueue queue); - - // --- Deferred buffer deletion --- - /// A buffer queued for destruction once the GPU is guaranteed done referencing it. - struct DeferredBufferDelete { - VkBuffer buffer; - VmaAllocation memory; - VkDevice device; - VkDeviceSize allocSize; - VkBufferUsageFlags usage; - uint32_t memoryTypeIndex; - uint64_t frameNumber; - VkDeviceSize bufferSize; - }; + /// Allocates a VMA-backed VkBuffer with the given usage/memory properties. + /// Constant buffers get CPU-cached memory since every bind reads them, and + /// staging buffers come from the resident staging pool. + void CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage, + VkMemoryPropertyFlags properties, VkBuffer& buffer, + VmaAllocation& memory, VkDeviceSize* outAllocSize = nullptr, + uint32_t* outMemTypeIdx = nullptr); + + /// Records a GPU-side copy between buffers, batched or submitted immediately + /// per SetBatchingEnabled. + void CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size); + + /// Finds a physical device memory type matching the filter and required + /// properties. + uint32_t FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); + + VkDevice m_device = VK_NULL_HANDLE; + VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; + VkCommandPool m_commandPool = VK_NULL_HANDLE; + VkQueue m_graphicsQueue = VK_NULL_HANDLE; + + VkBuffer m_buffer = VK_NULL_HANDLE; + VmaAllocation m_memory = VK_NULL_HANDLE; + VkDeviceSize m_allocSize = 0; // backing memory size (memReq) + VkDeviceSize m_bufferSize = 0; // logical VkBuffer size (bucketed) + VkBufferUsageFlags m_usage = 0; + uint32_t m_memoryTypeIndex = 0; + + VkBuffer m_stagingBuffer = VK_NULL_HANDLE; + VmaAllocation m_stagingMemory = VK_NULL_HANDLE; + + void* m_mappedData = nullptr; + + // --- Batched transfer state --- + static bool s_batchingEnabled; + static bool s_batchActive; + static VkCommandBuffer s_batchCommandBuffer; + static VkDevice s_batchDevice; + static VkCommandPool s_batchCommandPool; + static VkQueue s_batchQueue; + static std::vector s_pendingCleanup; + + /// Opens the shared batch command buffer if one isn't already recording. + static void EnsureBatchStarted(VkDevice device, VkCommandPool pool, + VkQueue queue); + + // --- Deferred buffer deletion --- + /// A buffer queued for destruction once the GPU is guaranteed done referencing + /// it. + struct DeferredBufferDelete { + VkBuffer buffer; + VmaAllocation memory; + VkDevice device; + VkDeviceSize allocSize; + VkBufferUsageFlags usage; + uint32_t memoryTypeIndex; + uint64_t frameNumber; + VkDeviceSize bufferSize; + }; static std::vector s_deferredDeletions; static uint64_t s_frameNumber; @@ -163,18 +163,20 @@ class ENGINE_API VulkanBuffer : public BufferBase { static void EvictPoolOverBudget(); public: - // Called by the renderer once per submitted frame, after its fence wait, - // to safely destroy buffers that are no longer referenced by the GPU. - static void ProcessDeferredDeletions(uint32_t maxFramesInFlight); - static void FlushAllDeferredDeletions(); - static void AdvanceDeletionFrame() { s_frameNumber++; } - - // VRAM tracking - static VkDeviceSize GetTotalAllocatedBytes(); - static VkDeviceSize GetDeviceLocalHeapSize(); - static VkDeviceSize GetDeviceLocalAllocatedBytes(); - static void SetPhysicalDevice(VkPhysicalDevice device); - static void UntrackAllocation(VkDeviceSize size) { s_totalAllocatedBytes -= size; } + // Called by the renderer once per submitted frame, after its fence wait, + // to safely destroy buffers that are no longer referenced by the GPU. + static void ProcessDeferredDeletions(uint32_t maxFramesInFlight); + static void FlushAllDeferredDeletions(); + static void AdvanceDeletionFrame() { s_frameNumber++; } + + // VRAM tracking + static VkDeviceSize GetTotalAllocatedBytes(); + static VkDeviceSize GetDeviceLocalHeapSize(); + static VkDeviceSize GetDeviceLocalAllocatedBytes(); + static void SetPhysicalDevice(VkPhysicalDevice device); + static void UntrackAllocation(VkDeviceSize size) { + s_totalAllocatedBytes -= size; + } static void UntrackAllocation(VkDeviceSize size, uint32_t memTypeIdx) { s_totalAllocatedBytes -= size; if (memTypeIdx < s_memTypeCount) diff --git a/include/private/Graphics/Vulkan/VulkanInternal.hpp b/include/private/Graphics/Vulkan/VulkanInternal.hpp index 3f5b66c3..20f14996 100644 --- a/include/private/Graphics/Vulkan/VulkanInternal.hpp +++ b/include/private/Graphics/Vulkan/VulkanInternal.hpp @@ -39,7 +39,8 @@ bool InvertMat4(const T M[16], T out[16]) { for (int c = 0; c < 4; ++c) m[r][4 + c] = (r == c) ? T(1) : T(0); } for (int col = 0; col < 4; ++col) { - int pivot = -1; T maxv = T(0); + int pivot = -1; + T maxv = T(0); for (int row = col; row < 4; ++row) { T v = std::abs(m[row][col]); if (v > maxv) { maxv = v; pivot = row; } diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 93591a90..cde5d94c 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -1,21 +1,23 @@ #ifndef VULKANRENDERER_HPP #define VULKANRENDERER_HPP -#include "../Common/Renderer.hpp" -#include "../Common/RenderContext.hpp" -#include "Graphics/Vulkan/VulkanShader.hpp" -#include "Graphics/Vulkan/VulkanTexture.hpp" -#include "Core/Logger.hpp" +#include +#include #include -#include "Graphics/Vulkan/VulkanBuffer.hpp" + #include #include +#include #include -#include #include -#include -#include -#include +#include + +#include "../Common/RenderContext.hpp" +#include "../Common/Renderer.hpp" +#include "Core/Logger.hpp" +#include "Graphics/Vulkan/VulkanBuffer.hpp" +#include "Graphics/Vulkan/VulkanShader.hpp" +#include "Graphics/Vulkan/VulkanTexture.hpp" // Shadow caster depth bias #define SHADOW_DEPTH_BIAS_CONSTANT 1.25f @@ -237,432 +239,442 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { } private: - /// Compiles the skybox shaders on first use, sizes the skybox descriptor - /// sets to the swapchain image count, and creates the skybox pipeline. - bool CreateSkyboxPipeline(); - /// Compiles the skinned shaders on first use and creates the forward - /// skinned pipeline. - bool CreateSkinnedPipeline(); - - // Deferred rendering - /// Creates the GBuffer color images, render passes, descriptors, and - /// pipelines that make up the deferred lighting path. - bool CreateGBufferResources(); - /// Creates the GBuffer render pass with its three color attachments and depth. - bool CreateGBufferRenderPass(); - /// Creates the GBuffer framebuffer binding the GBuffer images and depth. - bool CreateGBufferFramebuffer(); - /// Compiles the GBuffer shaders and creates the geometry pipeline, reusing pipelineLay. - bool CreateGBufferPipeline(); - /// Compiles the skinned GBuffer shaders so skinned meshes write into the GBuffer. - bool CreateSkinnedGbufferPipeline(); - /// Creates the deferred lighting render pass with a single color attachment. - bool CreateLightingRenderPass(); - /// Creates one lighting pass framebuffer per swapchain image, all aliasing the HDR target. - bool CreateLightingFramebuffers(); - /// Compiles the lighting shaders and creates the fullscreen lighting pipeline. - bool CreateLightingPipeline(); - /// Creates the forward transparent render pass writing into the HDR scene image. - bool CreateForwardRenderPass(); - /// Creates one forward transparent framebuffer per swapchain image, all aliasing the HDR target. - bool CreateForwardFramebuffers(); - /// Creates the GBuffer sampler descriptor set layout, pool, and per-frame sets. - bool CreateGBufferDescriptorSets(); - /// Creates the per-frame deferred constant buffer holding InvViewProj and screen size. - bool CreateDeferredCBResources(); - /// Creates the PBR material descriptor layout, pool, ring of sets, and GBuffer geometry pipeline layout. - bool CreatePBRMaterialResources(); - /// Appends one chunk of PBR material sets and UBO slots to a frame's ring. - bool AddPBRMaterialChunk(uint32_t frame); - /// Destroys every PBR material chunk of every frame. - void DestroyPBRMaterialChunks(); - /// Creates stub IBL irradiance, prefilter, and BRDF LUT images, samplers, and descriptor set. - bool CreateIBLResources(); - /// Destroys all GBuffer, lighting, and forward transparent pass resources. - void CleanupGBufferResources(); - /// Destroys the IBL images, samplers, and descriptor resources. - void CleanupIBLResources(); - /// Writes the GBuffer, depth, and shadow images into the sampler descriptor sets before the lighting pass. - void UpdateGBufferDescriptors(); - - // Shadow mapping - /// Creates the shadow depth image, sampler, render pass, and framebuffer. - bool CreateShadowResources(); - /// Compiles the shadow depth shader and creates the shadow pass pipeline. - bool CreateShadowPipeline(); - /// Creates the per-frame light and shadow UBO buffers and descriptor sets. - bool CreateShadowLightUBOResources(); - /// Destroys the shadow map image, pipeline, render pass, and light UBO resources. - void CleanupShadowResources(); - /// Creates the Vulkan instance. The validation layer is on in Debug builds - /// or with --validate, and --syncval adds synchronization validation. - bool InitVulkan(); - /// Creates the SDL-backed Vulkan presentation surface. - bool CreateSurface(); - /// Selects the physical GPU and creates the logical device and queues. - bool CreateDevice(); - /// Creates the swapchain from the queried surface capabilities. - bool CreateSwapChain(); - /// Rebuilds the swapchain and its dependents after resize or resolution change. - bool RecreateSwapChain(); - /// True when the surface wants a different extent than the swapchain. - bool SurfaceExtentChanged(); - /// Creates an image view for each swapchain image. - bool CreateImageViews(); - /// Creates the layout shared by the forward, skybox, debug line, skinned, - /// and shadow pipelines. - bool CreateMainPipelineLayout(); - /// Creates the main forward graphics pipeline, plus its layout and shader - /// on first use. - bool CreateGraphicsPipeline(); - /// Creates the pipeline cache, seeded from disk when the saved blob was - /// written by this device and driver. - void CreatePipelineCache(); - /// Saves the pipeline cache to disk when it changed, then destroys it. - void DestroyPipelineCache(); - /// Creates the main forward render pass with optional MSAA color and resolve attachments. - bool CreateRenderPass(); - /// Creates one framebuffer per swapchain image for the main render pass. - bool CreateFrameBuffer(); - /// Creates the graphics command pool. - bool CreateCommandPool(); - /// Allocates one primary command buffer per frame in flight. - bool CreateCommandBuffer(); - /// Creates the per-swapchain-image semaphores and per-frame fences and - /// transfer semaphores. - bool CreateSyncObjects(); - /// Grows the per-image semaphores to the swapchain image count and - /// clears the image-to-fence table. - bool CreateImageSyncObjects(); - /// Creates the depth image, memory, and image view. - bool CreateDepthResources(); - /// Creates the texture, bone UBO, light UBO, and shadow sampler descriptor set layouts. - bool CreateDescriptorSetLayout(); - /// Creates the descriptor pool backing the per-texture descriptor sets. - bool CreateDescriptorPool(); - /// Allocates one texture descriptor set per swapchain image. - bool AllocateDescriptorSets(); - /// Creates the fallback 1x1 white texture and writes it into the global descriptor sets. - bool CreateDefaultTexture(); - /// Allocates and writes a per-texture descriptor set for the given texture. - void WriteTextureDescriptors(VulkanTexture* texture); - /// Registers the debug messenger callback when the validation layer is on. - bool SetupDebugMessenger(); - - /// Destroys the swapchain, its image views, and framebuffers. - void CleanupSwapChain(); - /// Destroys the depth image, memory, and image view. - void CleanupDepthResources(); - - // MSAA resources - /// Creates the MSAA color image used as the multisampled render target. - bool CreateMSAAColorResources(); - /// Destroys the MSAA color image, view, and memory. - void CleanupMSAAColorResources(); - /// Queries the highest MSAA sample count the GPU supports. - VkSampleCountFlagBits GetMaxUsableSampleCount(); - - /// No-op; Vulkan polygon mode changes require pipeline recreation. - virtual void ConfigureRenderMode() override; - /// No-op; Vulkan cull mode changes require pipeline recreation. - virtual void ConfigureRenderFace() override; - - /// Builds one queue create info per unique queue family index. - std::vector - GetUniqueQueueCreateInfos(); - - /// Queries surface capabilities, formats, and present modes. - std::optional QuerySwapchain(); - - /// Picks a UNORM surface format to avoid double sRGB encoding. - VkSurfaceFormatKHR ChooseFormat( - const std::vector& formats); - /// Picks FIFO when VSync is on, otherwise MAILBOX or IMMEDIATE. - VkPresentModeKHR ChoosePresentMode( - const std::vector& modes); - /// Clamps the window size to the surface's supported extent. - VkExtent2D ChooseExtend(const VkSurfaceCapabilitiesKHR& caps); - - /// Picks the first supported depth-stencil format from the candidate list. - VkFormat FindDepthFormat(); - /// Finds a memory type index matching the filter and property flags. - uint32_t FindMemoryType(uint32_t typeFilter, - VkMemoryPropertyFlags properties); - - /// Fills the debug messenger create info with severity and callback. - void PopulateDebugMessengerCreateInfo( - VkDebugUtilsMessengerCreateInfoEXT& createInfo); - - /// Debug messenger callback that routes Vulkan messages to the logger. - static VkBool32 Validation( - VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT messageTypes, - const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, - void* pUserData); - - bool bRender = true; - bool bFrameStarted = false; - - static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; - uint32_t currentFrame = 0; - uint32_t CurrentFrameIndex = 0; - uint32_t m_semaphoreIndex = 0; // cycles through swapchain image count - VkInstance instance = VK_NULL_HANDLE; - VkSurfaceKHR surface = VK_NULL_HANDLE; - VkRenderPass renderPass = VK_NULL_HANDLE; - VkSwapchainKHR swapChain = VK_NULL_HANDLE; - VkCommandPool commands = VK_NULL_HANDLE; - std::vector commandBuffers; - VkCommandBuffer command = VK_NULL_HANDLE; // alias for commandBuffers[currentFrame] - std::vector swapChainFramebuffers; - std::vector swapChainImages; - std::vector swapChainImageViews; - VkFormat scImageFormat; - VkExtent2D scExtent; - VkDevice device = VK_NULL_HANDLE; - VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; - std::vector GPUs; - VkPipeline pipeline = VK_NULL_HANDLE; - VkPipelineLayout pipelineLay = VK_NULL_HANDLE; - VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; - // Loaded blob size and hash, so an unchanged cache is not rewritten - size_t m_pipelineCacheLoadedSize = 0; - uint64_t m_pipelineCacheLoadedHash = 0; - std::vector imageAvailableSemaphores; - std::vector renderFinishedSemaphores; - std::vector inFlightFences; - std::vector imagesInFlight; - // Submission serial of the last frame recorded in each frame slot - std::array m_slotSerial{}; - uint64_t m_submitSerial = 0; - VulkanShader* simpleShader = nullptr; - VkClearValue clearColor; - - // Depth buffer - VkImage depthImage = VK_NULL_HANDLE; - VkDeviceMemory depthImageMemory = VK_NULL_HANDLE; - VkImageView depthImageView = VK_NULL_HANDLE; - VkFormat depthFormat; - - // MSAA color buffer (multisample resolve target) - VkSampleCountFlagBits m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; - VkImage m_msaaColorImage = VK_NULL_HANDLE; - VkDeviceMemory m_msaaColorImageMemory = VK_NULL_HANDLE; - VkImageView m_msaaColorImageView = VK_NULL_HANDLE; - - // Descriptor sets for uniform buffers - VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool descriptorPool = VK_NULL_HANDLE; - std::vector descriptorSets; - - QueueIndices QueueIDs; - VkQueue graphicsQueue = VK_NULL_HANDLE; - VkQueue computeQueue = VK_NULL_HANDLE; - VkQueue transferQueue = VK_NULL_HANDLE; - VkQueue presentQueue = VK_NULL_HANDLE; - - VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; - PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = - nullptr; - bool m_validationEnabled = false; - - Window* sdlWindow; - - // Texture binding - bool m_textureDescriptorsWritten = false; - VulkanTexture* m_defaultTexture = nullptr; - - // Skybox pipeline - VkPipeline skyboxPipeline = VK_NULL_HANDLE; - VulkanShader* skyboxShader = nullptr; - VkDescriptorPool skyboxDescriptorPool = VK_NULL_HANDLE; - std::vector skyboxDescriptorSets; - bool m_skyboxDescriptorsWritten = false; - VkImageView m_skyboxCubemapView = VK_NULL_HANDLE; // cached for MSAA re-bind - VkSampler m_skyboxCubemapSampler = VK_NULL_HANDLE; // cached for MSAA re-bind - uint32_t m_skyboxViewSerial = 0; // bumped whenever the cached view changes - - // Skinned pipeline (uses skinned shaders with bone UBO) - VkPipeline skinnedPipeline = VK_NULL_HANDLE; - VulkanShader* skinnedShader = nullptr; - - // Debug line pipeline - VkPipeline debugLinePipeline = VK_NULL_HANDLE; - VulkanShader* debugLineShader = nullptr; - /// Compiles the debug line shaders and creates the line-list pipeline. - bool CreateDebugLinePipeline(); - - // Custom vertex format pipelines (one per registered VertexFormatHandle) - /// Render pass a custom-format pipeline variant is built for. - enum class CustomPass : uint8_t { - Main, - Shadow, - GBuffer, - Transparent, - Count - }; - static constexpr size_t CUSTOM_PASS_COUNT = - static_cast(CustomPass::Count); - /// The pipeline variants a registered vertex layout can drive, indexed by - /// CustomPass. A failed flag marks a variant as permanently absent so - /// draws skip it instead of retrying compilation every frame. - struct CustomFormatPipelines { - std::array pipelines{}; - std::array failed{}; - }; - // Indexed by handle - 1 (handles are dense and start at 1) - std::vector m_customFormatPipelines; - // Registered formats whose active-path variants were prebuilt - VertexFormatHandle m_customFormatsPrebuilt = 0; - VertexFormatHandle m_activeCustomFormat = 0; - /// Set while the active custom format has no pipeline for the current pass; - /// draws are dropped rather than issued against a mismatched vertex layout. - bool m_customFormatUnbound = false; - /// True while the bound vertex buffer's format has no pipeline for this pass. - bool CustomFormatDrawsSuppressed() const; - /// True when the render pass a custom-format variant targets exists. - bool CustomPassAvailable(CustomPass pass) const; - /// Returns a format's pipeline for a pass, compiling it on first request. - VkPipeline GetCustomFormatPipeline(VertexFormatHandle format, - CustomPass pass); - /// Compiles one pipeline variant of a registered layout; returns - /// VK_NULL_HANDLE when its stem is empty or compilation fails. - VkPipeline BuildCustomFormatPipeline(VertexFormatHandle format, - const VertexLayoutDesc& desc, - CustomPass pass); - /// Compiles the variants the active render path draws with for every - /// registered format not prebuilt yet. - void PrebuildCustomFormatPipelines(); - /// Destroys every cached custom-format pipeline (all variants, all formats). - void DestroyCustomFormatPipelines(); - - // Bone palette ring (set 1, binding 0, dynamic offset per bind) - struct BoneUBOChunk { - VkDescriptorPool pool = VK_NULL_HANDLE; - VkDescriptorSet set = VK_NULL_HANDLE; - VkBuffer buffer = VK_NULL_HANDLE; - VkDeviceMemory memory = VK_NULL_HANDLE; - void* mapped = nullptr; - }; - static constexpr uint32_t BONE_MATRIX_BYTES = sizeof(float) * 16; - static constexpr uint32_t BONE_PALETTE_BYTES = - MAX_BONES * BONE_MATRIX_BYTES; - static constexpr uint32_t BONE_SLOTS_PER_CHUNK = 32; - VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; - std::array, MAX_FRAMES_IN_FLIGHT> m_boneChunks; - VkDeviceSize m_boneSlotStride = BONE_PALETTE_BYTES; - uint32_t m_boneSlot[MAX_FRAMES_IN_FLIGHT] = {}; - bool m_boneUBOCreated = false; - /// Creates each frame's first bone ring chunk; slot 0 holds identity. - bool CreateBoneUBOResources(); - /// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring. - bool AddBoneUBOChunk(uint32_t frame); - /// Destroys every bone ring chunk of every frame. - void CleanupBoneUBOResources(); - /// Binds the identity palette (ring slot 0) at set 1. - void BindIdentityBonePalette(VkPipelineLayout layout); - - // ImGUI - VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; - - // Shadow mapping resources - VkImage m_shadowImage = VK_NULL_HANDLE; - VkDeviceMemory m_shadowImageMemory = VK_NULL_HANDLE; - VkImageView m_shadowImageView = VK_NULL_HANDLE; - VkSampler m_shadowSampler = VK_NULL_HANDLE; // compare sampler (hardware PCF) - VkSampler m_shadowRawSampler = VK_NULL_HANDLE; // non-compare sampler (PCSS blocker search) - VkRenderPass m_shadowRenderPass = VK_NULL_HANDLE; - VkFramebuffer m_shadowFramebuffer = VK_NULL_HANDLE; - VkPipeline m_shadowPipeline = VK_NULL_HANDLE; - VulkanShader* m_shadowShader = nullptr; - bool m_shadowPassActive = false; - // m_shadowResourcesCreated lives in the Renderer base (shared with the - // shadow-resolution change-request logic) - - // Shadow push-constant memo: LightVP*World is frame-constant per unique - // World, so cache it and skip the matmul when consecutive casters (all - // draws share the identity transform) reuse the same World matrix. - float m_shadowWorldCache[16] = {}; - float m_shadowPCCache[32] = {}; - bool m_shadowPCCacheValid = false; - // Layout m_shadowPCCache was last pushed with in this shadow pass, or - // null once anything else overwrote the push constants. - VkPipelineLayout m_shadowPCPushedLayout = VK_NULL_HANDLE; - - // Light VP matrix (stored as raw floats for push constant computation) - float m_lightVP[16] = {}; - // Staging slot: SetLightVP writes here; BeginRender copies it to - // m_lightVP before the shadow pass. Keeps m_lightVP stable for the - // entire frame so shadow pass and main pass agree on the transform. - float m_pendingLightVP[16] = {}; - bool m_hasPendingLightVP = false; - // Matrix the shadow map was last rendered with - float m_shadowMapLightVP[16] = {}; - bool m_shadowMapRendered = false; - - // Light/Shadow UBO (set 2, binding 0) - VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool m_lightUBODescriptorPool = VK_NULL_HANDLE; - std::array m_lightUBOBuffers = {}; - std::array m_lightUBOMemory = {}; - std::array m_lightUBOMapped = {}; - std::array m_lightUBODescriptorSets = {}; - - // Shadow map sampler descriptor (set 3, binding 0) - VkDescriptorSetLayout m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool m_shadowSamplerDescriptorPool = VK_NULL_HANDLE; - std::array m_shadowSamplerDescriptorSets = {}; - bool m_lightUBOCreated = false; - - // Async buffer transfer (zero-CPU-blocking GPU uploads) - std::array m_transferSemaphores = {}; - std::array m_asyncFlush; - - // ---- Deferred GBuffer ---- - // 3 color attachments: RT0=AlbedoAO, RT1=NormalRough, RT2=MetalEmit. - // World position is reconstructed from the depth buffer + InvViewProj in - // the lighting/SSAO/SSR passes (no RGBA32F worldpos RT) to cut GBuffer - // bandwidth on a fill-bound renderer. - static constexpr uint32_t GBUFFER_COUNT = 3; - VkImage m_gbufferImages[GBUFFER_COUNT] = {}; - VkDeviceMemory m_gbufferMemory[GBUFFER_COUNT] = {}; - VkImageView m_gbufferViews[GBUFFER_COUNT] = {}; - /// GBuffer attachment formats: RT0 AlbedoAO, RT1 NormalRough, RT2 MetalEmit. Defined in VulkanDeferred.cpp. - static const VkFormat m_gbufferFormats[GBUFFER_COUNT]; - VkRenderPass m_gbufferRenderPass = VK_NULL_HANDLE; - VkFramebuffer m_gbufferFramebuffer = VK_NULL_HANDLE; - VkPipeline m_gbufferPipeline = VK_NULL_HANDLE; - VkPipeline m_skinnedGbufferPipeline = VK_NULL_HANDLE; - VkPipelineLayout m_gbufferPipelineLayout = VK_NULL_HANDLE; - VulkanShader* m_gbufferShader = nullptr; - bool m_gbufferResourcesCreated = false; - bool m_inGeometryPass = false; - - // Lighting pass - VkRenderPass m_lightingRenderPass = VK_NULL_HANDLE; - std::vector m_lightingFramebuffers; // one per swapchain image - VkPipeline m_lightingPipeline = VK_NULL_HANDLE; - VkPipelineLayout m_lightingPipelineLayout = VK_NULL_HANDLE; - VulkanShader* m_lightingShader = nullptr; - - // Forward transparent pass - VkRenderPass m_forwardRenderPass = VK_NULL_HANDLE; - std::vector m_forwardFramebuffers; // one per swapchain image (color+depth, non-MSAA) - bool m_inForwardTransparentPass = false; - bool m_forwardPassOpen = false; // true when forward RP is currently recording - - // GBuffer sampler descriptor set (set 0 in lighting pass) - VkDescriptorSetLayout m_gbufferSamplerDSL = VK_NULL_HANDLE; - VkDescriptorPool m_gbufferSamplerPool = VK_NULL_HANDLE; - std::array m_gbufferSamplerSets = {}; - VkSampler m_gbufferSampler = VK_NULL_HANDLE; - VkSampler m_depthSampler = VK_NULL_HANDLE; - - // Deferred CB UBO (set 1 in lighting pass): InvViewProj + screenSize + near/far - struct DeferredCBData { - float InvViewProj[16]; - float ScreenW, ScreenH, NearP, FarP; - }; + /// Compiles the skybox shaders on first use, sizes the skybox descriptor + /// sets to the swapchain image count, and creates the skybox pipeline. + bool CreateSkyboxPipeline(); + /// Compiles the skinned shaders on first use and creates the forward + /// skinned pipeline. + bool CreateSkinnedPipeline(); + + // Deferred rendering + /// Creates the GBuffer color images, render passes, descriptors, and + /// pipelines that make up the deferred lighting path. + bool CreateGBufferResources(); + /// Creates the GBuffer render pass with its three color attachments and depth. + bool CreateGBufferRenderPass(); + /// Creates the GBuffer framebuffer binding the GBuffer images and depth. + bool CreateGBufferFramebuffer(); + /// Compiles the GBuffer shaders and creates the geometry pipeline, reusing + /// pipelineLay. + bool CreateGBufferPipeline(); + /// Compiles the skinned GBuffer shaders so skinned meshes write into the + /// GBuffer. + bool CreateSkinnedGbufferPipeline(); + /// Creates the deferred lighting render pass with a single color attachment. + bool CreateLightingRenderPass(); + /// Creates one lighting pass framebuffer per swapchain image, all aliasing the + /// HDR target. + bool CreateLightingFramebuffers(); + /// Compiles the lighting shaders and creates the fullscreen lighting pipeline. + bool CreateLightingPipeline(); + /// Creates the forward transparent render pass writing into the HDR scene + /// image. + bool CreateForwardRenderPass(); + /// Creates one forward transparent framebuffer per swapchain image, all + /// aliasing the HDR target. + bool CreateForwardFramebuffers(); + /// Creates the GBuffer sampler descriptor set layout, pool, and per-frame + /// sets. + bool CreateGBufferDescriptorSets(); + /// Creates the per-frame deferred constant buffer holding InvViewProj and + /// screen size. + bool CreateDeferredCBResources(); + /// Creates the PBR material descriptor layout, pool, ring of sets, and GBuffer + /// geometry pipeline layout. + bool CreatePBRMaterialResources(); + /// Appends one chunk of PBR material sets and UBO slots to a frame's ring. + bool AddPBRMaterialChunk(uint32_t frame); + /// Destroys every PBR material chunk of every frame. + void DestroyPBRMaterialChunks(); + /// Creates stub IBL irradiance, prefilter, and BRDF LUT images, samplers, and + /// descriptor set. + bool CreateIBLResources(); + /// Destroys all GBuffer, lighting, and forward transparent pass resources. + void CleanupGBufferResources(); + /// Destroys the IBL images, samplers, and descriptor resources. + void CleanupIBLResources(); + /// Writes the GBuffer, depth, and shadow images into the sampler descriptor + /// sets before the lighting pass. + void UpdateGBufferDescriptors(); + + // Shadow mapping + /// Creates the shadow depth image, sampler, render pass, and framebuffer. + bool CreateShadowResources(); + /// Compiles the shadow depth shader and creates the shadow pass pipeline. + bool CreateShadowPipeline(); + /// Creates the per-frame light and shadow UBO buffers and descriptor sets. + bool CreateShadowLightUBOResources(); + /// Destroys the shadow map image, pipeline, render pass, and light UBO + /// resources. + void CleanupShadowResources(); + /// Creates the Vulkan instance. The validation layer is on in Debug builds + /// or with --validate, and --syncval adds synchronization validation. + bool InitVulkan(); + /// Creates the SDL-backed Vulkan presentation surface. + bool CreateSurface(); + /// Selects the physical GPU and creates the logical device and queues. + bool CreateDevice(); + /// Creates the swapchain from the queried surface capabilities. + bool CreateSwapChain(); + /// Rebuilds the swapchain and its dependents after resize or resolution + /// change. + bool RecreateSwapChain(); + /// True when the surface wants a different extent than the swapchain. + bool SurfaceExtentChanged(); + /// Creates an image view for each swapchain image. + bool CreateImageViews(); + /// Creates the layout shared by the forward, skybox, debug line, skinned, + /// and shadow pipelines. + bool CreateMainPipelineLayout(); + /// Creates the main forward graphics pipeline, plus its layout and shader + /// on first use. + bool CreateGraphicsPipeline(); + /// Creates the pipeline cache, seeded from disk when the saved blob was + /// written by this device and driver. + void CreatePipelineCache(); + /// Saves the pipeline cache to disk when it changed, then destroys it. + void DestroyPipelineCache(); + /// Creates the main forward render pass with optional MSAA color and resolve + /// attachments. + bool CreateRenderPass(); + /// Creates one framebuffer per swapchain image for the main render pass. + bool CreateFrameBuffer(); + /// Creates the graphics command pool. + bool CreateCommandPool(); + /// Allocates one primary command buffer per frame in flight. + bool CreateCommandBuffer(); + /// Creates the per-swapchain-image semaphores and per-frame fences and + /// transfer semaphores. + bool CreateSyncObjects(); + /// Grows the per-image semaphores to the swapchain image count and + /// clears the image-to-fence table. + bool CreateImageSyncObjects(); + /// Creates the depth image, memory, and image view. + bool CreateDepthResources(); + /// Creates the texture, bone UBO, light UBO, and shadow sampler descriptor set + /// layouts. + bool CreateDescriptorSetLayout(); + /// Creates the descriptor pool backing the per-texture descriptor sets. + bool CreateDescriptorPool(); + /// Allocates one texture descriptor set per swapchain image. + bool AllocateDescriptorSets(); + /// Creates the fallback 1x1 white texture and writes it into the global + /// descriptor sets. + bool CreateDefaultTexture(); + /// Allocates and writes a per-texture descriptor set for the given texture. + void WriteTextureDescriptors(VulkanTexture* texture); + /// Registers the debug messenger callback when the validation layer is on. + bool SetupDebugMessenger(); + + /// Destroys the swapchain, its image views, and framebuffers. + void CleanupSwapChain(); + /// Destroys the depth image, memory, and image view. + void CleanupDepthResources(); + + // MSAA resources + /// Creates the MSAA color image used as the multisampled render target. + bool CreateMSAAColorResources(); + /// Destroys the MSAA color image, view, and memory. + void CleanupMSAAColorResources(); + /// Queries the highest MSAA sample count the GPU supports. + VkSampleCountFlagBits GetMaxUsableSampleCount(); + + /// No-op; Vulkan polygon mode changes require pipeline recreation. + virtual void ConfigureRenderMode() override; + /// No-op; Vulkan cull mode changes require pipeline recreation. + virtual void ConfigureRenderFace() override; + + /// Builds one queue create info per unique queue family index. + std::vector GetUniqueQueueCreateInfos(); + + /// Queries surface capabilities, formats, and present modes. + std::optional QuerySwapchain(); + + /// Picks a UNORM surface format to avoid double sRGB encoding. + VkSurfaceFormatKHR ChooseFormat( + const std::vector& formats); + /// Picks FIFO when VSync is on, otherwise MAILBOX or IMMEDIATE. + VkPresentModeKHR ChoosePresentMode(const std::vector& modes); + /// Clamps the window size to the surface's supported extent. + VkExtent2D ChooseExtend(const VkSurfaceCapabilitiesKHR& caps); + + /// Picks the first supported depth-stencil format from the candidate list. + VkFormat FindDepthFormat(); + /// Finds a memory type index matching the filter and property flags. + uint32_t FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); + + /// Fills the debug messenger create info with severity and callback. + void PopulateDebugMessengerCreateInfo( + VkDebugUtilsMessengerCreateInfoEXT& createInfo); + + /// Debug messenger callback that routes Vulkan messages to the logger. + static VkBool32 Validation( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageTypes, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData); + + bool bRender = true; + bool bFrameStarted = false; + + static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; + uint32_t currentFrame = 0; + uint32_t CurrentFrameIndex = 0; + uint32_t m_semaphoreIndex = 0; // cycles through swapchain image count + VkInstance instance = VK_NULL_HANDLE; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkSwapchainKHR swapChain = VK_NULL_HANDLE; + VkCommandPool commands = VK_NULL_HANDLE; + std::vector commandBuffers; + VkCommandBuffer command = + VK_NULL_HANDLE; // alias for commandBuffers[currentFrame] + std::vector swapChainFramebuffers; + std::vector swapChainImages; + std::vector swapChainImageViews; + VkFormat scImageFormat; + VkExtent2D scExtent; + VkDevice device = VK_NULL_HANDLE; + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + std::vector GPUs; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLay = VK_NULL_HANDLE; + VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; + // Loaded blob size and hash, so an unchanged cache is not rewritten + size_t m_pipelineCacheLoadedSize = 0; + uint64_t m_pipelineCacheLoadedHash = 0; + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + std::vector imagesInFlight; + // Submission serial of the last frame recorded in each frame slot + std::array m_slotSerial{}; + uint64_t m_submitSerial = 0; + VulkanShader* simpleShader = nullptr; + VkClearValue clearColor; + + // Depth buffer + VkImage depthImage = VK_NULL_HANDLE; + VkDeviceMemory depthImageMemory = VK_NULL_HANDLE; + VkImageView depthImageView = VK_NULL_HANDLE; + VkFormat depthFormat; + + // MSAA color buffer (multisample resolve target) + VkSampleCountFlagBits m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; + VkImage m_msaaColorImage = VK_NULL_HANDLE; + VkDeviceMemory m_msaaColorImageMemory = VK_NULL_HANDLE; + VkImageView m_msaaColorImageView = VK_NULL_HANDLE; + + // Descriptor sets for uniform buffers + VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + std::vector descriptorSets; + + QueueIndices QueueIDs; + VkQueue graphicsQueue = VK_NULL_HANDLE; + VkQueue computeQueue = VK_NULL_HANDLE; + VkQueue transferQueue = VK_NULL_HANDLE; + VkQueue presentQueue = VK_NULL_HANDLE; + + VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; + PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = nullptr; + bool m_validationEnabled = false; + + Window* sdlWindow; + + // Texture binding + bool m_textureDescriptorsWritten = false; + VulkanTexture* m_defaultTexture = nullptr; + + // Skybox pipeline + VkPipeline skyboxPipeline = VK_NULL_HANDLE; + VulkanShader* skyboxShader = nullptr; + VkDescriptorPool skyboxDescriptorPool = VK_NULL_HANDLE; + std::vector skyboxDescriptorSets; + bool m_skyboxDescriptorsWritten = false; + VkImageView m_skyboxCubemapView = VK_NULL_HANDLE; // cached for MSAA re-bind + VkSampler m_skyboxCubemapSampler = VK_NULL_HANDLE; // cached for MSAA re-bind + uint32_t m_skyboxViewSerial = 0; // bumped whenever the cached view changes + + // Skinned pipeline (uses skinned shaders with bone UBO) + VkPipeline skinnedPipeline = VK_NULL_HANDLE; + VulkanShader* skinnedShader = nullptr; + + // Debug line pipeline + VkPipeline debugLinePipeline = VK_NULL_HANDLE; + VulkanShader* debugLineShader = nullptr; + /// Compiles the debug line shaders and creates the line-list pipeline. + bool CreateDebugLinePipeline(); + + // Custom vertex format pipelines (one per registered VertexFormatHandle) + /// Render pass a custom-format pipeline variant is built for. + enum class CustomPass : uint8_t { Main, Shadow, GBuffer, Transparent, Count }; + static constexpr size_t CUSTOM_PASS_COUNT = + static_cast(CustomPass::Count); + /// The pipeline variants a registered vertex layout can drive, indexed by + /// CustomPass. A failed flag marks a variant as permanently absent so + /// draws skip it instead of retrying compilation every frame. + struct CustomFormatPipelines { + std::array pipelines{}; + std::array failed{}; + }; + // Indexed by handle - 1 (handles are dense and start at 1) + std::vector m_customFormatPipelines; + // Registered formats whose active-path variants were prebuilt + VertexFormatHandle m_customFormatsPrebuilt = 0; + VertexFormatHandle m_activeCustomFormat = 0; + /// Set while the active custom format has no pipeline for the current pass; + /// draws are dropped rather than issued against a mismatched vertex layout. + bool m_customFormatUnbound = false; + /// True while the bound vertex buffer's format has no pipeline for this pass. + bool CustomFormatDrawsSuppressed() const; + /// True when the render pass a custom-format variant targets exists. + bool CustomPassAvailable(CustomPass pass) const; + /// Returns a format's pipeline for a pass, compiling it on first request. + VkPipeline GetCustomFormatPipeline(VertexFormatHandle format, CustomPass pass); + /// Compiles one pipeline variant of a registered layout; returns + /// VK_NULL_HANDLE when its stem is empty or compilation fails. + VkPipeline BuildCustomFormatPipeline(VertexFormatHandle format, + const VertexLayoutDesc& desc, + CustomPass pass); + /// Compiles the variants the active render path draws with for every + /// registered format not prebuilt yet. + void PrebuildCustomFormatPipelines(); + /// Destroys every cached custom-format pipeline (all variants, all formats). + void DestroyCustomFormatPipelines(); + + // Bone palette ring (set 1, binding 0, dynamic offset per bind) + struct BoneUBOChunk { + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + void* mapped = nullptr; + }; + static constexpr uint32_t BONE_MATRIX_BYTES = sizeof(float) * 16; + static constexpr uint32_t BONE_PALETTE_BYTES = MAX_BONES * BONE_MATRIX_BYTES; + static constexpr uint32_t BONE_SLOTS_PER_CHUNK = 32; + VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; + std::array, MAX_FRAMES_IN_FLIGHT> m_boneChunks; + VkDeviceSize m_boneSlotStride = BONE_PALETTE_BYTES; + uint32_t m_boneSlot[MAX_FRAMES_IN_FLIGHT] = {}; + bool m_boneUBOCreated = false; + /// Creates each frame's first bone ring chunk; slot 0 holds identity. + bool CreateBoneUBOResources(); + /// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring. + bool AddBoneUBOChunk(uint32_t frame); + /// Destroys every bone ring chunk of every frame. + void CleanupBoneUBOResources(); + /// Binds the identity palette (ring slot 0) at set 1. + void BindIdentityBonePalette(VkPipelineLayout layout); + + // ImGUI + VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; + + // Shadow mapping resources + VkImage m_shadowImage = VK_NULL_HANDLE; + VkDeviceMemory m_shadowImageMemory = VK_NULL_HANDLE; + VkImageView m_shadowImageView = VK_NULL_HANDLE; + VkSampler m_shadowSampler = VK_NULL_HANDLE; // compare sampler (hardware PCF) + VkSampler m_shadowRawSampler = + VK_NULL_HANDLE; // non-compare sampler (PCSS blocker search) + VkRenderPass m_shadowRenderPass = VK_NULL_HANDLE; + VkFramebuffer m_shadowFramebuffer = VK_NULL_HANDLE; + VkPipeline m_shadowPipeline = VK_NULL_HANDLE; + VulkanShader* m_shadowShader = nullptr; + bool m_shadowPassActive = false; + // m_shadowResourcesCreated lives in the Renderer base (shared with the + // shadow-resolution change-request logic) + + // Shadow push-constant memo: LightVP*World is frame-constant per unique + // World, so cache it and skip the matmul when consecutive casters (all + // draws share the identity transform) reuse the same World matrix. + float m_shadowWorldCache[16] = {}; + float m_shadowPCCache[32] = {}; + bool m_shadowPCCacheValid = false; + // Layout m_shadowPCCache was last pushed with in this shadow pass, or + // null once anything else overwrote the push constants. + VkPipelineLayout m_shadowPCPushedLayout = VK_NULL_HANDLE; + + // Light VP matrix (stored as raw floats for push constant computation) + float m_lightVP[16] = {}; + // Staging slot: SetLightVP writes here; BeginRender copies it to + // m_lightVP before the shadow pass. Keeps m_lightVP stable for the + // entire frame so shadow pass and main pass agree on the transform. + float m_pendingLightVP[16] = {}; + bool m_hasPendingLightVP = false; + // Matrix the shadow map was last rendered with + float m_shadowMapLightVP[16] = {}; + bool m_shadowMapRendered = false; + + // Light/Shadow UBO (set 2, binding 0) + VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool m_lightUBODescriptorPool = VK_NULL_HANDLE; + std::array m_lightUBOBuffers = {}; + std::array m_lightUBOMemory = {}; + std::array m_lightUBOMapped = {}; + std::array m_lightUBODescriptorSets = + {}; + + // Shadow map sampler descriptor (set 3, binding 0) + VkDescriptorSetLayout m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool m_shadowSamplerDescriptorPool = VK_NULL_HANDLE; + std::array + m_shadowSamplerDescriptorSets = {}; + bool m_lightUBOCreated = false; + + // Async buffer transfer (zero-CPU-blocking GPU uploads) + std::array m_transferSemaphores = {}; + std::array m_asyncFlush; + + // ---- Deferred GBuffer ---- + // 3 color attachments: RT0=AlbedoAO, RT1=NormalRough, RT2=MetalEmit. + // World position is reconstructed from the depth buffer + InvViewProj in + // the lighting/SSAO/SSR passes (no RGBA32F worldpos RT) to cut GBuffer + // bandwidth on a fill-bound renderer. + static constexpr uint32_t GBUFFER_COUNT = 3; + VkImage m_gbufferImages[GBUFFER_COUNT] = {}; + VkDeviceMemory m_gbufferMemory[GBUFFER_COUNT] = {}; + VkImageView m_gbufferViews[GBUFFER_COUNT] = {}; + /// GBuffer attachment formats: RT0 AlbedoAO, RT1 NormalRough, RT2 MetalEmit. + /// Defined in VulkanDeferred.cpp. + static const VkFormat m_gbufferFormats[GBUFFER_COUNT]; + VkRenderPass m_gbufferRenderPass = VK_NULL_HANDLE; + VkFramebuffer m_gbufferFramebuffer = VK_NULL_HANDLE; + VkPipeline m_gbufferPipeline = VK_NULL_HANDLE; + VkPipeline m_skinnedGbufferPipeline = VK_NULL_HANDLE; + VkPipelineLayout m_gbufferPipelineLayout = VK_NULL_HANDLE; + VulkanShader* m_gbufferShader = nullptr; + bool m_gbufferResourcesCreated = false; + bool m_inGeometryPass = false; + + // Lighting pass + VkRenderPass m_lightingRenderPass = VK_NULL_HANDLE; + std::vector m_lightingFramebuffers; // one per swapchain image + VkPipeline m_lightingPipeline = VK_NULL_HANDLE; + VkPipelineLayout m_lightingPipelineLayout = VK_NULL_HANDLE; + VulkanShader* m_lightingShader = nullptr; + + // Forward transparent pass + VkRenderPass m_forwardRenderPass = VK_NULL_HANDLE; + std::vector + m_forwardFramebuffers; // one per swapchain image (color+depth, non-MSAA) + bool m_inForwardTransparentPass = false; + bool m_forwardPassOpen = false; // true when forward RP is currently recording + + // GBuffer sampler descriptor set (set 0 in lighting pass) + VkDescriptorSetLayout m_gbufferSamplerDSL = VK_NULL_HANDLE; + VkDescriptorPool m_gbufferSamplerPool = VK_NULL_HANDLE; + std::array m_gbufferSamplerSets = {}; + VkSampler m_gbufferSampler = VK_NULL_HANDLE; + VkSampler m_depthSampler = VK_NULL_HANDLE; + + // Deferred CB UBO (set 1 in lighting pass): InvViewProj + screenSize + + // near/far + struct DeferredCBData { + float InvViewProj[16]; + float ScreenW, ScreenH, NearP, FarP; + }; VkDescriptorSetLayout m_deferredCBDSL = VK_NULL_HANDLE; VkDescriptorPool m_deferredCBPool = VK_NULL_HANDLE; std::array m_deferredCBBuffers = {}; @@ -876,7 +888,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { // Ping-pong history accumulation with depth-based reprojection // and 3x3 YCoCg variance clipping. struct alignas(16) TAAParams { - float CurrentToPrevClip[16]; // inverse(current VP) * previous VP, unjittered + float CurrentToPrevClip[16]; // inverse(current VP) * previous VP, + // unjittered float ScreenW, ScreenH; float BlendFactor; float _pad; @@ -885,7 +898,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool m_taaResourcesCreated = false; // m_taaEnabled is inherited from RenderEngine::Renderer (base class). uint64_t m_taaFrameIdx = 0; // global counter; ping-pong = idx % 2 - bool m_taaRanLastFrame = false; // false restarts accumulation + bool m_taaRanLastFrame = false; // false restarts accumulation VkImage m_taaImages[2] = {}; VkDeviceMemory m_taaMemory[2] = {}; diff --git a/include/private/Graphics/Vulkan/VulkanTexture.hpp b/include/private/Graphics/Vulkan/VulkanTexture.hpp index 6096fdaf..c9cff5d4 100644 --- a/include/private/Graphics/Vulkan/VulkanTexture.hpp +++ b/include/private/Graphics/Vulkan/VulkanTexture.hpp @@ -58,7 +58,7 @@ class ENGINE_API VulkanTexture : public Texture { /// Destroys every released texture; the caller guarantees the GPU is idle. static void DestroyAllReleased(); -private: + private: /// Hands the descriptor sets and GPU objects to the release queue so they /// outlive any frame still in flight. void Cleanup(); diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index 7b038b07..b847a51f 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -156,7 +156,7 @@ class ENGINE_API Application { /// Deferred rendering const bool GetIsDeferredEnabled() const; - void SetDeferredEnabled(bool enabled); + void SetDeferredEnabled(bool enabled); // Screen-space ambient occlusion (deferred path only) bool IsSSAOEnabled() const; @@ -185,7 +185,7 @@ class ENGINE_API Application { /// Post-TAA contrast-adaptive sharpening strength, 0 = off, 1 = strongest. float GetSharpenStrength() const; /// Sets post-TAA sharpening strength; only applied on frames TAA resolves. - void SetSharpenStrength(float strength); + void SetSharpenStrength(float strength); // Bloom (deferred path only) bool IsBloomEnabled() const; diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 63b5c150..86e21a6d 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -155,7 +155,7 @@ namespace Sleak { float lastTime = FrameTimer.Elapsed(); float accumulator = 0.0f; - const float fixedTimestep = 1.0f / 60.0f; + const float fixedTimestep = 1.0f / 60.0f; // Frame capture const std::string capturePath = CommandLine::GetValue("-capture"); @@ -425,8 +425,12 @@ namespace Sleak { bool Application::GetVSync() const { return renderer->GetVSync(); } void Application::SetVSync(bool enabled) { renderer->SetVSync(enabled); } - const bool Application::GetIsDeferredEnabled() const { return renderer->GetDeferredEnabled(); } - void Application::SetDeferredEnabled(bool enable) { renderer->SetDeferredEnabled(enable); } + const bool Application::GetIsDeferredEnabled() const { + return renderer->GetDeferredEnabled(); + } + void Application::SetDeferredEnabled(bool enable) { + renderer->SetDeferredEnabled(enable); + } bool Application::IsSSAOEnabled() const { return renderer->IsSSAOEnabled(); } void Application::SetSSAOEnabled(bool e) { renderer->SetSSAOEnabled(e); } @@ -448,8 +452,12 @@ namespace Sleak { bool Application::IsTAAEnabled() const { return renderer->IsTAAEnabled(); } void Application::SetTAAEnabled(bool e) { renderer->SetTAAEnabled(e); } - float Application::GetSharpenStrength() const { return renderer->GetSharpenStrength(); } - void Application::SetSharpenStrength(float s) { renderer->SetSharpenStrength(s); } + float Application::GetSharpenStrength() const { + return renderer->GetSharpenStrength(); + } + void Application::SetSharpenStrength(float s) { + renderer->SetSharpenStrength(s); + } bool Application::IsBloomEnabled() const { return renderer->IsBloomEnabled(); } void Application::SetBloomEnabled(bool e) { renderer->SetBloomEnabled(e); } diff --git a/src/Core/Window.cpp b/src/Core/Window.cpp index 79dbe2d8..d82b8d88 100644 --- a/src/Core/Window.cpp +++ b/src/Core/Window.cpp @@ -1,14 +1,14 @@ #include "../../include/private/Core/Window.hpp" -#include "../../include/private/Graphics/Common/RendererFactory.hpp" -#include -#include #include #include #include #include #include +#include +#include +#include "../../include/private/Graphics/Common/RendererFactory.hpp" namespace Sleak { @@ -79,14 +79,15 @@ bool Window::InitializeWindow() { SLEAK_FATAL("Failed to initialize SDL: {0}", SDL_GetError()); return false; } - if (m_inputLocked) SDL_SetEventFilter(DropUserInput, nullptr); + if (m_inputLocked) SDL_SetEventFilter(DropUserInput, nullptr); - SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, GraphicsAPI | SDL_WINDOW_RESIZABLE); + SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, + GraphicsAPI | SDL_WINDOW_RESIZABLE); - if (!SDLWindow) { - SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); - return false; - } + if (!SDLWindow) { + SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); + return false; + } if (SDL_Surface* icon = SDL_LoadBMP("assets/branding/icon.bmp")) { SDL_SetWindowIcon(SDLWindow, icon); diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index 30d6b945..0bef3477 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -46,9 +46,8 @@ bool VulkanRenderer::CreateHDRSceneResources() { info.tiling = VK_IMAGE_TILING_OPTIMAL; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // TRANSFER_SRC_BIT: MSAA resolve from the forward render pass. - info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; info.samples = VK_SAMPLE_COUNT_1_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateImage(device, &info, nullptr, &m_hdrSceneImage) != VK_SUCCESS) return false; @@ -330,7 +329,7 @@ bool VulkanRenderer::CreateBloomPipelines() { VkPushConstantRange compPushRange{}; compPushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; compPushRange.offset = 0; - compPushRange.size = 32; + compPushRange.size = 32; VkPipelineLayoutCreateInfo cliInfo{}; cliInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; @@ -777,7 +776,8 @@ void VulkanRenderer::RenderBloomCompositePass() { pc.gamma = m_tonemapEnabled ? m_gamma : 1.0f; pc.ssrEnabled = (ssrReady && m_ssrEnabled) ? 1u : 0u; // Sharpening an aliased (non-TAA) frame only amplifies the stair-steps - pc.sharpenStrength = (m_taaResolvedView != VK_NULL_HANDLE) ? m_sharpenStrength : 0.0f; + pc.sharpenStrength = + (m_taaResolvedView != VK_NULL_HANDLE) ? m_sharpenStrength : 0.0f; vkCmdPushConstants(command, m_bloomCompositePipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); vkCmdDraw(command, 3, 1, 0, 0); diff --git a/src/Graphics/Vulkan/VulkanBuffer.cpp b/src/Graphics/Vulkan/VulkanBuffer.cpp index b2a89813..98d6cd9c 100644 --- a/src/Graphics/Vulkan/VulkanBuffer.cpp +++ b/src/Graphics/Vulkan/VulkanBuffer.cpp @@ -498,7 +498,8 @@ void VulkanBuffer::CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, if (s_batchActive) { VkBufferCopy copyRegion{}; copyRegion.size = size; - vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, ©Region); + vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, + ©Region); return; } diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index 80b4ec38..63eb4f32 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -578,7 +578,7 @@ bool VulkanRenderer::CreateForwardRenderPass() { depthAtt.format = depthFormat; depthAtt.samples = VK_SAMPLE_COUNT_1_BIT; depthAtt.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAtt.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; @@ -599,22 +599,22 @@ bool VulkanRenderer::CreateForwardRenderPass() { subpass.pDepthStencilAttachment = &depthRef; std::array deps{}; - deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; - deps[0].dstSubpass = 0; - deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; // Feeds the post passes - deps[1].srcSubpass = 0; - deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; - deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; - deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | - VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; @@ -628,7 +628,7 @@ bool VulkanRenderer::CreateForwardRenderPass() { rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; rpInfo.dependencyCount = static_cast(deps.size()); - rpInfo.pDependencies = deps.data(); + rpInfo.pDependencies = deps.data(); return vkCreateRenderPass(device, &rpInfo, nullptr, &m_forwardRenderPass) == VK_SUCCESS; } diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 94a3f34c..67199c0d 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -67,7 +67,8 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { SLEAK_RETURN_ERR("Failed to create light UBO descriptor set layout!"); // Set 3: shadow map samplers — binding 0 = compare sampler (PCF), - // binding 1 = raw sampler (PCSS blocker search) + // binding 1 = raw sampler (PCSS blocker + // search) // Bindings 2-5: scene color, depth, sky, SceneReadUBO (scene-read formats) std::array shadowSamplerBindings{}; diff --git a/src/Graphics/Vulkan/VulkanDevice.cpp b/src/Graphics/Vulkan/VulkanDevice.cpp index ef31490b..f341a080 100644 --- a/src/Graphics/Vulkan/VulkanDevice.cpp +++ b/src/Graphics/Vulkan/VulkanDevice.cpp @@ -1,7 +1,5 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Vulkan/VulkanBuffer.hpp" - #include + #include #include #include @@ -11,6 +9,9 @@ #include #include #include + +#include "../../include/private/Graphics/Vulkan/VulkanBuffer.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" #include "Core/CommandLine.hpp" #include "Core/Logger.hpp" #include "SDL3/SDL_error.h" @@ -23,165 +24,168 @@ namespace Sleak { namespace RenderEngine { -/// Creates the Vulkan instance. The validation layer is on in Debug builds -/// or with --validate, and --syncval adds synchronization validation. -bool VulkanRenderer::InitVulkan() { - try { - instance = VK_NULL_HANDLE; - - std::vector requiredExtensions = { - VK_KHR_SURFACE_EXTENSION_NAME, - VK_EXT_DEBUG_UTILS_EXTENSION_NAME - }; - - #ifdef PLATFORM_LINUX - { - // Only request surface extensions actually available - // (blindly requesting both breaks capture tools like RenderDoc) - uint32_t surfExtCount = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, nullptr); - std::vector surfExts(surfExtCount); - vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, surfExts.data()); - auto hasSurfExt = [&](const char* name) { - for (auto& e : surfExts) - if (strcmp(e.extensionName, name) == 0) return true; - return false; - }; - if (hasSurfExt("VK_KHR_wayland_surface")) - requiredExtensions.push_back("VK_KHR_wayland_surface"); - if (hasSurfExt("VK_KHR_xlib_surface")) - requiredExtensions.push_back("VK_KHR_xlib_surface"); - } - #elif defined(PLATFORM_WIN) + /// Creates the Vulkan instance. The validation layer is on in Debug builds + /// or with --validate, and --syncval adds synchronization validation. + bool VulkanRenderer::InitVulkan() { + try { + instance = VK_NULL_HANDLE; + + std::vector requiredExtensions = { + VK_KHR_SURFACE_EXTENSION_NAME, + VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + +#ifdef PLATFORM_LINUX + { + // Only request surface extensions actually available + // (blindly requesting both breaks capture tools like RenderDoc) + uint32_t surfExtCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, + nullptr); + std::vector surfExts(surfExtCount); + vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, + surfExts.data()); + auto hasSurfExt = [&](const char* name) { + for (auto& e : surfExts) + if (strcmp(e.extensionName, name) == 0) return true; + return false; + }; + if (hasSurfExt("VK_KHR_wayland_surface")) + requiredExtensions.push_back("VK_KHR_wayland_surface"); + if (hasSurfExt("VK_KHR_xlib_surface")) + requiredExtensions.push_back("VK_KHR_xlib_surface"); + } +#elif defined(PLATFORM_WIN) requiredExtensions.push_back( VK_KHR_WIN32_SURFACE_EXTENSION_NAME); - #elif defined(TARGET_OS_MAC) || defined(TARGET_OS_IOS) +#elif defined(TARGET_OS_MAC) || defined(TARGET_OS_IOS) requiredExtensions.push_back(VK_MVK_MOLTENVK_EXTENSION_NAME); - #endif +#endif - // Validation layer gating - const bool syncValidation = CommandLine::HasFlag("--syncval"); - bool wantValidation = - syncValidation || CommandLine::HasFlag("--validate"); + // Validation layer gating + const bool syncValidation = CommandLine::HasFlag("--syncval"); + bool wantValidation = + syncValidation || CommandLine::HasFlag("--validate"); #ifdef _DEBUG - wantValidation = true; + wantValidation = true; #endif - const char* desiredLayer = "VK_LAYER_KHRONOS_validation"; - std::vector enabledLayers; - if (wantValidation) { - uint32_t layerCount = 0; - vkEnumerateInstanceLayerProperties(&layerCount, nullptr); - std::vector availableLayers(layerCount); - vkEnumerateInstanceLayerProperties(&layerCount, - availableLayers.data()); - - for (const auto& layer : availableLayers) { - if (strcmp(layer.layerName, desiredLayer) == 0) { - enabledLayers.push_back(desiredLayer); - SLEAK_INFO("Vulkan validation layer enabled"); - break; + const char* desiredLayer = "VK_LAYER_KHRONOS_validation"; + std::vector enabledLayers; + if (wantValidation) { + uint32_t layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, + availableLayers.data()); + + for (const auto& layer : availableLayers) { + if (strcmp(layer.layerName, desiredLayer) == 0) { + enabledLayers.push_back(desiredLayer); + SLEAK_INFO("Vulkan validation layer enabled"); + break; + } + } + if (enabledLayers.empty()) { + SLEAK_WARN( + "VK_LAYER_KHRONOS_validation not available — GPU " + "errors will not be reported"); } } + m_validationEnabled = !enabledLayers.empty(); if (enabledLayers.empty()) { - SLEAK_WARN("VK_LAYER_KHRONOS_validation not available — GPU errors will not be reported"); + auto it = std::find_if( + requiredExtensions.begin(), requiredExtensions.end(), + [](const char* ext) { + return strcmp(ext, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == + 0; + }); + if (it != requiredExtensions.end()) { + requiredExtensions.erase(it); + } } - } - m_validationEnabled = !enabledLayers.empty(); - if (enabledLayers.empty()) { - auto it = std::find_if( - requiredExtensions.begin(), requiredExtensions.end(), - [](const char* ext) { - return strcmp(ext, - VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0; - }); - if (it != requiredExtensions.end()) { - requiredExtensions.erase(it); + + // Sync validation settings + const VkBool32 syncEnabled = VK_TRUE; + VkLayerSettingEXT syncSetting{desiredLayer, "validate_sync", + VK_LAYER_SETTING_TYPE_BOOL32_EXT, 1, + &syncEnabled}; + VkLayerSettingsCreateInfoEXT layerSettings{}; + layerSettings.sType = + VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT; + layerSettings.settingCount = 1; + layerSettings.pSettings = &syncSetting; + bool useLayerSettings = false; + if (m_validationEnabled && syncValidation) { + uint32_t layerExtCount = 0; + vkEnumerateInstanceExtensionProperties(desiredLayer, + &layerExtCount, nullptr); + std::vector layerExts(layerExtCount); + vkEnumerateInstanceExtensionProperties( + desiredLayer, &layerExtCount, layerExts.data()); + for (const auto& ext : layerExts) { + if (strcmp(ext.extensionName, + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME) == 0) { + useLayerSettings = true; + break; + } + } + if (useLayerSettings) { + requiredExtensions.push_back( + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME); + SLEAK_INFO("Vulkan synchronization validation enabled"); + } else { + SLEAK_WARN( + "Vulkan synchronization validation needs " + "VK_EXT_layer_settings, which this layer lacks"); + } } - } - // Sync validation settings - const VkBool32 syncEnabled = VK_TRUE; - VkLayerSettingEXT syncSetting{desiredLayer, "validate_sync", - VK_LAYER_SETTING_TYPE_BOOL32_EXT, 1, - &syncEnabled}; - VkLayerSettingsCreateInfoEXT layerSettings{}; - layerSettings.sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT; - layerSettings.settingCount = 1; - layerSettings.pSettings = &syncSetting; - bool useLayerSettings = false; - if (m_validationEnabled && syncValidation) { - uint32_t layerExtCount = 0; - vkEnumerateInstanceExtensionProperties(desiredLayer, &layerExtCount, + // Check and list vulkan extensions + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); - std::vector layerExts(layerExtCount); - vkEnumerateInstanceExtensionProperties(desiredLayer, &layerExtCount, - layerExts.data()); - for (const auto& ext : layerExts) { - if (strcmp(ext.extensionName, - VK_EXT_LAYER_SETTINGS_EXTENSION_NAME) == 0) { - useLayerSettings = true; - break; - } + std::vector extensions(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, + extensions.data()); + + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "SleakEngine"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "Sleak Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_1; + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + createInfo.enabledExtensionCount = + static_cast(requiredExtensions.size()); + createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + createInfo.enabledLayerCount = + static_cast(enabledLayers.size()); + createInfo.ppEnabledLayerNames = enabledLayers.data(); + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (m_validationEnabled) { + PopulateDebugMessengerCreateInfo(debugCreateInfo); + if (useLayerSettings) debugCreateInfo.pNext = &layerSettings; + createInfo.pNext = &debugCreateInfo; } - if (useLayerSettings) { - requiredExtensions.push_back( - VK_EXT_LAYER_SETTINGS_EXTENSION_NAME); - SLEAK_INFO("Vulkan synchronization validation enabled"); - } else { - SLEAK_WARN( - "Vulkan synchronization validation needs " - "VK_EXT_layer_settings, which this layer lacks"); + + VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); + if (result != VK_SUCCESS) { + SLEAK_ERROR("Failed to create Vulkan instance!"); + return false; } - } - // Check and list vulkan extensions - uint32_t extensionCount = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, - nullptr); - std::vector extensions(extensionCount); - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, - extensions.data()); - - VkApplicationInfo appInfo{}; - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "SleakEngine"; - appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.pEngineName = "Sleak Engine"; - appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.apiVersion = VK_API_VERSION_1_1; - - VkInstanceCreateInfo createInfo{}; - createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - createInfo.pApplicationInfo = &appInfo; - createInfo.enabledExtensionCount = - static_cast(requiredExtensions.size()); - createInfo.ppEnabledExtensionNames = requiredExtensions.data(); - createInfo.enabledLayerCount = - static_cast(enabledLayers.size()); - createInfo.ppEnabledLayerNames = enabledLayers.data(); - - VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - if (m_validationEnabled) { - PopulateDebugMessengerCreateInfo(debugCreateInfo); - if (useLayerSettings) debugCreateInfo.pNext = &layerSettings; - createInfo.pNext = &debugCreateInfo; - } + SLEAK_INFO("Vulkan instance created successfully."); + return true; - VkResult result = - vkCreateInstance(&createInfo, nullptr, &instance); - if (result != VK_SUCCESS) { - SLEAK_ERROR("Failed to create Vulkan instance!"); + } catch (const std::exception& e) { + SLEAK_ERROR("Exception in InitVulkan: {}", e.what()); return false; } - - SLEAK_INFO("Vulkan instance created successfully."); - return true; - - } catch (const std::exception& e) { - SLEAK_ERROR("Exception in InitVulkan: {}", e.what()); - return false; } -} /// Selects the physical GPU and creates the logical device and queues. bool VulkanRenderer::CreateDevice() { diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 8f015598..8d5d7685 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -1,5 +1,3 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" - #include #include #include @@ -11,160 +9,162 @@ #include #include #include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" #include "Core/Logger.hpp" namespace Sleak { namespace RenderEngine { -namespace { + namespace { -// Relative to the working directory, which is the executable's directory -constexpr const char* kPipelineCacheDir = "cache"; -constexpr const char* kPipelineCachePath = "cache/vulkan_pipelines.bin"; -constexpr const char* kPipelineCacheTempPath = "cache/vulkan_pipelines.bin.tmp"; -constexpr size_t kPipelineCacheHeaderSize = - sizeof(VkPipelineCacheHeaderVersionOne); + // Relative to the working directory, which is the executable's directory + constexpr const char* kPipelineCacheDir = "cache"; + constexpr const char* kPipelineCachePath = "cache/vulkan_pipelines.bin"; + constexpr const char* kPipelineCacheTempPath = + "cache/vulkan_pipelines.bin.tmp"; + constexpr size_t kPipelineCacheHeaderSize = + sizeof(VkPipelineCacheHeaderVersionOne); #define FNV1A_OFFSET_BASIS 14695981039346656037ull #define FNV1A_PRIME 1099511628211ull -/// FNV-1a hash, used to skip rewriting an unchanged pipeline cache. -uint64_t HashBytes(const std::vector& data) { - uint64_t hash = FNV1A_OFFSET_BASIS; - for (char c : data) { - hash ^= static_cast(c); - hash *= FNV1A_PRIME; + /// FNV-1a hash, used to skip rewriting an unchanged pipeline cache. + uint64_t HashBytes(const std::vector& data) { + uint64_t hash = FNV1A_OFFSET_BASIS; + for (char c : data) { + hash ^= static_cast(c); + hash *= FNV1A_PRIME; + } + return hash; } - return hash; -} -/// Reads one little-endian field of a pipeline cache header. -uint32_t ReadLE32(const char* bytes) { - const auto* b = reinterpret_cast(bytes); - return static_cast(b[0]) | static_cast(b[1]) << 8 | - static_cast(b[2]) << 16 | - static_cast(b[3]) << 24; -} + /// Reads one little-endian field of a pipeline cache header. + uint32_t ReadLE32(const char* bytes) { + const auto* b = reinterpret_cast(bytes); + return static_cast(b[0]) | static_cast(b[1]) << 8 | + static_cast(b[2]) << 16 | + static_cast(b[3]) << 24; + } -} // namespace - -/// Creates the pipeline cache, seeded from disk when the saved blob was -/// written by this device and driver. -void VulkanRenderer::CreatePipelineCache() { - std::vector blob; - std::ifstream file(kPipelineCachePath, std::ios::binary | std::ios::ate); - if (file) { - const std::streamoff size = file.tellg(); - if (size > 0) { - blob.resize(static_cast(size)); - file.seekg(0); - if (!file.read(blob.data(), size)) blob.clear(); + } // namespace + + /// Creates the pipeline cache, seeded from disk when the saved blob was + /// written by this device and driver. + void VulkanRenderer::CreatePipelineCache() { + std::vector blob; + std::ifstream file(kPipelineCachePath, + std::ios::binary | std::ios::ate); + if (file) { + const std::streamoff size = file.tellg(); + if (size > 0) { + blob.resize(static_cast(size)); + file.seekg(0); + if (!file.read(blob.data(), size)) blob.clear(); + } } - } - file.close(); - - // Header check - if (!blob.empty()) { - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(physicalDevice, &props); - const bool valid = - blob.size() >= kPipelineCacheHeaderSize && - ReadLE32(blob.data()) >= kPipelineCacheHeaderSize && - ReadLE32(blob.data()) <= blob.size() && - ReadLE32(blob.data() + - offsetof(VkPipelineCacheHeaderVersionOne, - headerVersion)) == - static_cast(VK_PIPELINE_CACHE_HEADER_VERSION_ONE) && - ReadLE32(blob.data() + - offsetof(VkPipelineCacheHeaderVersionOne, vendorID)) == - props.vendorID && - ReadLE32(blob.data() + - offsetof(VkPipelineCacheHeaderVersionOne, deviceID)) == - props.deviceID && - std::memcmp(blob.data() + - offsetof(VkPipelineCacheHeaderVersionOne, - pipelineCacheUUID), - props.pipelineCacheUUID, - VK_UUID_SIZE) == 0; - if (!valid) { - SLEAK_INFO( - "Pipeline cache: ignoring blob from another device or " - "driver"); - blob.clear(); + file.close(); + + // Header check + if (!blob.empty()) { + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + const bool valid = + blob.size() >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) <= blob.size() && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + headerVersion)) == + static_cast( + VK_PIPELINE_CACHE_HEADER_VERSION_ONE) && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + vendorID)) == props.vendorID && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + deviceID)) == props.deviceID && + std::memcmp( + blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + pipelineCacheUUID), + props.pipelineCacheUUID, VK_UUID_SIZE) == 0; + if (!valid) { + SLEAK_INFO( + "Pipeline cache: ignoring blob from another device or " + "driver"); + blob.clear(); + } } - } - VkPipelineCacheCreateInfo info{}; - info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; - info.initialDataSize = blob.size(); - info.pInitialData = blob.empty() ? nullptr : blob.data(); - VkResult result = - vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); - if (result != VK_SUCCESS && !blob.empty()) { - blob.clear(); - info.initialDataSize = 0; - info.pInitialData = nullptr; - result = + VkPipelineCacheCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + info.initialDataSize = blob.size(); + info.pInitialData = blob.empty() ? nullptr : blob.data(); + VkResult result = vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); - } - if (result != VK_SUCCESS) { - m_pipelineCache = VK_NULL_HANDLE; - SLEAK_WARN( - "Pipeline cache: creation failed, pipelines compile uncached"); - return; - } - - m_pipelineCacheLoadedSize = blob.size(); - m_pipelineCacheLoadedHash = blob.empty() ? 0 : HashBytes(blob); - SLEAK_INFO("Pipeline cache: {} ({} bytes)", - blob.empty() ? "empty" : "loaded", blob.size()); -} + if (result != VK_SUCCESS && !blob.empty()) { + blob.clear(); + info.initialDataSize = 0; + info.pInitialData = nullptr; + result = + vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); + } + if (result != VK_SUCCESS) { + m_pipelineCache = VK_NULL_HANDLE; + SLEAK_WARN( + "Pipeline cache: creation failed, pipelines compile uncached"); + return; + } -/// Saves the pipeline cache to disk when it changed, then destroys it. Writes -/// a temp file and renames it so a failed write never leaves a torn cache. -void VulkanRenderer::DestroyPipelineCache() { - if (m_pipelineCache == VK_NULL_HANDLE) return; - - std::vector data; - size_t size = 0; - if (vkGetPipelineCacheData(device, m_pipelineCache, &size, nullptr) == - VK_SUCCESS && - size > 0) { - data.resize(size); - if (vkGetPipelineCacheData(device, m_pipelineCache, &size, - data.data()) == VK_SUCCESS) - data.resize(size); - else - data.clear(); + m_pipelineCacheLoadedSize = blob.size(); + m_pipelineCacheLoadedHash = blob.empty() ? 0 : HashBytes(blob); + SLEAK_INFO("Pipeline cache: {} ({} bytes)", + blob.empty() ? "empty" : "loaded", blob.size()); } - vkDestroyPipelineCache(device, m_pipelineCache, nullptr); - m_pipelineCache = VK_NULL_HANDLE; - if (data.empty()) return; - if (data.size() == m_pipelineCacheLoadedSize && - HashBytes(data) == m_pipelineCacheLoadedHash) - return; + /// Saves the pipeline cache to disk when it changed, then destroys it. + /// Writes a temp file and renames it so a failed write never leaves a torn + /// cache. + void VulkanRenderer::DestroyPipelineCache() { + if (m_pipelineCache == VK_NULL_HANDLE) return; + + std::vector data; + size_t size = 0; + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, nullptr) == + VK_SUCCESS && + size > 0) { + data.resize(size); + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, + data.data()) == VK_SUCCESS) + data.resize(size); + else + data.clear(); + } + vkDestroyPipelineCache(device, m_pipelineCache, nullptr); + m_pipelineCache = VK_NULL_HANDLE; - std::error_code ec; - std::filesystem::create_directories(kPipelineCacheDir, ec); - std::ofstream out(kPipelineCacheTempPath, - std::ios::binary | std::ios::trunc); - out.write(data.data(), static_cast(data.size())); - out.close(); - if (!out) { - SLEAK_WARN("Pipeline cache: could not write {}", - kPipelineCacheTempPath); - std::filesystem::remove(kPipelineCacheTempPath, ec); - return; - } - std::filesystem::rename(kPipelineCacheTempPath, kPipelineCachePath, ec); - if (ec) { - SLEAK_WARN("Pipeline cache: could not replace {}: {}", - kPipelineCachePath, ec.message()); - std::filesystem::remove(kPipelineCacheTempPath, ec); - return; + if (data.empty()) return; + if (data.size() == m_pipelineCacheLoadedSize && + HashBytes(data) == m_pipelineCacheLoadedHash) + return; + + std::error_code ec; + std::filesystem::create_directories(kPipelineCacheDir, ec); + std::ofstream out(kPipelineCacheTempPath, + std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(data.size())); + out.close(); + if (!out) { + SLEAK_WARN("Pipeline cache: could not write {}", + kPipelineCacheTempPath); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + std::filesystem::rename(kPipelineCacheTempPath, kPipelineCachePath, ec); + if (ec) { + SLEAK_WARN("Pipeline cache: could not replace {}: {}", + kPipelineCachePath, ec.message()); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + SLEAK_INFO("Pipeline cache: saved {} bytes", data.size()); } - SLEAK_INFO("Pipeline cache: saved {} bytes", data.size()); -} /// Binds the skybox pipeline and its descriptor set for the current frame. void VulkanRenderer::BeginSkyboxPass() { @@ -408,7 +408,6 @@ bool VulkanRenderer::CreateGraphicsPipeline() { return true; } - /// Creates the main forward render pass with optional MSAA color and resolve attachments. bool VulkanRenderer::CreateRenderPass() { const bool msaaEnabled = (m_msaaSamples != VK_SAMPLE_COUNT_1_BIT); @@ -1062,8 +1061,8 @@ VkPipeline VulkanRenderer::BuildCustomFormatPipeline( blend.colorBlendOp = VK_BLEND_OP_ADD; // Transparent draws zero the HDR scene alpha: TAA's reactive mask const bool transparent = pass == CustomPass::Transparent; - blend.srcAlphaBlendFactor = transparent ? VK_BLEND_FACTOR_ZERO - : VK_BLEND_FACTOR_ONE; + blend.srcAlphaBlendFactor = + transparent ? VK_BLEND_FACTOR_ZERO : VK_BLEND_FACTOR_ONE; blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; blend.alphaBlendOp = VK_BLEND_OP_ADD; } @@ -1129,7 +1128,6 @@ void VulkanRenderer::PrebuildCustomFormatPipelines() { } } - /// Binds the custom-format pipeline matching the currently active render pass. void VulkanRenderer::BeginCustomFormatPass(VertexFormatHandle format) { if (!bFrameStarted) return; @@ -1195,7 +1193,6 @@ void VulkanRenderer::EndCustomFormatPass() { } } - /// Destroys every cached custom-format pipeline and clears the table. void VulkanRenderer::DestroyCustomFormatPipelines() { for (const CustomFormatPipelines& pipes : m_customFormatPipelines) { diff --git a/src/Graphics/Vulkan/VulkanProfiler.cpp b/src/Graphics/Vulkan/VulkanProfiler.cpp index df876763..1937d8a9 100644 --- a/src/Graphics/Vulkan/VulkanProfiler.cpp +++ b/src/Graphics/Vulkan/VulkanProfiler.cpp @@ -17,9 +17,9 @@ namespace { constexpr float kGpuLogIntervalSeconds = 2.0f; -constexpr std::array kGpuRowNames = { - "shadow", "gbuffer", "ssao", "lighting", "forward", "taa", - "ssr", "bloom", "tonemap", "ui", "other", "total"}; +constexpr std::array kGpuRowNames = {"shadow", "gbuffer", "ssao", "lighting", + "forward", "taa", "ssr", "bloom", + "tonemap", "ui", "other", "total"}; } // namespace @@ -61,7 +61,9 @@ bool VulkanRenderer::CreateGpuProfiler() { } m_gpuTickNs = static_cast(props.limits.timestampPeriod); - m_gpuTickMask = validBits >= std::numeric_limits::digits ? ~0ull : ((1ull << validBits) - 1); + m_gpuTickMask = validBits >= std::numeric_limits::digits + ? ~0ull + : ((1ull << validBits) - 1); m_gpuSlots = {}; m_gpuSumMs = {}; m_gpuMaxMs = {}; diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index aa3f5c1f..177a1db2 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -852,8 +852,8 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, // Pushes survive pipeline binds if (m_shadowPCPushedLayout != activeLayout) { vkCmdPushConstants(command, activeLayout, - VK_SHADER_STAGE_VERTEX_BIT, 0, PUSH_CONSTANT_BYTES, - m_shadowPCCache); + VK_SHADER_STAGE_VERTEX_BIT, 0, + PUSH_CONSTANT_BYTES, m_shadowPCCache); m_shadowPCPushedLayout = activeLayout; } } else { @@ -1897,7 +1897,10 @@ bool VulkanRenderer::CreateGBufferResources() { if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } if (skyboxPipeline) { vkDestroyPipeline(device, skyboxPipeline, nullptr); skyboxPipeline = VK_NULL_HANDLE; } if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; } - if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; } + if (skinnedPipeline) { + vkDestroyPipeline(device, skinnedPipeline, nullptr); + skinnedPipeline = VK_NULL_HANDLE; + } DestroyCustomFormatPipelines(); CreateGraphicsPipeline(); diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index ffdbc3fe..0d105537 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -1,12 +1,12 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" - #include #include #include #include #include #include + +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" #include "Core/Logger.hpp" namespace Sleak { @@ -417,8 +417,9 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { } // Create descriptor pool for light UBO + shadow samplers. - // Two shadow samplers per frame now (compare + raw for PCSS blocker search). - // Set 3 also carries the scene-read color, depth, sky, and UBO bindings. + // Two shadow samplers per frame now (compare + raw for PCSS blocker + // search). Set 3 also carries the scene-read color, depth, sky, and UBO + // bindings. std::array poolSizes{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSizes[0].descriptorCount = diff --git a/src/Graphics/Vulkan/VulkanSwapchain.cpp b/src/Graphics/Vulkan/VulkanSwapchain.cpp index 1cba8a20..b5ca6150 100644 --- a/src/Graphics/Vulkan/VulkanSwapchain.cpp +++ b/src/Graphics/Vulkan/VulkanSwapchain.cpp @@ -12,25 +12,25 @@ namespace Sleak { namespace RenderEngine { -namespace { - -/// Short name of a present mode for logs. -const char* PresentModeName(VkPresentModeKHR mode) { - switch (mode) { - case VK_PRESENT_MODE_IMMEDIATE_KHR: - return "IMMEDIATE"; - case VK_PRESENT_MODE_MAILBOX_KHR: - return "MAILBOX"; - case VK_PRESENT_MODE_FIFO_KHR: - return "FIFO"; - case VK_PRESENT_MODE_FIFO_RELAXED_KHR: - return "FIFO_RELAXED"; - default: - return "OTHER"; + namespace { + + /// Short name of a present mode for logs. + const char* PresentModeName(VkPresentModeKHR mode) { + switch (mode) { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "IMMEDIATE"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "MAILBOX"; + case VK_PRESENT_MODE_FIFO_KHR: + return "FIFO"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "FIFO_RELAXED"; + default: + return "OTHER"; + } } -} -} // namespace + } // namespace /// Creates the swapchain from the queried surface capabilities. bool VulkanRenderer::CreateSwapChain() { @@ -472,6 +472,5 @@ bool VulkanRenderer::SurfaceExtentChanged() { const VkExtent2D extent = ChooseExtend(caps); return extent.width != scExtent.width || extent.height != scExtent.height; } - } } diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 4a2a31bc..9cdc7877 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -30,9 +30,8 @@ bool VulkanRenderer::CreateTAAResources() { ic.format = fmt; ic.tiling = VK_IMAGE_TILING_OPTIMAL; ic.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - ic.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + ic.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; ic.samples = VK_SAMPLE_COUNT_1_BIT; ic.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateImage(device, &ic, nullptr, &m_taaImages[i]) != VK_SUCCESS) return false; @@ -149,17 +148,17 @@ bool VulkanRenderer::CreateTAAResources() { sub.pColorAttachments = &ref; std::array deps{}; - deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; - deps[0].dstSubpass = 0; - deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; // Feeds history and the post passes - deps[1].srcSubpass = 0; - deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; - deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; @@ -171,7 +170,7 @@ bool VulkanRenderer::CreateTAAResources() { rpi.subpassCount = 1; rpi.pSubpasses = ⊂ rpi.dependencyCount = static_cast(deps.size()); - rpi.pDependencies = deps.data(); + rpi.pDependencies = deps.data(); if (vkCreateRenderPass(device, &rpi, nullptr, &m_taaRenderPass) != VK_SUCCESS) return false; } @@ -402,8 +401,9 @@ void VulkanRenderer::CleanupTAAResources() { m_taaResourcesCreated = false; } -/// Computes the current-to-previous clip reprojection and blend factor into the TAA UBO. -/// Uses double: in float, inverse(VP) * VP drifts from identity far from the origin. +/// Computes the current-to-previous clip reprojection and blend factor into the +/// TAA UBO. Uses double: in float, inverse(VP) * VP drifts from identity far +/// from the origin. void VulkanRenderer::UpdateTAAUBO() { if (!m_taaResourcesCreated || !m_taaUboMapped[currentFrame]) return; @@ -426,7 +426,8 @@ void VulkanRenderer::UpdateTAAUBO() { if (InvertMat4(currentVP, invCurrentVP)) { MatMul4(invCurrentVP, m_prevViewProj, currentToPrev); } else { - for (int i = 0; i < 16; ++i) currentToPrev[i] = (i / 4 == i % 4) ? 1.0 : 0.0; + for (int i = 0; i < 16; ++i) + currentToPrev[i] = (i / 4 == i % 4) ? 1.0 : 0.0; } TAAParams p{}; @@ -451,7 +452,8 @@ void VulkanRenderer::RenderTAAPass() { m_taaRanLastFrame = false; return; } - // History and the previous VP are stale after TAA was off: restart accumulation + // History and the previous VP are stale after TAA was off: restart + // accumulation if (!m_taaRanLastFrame) m_taaFrameIdx = 0; m_taaRanLastFrame = true; diff --git a/src/Graphics/Vulkan/VulkanTexture.cpp b/src/Graphics/Vulkan/VulkanTexture.cpp index b961fb79..a3328115 100644 --- a/src/Graphics/Vulkan/VulkanTexture.cpp +++ b/src/Graphics/Vulkan/VulkanTexture.cpp @@ -1,10 +1,12 @@ #include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" + #include -#include #include -#include + +#include #include #include +#include #include #include #include From fe77bd343e41688908fd5ba6ea979c460be4e465 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 25 Oct 2026 21:55:23 +0300 Subject: [PATCH 36/36] fix: avoid the windows far macro in the vulkan scene-read clear --- src/Graphics/Vulkan/VulkanSceneRead.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Graphics/Vulkan/VulkanSceneRead.cpp b/src/Graphics/Vulkan/VulkanSceneRead.cpp index afcb3a17..a49e37d2 100644 --- a/src/Graphics/Vulkan/VulkanSceneRead.cpp +++ b/src/Graphics/Vulkan/VulkanSceneRead.cpp @@ -162,10 +162,11 @@ void ClearToShaderRead(VkDevice device, VkCommandPool pool, VkQueue queue, const VkImageSubresourceRange range = {t.aspect, 0, 1, 0, t.layers}; if (t.aspect & VK_IMAGE_ASPECT_DEPTH_BIT) { - const VkClearDepthStencilValue far = {FAR_DEPTH, 0}; + // windows.h defines `far` as an empty macro + const VkClearDepthStencilValue farDepth = {FAR_DEPTH, 0}; vkCmdClearDepthStencilImage(cmd, t.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - &far, 1, &range); + &farDepth, 1, &range); } else { const VkClearColorValue zero = {}; vkCmdClearColorImage(cmd, t.image,