Files
fs2open.github.com/code/graphics/vulkan/VulkanDeferred.cpp
T
LuytenKy e76e844d02 Vulkan: Skip MSAA resolve when no deferred pass is active (#7854)
* stub check for msaa

* initialize viewport variables
2026-10-15 18:04:32 +02:00

1098 lines
44 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/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(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
// 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(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
// 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(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
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();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Global), globalSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Material), materialSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::PerDraw), perDrawSet, {});
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();
// 1. End G-buffer render pass
// All 6 color attachments → eShaderReadOnlyOptimal
// Depth → eDepthStencilAttachmentOptimal
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.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(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
// Set 1: Material
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
// 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(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
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);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
// 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