Files
dustinspace217andDustinspace217 8ca1ad9bbc P3: Vulkan: draw decals, and draw them like OpenGL does (#7879)
* Vulkan: bind all four model-matrix attribute locations for decals

The Vulkan shader input mask had one bit per listed attribute, so the
pipeline dropped the MATRIX4 attributes at locations 9-11 and the decal
vertex shader read three of its four instance-matrix columns from unbound
inputs. No decal was ever drawn in Vulkan.

The mask now covers four locations for VATTRIB_MODEL_MATRIX. It is built
by shader_vertex_input_mask() in shader_types so both Vulkan call sites
share it and a unit test covers it without a Vulkan build.

Fixes #7866

* Vulkan: build the decal pipeline with the material's shader flags

The decal pipeline config set the shader type but not the flags, so the
decal shader variant was always compiled without USE_NORMAL_MAP and fell
back to screen-space derivative normals for angle rejection. Far more of
the decal volume passed the angle test than in OpenGL, which lit the
decals brighter and wider. The pipeline now takes the flags the material
reports, as the other draw paths already do.

* Review fixes: exact input-mask test, decal flag polarity test, mask width check

Pin the full mask per shader type instead of the presence of the matrix
bits, so an expansion of any other attribute fails; pin that a decal
without a normal map is the one that reads the scene normal buffer; add a
compile-time bound on the mask width; trim two comments.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-04 18:17:31 +01:00

1099 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.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(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