mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
Raytraced shadow penumbras needed a hand-authored $SunAngularSize: in stars.tbl, which no shipped content sets -- so in practice nothing ever got a penumbra, and the shadow-mapped path's $Shadow Smoothness Factor: had no relation to it at all. Derive a size from the sun bitmap instead and drive both methods from the same number. g3_render_rect_screen_aligned_2d() sizes the sun quad so its rad is the tangent of the half-angle it subtends, making tan(angular radius) equal to 0.05 * the mission's +Scale: * the emitting disc's fraction of the bitmap. That fraction is the area of the pixels at or above 90% of the brightest one, converted to an equivalent radius. Across 90 sun bitmaps from retail, the MediaVPs, Blue Planet and BtA it lands at a median of 0.261 (retail's chunkier art at 0.487), and the threshold acts as a constant factor rather than a per-bitmap judgement call -- Spearman rho 0.96 between a 90% and a 50% cutoff. Taken literally that yields suns 3.9-8.5x Sol's apparent diameter once +Scale: is folded in (median 1.55, up to 5.0 in retail's own missions), so the measurement is scaled by 0.25 and clamped to 1.5 degrees, putting typical content near Sol. Derivation is the default: $SunAngularSize: 0 asks for hard shadows, an explicit value still wins, and missions can also set +AngularSize: on an individual sun to override both, with FRED and QtFRED UI (validation and fallback included) to author it. Bitmaps that can't be read -- and the deliberately blank sun bitmaps mods ship to get a light source with no visible disc -- fall back to hard shadows rather than to the widest possible penumbra. That angular size now sizes the raytraced penumbra cone directly (traceShadowRayCone() in shadows.sdr), with sample count following the same Shadow_quality tier that picks the shadow map's resolution (1 ray at Low, up to 16 at Ultra) rather than being a separate setting. The shadow-mapped path's tabled smoothness values were tuned by eye against Sol-sized retail/MediaVPs art, so shadow_smoothness_scale() in shadows.cpp scales them by the active sun's size relative to Sol, clamped to what the shadow map can represent (shadow_clamp_smoothness()). The two methods still disagree about occluder distance -- the raytracer uses the real one, the shadow map's tabled values stand in for a blocker distance pinned to the cascade's own extent -- so RT_SHADOW_SUN_SIZE_CALIBRATION in shadows.sdr is a constant factor that makes a given $SunAngularSize: read as roughly the same softness either way; it's the number to revisit if the shadow map ever grows a real blocker search. Also adds RTAO (raytraced ambient occlusion) sharing the same TLAS as the shadow rays, with lighting-profile-driven radius/strength, and lab controls for RT shadow quality (Low/High), the local light cap, RT shadow samples, RTAO, and an angular-size override for comparing both shadow methods against the same sun. All are safe to change live: they only gate which lights are picked as shadow casters or scale existing uniforms while filling the per-frame light data. Co-Authored-By: Claude Opus 5 <[email protected]>
1110 lines
45 KiB
C++
1110 lines
45 KiB
C++
|
|
#include "VulkanDeferred.h"
|
|
|
|
#include <array>
|
|
|
|
#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<ImageBarrier2, 6> barriers;
|
|
|
|
std::array<vk::Image, 5> 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<const ImageBarrier2>(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<ImageBarrier2, 2> 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<const ImageBarrier2>(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<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> 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<uint32_t>(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<ImageBarrier2, 2> 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<const ImageBarrier2>(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<vk::ClearValue, VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
|
|
clearValues[VulkanDeferredGBuffer::GBUF_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
|
|
clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<float>(extent.width);
|
|
viewport.height = static_cast<float>(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<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> 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<vk::ClearValue, 7> clearValues{};
|
|
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<vk::ClearValue, 2> 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<uint32_t>(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<uint32_t>(shadowSize), static_cast<uint32_t>(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<float>(shadowSize), static_cast<float>(shadowSize), 0.0f, 1.0f);
|
|
stateTracker->setScissor(0, 0, static_cast<uint32_t>(shadowSize), static_cast<uint32_t>(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<vk::ClearValue, VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT + 1> clearValues{};
|
|
clearValues[VulkanDeferredGBuffer::MSAA_COLOR_ATTACHMENT_COUNT].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<vk::ClearValue, 7> clearValues{};
|
|
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<float>(gr_screen.offset_x);
|
|
viewport.y = static_cast<float>(gr_screen.offset_y);
|
|
viewport.width = static_cast<float>(gr_screen.clip_width);
|
|
viewport.height = static_cast<float>(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<uint32_t>(gr_screen.clip_width), static_cast<uint32_t>(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<vk::ClearValue, 7> clearValues{};
|
|
clearValues[6].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
|
|
rpBegin.clearValueCount = static_cast<uint32_t>(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<float>(extent.height),
|
|
static_cast<float>(extent.width),
|
|
-static_cast<float>(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<size_t>(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<size_t>(uniform_block_type::Matrices)));
|
|
writer.setBuffer(PerDrawBinding::DecalInfo,
|
|
getPendingBufInfo(static_cast<size_t>(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<vk::DeviceSize>(instFrameOffset));
|
|
|
|
stateTracker->bindIndexBuffer(boxIBO, 0, vk::IndexType::eUint32);
|
|
|
|
// Flush dynamic state and draw
|
|
stateTracker->applyDynamicState();
|
|
|
|
auto cmdBuffer = stateTracker->getCommandBuffer();
|
|
cmdBuffer.drawIndexed(
|
|
static_cast<uint32_t>(num_elements), // index count
|
|
static_cast<uint32_t>(num_instances), // instance count
|
|
0, // first index
|
|
0, // vertex offset
|
|
0 // first instance
|
|
);
|
|
}
|
|
|
|
} // namespace graphics::vulkan
|
|
|