#include "VulkanPostProcessing.h" #include #include "cmdline/cmdline.h" #include "gr_vulkan.h" #include "VulkanRenderer.h" #include "VulkanBuffer.h" #include "VulkanPipeline.h" #include "VulkanState.h" #include "VulkanDescriptorManager.h" #include "graphics/util/uniform_structs.h" #include "graphics/post_processing.h" #include "graphics/grinternal.h" #include "graphics/shadows.h" #include "graphics/2d.h" #include "lighting/lighting_profiles.h" #include "lighting/lighting.h" #include "math/vecmat.h" #include "nebula/neb.h" #include "nebula/volumetrics.h" #include "mission/missionparse.h" extern float Sun_spot; extern int Game_subspace_effect; extern SCP_vector Lights; extern int Num_lights; namespace graphics::vulkan { // Global post-processor pointer static VulkanPostProcessor* g_postProcessor = nullptr; VulkanPostProcessor* getPostProcessor() { return g_postProcessor; } void setPostProcessor(VulkanPostProcessor* pp) { g_postProcessor = pp; } bool VulkanPostProcessor::init(vk::Device device, vk::PhysicalDevice physDevice, VulkanMemoryManager* memMgr, vk::Extent2D extent, vk::Format depthFormat, bool hdrActive) { if (m_initialized) { return true; } m_ctx.device = device; m_ctx.memoryManager = memMgr; m_ctx.sceneExtent = extent; m_ctx.depthFormat = depthFormat; m_ctx.hdrActive = hdrActive; // In HDR the tonemapped scene must be able to exceed paper white, so use fp16. m_ctx.ldrFormat = hdrActive ? HDR_COLOR_FORMAT : LDR_COLOR_FORMAT; // Verify RGBA16F support for color attachment + sampling { vk::FormatProperties props = physDevice.getFormatProperties(HDR_COLOR_FORMAT); if (!(props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment) || !(props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage)) { nprintf(("vulkan", "VulkanPostProcessor: RGBA16F not supported for color attachment + sampling!\n")); return false; } } // Create the extent-sized scene targets (color, depth, effect, depth copy) if (!createSceneTargets(extent)) { shutdown(); return false; } // Create HDR scene render pass // Attachment 0: Color (RGBA16F) // loadOp=eClear: clear to black each frame // finalLayout=eShaderReadOnlyOptimal: ready for post-processing sampling // Attachment 1: Depth // loadOp=eClear: clear to far plane // finalLayout=eDepthStencilAttachmentOptimal { std::array attachments; // Color attachments[0].format = HDR_COLOR_FORMAT; attachments[0].samples = vk::SampleCountFlagBits::e1; attachments[0].loadOp = vk::AttachmentLoadOp::eClear; attachments[0].storeOp = vk::AttachmentStoreOp::eStore; attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare; attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare; attachments[0].initialLayout = vk::ImageLayout::eUndefined; attachments[0].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal; // Depth — storeOp=eStore required for: // 1. copy_effect_texture mid-scene interruption (depth must survive render pass end/resume) // 2. lightshafts pass (samples scene depth after render pass ends) attachments[1].format = depthFormat; attachments[1].samples = vk::SampleCountFlagBits::e1; attachments[1].loadOp = vk::AttachmentLoadOp::eClear; attachments[1].storeOp = vk::AttachmentStoreOp::eStore; attachments[1].stencilLoadOp = vk::AttachmentLoadOp::eClear; attachments[1].stencilStoreOp = vk::AttachmentStoreOp::eStore; attachments[1].initialLayout = vk::ImageLayout::eUndefined; attachments[1].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; vk::AttachmentReference colorRef; colorRef.attachment = 0; colorRef.layout = vk::ImageLayout::eColorAttachmentOptimal; vk::AttachmentReference depthRef; depthRef.attachment = 1; depthRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; // Dependency: external → subpass 0. // Note: render-pass compatibility (sharing m_sceneFramebuffer between this // pass and m_sceneRenderPassLoad, and binding pipelines across both) is // defined only by attachment formats/samples and subpass structure -- // dependencies are NOT part of it. So each pass may carry the dependency it // actually needs; they happen to be identical here only because both begin // a frame's use of the same shared scene targets. vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; // srcAccessMask must make the PREVIOUS frame's writes to the (single- // instance, shared-across-frames) scene color + depth available before // this frame clears/loads them again -- otherwise frame N+1's // vkCmdBeginRenderPass races frame N's color storeOp / depth writes // (cross-frame WRITE_AFTER_WRITE, flagged by -gr_sync_validation). Also // covers the prior frame's post-processing sample (eShaderRead) and // copy_effect_texture transfer. dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests | vk::PipelineStageFlagBits::eFragmentShader | vk::PipelineStageFlagBits::eTransfer; dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests; dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead; dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentRead; vk::RenderPassCreateInfo rpInfo; rpInfo.attachmentCount = static_cast(attachments.size()); rpInfo.pAttachments = attachments.data(); rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; rpInfo.dependencyCount = 1; rpInfo.pDependencies = &dependency; try { m_sceneRenderPass = m_ctx.device.createRenderPass(rpInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene render pass: %s\n", e.what())); shutdown(); return false; } } // Create scene render pass with loadOp=eLoad (for resuming after copy_effect_texture) // Compatible with m_sceneRenderPass (same formats/samples) so shares the same framebuffer { std::array attachments; // Color — load existing content, keep final layout for post-processing attachments[0].format = HDR_COLOR_FORMAT; attachments[0].samples = vk::SampleCountFlagBits::e1; attachments[0].loadOp = vk::AttachmentLoadOp::eLoad; attachments[0].storeOp = vk::AttachmentStoreOp::eStore; attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare; attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare; attachments[0].initialLayout = vk::ImageLayout::eColorAttachmentOptimal; attachments[0].finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal; // Depth — load existing content attachments[1].format = depthFormat; attachments[1].samples = vk::SampleCountFlagBits::e1; attachments[1].loadOp = vk::AttachmentLoadOp::eLoad; attachments[1].storeOp = vk::AttachmentStoreOp::eStore; attachments[1].stencilLoadOp = vk::AttachmentLoadOp::eLoad; attachments[1].stencilStoreOp = vk::AttachmentStoreOp::eStore; attachments[1].initialLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; attachments[1].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; vk::AttachmentReference colorRef; colorRef.attachment = 0; colorRef.layout = vk::ImageLayout::eColorAttachmentOptimal; vk::AttachmentReference depthRef; depthRef.attachment = 1; depthRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal; vk::SubpassDescription subpass; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; // Kept identical to m_sceneRenderPass's dependency because both passes begin // a frame's use of the same shared scene targets and need the same // cross-frame ordering -- NOT because compatibility requires it (it does // not; compatibility ignores dependencies). vk::SubpassDependency dependency; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; // srcAccessMask must make the PREVIOUS frame's writes to the (single- // instance, shared-across-frames) scene color + depth available before // this frame clears/loads them again -- otherwise frame N+1's // vkCmdBeginRenderPass races frame N's color storeOp / depth writes // (cross-frame WRITE_AFTER_WRITE, flagged by -gr_sync_validation). Also // covers the prior frame's post-processing sample (eShaderRead) and // copy_effect_texture transfer. dependency.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests | vk::PipelineStageFlagBits::eFragmentShader | vk::PipelineStageFlagBits::eTransfer; dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests; dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead; dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentRead; vk::RenderPassCreateInfo rpInfo; rpInfo.attachmentCount = static_cast(attachments.size()); rpInfo.pAttachments = attachments.data(); rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; rpInfo.dependencyCount = 1; rpInfo.pDependencies = &dependency; try { m_sceneRenderPassLoad = m_ctx.device.createRenderPass(rpInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene load render pass: %s\n", e.what())); shutdown(); return false; } } // Create scene framebuffer if (!createSceneFramebuffer()) { shutdown(); return false; } // Create linear sampler for post-processing texture reads { vk::SamplerCreateInfo samplerInfo; samplerInfo.magFilter = vk::Filter::eLinear; samplerInfo.minFilter = vk::Filter::eLinear; samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; samplerInfo.mipLodBias = 0.0f; samplerInfo.anisotropyEnable = VK_FALSE; samplerInfo.compareEnable = VK_FALSE; samplerInfo.minLod = 0.0f; samplerInfo.maxLod = 0.0f; samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueBlack; try { m_ctx.linearSampler = m_ctx.device.createSampler(samplerInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create sampler: %s\n", e.what())); shutdown(); return false; } } // Create mipmap sampler for bloom textures (supports textureLod) { vk::SamplerCreateInfo samplerInfo; samplerInfo.magFilter = vk::Filter::eLinear; samplerInfo.minFilter = vk::Filter::eLinear; samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; samplerInfo.mipLodBias = 0.0f; samplerInfo.anisotropyEnable = VK_FALSE; samplerInfo.compareEnable = VK_FALSE; samplerInfo.minLod = 0.0f; samplerInfo.maxLod = static_cast(VulkanBloom::MAX_MIP_BLUR_LEVELS); samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueBlack; try { m_ctx.mipmapSampler = m_ctx.device.createSampler(samplerInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create mipmap sampler: %s\n", e.what())); shutdown(); return false; } } // Tonemap (blitToSwapChain) and output-encode UBO data comes from the shared // per-frame scratch ring below -- no dedicated persistent UBOs. A single- // instance buffer would risk being rewritten every frame while a prior // in-flight frame's GPU work is still reading it. // Create the shared scratch UBO ring (used by every subsystem's drawFullscreenTriangle) if (!m_ctx.initScratchUBO()) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scratch UBO!\n")); shutdown(); return false; } // Initialize bloom resources (non-fatal if it fails) if (!initBloom()) { nprintf(("vulkan", "VulkanPostProcessor: Bloom initialization failed (non-fatal)\n")); } // Initialize LDR targets for tonemapping + FXAA (non-fatal if it fails) if (!m_ldr.init(m_ctx, m_sceneColor, m_sceneDepth, m_bloom)) { nprintf(("vulkan", "VulkanPostProcessor: LDR target initialization failed (non-fatal)\n")); } // Initialize SMAA resources (non-fatal if it fails; requires LDR targets) if (m_ldr.isInitialized() && !m_smaa.init(m_ctx, m_ldr)) { nprintf(("vulkan", "VulkanPostProcessor: SMAA initialization failed (non-fatal)\n")); } // Initialize distortion ping-pong textures (non-fatal if it fails) m_distortion.init(m_ctx); // Initialize G-buffer for deferred lighting (non-fatal) if (!initGBuffer()) { nprintf(("vulkan", "VulkanPostProcessor: G-buffer initialization failed (non-fatal)\n")); } // Wire the deferred light accumulation + fog subsystems (resources are lazy). m_lighting.init(m_ctx, m_sceneColor, m_deferred, m_shadow); m_fog.init(m_ctx, m_sceneColor, m_sceneDepth, m_sceneDepthCopy, m_deferred); // Initialize MSAA resources if MSAA is enabled and G-buffer is ready if (m_deferred.isInitialized() && Cmdline_msaa_enabled > 0) { if (!initMSAA()) { nprintf(("vulkan", "VulkanPostProcessor: MSAA initialization failed (non-fatal, disabling MSAA)\n")); Cmdline_msaa_enabled = 0; } } m_initialized = true; nprintf(("vulkan", "VulkanPostProcessor: Initialized (%ux%u, RGBA16F scene color)\n", extent.width, extent.height)); return true; } void VulkanPostProcessor::shutdown() { if (m_ctx.device) { m_ctx.device.waitIdle(); m_fog.shutdown(); shutdownShadowPass(); shadow_cascade_params_shutdown(); shutdownMSAA(); m_lighting.shutdown(); shutdownGBuffer(); m_smaa.shutdown(); m_ldr.shutdown(); shutdownBloom(); m_ctx.shutdownScratchUBO(); if (m_ctx.mipmapSampler) { m_ctx.device.destroySampler(m_ctx.mipmapSampler); m_ctx.mipmapSampler = nullptr; } if (m_ctx.linearSampler) { m_ctx.device.destroySampler(m_ctx.linearSampler); m_ctx.linearSampler = nullptr; } destroySceneTargets(); if (m_sceneRenderPassLoad) { m_ctx.device.destroyRenderPass(m_sceneRenderPassLoad); m_sceneRenderPassLoad = nullptr; } if (m_sceneRenderPass) { m_ctx.device.destroyRenderPass(m_sceneRenderPass); m_sceneRenderPass = nullptr; } // Destroy distortion textures m_distortion.shutdown(); } m_initialized = false; } bool VulkanPostProcessor::createSceneTargets(vk::Extent2D extent) { // HDR scene color target (RGBA16F) // eTransferSrc needed for copy_effect_texture (mid-scene snapshot) // eTransferDst needed for deferred_lighting_finish (emissive→color copy) if (!createImage(extent.width, extent.height, HDR_COLOR_FORMAT, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst, vk::ImageAspectFlagBits::eColor, m_sceneColor.image, m_sceneColor.view, m_sceneColor.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene color image!\n")); return false; } m_sceneColor.format = HDR_COLOR_FORMAT; m_sceneColor.width = extent.width; m_sceneColor.height = extent.height; // Scene depth target // eTransferDst needed for restoreSceneDepth (backup→depth copy after the cockpit render) if (!createImage(extent.width, extent.height, m_ctx.depthFormat, vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst, vk::ImageAspectFlagBits::eDepth, // View uses depth-only aspect m_sceneDepth.image, m_sceneDepth.view, m_sceneDepth.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth image!\n")); return false; } m_sceneDepth.format = m_ctx.depthFormat; m_sceneDepth.width = extent.width; m_sceneDepth.height = extent.height; // Effect/composite texture (RGBA16F, snapshot of scene color for distortion/soft particles) if (!createImage(extent.width, extent.height, HDR_COLOR_FORMAT, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::ImageAspectFlagBits::eColor, m_sceneEffect.image, m_sceneEffect.view, m_sceneEffect.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene effect image!\n")); return false; } m_sceneEffect.format = HDR_COLOR_FORMAT; m_sceneEffect.width = extent.width; m_sceneEffect.height = extent.height; // Scene depth copy (samplable copy for soft particles) // Same depth format, usage: eTransferDst (copy target) + eSampled (fragment shader reads) if (!createImage(extent.width, extent.height, m_ctx.depthFormat, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::ImageAspectFlagBits::eDepth, m_sceneDepthCopy.image, m_sceneDepthCopy.view, m_sceneDepthCopy.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth copy image!\n")); return false; } m_sceneDepthCopy.format = m_ctx.depthFormat; m_sceneDepthCopy.width = extent.width; m_sceneDepthCopy.height = extent.height; // Scene depth backup (holds the scene's depth while the cockpit renders) // Copy target on the way out, copy source on the way back in. No shader ever reads it, // but eSampled stays: createImage() always builds a view, and a view needs at least one // non-transfer usage bit (VUID-VkImageViewCreateInfo-image-04441). if (!createImage(extent.width, extent.height, m_ctx.depthFormat, vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled, vk::ImageAspectFlagBits::eDepth, m_sceneDepthBackup.image, m_sceneDepthBackup.view, m_sceneDepthBackup.allocation)) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene depth backup image!\n")); return false; } m_sceneDepthBackup.format = m_ctx.depthFormat; m_sceneDepthBackup.width = extent.width; m_sceneDepthBackup.height = extent.height; return true; } void VulkanPostProcessor::destroySceneTargets() { if (m_sceneFramebuffer) { m_ctx.device.destroyFramebuffer(m_sceneFramebuffer); m_sceneFramebuffer = nullptr; } m_ctx.destroyTarget(m_sceneEffect); m_ctx.destroyTarget(m_sceneColor); m_ctx.destroyTarget(m_sceneDepth); m_ctx.destroyTarget(m_sceneDepthCopy); m_ctx.destroyTarget(m_sceneDepthBackup); } bool VulkanPostProcessor::createSceneFramebuffer() { std::array fbAttachments = {m_sceneColor.view, m_sceneDepth.view}; vk::FramebufferCreateInfo fbInfo; fbInfo.renderPass = m_sceneRenderPass; fbInfo.attachmentCount = static_cast(fbAttachments.size()); fbInfo.pAttachments = fbAttachments.data(); fbInfo.width = m_ctx.sceneExtent.width; fbInfo.height = m_ctx.sceneExtent.height; fbInfo.layers = 1; try { m_sceneFramebuffer = m_ctx.device.createFramebuffer(fbInfo); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPostProcessor: Failed to create scene framebuffer: %s\n", e.what())); return false; } return true; } bool VulkanPostProcessor::resize(vk::Extent2D newExtent) { if (!m_initialized) { return false; } if (newExtent == m_ctx.sceneExtent) { return true; } // Caller (recreateSwapChain) has already waited for the device to go idle. m_ctx.sceneExtent = newExtent; // Scene targets first: every subsystem framebuffer that wires scene // color/depth needs the recreated views. destroySceneTargets(); if (!createSceneTargets(newExtent) || !createSceneFramebuffer()) { nprintf(("vulkan", "VulkanPostProcessor: Failed to recreate scene targets on resize!\n")); return false; } // Subsystems recreate only their extent-sized targets/framebuffers; their // render passes, samplers, and lookup textures are extent-independent. // Failures are non-fatal and disable the subsystem, matching init(). if (m_bloom.isInitialized() && !m_bloom.resize()) { nprintf(("vulkan", "VulkanPostProcessor: Bloom resize failed, disabling bloom\n")); m_bloom.shutdown(); } if (m_ldr.isInitialized() && !m_ldr.resize()) { nprintf(("vulkan", "VulkanPostProcessor: LDR resize failed, disabling LDR + SMAA\n")); m_smaa.shutdown(); m_ldr.shutdown(); } if (m_smaa.isInitialized() && !m_smaa.resize()) { nprintf(("vulkan", "VulkanPostProcessor: SMAA resize failed, disabling SMAA\n")); m_smaa.shutdown(); } if (m_deferred.isInitialized() && !m_deferred.resize()) { nprintf(("vulkan", "VulkanPostProcessor: G-buffer resize failed, disabling deferred lighting\n")); m_deferred.shutdownMsaa(); m_deferred.shutdown(); } // The light-accumulation framebuffer attaches the (just recreated) G-buffer // composite view — must come after the G-buffer resize. if (!m_lighting.onResize()) { nprintf(("vulkan", "VulkanPostProcessor: Deferred-lighting resize failed, disabling it\n")); m_lighting.shutdown(); } // The fog framebuffer attaches the (just recreated) scene color view. if (!m_fog.onResize()) { nprintf(("vulkan", "VulkanPostProcessor: Fog resize failed, disabling fog\n")); m_fog.shutdown(); } // Distortion (fixed 32x32) and the shadow map (sized from Shadow_quality) // are extent-independent. nprintf(("vulkan", "VulkanPostProcessor: Resized to %ux%u\n", newExtent.width, newExtent.height)); return true; } void VulkanPostProcessor::copyEffectTexture(vk::CommandBuffer cmd) const { // Called mid-scene, outside a render pass. // Scene color is in eShaderReadOnlyOptimal (from the ended scene render pass). // Copies scene color → effect texture so distortion/soft particle shaders can sample it. copyImageToImage(cmd, m_sceneColor.image, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eColorAttachmentOptimal, m_sceneEffect.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal, m_ctx.sceneExtent); } void VulkanPostProcessor::copySceneDepth(vk::CommandBuffer cmd) const { // Called mid-scene, outside a render pass. // Copies scene depth → depth copy texture so soft particle shaders can sample it. // Scene depth is in eDepthStencilAttachmentOptimal (from the ended scene render pass). copyImageToImage(cmd, m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, m_sceneDepthCopy.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal, m_ctx.sceneExtent, imageAspectFromFormat(m_ctx.depthFormat)); } void VulkanPostProcessor::saveSceneDepth(vk::CommandBuffer cmd) const { // Called outside a render pass, so scene depth is in eDepthStencilAttachmentOptimal // (the scene/G-buffer render pass finalLayout). The backup keeps no content between // frames, hence eUndefined: the copy overwrites every texel. copyImageToImage(cmd, m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, m_sceneDepthBackup.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal, m_ctx.sceneExtent, imageAspectFromFormat(m_ctx.depthFormat)); } void VulkanPostProcessor::restoreSceneDepth(vk::CommandBuffer cmd) const { // The backup was left in eTransferSrcOptimal by saveSceneDepth() and stays there -- // the next save transitions it from eUndefined anyway. copyImageToImage(cmd, m_sceneDepthBackup.image, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eTransferSrcOptimal, m_sceneDepth.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, m_ctx.sceneExtent, imageAspectFromFormat(m_ctx.depthFormat)); } void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd) { // C8: unlike PostProcessContext::drawFullscreenTriangle (which begins and ends // its OWN render pass), blitToSwapChain draws INTO the caller's already-active // composition render pass — it keys the pipeline on // stateTracker->getCurrentRenderPass() and goes through the state tracker // (bindPipeline/setViewport) rather than raw cmd recording, because the HUD/2D // draws that follow it in the same pass must see consistent tracker state. That // in-pass, tracker-routed shape is why it can't be folded into the standalone // helper. // // If LDR targets exist, executeTonemap()+executeFXAA() already ran. // Blit from the latest post-processing result with passthrough settings. // Otherwise, fall back to direct HDR→swap chain tonemapping. bool useLdr = m_ldr.isInitialized(); auto* pipelineMgr = getPipelineManager(); auto* descriptorMgr = getDescriptorManager(); auto* stateTracker = getStateTracker(); auto* bufferMgr = getBufferManager(); if (!pipelineMgr || !descriptorMgr || !stateTracker || !bufferMgr) { return; } GR_DEBUG_SCOPE("Draw scene texture"); // Build pipeline config for tonemapping (fullscreen, no depth, no blending) PipelineConfig config; config.shaderType = SDR_TYPE_POST_PROCESS_TONEMAPPING; config.shaderFlags = useLdr ? SDR_FLAG_TONEMAPPING_LINEAR_OUT : 0; config.vertexLayoutHash = 0; // Empty vertex layout config.primitiveType = PRIM_TYPE_TRIS; config.depthMode = ZBUFFER_TYPE_NONE; config.blendMode = ALPHA_BLEND_NONE; config.cullEnabled = false; config.depthWriteEnabled = false; config.renderPass = stateTracker->getCurrentRenderPass(); // Get or create the pipeline vertex_layout emptyLayout; // No vertex components vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout); if (!pipeline) { nprintf(("vulkan", "VulkanPostProcessor: Failed to get tonemapping pipeline!\n")); return; } vk::PipelineLayout pipelineLayout = pipelineMgr->getPipelineLayout(); stateTracker->bindPipeline(pipeline, pipelineLayout); // Set viewport (non-flipped for post-processing — textures are already // in the correct Vulkan orientation, no Y-flip needed) stateTracker->setViewport(0.0f, 0.0f, static_cast(m_ctx.sceneExtent.width), static_cast(m_ctx.sceneExtent.height)); stateTracker->applyDynamicState(); DescriptorWriter writer; writer.reset(m_ctx.device, descriptorMgr->getFallbacks()); // Set 1: Material — source texture at array slot 0 vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material)); { std::array texArrayInfos; texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D); texArrayInfos[0].sampler = m_ctx.linearSampler; texArrayInfos[0].imageView = m_ldr.postEffectsApplied() ? m_ldr.luminanceView() : useLdr ? m_ldr.ldrView() : m_sceneColor.view; writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos); } // Set 2: PerDraw — tonemapping UBO (from the per-frame scratch ring) vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw)); // LDR path: passthrough (tonemapping already ran). Fallback path: live // parameters from the engine lighting profile. graphics::generic_data::tonemapping_data tmData; memset(&tmData, 0, sizeof(tmData)); if (useLdr) { tmData.exposure = 1.0f; tmData.tonemapper = 0; // Linear passthrough } else { namespace ltp = lighting_profiles; auto ppc = ltp::current_piecewise_intermediates(); tmData.exposure = ltp::current_exposure(); tmData.tonemapper = static_cast(ltp::current_tonemapper()); tmData.x0 = ppc.x0; tmData.y0 = ppc.y0; tmData.x1 = ppc.x1; tmData.toe_B = ppc.toe_B; tmData.toe_lnA = ppc.toe_lnA; tmData.sh_B = ppc.sh_B; tmData.sh_lnA = ppc.sh_lnA; tmData.sh_offsetX = ppc.sh_offsetX; tmData.sh_offsetY = ppc.sh_offsetY; } { vk::DeviceSize slotOffset = m_ctx.scratchRing.alloc(descriptorMgr->getCurrentFrame(), &tmData, sizeof(tmData)); writer.setBuffer(PerDrawBinding::GenericData, {m_ctx.scratchRing.buffer(), slotOffset, m_ctx.scratchRing.slotSize()}); } writer.flush(); stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet); stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet); // Draw fullscreen triangle (3 vertices from gl_VertexIndex, no vertex buffer) cmd.draw(3, 1, 0, 0); } void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer, vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler, int shaderType, unsigned int shaderFlags, const void* uboData, size_t uboSize) { auto* pipelineMgr = getPipelineManager(); auto* descriptorMgr = getDescriptorManager(); if (!pipelineMgr || !descriptorMgr || !m_ctx.scratchRing.isValid()) { return; } GR_DEBUG_SCOPE("Draw scene texture"); // The UBO comes from the shared per-frame scratch ring; its per-frame budget // covers every fullscreen pass of the frame including this final encode. PipelineConfig config; config.shaderType = static_cast(shaderType); config.shaderFlags = shaderFlags; // selects the SDR vs HDR10_OUTPUT gamma-correct variant config.vertexLayoutHash = 0; config.primitiveType = PRIM_TYPE_TRIS; config.depthMode = ZBUFFER_TYPE_NONE; config.blendMode = ALPHA_BLEND_NONE; config.cullEnabled = false; config.depthWriteEnabled = false; config.renderPass = renderPass; vertex_layout emptyLayout; vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout); if (!pipeline) { mprintf(("VulkanPostProcessor: Failed to get output-encode pipeline (shaderType=%d)!\n", shaderType)); return; } vk::PipelineLayout pipelineLayout = pipelineMgr->getPipelineLayout(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = renderPass; rpBegin.framebuffer = framebuffer; rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); vk::Viewport viewport; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(extent.width); viewport.height = static_cast(extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; cmd.setViewport(0, viewport); vk::Rect2D scissor; scissor.offset = vk::Offset2D(0, 0); scissor.extent = extent; cmd.setScissor(0, scissor); DescriptorWriter writer; writer.reset(m_ctx.device, descriptorMgr->getFallbacks()); vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material)); { std::array texArrayInfos; texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D); texArrayInfos[0].sampler = sampler; texArrayInfos[0].imageView = sourceView; writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos); } vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw)); { vk::DeviceSize slotOffset = m_ctx.scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize); writer.setBuffer(PerDrawBinding::GenericData, {m_ctx.scratchRing.buffer(), slotOffset, m_ctx.scratchRing.slotSize()}); } writer.flush(); cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, static_cast(DescriptorSetIndex::Material), {materialSet, perDrawSet}, {}); cmd.draw(3, 1, 0, 0); cmd.endRenderPass(); } void VulkanPostProcessor::encodeOutput(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer, vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler, float paperwhiteNits, float peakNits, float gamma) { // HDR10/PQ/BT.2020 encode leg (plus the user gamma slider): the HDR10_OUTPUT // variant of the shared gamma-correct shader. graphics::generic_data::gamma_encode_data encodeData; memset(&encodeData, 0, sizeof(encodeData)); encodeData.gamma = gamma; encodeData.hdr_paperwhite_nits = paperwhiteNits; encodeData.hdr_peak_nits = peakNits; encodeToSwapChainPass(cmd, renderPass, framebuffer, extent, sourceView, sampler, SDR_TYPE_GAMMA_BLIT, SDR_FLAG_GAMMA_HDR10, &encodeData, sizeof(encodeData)); } void VulkanPostProcessor::encodeOutputSdr(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer, vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler, float gamma) { // SDR leg: the composition image already carries the final display-ready // sRGB-encoded frame (3D scene + menus/HUD alike); this just applies the user // gamma/brightness slider on the way into the swap chain image. graphics::generic_data::gamma_encode_data blitData; memset(&blitData, 0, sizeof(blitData)); blitData.gamma = gamma; encodeToSwapChainPass(cmd, renderPass, framebuffer, extent, sourceView, sampler, SDR_TYPE_GAMMA_BLIT, 0, &blitData, sizeof(blitData)); } // No-op: In OpenGL, begin/end push/pop an FBO and run the post-processing // pipeline. In Vulkan, this is handled by vulkan_scene_texture_begin/end // which manage the HDR render pass and post-processing passes. These // functions are not actively called by the engine. void vulkan_post_process_begin() {} void vulkan_post_process_end() {} // Isolate the cockpit's depth from the scene's, the way OpenGL does by swapping the // depth attachment between Scene_depth_texture and Cockpit_depth_texture. A Vulkan // framebuffer owns its attachments, so the scene depth is parked in a backup image and // the live depth buffer is cleared for the cockpit; the restore puts the scene back. // // Both halves are needed. Without the clear the ship hull the scene already drew wins // the depth test against the cockpit around the camera. Without the restore the // post-processing passes that sample scene depth (lightshafts) see cockpit depth. void vulkan_post_process_save_zbuffer() { auto* renderer = getRendererInstance(); if (renderer != nullptr) { renderer->saveSceneDepth(); } // Unconditional, exactly as in OpenGL: when there is nothing to park (post-processing // off, or no scene render pass), clearing is still what gives the cockpit a depth // buffer of its own. gr_zbuffer_clear(TRUE); } void vulkan_post_process_restore_zbuffer() { auto* renderer = getRendererInstance(); if (renderer != nullptr) { renderer->restoreSceneDepth(); } } void vulkan_post_process_set_effect(const char* name, int value, const vec3d* rgb) { if (!Gr_post_processing_enabled || !graphics::Post_processing_manager) { return; } if (name == nullptr) { return; } auto& ls_params = graphics::Post_processing_manager->getLightshaftParams(); if (!stricmp("lightshafts", name)) { ls_params.intensity = value / 100.0f; ls_params.on = !!value; return; } auto& postEffects = graphics::Post_processing_manager->getPostEffects(); for (auto & postEffect : postEffects) { if (!stricmp(postEffect.name.c_str(), name)) { postEffect.intensity = (value / postEffect.div) + postEffect.add; if ((rgb != nullptr) && !(vmd_zero_vector == *rgb)) { postEffect.rgb = *rgb; } break; } } } void vulkan_post_process_set_defaults() { if (!graphics::Post_processing_manager) { return; } auto& postEffects = graphics::Post_processing_manager->getPostEffects(); for (auto& effect : postEffects) { effect.intensity = effect.default_intensity; } } } // namespace graphics::vulkan