Files
fs2open.github.com/code/graphics/vulkan/VulkanRendererLoop.cpp
T
Shivansps 1d0dd687cd Android Build (#6992)
* android build support

* Update es_compatibility.h

* Support reporting flags in android

* include pref_path in json v2

* add vulkan headers

* shaderc includes

* remove headers

* force use prebuilt vulkan

* fix vulkan inverted screen

* fix find sdl3 precompiled android

* Update CMakeLists.txt

* replace sdl2 android function names for sdl3 versions

* change sdl includes in osapi

* fix sdl3 includes, again

* rename function to sdl3 name

* remove unused definition

* copy libshaderc_shared.so to output in android

* do not look for vulkan-loader in android

* revert changes to SDL2.cmake

* add comment to vulkan pretransform setup

* cleanup maxsize function

* rename new android functions in osapi

* android trap back button

* Remove calculate_max_resize_max_size

* touch ui opacity control

* change opacity control to int

* fix opacity slider

* Corrected android working folder fallback and set borderless mode as default

* Implement shared helper for JNI calls

* Add version tag and sha256 to android prebuilt libs

* vulkan android check eSuboptimal

* An already requested rebuild also avoids redundant Android surface queries

* add cache bool to cmakelist

* set string ids
2026-11-05 20:56:36 +01:00

658 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;
}
// 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