Files
dustinspace217andDustinspace217 9f2a41032d Vulkan: apply Lighting changes without a restart (#7974)
Changing Lighting in the options recompiles every shader, but Vulkan kept drawing with the pipelines it had already
built, and a pipeline keeps the shader code it was created from. So the new lighting level only showed up after a
restart. OpenGL applies it at once.

After the recompile, mark the cached pipelines stale. At the start of the next frame, before anything is recorded,
wait for the GPU and drop them; the next draws build them again from the new shaders. The recompile can run in the
middle of a frame (from the options menu or a script), so the drop waits for the next frame instead of happening there.

The recompile also rebuilds the anti-aliasing family that is not in use, which cannot compile under the current mode.
Its cached pipeline used to hide that; now the failed variant is dropped so it compiles again when that mode returns.

Fixes #7967

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-07 19:56:30 +01:00

666 lines
25 KiB
C++

#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<uint32_t>(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<float>(desc.extent.height),
static_cast<float>(desc.extent.width),
-static_cast<float>(desc.extent.height));
break;
case PassViewport::NoFlip:
m_stateTracker->setViewport(0.0f, 0.0f,
static_cast<float>(desc.extent.width),
static_cast<float>(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;
}
// before recording: the recompile can run mid-frame, while the frame being recorded still holds old pipelines
if (m_pipelinesStale) {
mprintf(("Vulkan: shaders were recompiled, rebuilding pipelines\n"));
m_device->waitIdle();
m_pipelineManager->forgetAllPipelines();
m_pipelinesStale = false;
}
// 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<vk::ClearValue, 2> 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<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> 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<vk::ClearValue, 2> 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<vk::ClearValue, 2> 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<float>(m_renderExtent.height),
static_cast<float>(m_renderExtent.width),
-static_cast<float>(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<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> 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<vk::ClearValue, 2> 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<vk::ClearValue, 2> clearValues;
clearValues[0].color = m_stateTracker->getClearColor();
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
const bool hasDepth = static_cast<bool>(ts->depthImage);
PassBeginDesc pass;
pass.renderPass = ts->renderPass;
pass.framebuffer = fb;
pass.extent = vk::Extent2D(ts->width, ts->height);
pass.clearValues = ArrayView<vk::ClearValue>(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<vk::ClearValue, 2> 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<vk::ClearValue, 2> 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<vk::ClearValue, 2> clearValues;
clearValues[0].color = m_stateTracker->getClearColor();
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
const bool hasDepth = static_cast<bool>(ts->depthImage);
PassBeginDesc pass;
pass.renderPass = ts->renderPassLoad;
pass.framebuffer = ts->framebuffer;
pass.extent = vk::Extent2D(ts->width, ts->height);
pass.clearValues = ArrayView<vk::ClearValue>(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