#include "VulkanRenderer.h" #include "VulkanBarrier.h" #include "VulkanBuffer.h" #include "VulkanTexture.h" #include "backends/imgui_impl_vulkan.h" #include "graphics/2d.h" #include "lighting/lighting.h" extern float flFrametime; namespace graphics::vulkan { void VulkanRenderer::beginTrackedRenderPass(const PassBeginDesc& desc) { vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = desc.renderPass; rpBegin.framebuffer = desc.framebuffer; rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = desc.extent; rpBegin.clearValueCount = static_cast(desc.clearValues.size); rpBegin.pClearValues = desc.clearValues.data; m_currentCommandBuffer.beginRenderPass(rpBegin, vk::SubpassContents::eInline); m_stateTracker->setRenderPass(desc.renderPass, 0); m_stateTracker->setColorAttachmentCount(desc.colorAttachmentCount); m_stateTracker->setCurrentSampleCount(desc.sampleCount); m_stateTracker->setRenderArea(desc.extent); switch (desc.viewport) { case PassViewport::FlipY: // Negative viewport height for OpenGL-compatible Y-up NDC (VK_KHR_maintenance1) m_stateTracker->setViewport(0.0f, static_cast(desc.extent.height), static_cast(desc.extent.width), -static_cast(desc.extent.height)); break; case PassViewport::NoFlip: m_stateTracker->setViewport(0.0f, 0.0f, static_cast(desc.extent.width), static_cast(desc.extent.height)); break; case PassViewport::Keep: break; } } void VulkanRenderer::acquireNextSwapChainImage() { m_frames[m_currentFrame]->waitForFinish(); // Acquire an image, recreating the swap chain as often as needed (bounded). // The frame must never proceed without an acquired image: its image-available // semaphore would never be signaled and the submit would deadlock/corrupt. constexpr int MAX_ACQUIRE_ATTEMPTS = 5; uint32_t imageIndex = 0; SwapChainStatus status = SwapChainStatus::eOutOfDate; for (int attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; ++attempt) { // Recreate if flagged (from a previous frame, a failed acquire below, or // a suboptimal present). Waits out a minimized window (0x0 extent). if (m_swapChainNeedsRecreation) { while (!recreateSwapChain()) { os_sleep(100); SDL_PumpEvents(); } } status = m_frames[m_currentFrame]->acquireSwapchainImage(imageIndex); if (status != SwapChainStatus::eOutOfDate) { break; } m_swapChainNeedsRecreation = true; } if (status == SwapChainStatus::eOutOfDate) { Error(LOCATION, "Vulkan: failed to acquire a swap chain image after %d recreation attempts!", MAX_ACQUIRE_ATTEMPTS); } if (shouldRecreateSwapChain(status)) { m_swapChainNeedsRecreation = true; } m_currentSwapChainImage = imageIndex; // Ensure that this image is no longer in use if (m_swapChainImageRenderImage[m_currentSwapChainImage]) { m_swapChainImageRenderImage[m_currentSwapChainImage]->waitForFinish(); } // Reserve the image as in use m_swapChainImageRenderImage[m_currentSwapChainImage] = m_frames[m_currentFrame].get(); } void VulkanRenderer::setupFrame() { if (m_frameInProgress) { Warning(LOCATION, "VulkanRenderer::setupFrame called while frame already in progress!"); return; } // Free completed texture upload command buffers Assertion(m_textureManager, "Vulkan TextureManager not initialized in setupFrame!"); m_textureManager->frameStart(); // Allocate command buffer for this frame vk::CommandBufferAllocateInfo cmdBufferAlloc; cmdBufferAlloc.commandPool = m_graphicsCommandPool.get(); cmdBufferAlloc.level = vk::CommandBufferLevel::ePrimary; cmdBufferAlloc.commandBufferCount = 1; auto cmdBufs = m_device->allocateCommandBuffers(cmdBufferAlloc); m_currentCommandBuffers.assign(cmdBufs.begin(), cmdBufs.end()); m_currentCommandBuffer = m_currentCommandBuffers.front(); // Begin command buffer vk::CommandBufferBeginInfo beginInfo; beginInfo.flags |= vk::CommandBufferUsageFlagBits::eOneTimeSubmit; m_currentCommandBuffer.begin(beginInfo); Assertion(m_descriptorManager, "Vulkan DescriptorManager not initialized in setupFrame!"); m_descriptorManager->beginFrame(); Assertion(m_stateTracker, "Vulkan StateTracker not initialized in setupFrame!"); m_stateTracker->beginFrame(m_currentCommandBuffer); // Reset the post-processing scratch UBO ring cursor for this frame-in-flight // slot (safe: acquireNextSwapChainImage already waited on this slot's fence) if (m_postProcessor) { m_postProcessor->beginFrame(m_currentFrame); } // Reset timestamp queries that were written last frame (must be outside render pass) if (m_queryManager) { m_queryManager->beginFrame(m_currentCommandBuffer); } // Reset per-frame flags m_sceneDepthCopiedThisFrame = false; m_sceneDepthSaved = false; // Reset per-frame draw statistics Assertion(m_drawManager, "Vulkan DrawManager not initialized in setupFrame!"); m_drawManager->resetFrameStats(); // Begin the composition render pass (clears color to black, depth to far plane) std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); PassBeginDesc pass; pass.renderPass = m_renderPass.get(); pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get(); pass.extent = m_renderExtent; pass.clearValues = clearValues; beginTrackedRenderPass(pass); m_frameInProgress = true; } void VulkanRenderer::flip() { if (!m_frameInProgress) { nprintf(("Vulkan", "VulkanRenderer::flip called without frame in progress, skipping\n")); return; } // Print per-frame diagnostic summary before ending Assertion(m_drawManager, "Vulkan DrawManager not initialized in flip!"); m_drawManager->printFrameStats(); // End the composition render pass (composition image is now in // eShaderReadOnlyOptimal) and run the final output-encode pass that writes // the actual swap chain image (SDR passthrough or HDR10 PQ/BT.2020). // Tolerates the pass having already been ended: the OpenXR flip has to end // it itself to record its swapchain blit before handing off to us. endCurrentRenderPass(); #ifdef __APPLE__ // MoltenVK: the encode render pass's VK_SUBPASS_EXTERNAL dependency (see // createEncodeRenderPass(): srcStage=eColorAttachmentOutput|eFragmentShader, // srcAccess=eColorAttachmentWrite, dst covers the encode's eShaderRead) is // spec-legal and sufficient on desktop Vulkan drivers, and -gr_sync_validation // confirms the composition-image ordering is clean there without this // barrier. It is kept only because MoltenVK's translation of the implicit // cross-render-pass sync into Metal fences/barriers has historically been // unreliable (tearing/garbage in the composition image once the encode pass // samples it). RE-TEST on macOS hardware; if MoltenVK now honors the subpass // dependency, this explicit barrier can be removed. if (m_currentSwapChainImage < m_compositionImages.size()) { ImageBarrier2 compositionBarrier; compositionBarrier.image = m_compositionImages[m_currentSwapChainImage].get(); compositionBarrier.levelCount = 1; compositionBarrier.layerCount = 1; compositionBarrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; compositionBarrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; compositionBarrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; compositionBarrier.srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite; compositionBarrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader; compositionBarrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead; cmdImageBarrier(m_currentCommandBuffer, compositionBarrier); } #endif encodeToSwapChain(); m_stateTracker->endFrame(); // End command buffer m_currentCommandBuffer.end(); // Set up cleanup callback for command buffers auto buffersToFree = m_currentCommandBuffers; m_frames[m_currentFrame]->onFrameFinished([this, buffersToFree]() mutable { m_device->freeCommandBuffers(m_graphicsCommandPool.get(), buffersToFree); }); // Submit and present auto presentStatus = m_frames[m_currentFrame]->submitAndPresent(m_currentCommandBuffers, m_renderingFinishedSemaphores[m_currentSwapChainImage].get()); if (shouldRecreateSwapChain(presentStatus)) { m_swapChainNeedsRecreation = true; } // Notify query manager that this frame's command buffer was submitted if (m_queryManager) { m_queryManager->notifySubmission(); } // Track which swap chain image was just presented so saveScreen() can read it m_previousSwapChainImage = m_currentSwapChainImage; // Clear current command buffer reference m_currentCommandBuffer = nullptr; m_currentCommandBuffers.clear(); m_frameInProgress = false; // Advance the single frame-in-flight index and push it to every consumer that // tracks it, so they all agree by construction. Done immediately after // incrementing so any buffer/descriptor operations before setupFrame() use the // correct frame. m_currentFrame = (m_currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; ++m_frameNumber; m_bufferManager->setCurrentFrame(m_currentFrame, m_frameNumber); m_descriptorManager->setCurrentFrame(m_currentFrame); acquireNextSwapChainImage(); // Process deferred resource deletions AFTER the fence wait in // acquireNextSwapChainImage, so we know the previous frame's commands // (including async upload CBs) have completed before destroying resources. m_deletionQueue->processDestructions(); } void VulkanRenderer::beginSceneRendering() { if (!m_postProcessor || !m_postProcessor->isInitialized()) { return; } if (m_sceneRendering) { return; } // End the current swap chain render pass m_currentCommandBuffer.endRenderPass(); // Use G-buffer render pass when deferred lighting is enabled and G-buffer is ready m_useGbufRenderPass = m_postProcessor->deferred().isInitialized() && light_deferred_enabled(); // Begin the HDR scene render pass (or G-buffer render pass for deferred) PassBeginDesc pass; pass.extent = m_postProcessor->getSceneExtent(); if (m_useGbufRenderPass) { // Clear values: 6 color + depth std::array clearValues{}; clearValues[VulkanDeferredGBuffer::GBUF_ATT_COLOR].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[VulkanDeferredGBuffer::GBUF_ATT_POSITION].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); clearValues[VulkanDeferredGBuffer::GBUF_ATT_NORMAL].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); clearValues[VulkanDeferredGBuffer::GBUF_ATT_SPECULAR].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); clearValues[VulkanDeferredGBuffer::GBUF_ATT_EMISSIVE].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); clearValues[VulkanDeferredGBuffer::GBUF_ATT_COMPOSITE].color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); pass.renderPass = m_postProcessor->deferred().renderPass(); pass.framebuffer = m_postProcessor->deferred().framebuffer(); pass.clearValues = clearValues; pass.colorAttachmentCount = VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT; beginTrackedRenderPass(pass); } else { std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); pass.renderPass = m_postProcessor->getSceneRenderPass(); pass.framebuffer = m_postProcessor->getSceneFramebuffer(); pass.clearValues = clearValues; beginTrackedRenderPass(pass); } m_sceneRendering = true; } void VulkanRenderer::resumeSceneRendering() { PassBeginDesc pass; pass.renderPass = m_postProcessor->getSceneRenderPassLoad(); pass.framebuffer = m_postProcessor->getSceneFramebuffer(); pass.extent = m_postProcessor->getSceneExtent(); beginTrackedRenderPass(pass); } void VulkanRenderer::endSceneRendering() { if (!m_postProcessor || !m_postProcessor->isInitialized()) { return; } if (!m_sceneRendering) { return; } // End HDR scene render pass (transitions scene color to eShaderReadOnlyOptimal) m_currentCommandBuffer.endRenderPass(); // Update distortion ping-pong textures (every ~30ms, matching OpenGL) if (Gr_framebuffer_effects.any_set()) { m_postProcessor->updateDistortion(m_currentCommandBuffer, flFrametime); } // Execute post-processing passes (all between HDR scene pass and swap chain pass) m_postProcessor->executeBloom(m_currentCommandBuffer); m_postProcessor->executeTonemap(m_currentCommandBuffer); m_postProcessor->executeFXAA(m_currentCommandBuffer); m_postProcessor->executeSMAA(m_currentCommandBuffer); m_postProcessor->executeLightshafts(m_currentCommandBuffer); m_postProcessor->executePostEffects(m_currentCommandBuffer); // Begin the resumed swap chain render pass (loadOp=eLoad to preserve pre-scene content). // Non-flipped viewport for the post-processing blit (HDR texture is already in // Vulkan orientation); restored to flipped for HUD rendering below. std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); PassBeginDesc pass; pass.renderPass = m_renderPassLoad.get(); pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get(); pass.extent = m_renderExtent; pass.clearValues = clearValues; pass.viewport = PassViewport::NoFlip; beginTrackedRenderPass(pass); // Blit the HDR scene to swap chain through post-processing m_postProcessor->blitToSwapChain(m_currentCommandBuffer); // Restore Y-flipped viewport for HUD rendering m_stateTracker->setViewport(0.0f, static_cast(m_renderExtent.height), static_cast(m_renderExtent.width), -static_cast(m_renderExtent.height)); m_sceneRendering = false; m_useGbufRenderPass = false; } void VulkanRenderer::copyEffectTexture() { if (!m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { return; } // End the current scene render pass // This transitions scene color to eShaderReadOnlyOptimal (the render pass's finalLayout) // For G-buffer: all 6 color attachments transition to eShaderReadOnlyOptimal m_currentCommandBuffer.endRenderPass(); // Copy scene color → effect texture (handles scene color transitions) m_postProcessor->copyEffectTexture(m_currentCommandBuffer); // If G-buffer is active, transition attachments 1-5 for render pass resume if (m_useGbufRenderPass) { m_postProcessor->deferred().transitionForResume(m_currentCommandBuffer); } // Resume the scene render pass with loadOp=eLoad to preserve existing content // Scene color is now in eColorAttachmentOptimal (copyEffectTexture transitions it back) // Depth is still in eDepthStencilAttachmentOptimal (untouched by the copy) resumeScenePassAfterCopy(); } // Shared tail of copyEffectTexture()/copySceneDepthForParticles(): resume the // scene (or G-buffer) render pass with loadOp=eLoad after a mid-scene copy. void VulkanRenderer::resumeScenePassAfterCopy() { PassBeginDesc pass; pass.extent = m_postProcessor->getSceneExtent(); if (m_useGbufRenderPass) { // Clear values ignored for eLoad but array must cover all attachments std::array clearValues{}; clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); pass.renderPass = m_postProcessor->deferred().renderPassLoad(); pass.framebuffer = m_postProcessor->deferred().framebuffer(); pass.clearValues = clearValues; pass.colorAttachmentCount = VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT; beginTrackedRenderPass(pass); } else { std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); pass.renderPass = m_postProcessor->getSceneRenderPassLoad(); pass.framebuffer = m_postProcessor->getSceneFramebuffer(); pass.clearValues = clearValues; beginTrackedRenderPass(pass); } } void VulkanRenderer::copySceneDepthForParticles() { if (m_sceneDepthCopiedThisFrame || !m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { return; } // End the current scene render pass // This transitions: color → eShaderReadOnlyOptimal, depth → eDepthStencilAttachmentOptimal // For G-buffer: all 6 color attachments → eShaderReadOnlyOptimal m_currentCommandBuffer.endRenderPass(); // Copy scene depth → samplable depth copy (handles all depth image transitions) m_postProcessor->copySceneDepth(m_currentCommandBuffer); resumeScenePassAfterDepthCopy(); m_sceneDepthCopiedThisFrame = true; } // Shared tail of every mid-scene copy that touches only depth: put the color // attachments back the way the resumed loadOp=eLoad pass expects them, then resume. void VulkanRenderer::resumeScenePassAfterDepthCopy() { // Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal // (needed for the resumed render pass with loadOp=eLoad, which expects // initialLayout=eColorAttachmentOptimal; a depth copy only touches depth) { ImageBarrier2 barrier; barrier.image = m_postProcessor->getSceneColorImage(); barrier.levelCount = 1; barrier.layerCount = 1; barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; barrier.srcAccess = {}; barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; // eColorAttachmentRead as well as Write: resumeScenePassAfterCopy() resumes // the scene pass with loadOp=eLoad, which reads this attachment; the read // must be ordered after the transition (else READ_AFTER_WRITE). barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; cmdImageBarrier(m_currentCommandBuffer, barrier); } // If G-buffer is active, transition attachments 1-5 for render pass resume if (m_useGbufRenderPass) { m_postProcessor->deferred().transitionForResume(m_currentCommandBuffer); } // Resume the scene render pass with loadOp=eLoad resumeScenePassAfterCopy(); } // Head of every mid-scene depth copy: end the scene (or G-buffer) pass, which leaves the color // attachments in eShaderReadOnlyOptimal and depth in eDepthStencilAttachmentOptimal. // resumeScenePassAfterCopy() knows the scene pass and the non-MSAA G-buffer pass only. The // player-ship callers run between one deferred pass and the next, so the multisampled G-buffer // pass is never live -- resuming the wrong pass would desync the state tracker. void VulkanRenderer::endScenePassForDepthCopy() { Assertion(m_stateTracker->getCurrentSampleCount() == vk::SampleCountFlagBits::e1, "Tried to copy the scene depth while a multisampled render pass was active!"); m_currentCommandBuffer.endRenderPass(); } // The cockpit model is rendered around the camera, inside the ship hull that the scene // pass already drew, so it needs a depth buffer that does not know about that hull -- // otherwise the hull wins the depth test and shows through the cockpit. OpenGL swaps // the depth attachment to Cockpit_depth_texture for the duration; Vulkan bakes the // attachment into the framebuffer, so the scene depth is copied aside and cleared // instead. Only the scene depth image is parked: with MSAA the cockpit renders against // the multisampled depth, which the G-buffer pass clears on its own, and // gr_deferred_lighting_msaa() resolves back into the scene depth before restore runs. void VulkanRenderer::saveSceneDepth() { if (m_sceneDepthSaved || !m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { return; } endScenePassForDepthCopy(); m_postProcessor->saveSceneDepth(m_currentCommandBuffer); resumeScenePassAfterDepthCopy(); m_sceneDepthSaved = true; } void VulkanRenderer::restoreSceneDepth() { if (!m_sceneDepthSaved) { return; } // Cleared first: a scene that ends between the save and the restore (a failed // resize dropping the post-processor, say) must not leave the flag set for the // next frame's restore to act on stale content. m_sceneDepthSaved = false; if (!m_sceneRendering || !m_postProcessor || !m_postProcessor->isInitialized()) { return; } endScenePassForDepthCopy(); m_postProcessor->restoreSceneDepth(m_currentCommandBuffer); resumeScenePassAfterDepthCopy(); } void VulkanRenderer::beginRenderTarget(tcache_slot_vulkan* ts, int face) { // End current render pass (swap chain or previous RT face) m_currentCommandBuffer.endRenderPass(); // Select the correct framebuffer for cubemap face or flat RT vk::Framebuffer fb = (ts->isCubemap && face >= 0 && face < 6) ? ts->cubeFaceFramebuffers[face] : ts->framebuffer; // Clear values must match the render pass attachments: color at index 0, and // depth at index 1 when this target has a depth attachment. std::array clearValues; clearValues[0].color = m_stateTracker->getClearColor(); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); const bool hasDepth = static_cast(ts->depthImage); PassBeginDesc pass; pass.renderPass = ts->renderPass; pass.framebuffer = fb; pass.extent = vk::Extent2D(ts->width, ts->height); pass.clearValues = ArrayView(clearValues.data(), hasDepth ? 2 : 1); // The engine sets the RTT viewport itself via gr_set_viewport (positive // height; the RTT projection matrix handles the Y-flip), so keep it. pass.viewport = PassViewport::Keep; beginTrackedRenderPass(pass); m_renderTargetActive = true; } void VulkanRenderer::endRenderTarget(tcache_slot_vulkan* ts) { if (!m_renderTargetActive) { return; } // End the RT render pass (finalLayout transitions mip 0 to eShaderReadOnlyOptimal) m_currentCommandBuffer.endRenderPass(); m_renderTargetActive = false; // Render passes only ever draw into mip 0 (each face's framebuffer is a // single-mip view), so higher mips are otherwise left uninitialized. // Regenerate the chain now via blits so effects that sample it with an // explicit LOD (e.g. the env map's roughness-based reflection blur) see // real data instead of garbage/undefined content. if (ts && ts->mipLevels > 1) { uint32_t layers = ts->isCubemap ? 6u : ts->arrayLayers; ImageBarrier2 barrier; barrier.image = ts->image; barrier.levelCount = 1; barrier.layerCount = layers; barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; barrier.srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite; barrier.dstStage = vk::PipelineStageFlagBits2::eBlit; barrier.dstAccess = vk::AccessFlagBits2::eTransferRead; cmdImageBarrier(m_currentCommandBuffer, barrier); vulkan_generate_mipmap_chain(m_currentCommandBuffer, ts->image, ts->width, ts->height, ts->mipLevels, layers); } resumeSwapChainPass(); } void VulkanRenderer::resumeSwapChainPass() { std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); PassBeginDesc pass; pass.renderPass = m_renderPassLoad.get(); pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get(); pass.extent = m_renderExtent; pass.clearValues = clearValues; beginTrackedRenderPass(pass); } void VulkanRenderer::restartCompositionPass() { std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); PassBeginDesc pass; pass.renderPass = m_renderPass.get(); pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get(); pass.extent = m_renderExtent; pass.clearValues = clearValues; beginTrackedRenderPass(pass); } void VulkanRenderer::endCurrentRenderPass() { if (!m_stateTracker->getCurrentRenderPass()) { return; } m_currentCommandBuffer.endRenderPass(); m_stateTracker->setRenderPass(vk::RenderPass()); } void VulkanRenderer::resumeRenderTargetPass(tcache_slot_vulkan* ts) { // loadOp=eLoad on color keeps whatever was already drawn into the target; depth (if // any) still clears. Only the depth clear value is consumed, but supply both so the // index layout matches the pass attachments. std::array clearValues; clearValues[0].color = m_stateTracker->getClearColor(); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); const bool hasDepth = static_cast(ts->depthImage); PassBeginDesc pass; pass.renderPass = ts->renderPassLoad; pass.framebuffer = ts->framebuffer; pass.extent = vk::Extent2D(ts->width, ts->height); pass.clearValues = ArrayView(clearValues.data(), hasDepth ? 2 : 1); // Match beginRenderTarget: the engine drives the RTT viewport via gr_set_viewport. pass.viewport = PassViewport::Keep; beginTrackedRenderPass(pass); m_renderTargetActive = true; } } // namespace graphics::vulkan