#include "VulkanDeferred.h" #include #include "VulkanRenderer.h" #include "VulkanBarrier.h" #include "VulkanBuffer.h" #include "VulkanTexture.h" #include "VulkanDescriptorManager.h" #include "VulkanPipeline.h" #include "VulkanState.h" #include "VulkanDraw.h" #include "VulkanPostProcessing.h" #include "gr_vulkan.h" #include "cmdline/cmdline.h" #include "graphics/2d.h" #include "graphics/matrix.h" #include "graphics/material.h" #include "graphics/grinternal.h" #include "graphics/rtao.h" #include "graphics/shadows.h" #include "lighting/lighting.h" #include "mission/missionparse.h" #include "nebula/neb.h" #include "nebula/volumetrics.h" #include "render/3d.h" namespace graphics::vulkan { namespace { bool s_vulkanOverrideFog = false; } // anonymous namespace /** * @brief Transition all 6 MSAA images between two layout states. * * Builds a 6-barrier batch (5 color + 1 depth) and submits via pipelineBarrier. */ static void transitionMsaaImages(vk::CommandBuffer cmd, VulkanPostProcessor* pp, vk::ImageLayout colorOldLayout, vk::ImageLayout colorNewLayout, vk::AccessFlags2 colorSrcAccess, vk::AccessFlags2 colorDstAccess, vk::ImageLayout depthOldLayout, vk::ImageLayout depthNewLayout, vk::AccessFlags2 depthSrcAccess, vk::AccessFlags2 depthDstAccess, vk::PipelineStageFlags2 srcStage, vk::PipelineStageFlags2 dstStage) { std::array barriers; std::array msaaImages = { pp->deferred().msaaColorImage(), pp->deferred().msaaPositionImage(), pp->deferred().msaaNormalImage(), pp->deferred().msaaSpecularImage(), pp->deferred().msaaEmissiveImage(), }; for (size_t i = 0; i < msaaImages.size(); ++i) { barriers[i].image = msaaImages[i]; barriers[i].levelCount = 1; barriers[i].layerCount = 1; barriers[i].oldLayout = colorOldLayout; barriers[i].newLayout = colorNewLayout; barriers[i].srcStage = srcStage; barriers[i].srcAccess = colorSrcAccess; barriers[i].dstStage = dstStage; barriers[i].dstAccess = colorDstAccess; } barriers[5].image = pp->deferred().msaaDepthImage(); barriers[5].aspectMask = imageAspectFromFormat(pp->getDepthFormat()); barriers[5].levelCount = 1; barriers[5].layerCount = 1; barriers[5].oldLayout = depthOldLayout; barriers[5].newLayout = depthNewLayout; barriers[5].srcStage = srcStage; barriers[5].srcAccess = depthSrcAccess; barriers[5].dstStage = dstStage; barriers[5].dstAccess = depthDstAccess; cmdImageBarriers(cmd, ArrayView(barriers.data(), barriers.size())); } // ========== Deferred Lighting ========== void vulkan_deferred_lighting_begin(bool clearNonColorBufs) { if (!light_deferred_enabled()) { return; } auto* pp = getPostProcessor(); if (!pp || !pp->deferred().isInitialized()) { return; } auto* renderer = getRendererInstance(); if (!renderer->isSceneRendering()) { return; } GR_DEBUG_SCOPE("Deferred lighting begin"); auto* stateTracker = getStateTracker(); vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); const bool msaaActive = (Cmdline_msaa_enabled > 0 && pp->deferred().isMsaaInitialized()); // End the current G-buffer render pass to perform the color→emissive copy. // All 6 color attachments transition to eShaderReadOnlyOptimal (finalLayout). cmd.endRenderPass(); // Copy scene color → non-MSAA emissive (pre-deferred content becomes emissive). // Skip both post-barriers — conditional MSAA/non-MSAA code below handles transitions. copyImageToImage(cmd, pp->getSceneColorImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal, pp->deferred().emissiveImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferDstOptimal, pp->getSceneExtent()); if (msaaActive) { // --- MSAA path --- // Transition scene color: eTransferSrcOptimal → eShaderReadOnlyOptimal // (will be sampled inside MSAA pass to fill emissive) // Transition non-MSAA emissive: eTransferDstOptimal → eShaderReadOnlyOptimal (preserved for later) { std::array barriers; barriers[0].image = pp->getSceneColorImage(); barriers[0].levelCount = 1; barriers[0].layerCount = 1; barriers[0].oldLayout = vk::ImageLayout::eTransferSrcOptimal; barriers[0].newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barriers[0].srcStage = vk::PipelineStageFlagBits2::eTransfer; barriers[0].srcAccess = vk::AccessFlagBits2::eTransferRead; barriers[0].dstStage = vk::PipelineStageFlagBits2::eFragmentShader; barriers[0].dstAccess = vk::AccessFlagBits2::eShaderSampledRead; barriers[1].image = pp->deferred().emissiveImage(); barriers[1].levelCount = 1; barriers[1].layerCount = 1; barriers[1].oldLayout = vk::ImageLayout::eTransferDstOptimal; barriers[1].newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barriers[1].srcStage = vk::PipelineStageFlagBits2::eTransfer; barriers[1].srcAccess = vk::AccessFlagBits2::eTransferWrite; barriers[1].dstStage = vk::PipelineStageFlagBits2::eFragmentShader; barriers[1].dstAccess = {}; cmdImageBarriers(cmd, ArrayView(barriers.data(), barriers.size())); } // Transition MSAA images to expected initial layouts pp->deferred().transitionMsaaForBegin(cmd); // Copy scene color → MSAA emissive via dedicated single-attachment render pass. // bindGlobalSet=true: this can run before any material draw this frame has // bound Set 0. sampleCount matches the MSAA G-buffer this feeds into. pp->context().drawFullscreenTriangle(cmd, pp->deferred().msaaEmissiveCopyRenderPass(), pp->deferred().msaaEmissiveCopyFramebuffer(), pp->getSceneExtent(), SDR_TYPE_COPY, pp->getSceneColorView(), pp->getSceneColorSampler(), nullptr, 0, ALPHA_BLEND_NONE, 0, renderer->getMsaaSampleCount(), true); // Begin MSAA G-buffer render pass (eClear for 0-3+depth, eLoad for emissive [4]) { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().msaaRenderPass(); rpBegin.framebuffer = pp->deferred().msaaFramebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[VulkanDeferredGBuffer::GBUF_ATT_COLOR].color.setFloat32({0.0f, 0.0f, 0.0f, 0.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::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().msaaRenderPass(), 0); stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT); stateTracker->setCurrentSampleCount(renderer->getMsaaSampleCount()); } } else { // --- Non-MSAA path (original) --- // Transition scene color back to eColorAttachmentOptimal. // Transition emissive to eShaderReadOnlyOptimal (where transitionGbufForResume expects it). { std::array barriers; barriers[0].image = pp->getSceneColorImage(); barriers[0].levelCount = 1; barriers[0].layerCount = 1; barriers[0].oldLayout = vk::ImageLayout::eTransferSrcOptimal; barriers[0].newLayout = vk::ImageLayout::eColorAttachmentOptimal; barriers[0].srcStage = vk::PipelineStageFlagBits2::eTransfer; barriers[0].srcAccess = vk::AccessFlagBits2::eTransferRead; barriers[0].dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; // eColorAttachmentRead as well as Write: the resumed non-MSAA G-buffer // pass loads attachment 0 (scene color, loadOp=eLoad), a read that must // be ordered after this transition -- otherwise a READ_AFTER_WRITE vs // the layout transition (flagged by -gr_sync_validation on the deferred // forward/RocketUI path). barriers[0].dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; barriers[1].image = pp->deferred().emissiveImage(); barriers[1].levelCount = 1; barriers[1].layerCount = 1; barriers[1].oldLayout = vk::ImageLayout::eTransferDstOptimal; barriers[1].newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barriers[1].srcStage = vk::PipelineStageFlagBits2::eTransfer; barriers[1].srcAccess = vk::AccessFlagBits2::eTransferWrite; barriers[1].dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; barriers[1].dstAccess = {}; cmdImageBarriers(cmd, ArrayView(barriers.data(), barriers.size())); } // Transition G-buffer attachments 1-5 from eShaderReadOnlyOptimal → eColorAttachmentOptimal pp->deferred().transitionForResume(cmd); // Resume G-buffer render pass with eLoad { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().renderPassLoad(); rpBegin.framebuffer = pp->deferred().framebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0); // The G-buffer pass has multiple color attachments; pipelines built while // it is bound must match, or deferred-on cockpits render incorrectly. stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT); } // Optionally clear non-color G-buffer attachments if (clearNonColorBufs) { vk::ClearAttachment clearAtt; clearAtt.aspectMask = vk::ImageAspectFlagBits::eColor; clearAtt.clearValue.color.setFloat32({0.0f, 0.0f, 0.0f, 0.0f}); auto extent = pp->getSceneExtent(); vk::ClearRect clearRect; clearRect.rect.offset = vk::Offset2D(0, 0); clearRect.rect.extent = extent; clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; for (uint32_t att : {VulkanDeferredGBuffer::GBUF_ATT_POSITION, VulkanDeferredGBuffer::GBUF_ATT_NORMAL, VulkanDeferredGBuffer::GBUF_ATT_SPECULAR, VulkanDeferredGBuffer::GBUF_ATT_COMPOSITE}) { clearAtt.colorAttachment = att; cmd.clearAttachments(clearAtt, clearRect); } } } renderer->setUseGbufRenderPass(true); Deferred_lighting = true; } void vulkan_deferred_lighting_msaa() { if (!Deferred_lighting) { return; } if (Cmdline_msaa_enabled <= 0) { return; } auto* pp = getPostProcessor(); if (!pp || !pp->deferred().isMsaaInitialized()) { return; } GR_DEBUG_SCOPE("MSAA Pass"); auto* stateTracker = getStateTracker(); vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); // End MSAA G-buffer render pass. // With finalLayout == subpass layout, all attachments stay in their subpass layouts: // colors remain eColorAttachmentOptimal, depth remains eDepthStencilAttachmentOptimal. cmd.endRenderPass(); // Reset sample count to 1x (resolve and subsequent passes are non-MSAA) stateTracker->setCurrentSampleCount(vk::SampleCountFlagBits::e1); // Transition all 6 MSAA images to eShaderReadOnlyOptimal for the resolve shader. // We use explicit barriers instead of render pass finalLayout transitions to // ensure the validation layer tracks the layout changes correctly. transitionMsaaImages(cmd, pp, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eColorAttachmentWrite, vk::AccessFlagBits2::eShaderSampledRead, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eDepthStencilAttachmentWrite, vk::AccessFlagBits2::eShaderSampledRead, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eLateFragmentTests, vk::PipelineStageFlagBits2::eFragmentShader); // Begin resolve render pass (non-MSAA, writes to standard G-buffer images) { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().msaaResolveRenderPass(); rpBegin.framebuffer = pp->deferred().msaaResolveFramebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; // 6 attachments: 5 color + depth. loadOp=eDontCare for all (fully overwritten). std::array clearValues{}; clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); auto* pipelineMgr = getPipelineManager(); auto* descriptorMgr = getDescriptorManager(); auto* texMgr = getTextureManager(); PipelineConfig config; config.shaderType = SDR_TYPE_MSAA_RESOLVE; switch (getRendererInstance()->getMsaaSampleCount()){ case vk::SampleCountFlagBits::e4: config.shaderFlags = SDR_FLAG_MSAA_SAMPLES_4; break; case vk::SampleCountFlagBits::e8: config.shaderFlags = SDR_FLAG_MSAA_SAMPLES_8; break; case vk::SampleCountFlagBits::e16: config.shaderFlags = SDR_FLAG_MSAA_SAMPLES_16; break; default: config.shaderFlags = 0; //Dynamic safety fallback break; } config.primitiveType = PRIM_TYPE_TRIS; config.depthMode = ZBUFFER_TYPE_WRITE; config.blendMode = ALPHA_BLEND_NONE; config.cullEnabled = false; config.depthWriteEnabled = true; config.renderPass = pp->deferred().msaaResolveRenderPass(); config.colorAttachmentCount = 5; vertex_layout emptyLayout; vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout); if (pipeline) { 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(descriptorMgr->getDevice(), descriptorMgr->getFallbacks()); // Global set (fallback — resolve shader doesn't use global bindings) vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global); Assert(globalSet); writer.writeSet(DescriptorSetIndex::Global, globalSet); // Material set: All 6 MSAA textures in binding 1 array (elements 0-5) // [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(DescriptorSetIndex::Material, materialSet); // Build texture array: elements 0-5 are MSAA textures, 6-15 are fallback vk::Sampler nearestSampler = texMgr->getSampler( vk::Filter::eNearest, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, false, 0.0f, false); std::array texImages; // MSAA textures at slots 0-5 // Fill all slots with MSAA color view (validation checks ALL elements // even though the shader only accesses 0-5 — sample count must match). texImages.fill({nearestSampler, pp->deferred().msaaColorView(), vk::ImageLayout::eShaderReadOnlyOptimal}); texImages[1] = {nearestSampler, pp->deferred().msaaPositionView(), vk::ImageLayout::eShaderReadOnlyOptimal}; texImages[2] = {nearestSampler, pp->deferred().msaaNormalView(), vk::ImageLayout::eShaderReadOnlyOptimal}; texImages[3] = {nearestSampler, pp->deferred().msaaSpecularView(), vk::ImageLayout::eShaderReadOnlyOptimal}; texImages[4] = {nearestSampler, pp->deferred().msaaEmissiveView(), vk::ImageLayout::eShaderReadOnlyOptimal}; texImages[5] = {nearestSampler, pp->deferred().msaaDepthView(), vk::ImageLayout::eShaderReadOnlyOptimal}; writer.setImageArray(MaterialBinding::TextureArray, texImages); // PerDraw set: GenericData UBO with {samples, fov} at binding 0 vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet); struct MsaaResolveData { int samples; float fov; } resolveData; resolveData.samples = Cmdline_msaa_enabled; resolveData.fov = g3_get_hfov(Proj_fov); auto& ring = pp->context().scratchRing; vk::DeviceSize slotOffset = ring.alloc(descriptorMgr->getCurrentFrame(), &resolveData, sizeof(resolveData)); writer.setBuffer(PerDrawBinding::GenericData, {ring.buffer(), slotOffset, ring.slotSize()}); writer.flush(); const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet}; writer.bindSets(cmd, pipelineMgr->getPipelineLayout(), DescriptorSetIndex::Global, sets); cmd.draw(3, 1, 0, 0); } cmd.endRenderPass(); } // Restore MSAA images to their resting layout (attachment-optimal) so they // match the validation layer's tracking state for the next frame. transitionMsaaImages(cmd, pp, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eColorAttachmentOptimal, vk::AccessFlagBits2::eShaderSampledRead, vk::AccessFlagBits2::eColorAttachmentWrite, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::AccessFlagBits2::eShaderSampledRead, vk::AccessFlagBits2::eDepthStencilAttachmentWrite, vk::PipelineStageFlagBits2::eFragmentShader, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eEarlyFragmentTests); // After resolve, the non-MSAA G-buffer has properly resolved data. // Color attachments 0-4 are in eShaderReadOnlyOptimal (from resolve pass finalLayout). // Depth is in eDepthStencilAttachmentOptimal. // Subsequent deferred_lighting_end/finish operate on the non-MSAA G-buffer unchanged. // Transition scene color from eShaderReadOnlyOptimal → eColorAttachmentOptimal // (deferred_lighting_end resumes the non-MSAA gbuf pass and needs scene color writable) { ImageBarrier2 barrier; barrier.image = pp->getSceneColorImage(); barrier.levelCount = 1; barrier.layerCount = 1; barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; barrier.srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite; barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; // eColorAttachmentRead as well as Write: the resumed non-MSAA G-buffer // pass loads attachment 0 (scene color, loadOp=eLoad), a read that must // be ordered after this transition (else READ_AFTER_WRITE vs the layout // transition, flagged by -gr_sync_validation). barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; cmdImageBarrier(cmd, barrier); } // Composite is not part of the resolve framebuffer, so its layout is // indeterminate (UNDEFINED on first frame, eTransferSrcOptimal from // previous frame's composite→scene copy, etc.). Use oldLayout=eUndefined // to transition it regardless of current state — content will be fully // overwritten by emissive→composite copy in deferred_lighting_finish(). { ImageBarrier2 barrier; barrier.image = pp->deferred().compositeImage(); barrier.levelCount = 1; barrier.layerCount = 1; barrier.oldLayout = vk::ImageLayout::eUndefined; barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe; barrier.srcAccess = {}; barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; barrier.dstAccess = {}; cmdImageBarrier(cmd, barrier); } // Transition G-buffer attachments 1-5 for resume // (all now in eShaderReadOnlyOptimal: 1-4 from resolve finalLayout, 5 from above) pp->deferred().transitionForResume(cmd); // Resume the non-MSAA G-buffer render pass with eLoad { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().renderPassLoad(); rpBegin.framebuffer = pp->deferred().framebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0); stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT); } } void vulkan_deferred_lighting_end() { if (!Deferred_lighting) { return; } Deferred_lighting = false; // After this, rendering goes back to writing only attachment 0. // The pipeline still has 6 blend states (matching the G-buffer render pass) // but the shader only outputs to location 0. Attachments 1-5 are untouched. } void vulkan_deferred_lighting_finish() { if (!light_deferred_enabled()) { return; } auto* pp = getPostProcessor(); if (!pp || !pp->deferred().isInitialized()) { return; } auto* renderer = getRendererInstance(); if (!renderer->isSceneRendering()) { return; } GR_DEBUG_SCOPE("Deferred lighting finish"); auto* stateTracker = getStateTracker(); vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); // TLAS for RTAO: when shadow rendering is off (Shadow_quality Disabled, or a // frame that skips shadows_render_all entirely), nothing has built this // frame's TLAS yet -- request it here. No-op when the shadow path already // built it (buildTlas()'s per-frame guard). Acceleration-structure builds // must be recorded outside a render pass, so this ends the G-buffer pass and // clears it from the state tracker -- which is why step 1 below only ends // the pass when it is still active. if (rtao_enabled()) { vulkan_build_shadow_tlas(); } // 1. End G-buffer render pass (unless the RTAO TLAS build above already did) // All 6 color attachments → eShaderReadOnlyOptimal // Depth → eDepthStencilAttachmentOptimal if (stateTracker->getCurrentRenderPass()) { cmd.endRenderPass(); } // 2. Copy emissive → composite (the emissive data becomes the base for light accumulation) // Emissive → eShaderReadOnlyOptimal (done), composite → eColorAttachmentOptimal (for light accum) copyImageToImage(cmd, pp->deferred().emissiveImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, pp->deferred().compositeImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eColorAttachmentOptimal, pp->getSceneExtent()); // 3. Render deferred lights (begins + ends light accum render pass internally) // After this, composite is in eShaderReadOnlyOptimal pp->renderDeferredLights(cmd); // 4. Fog rendering (between light accumulation and forward rendering) // Matches OpenGL flow in opengl_deferred_lighting_finish() bool bDrawFullNeb = The_mission.flags[Mission::Mission_Flags::Fullneb] && Neb2_render_mode != NEB2_RENDER_NONE && !s_vulkanOverrideFog; bool bDrawNebVolumetrics = The_mission.volumetrics && The_mission.volumetrics->get_enabled() && !s_vulkanOverrideFog; bool fogRendered = false; if (bDrawFullNeb) { // Scene fog reads composite + depth → writes scene color pp->renderSceneFog(cmd); fogRendered = true; if (bDrawNebVolumetrics) { // Copy scene color → composite so volumetric reads the fogged result copyImageToImage(cmd, pp->getSceneColorImage(), vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eColorAttachmentOptimal, pp->deferred().compositeImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, pp->getSceneExtent()); } } if (bDrawNebVolumetrics) { // Volumetric fog reads composite + emissive + depth + 3D volumes → writes scene color pp->renderVolumetricFog(cmd); fogRendered = true; } if (!fogRendered) { // No fog — copy composite → scene color (existing behavior) // Skip src post-barrier (composite not used again in this path) copyImageToImage(cmd, pp->deferred().compositeImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal, pp->getSceneColorImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eColorAttachmentOptimal, pp->getSceneExtent()); } // 5. Switch to scene render pass for forward transparent objects // After light accumulation, use the 2-attachment scene render pass instead // of the 6-attachment G-buffer pass. Forward-rendered transparent objects // only write to fragOut0 — using the G-buffer pass would leave undefined // values at attachment locations 1-5. renderer->setUseGbufRenderPass(false); stateTracker->setColorAttachmentCount(1); // Resume scene render pass (loadOp=eLoad) with depth preserved { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->getSceneRenderPassLoad(); rpBegin.framebuffer = pp->getSceneFramebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues; clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f}); clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->getSceneRenderPassLoad(), 0); } } void vulkan_override_fog(bool set_override) { s_vulkanOverrideFog = set_override; } // ========== Shadow Map Rendering ========== } // namespace graphics::vulkan extern bool gr_htl_projection_matrix_set; namespace graphics::vulkan { namespace { bool Glowpoint_override_save = false; bool Restore_swapchain_after_shadow_pass = false; // Non-null when the shadow pass was started while rendering into an off-screen render target // (e.g. SCPUI's 3D ship/weapon select preview). In that case the shadow pass must resume the // render target's pass afterwards, not the swapchain, or subsequent draws leak onto the screen. tcache_slot_vulkan* Restore_render_target_after_shadow_pass = nullptr; bool Shadow_pass_active = false; } // anonymous namespace void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_matrix, vec3d* eye_pos, bool first_pass) { if (Shadow_quality == ShadowQuality::Disabled || !getRendererInstance()->supportsShaderViewportLayerOutput()) { return; } auto* pp = getPostProcessor(); if (!pp) { return; } // Lazy-init shadow resources (framebuffer/render pass). The CPU-side cascade // bookkeeping (Shadow_frustums/Shadow_proj_matrix/Shadow_cascade_distances) // is sized eagerly at Vulkan startup via shadow_cascade_params_init() (see // graphics::vulkan::initialize() in gr_vulkan.cpp) -- shadows_start_render() // indexes those vectors before this function is ever called, so that can't // be deferred to first use here. if (!pp->shadow().isInitialized()) { if (!pp->initShadowPass()) { return; } } auto* stateTracker = getStateTracker(); vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); if (first_pass) { // End the current render pass but keep track if we need to resume the swapchain render pass or the scene render pass afterwards. Restore_swapchain_after_shadow_pass = !getRendererInstance()->isSceneRendering(); // If shadows are being rendered while an off-screen render target is bound (SCPUI 3D // ship/weapon select preview renders a rotating model via draw_model_rotating into an RTT), // remember that target so vulkan_shadow_map_end() resumes ITS pass instead of the swapchain. Restore_render_target_after_shadow_pass = nullptr; if (getRendererInstance()->isRenderTargetActive()) { auto* texMgr = getTextureManager(); const int rtHandle = texMgr ? texMgr->getCurrentRenderTarget() : -1; if (texMgr && rtHandle >= 0) { Restore_render_target_after_shadow_pass = texMgr->getTextureSlot(rtHandle); } } // vulkan_build_shadow_tlas() (called just before this, every frame shadows // are enabled) already ends whatever render pass was active -- TLAS builds // are illegal inside a render pass instance -- and nulls out the tracked // render pass. Only end it here if that didn't already happen. if (stateTracker->getCurrentRenderPass()) { cmd.endRenderPass(); } // Shadow render pass is always non-MSAA (1x sample count) stateTracker->setCurrentSampleCount(vk::SampleCountFlagBits::e1); // Begin shadow render pass (eClear, depth-only) { int shadowSize = pp->shadow().textureSize(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->shadow().renderPass(); rpBegin.framebuffer = pp->shadow().framebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = vk::Extent2D(static_cast(shadowSize), static_cast(shadowSize)); vk::ClearValue clearValue; clearValue.depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = 1; rpBegin.pClearValues = &clearValue; cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->shadow().renderPass(), 0); stateTracker->setColorAttachmentCount(0); } // Set viewport and scissor to shadow texture size { const int shadowSize = pp->shadow().textureSize(); stateTracker->setViewport(0.0f, 0.0f, static_cast(shadowSize), static_cast(shadowSize), 0.0f, 1.0f); stateTracker->setScissor(0, 0, static_cast(shadowSize), static_cast(shadowSize)); } Shadow_pass_active = true; Glowpoint_override_save = Glowpoint_override; Glowpoint_override = true; gr_htl_projection_matrix_set = true; } else { gr_end_view_matrix(); } gr_set_view_matrix(eye_pos, light_matrix); *shadow_view_matrix = gr_view_matrix; } void vulkan_shadow_map_end() { if (!Shadow_pass_active) { return; } auto* pp = getPostProcessor(); auto* stateTracker = getStateTracker(); auto* renderer = getRendererInstance(); vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); gr_end_view_matrix(); Shadow_pass_active = false; gr_zbuffer_set(ZBUFFER_TYPE_FULL); Glowpoint_override = Glowpoint_override_save; gr_htl_projection_matrix_set = false; // End shadow render pass (color transitions to eShaderReadOnlyOptimal via finalLayout) cmd.endRenderPass(); if ( Restore_render_target_after_shadow_pass ) { // Shadows were rendered inside an off-screen render target; resume its pass (loadOp=eLoad // so the pre-shadow contents are preserved) rather than the swapchain, otherwise the // following model draws would land on the screen at the RTT's viewport (top-left corner). renderer->resumeRenderTargetPass(Restore_render_target_after_shadow_pass); Restore_render_target_after_shadow_pass = nullptr; } else if ( Restore_swapchain_after_shadow_pass ) { renderer->resumeSwapChainPass(); } else if ( renderer->isUsingGbufRenderPass() ) { const bool msaaActive = (Cmdline_msaa_enabled > 0 && pp->deferred().isMsaaInitialized()); if ( msaaActive ) { // Resume MSAA G-buffer render pass pp->deferred().transitionMsaaForResume(cmd); auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().msaaRenderPassLoad(); rpBegin.framebuffer = pp->deferred().msaaFramebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().msaaRenderPassLoad(), 0); stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT); stateTracker->setCurrentSampleCount(getRendererInstance()->getMsaaSampleCount()); } else { // Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal // (Scene color was in eShaderReadOnlyOptimal from ending G-buffer pass before shadow start) { ImageBarrier2 barrier; barrier.image = pp->getSceneColorImage(); barrier.levelCount = 1; barrier.layerCount = 1; barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe; barrier.srcAccess = {}; barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; // eColorAttachmentRead too: the resumed G-buffer pass (renderPassLoad, // loadOp=eLoad) reads this attachment; order the read after the transition. barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; cmdImageBarrier(cmd, barrier); } // Transition G-buffer attachments 1-5 for resume pp->deferred().transitionForResume(cmd); // Resume G-buffer render pass with eLoad auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().renderPassLoad(); rpBegin.framebuffer = pp->deferred().framebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0); stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT); } } else { renderer->resumeSceneRendering(); } // Restore viewport and scissor to scene size { vk::Viewport viewport; viewport.x = static_cast(gr_screen.offset_x); viewport.y = static_cast(gr_screen.offset_y); viewport.width = static_cast(gr_screen.clip_width); viewport.height = static_cast(gr_screen.clip_height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; cmd.setViewport(0, viewport); vk::Rect2D scissor; scissor.offset = vk::Offset2D(gr_screen.offset_x, gr_screen.offset_y); scissor.extent = vk::Extent2D(static_cast(gr_screen.clip_width), static_cast(gr_screen.clip_height)); cmd.setScissor(0, scissor); } } // ========== Decal Pass ========== void vulkan_start_decal_pass() { auto* renderer = getRendererInstance(); auto* pp = getPostProcessor(); auto* stateTracker = getStateTracker(); if (!renderer->isSceneRendering() || !pp || !pp->deferred().isInitialized()) { return; } vk::CommandBuffer cmd = stateTracker->getCommandBuffer(); // End the G-buffer render pass (transitions all color attachments to eShaderReadOnlyOptimal) cmd.endRenderPass(); // Copy scene depth → samplable depth copy (for fragment depth reconstruction) pp->copySceneDepth(cmd); // Copy G-buffer normal → samplable normal copy (for angle rejection) pp->deferred().copyNormal(cmd); // Transition scene color: eShaderReadOnlyOptimal → eColorAttachmentOptimal { ImageBarrier2 barrier; barrier.image = pp->getSceneColorImage(); barrier.levelCount = 1; barrier.layerCount = 1; barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal; barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal; barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe; barrier.srcAccess = {}; barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput; // eColorAttachmentRead too: the resumed G-buffer pass (renderPassLoad, // loadOp=eLoad) reads this attachment; order the read after the transition. barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eColorAttachmentRead; cmdImageBarrier(cmd, barrier); } // Transition G-buffer attachments 1-5 for render pass resume pp->deferred().transitionForResume(cmd); // Resume G-buffer render pass with eLoad { auto extent = pp->getSceneExtent(); vk::RenderPassBeginInfo rpBegin; rpBegin.renderPass = pp->deferred().renderPassLoad(); rpBegin.framebuffer = pp->deferred().framebuffer(); rpBegin.renderArea.offset = vk::Offset2D(0, 0); rpBegin.renderArea.extent = extent; std::array clearValues{}; clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); rpBegin.clearValueCount = static_cast(clearValues.size()); rpBegin.pClearValues = clearValues.data(); cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline); stateTracker->setRenderPass(pp->deferred().renderPassLoad(), 0); stateTracker->setColorAttachmentCount(VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT); } // Restore viewport (Y-flipped for Vulkan scene rendering) auto extent = pp->getSceneExtent(); stateTracker->setViewport(0.0f, static_cast(extent.height), static_cast(extent.width), -static_cast(extent.height)); } void vulkan_stop_decal_pass() { // No-op — decals draw within the resumed G-buffer render pass } void vulkan_render_decals(decal_material* material_info, primitive_type prim_type, vertex_layout* layout, int num_elements, const indexed_vertex_source& buffers, const gr_buffer_handle& instance_buffer, int num_instances) { if (!material_info || !layout || num_instances <= 0) { return; } auto* stateTracker = getStateTracker(); auto* pipelineManager = getPipelineManager(); auto* descManager = getDescriptorManager(); auto* bufferManager = getBufferManager(); auto* drawManager = getDrawManager(); auto* texManager = getTextureManager(); auto* pp = getPostProcessor(); // Set up matrices gr_matrix_set_uniforms(); // Build pipeline config for decal rendering PipelineConfig config; config.shaderType = SDR_TYPE_DECAL; config.shaderFlags = material_info->get_shader_flags(); config.primitiveType = prim_type; config.depthMode = material_info->get_depth_mode(); config.depthWriteEnabled = false; config.cullEnabled = false; config.frontFaceCW = false; config.blendMode = material_info->get_blend_mode(); config.renderPass = stateTracker->getCurrentRenderPass(); config.colorAttachmentCount = stateTracker->getColorAttachmentCount(); // Per-attachment blend: active attachments (color, normal, emissive) get // the material's blend mode with RGB-only write mask. Inactive attachments get // write mask = 0 to avoid corrupting G-buffer data. config.perAttachmentBlendEnabled = true; for (uint32_t i = 0; i < config.colorAttachmentCount; ++i) { config.attachmentBlends[i].blendMode = ALPHA_BLEND_NONE; config.attachmentBlends[i].writeMask = {false, false, false, false}; } // Color/diffuse — use material blend mode 0 config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_COLOR].blendMode = material_info->get_blend_mode(0); config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_COLOR].writeMask = {true, true, true, false}; // Normal — always additive config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_NORMAL].blendMode = ALPHA_BLEND_ADDITIVE; config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_NORMAL].writeMask = {true, true, true, false}; // Emissive — use material blend mode 2 config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_EMISSIVE].blendMode = material_info->get_blend_mode(2); config.attachmentBlends[VulkanDeferredGBuffer::GBUF_ATT_EMISSIVE].writeMask = {true, true, true, false}; // Get or create pipeline vk::Pipeline pipeline = pipelineManager->getPipeline(config, *layout); if (!pipeline) { nprintf(("vulkan", "vulkan_render_decals: Failed to get pipeline!\n")); return; } stateTracker->bindPipeline(pipeline, pipelineManager->getPipelineLayout()); // Helper: get DescriptorBufferInfo from pending binding (null buffer = fallback) auto getPendingBufInfo = [&](size_t blockIdx) -> vk::DescriptorBufferInfo { const auto& pending = drawManager->getPendingUniformBinding(blockIdx); if (pending.valid) { vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle); if (buf) { return {buf, pending.offset, pending.size}; } } return {}; }; DescriptorWriter writer; writer.reset(descManager->getDevice(), descManager->getFallbacks()); // Set 0: Global (all fallback) vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global); Assert(globalSet); writer.writeSet(DescriptorSetIndex::Global, globalSet); // Set 1: Material vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material); Assert(materialSet); writer.writeSet(DescriptorSetIndex::Material, materialSet); // Binding 2: DecalGlobals UBO writer.setBuffer(MaterialBinding::DecalGlobals, getPendingBufInfo(static_cast(uniform_block_type::DecalGlobals))); // Binding 1: decal textures (diffuse, glow, normal as texture array) drawManager->bindMaterialTextures(material_info, &writer); // Binding 4: scene depth copy (for fragment depth reconstruction) { vk::Sampler nearestSampler = texManager->getSampler( vk::Filter::eNearest, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, false, 0.0f, false); vk::ImageView depthView = pp->getSceneDepthCopyView(); if (depthView && nearestSampler) { writer.setImage(MaterialBinding::DepthMap, {nearestSampler, depthView, vk::ImageLayout::eShaderReadOnlyOptimal}); } } // Binding 6: G-buffer normal copy (for angle rejection) { vk::Sampler nearestSampler = texManager->getSampler( vk::Filter::eNearest, vk::Filter::eNearest, vk::SamplerAddressMode::eClampToEdge, false, 0.0f, false); vk::ImageView normalView = pp->deferred().normalCopyView(); if (normalView && nearestSampler) { writer.setImage(MaterialBinding::DistortionMap, {nearestSampler, normalView, vk::ImageLayout::eShaderReadOnlyOptimal}); } } // Set 2: PerDraw vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw); Assert(perDrawSet); writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet); writer.setBuffer(PerDrawBinding::Matrices, getPendingBufInfo(static_cast(uniform_block_type::Matrices))); writer.setBuffer(PerDrawBinding::DecalInfo, getPendingBufInfo(static_cast(uniform_block_type::DecalInfo))); writer.flush(); stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet, nullptr); stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet, writer.dynamicOffsets(DescriptorSetIndex::Material)); stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet, writer.dynamicOffsets(DescriptorSetIndex::PerDraw)); // Bind vertex buffers: binding 0 = box VBO, binding 1 = instance buffer vk::Buffer boxVBO = bufferManager->getVkBuffer(buffers.Vbuffer_handle); vk::Buffer boxIBO = bufferManager->getVkBuffer(buffers.Ibuffer_handle); vk::Buffer instBuf = bufferManager->getVkBuffer(instance_buffer); if (!boxVBO || !boxIBO || !instBuf) { nprintf(("vulkan", "vulkan_render_decals: Missing buffer(s)!\n")); return; } stateTracker->bindVertexBuffer(0, boxVBO, 0); // Instance buffer needs frame base offset for streaming buffers size_t instFrameOffset = bufferManager->getFrameBaseOffset(instance_buffer); stateTracker->bindVertexBuffer(1, instBuf, static_cast(instFrameOffset)); stateTracker->bindIndexBuffer(boxIBO, 0, vk::IndexType::eUint32); // Flush dynamic state and draw stateTracker->applyDynamicState(); auto cmdBuffer = stateTracker->getCommandBuffer(); cmdBuffer.drawIndexed( static_cast(num_elements), // index count static_cast(num_instances), // instance count 0, // first index 0, // vertex offset 0 // first instance ); } } // namespace graphics::vulkan