mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* 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
658 lines
25 KiB
C++
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
|