Files
Fabian Woltermann 115491f8a9 Refactor RT shadow handling and Vulkan depth operations (#7685)
- Introduced `shadow_ray_params` to simplify ray origin and cull mask management.
- Shifted shadow ray origins to TLAS-relative space for improved precision and consistency, especially for cockpit rendering.
- Refactored cascade bind functions, adding `shadow_cascade_params_bind_deferred()` for consolidated offset/count handling in deferred lighting.
- Expanded TLAS to include cockpits and additional object types, ensuring parity with rasterized shadows.
- Introduced per-instance cull masks to address cockpit self-shadowing issues.
- Improved Vulkan depth isolation for cockpit rendering via backup/restore mechanism, ensuring correct depth sampling in later passes.
- Extracted reusable helper functions for cockpit rendering state logic.
2026-09-21 05:12:55 +02:00

1272 lines
51 KiB
C++

#pragma once
#include "globalincs/pstypes.h"
#include "lighting/lighting.h"
#include "VulkanMemory.h"
#include "VulkanConstants.h"
#include "VulkanPerFrameUbo.h"
#include <array>
#include <vulkan/vulkan.hpp>
namespace graphics::vulkan {
struct RenderTarget;
/**
* @brief Shared drawing infrastructure for post-processing subsystems
*
* Bundles the handles and helpers that every post-processing pass needs
* (logical device, memory manager, scene geometry, samplers, the per-draw
* scratch UBO ring, and the image/fullscreen-draw helpers). Owned by
* VulkanPostProcessor and intended to be passed by reference to the
* individual subsystem passes as they are extracted into their own types.
*/
struct PostProcessContext {
vk::Device device;
VulkanMemoryManager* memoryManager = nullptr;
vk::Extent2D sceneExtent;
vk::Format depthFormat = vk::Format::eUndefined;
// True when the renderer negotiated an HDR10 swap chain. Drives fp16 LDR
// intermediates and the HDR scene-tonemap path.
bool hdrActive = false;
// Display-referred LDR intermediate format: fp16 in HDR (so the scene can
// carry values above paper white), 8-bit UNORM in SDR.
vk::Format ldrFormat = LDR_COLOR_FORMAT;
// Shared samplers for post-processing texture reads
vk::Sampler linearSampler; // maxLod=0
vk::Sampler mipmapSampler; // mipmap support (bloom)
// Per-draw scratch UBO ring shared by all fullscreen effect passes.
// Per-frame-in-flight regions (see PerFrameUboRing) so the CPU never
// overwrites slots the previous frame's GPU work is still reading. The
// cursor is reset exactly once per frame in VulkanPostProcessor::beginFrame()
// -- the slot budget below covers ALL fullscreen passes of one frame
// (fog/volumetrics mid-scene + bloom/tonemap/AA/lightshafts/post-effects).
//
// Slot size must fit the largest fullscreen-pass UBO: volumetric_fog_data
// (guarded by a static_assert in VulkanPostProcessingFog.cpp).
static constexpr size_t SCRATCH_UBO_SLOT_SIZE = 512; // multiple of minUniformBufferOffsetAlignment
static constexpr uint32_t SCRATCH_UBO_MAX_SLOTS = 32;
PerFrameUboRing scratchRing;
/**
* @brief Destroy a RenderTarget's view/image/allocation and reset its fields
*
* Safe to call on a partially-created or already-destroyed target.
*/
void destroyTarget(RenderTarget& rt) const;
/**
* @brief Create a single-mip 2D image + view backed by GPU-only memory
*/
bool createImage(uint32_t width, uint32_t height, vk::Format format,
vk::ImageUsageFlags usage, vk::ImageAspectFlags aspect,
vk::Image& outImage, vk::ImageView& outView,
VulkanAllocation& outAllocation,
vk::SampleCountFlagBits sampleCount = vk::SampleCountFlagBits::e1)
const;
/**
* @brief Draw a fullscreen triangle through the post-processing pipeline
*
* Begins/ends the given render pass and binds material + per-draw sets.
* Optional UBO data is written into the next scratch UBO slot.
*
* @param sampleCount Pipeline sample count; only needed when renderPass is a
* multisampled render pass (e.g. an MSAA G-buffer pass).
* @param bindGlobalSet Also allocate/write/bind the Global descriptor set.
* Normal callers rely on Set 0 already being bound from
* frame setup; pass true for draws that may run before
* that binding happened this frame (e.g. mid-G-buffer-pass
* copies that run ahead of any material draw).
* @param clearColor When set, clears color attachment 0 to this RGBA value via
* vkCmdClearAttachments immediately after beginning the render
* pass, regardless of the render pass's own loadOp. For passes
* whose shader discards some pixels (e.g. SMAA edge detection)
* and therefore can't rely on loadOp=eDontCare leaving them at
* any particular value.
*/
void drawFullscreenTriangle(vk::CommandBuffer cmd, vk::RenderPass renderPass,
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
vk::ImageView textureView, vk::Sampler sampler,
const void* uboData, size_t uboSize,
int blendMode,
unsigned int shaderFlags = 0,
vk::SampleCountFlagBits sampleCount = vk::SampleCountFlagBits::e1,
bool bindGlobalSet = false,
const std::array<float, 4>* clearColor = nullptr);
/**
* @brief Draw a fullscreen triangle sampling multiple textures
*
* Like drawFullscreenTriangle, but binds `viewCount` textures (all through
* the shared linear sampler) as elements 0..viewCount-1 of the
* Material.TextureArray binding, for shaders that need more than one input
* (e.g. SMAA's blending-weight/neighborhood-blending passes).
*/
void drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::RenderPass renderPass,
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
const vk::ImageView* views, uint32_t viewCount,
const void* uboData, size_t uboSize);
/**
* @brief Generate a mip chain for an image (transitions mip 0 first)
*/
static void generateMipmaps(vk::CommandBuffer cmd, vk::Image image,
uint32_t width, uint32_t height, uint32_t mipLevels);
/**
* @brief Create the shared scratch UBO used by drawFullscreenTriangle
* @return false on failure (caller should treat as fatal)
*/
bool initScratchUBO();
void shutdownScratchUBO();
};
/**
* @brief A single-mip 2D color/depth render target (image + view + allocation)
*
* Shared value type used by the post-processor and its subsystems.
*/
struct RenderTarget {
vk::Image image;
vk::ImageView view;
VulkanAllocation allocation;
vk::Format format = vk::Format::eUndefined;
uint32_t width = 0;
uint32_t height = 0;
};
/**
* @brief Distortion ping-pong textures (32x32 RGBA8)
*
* Self-contained subsystem: owns two ping-pong textures plus a LINEAR/REPEAT
* sampler, and scrolls/refreshes them on a ~30ms timer to match OpenGL's
* gr_opengl_update_distortion(). Thruster rendering samples the most recently
* written texture via getTextureInfo().
*/
class VulkanDistortion {
public:
bool init(PostProcessContext& ctx);
void shutdown();
/**
* @brief Advance the ping-pong textures (no-op until the ~30ms timer fires)
* @param cmd Active command buffer (must be outside a render pass)
* @param frametime Time since last frame in seconds
*/
void update(vk::CommandBuffer cmd, float frametime);
bool isInitialized() const { return m_initialized; }
/**
* @brief DescriptorImageInfo for the current (most recently written) texture
*
* Returns a default-constructed info if not initialized.
*/
vk::DescriptorImageInfo getTextureInfo() const;
private:
PostProcessContext* m_ctx = nullptr;
std::array<RenderTarget, 2> m_tex;
int m_switch = 0; // Which texture is the current read source
float m_timer = 0.0f; // Accumulator for ~30ms update interval
vk::Sampler m_sampler; // LINEAR filter, REPEAT wrapping
bool m_initialized = false;
bool m_firstUpdate = true; // First update needs eUndefined old layout
};
/**
* @brief Cascaded shadow map (depth-only, hardware PCF) render target
*
* Self-contained, lazily-initialized subsystem: owns the layered depth array
* image, its comparison sampler, render pass, and layered framebuffer. Sized
* from the current Shadow_quality and the current total cascade count
* (Num_shadow_cascades + Num_cockpit_shadow_cascades) on init.
*/
class VulkanShadowMap {
public:
/**
* @brief Lazily create shadow resources (sized from Shadow_quality)
* @return false if shadows are disabled or creation failed
*/
bool init(PostProcessContext& ctx);
void shutdown();
bool isInitialized() const { return m_initialized; }
int textureSize() const { return m_textureSize; }
vk::ImageView depthView() const { return m_depth.view; }
vk::Image depthImage() const { return m_depth.image; }
vk::Sampler compareSampler() const { return m_compareSampler; }
vk::RenderPass renderPass() const { return m_renderPass; }
vk::Framebuffer framebuffer() const { return m_framebuffer; }
private:
PostProcessContext* m_ctx = nullptr;
RenderTarget m_depth; // D32F, 2D array (Num_shadow_cascades + Num_cockpit_shadow_cascades layers)
vk::Sampler m_compareSampler; // Depth-compare sampler for hardware PCF (sampler2DArrayShadow)
vk::RenderPass m_renderPass;
vk::Framebuffer m_framebuffer;
int m_textureSize = 0;
bool m_initialized = false;
};
/**
* @brief HDR bloom (bright-pass → mip blur → additive composite)
*
* Self-contained subsystem owning two half-resolution mip chains plus the
* bloom/composite render passes. Composites additively back into the scene
* color target, which is owned by the post-processor and referenced here.
*/
class VulkanBloom {
public:
static constexpr int MAX_MIP_BLUR_LEVELS = 4;
/**
* @brief Create bloom resources (sized to half the scene extent)
* @param sceneColor Scene HDR color target to composite into (must outlive this)
*/
bool init(PostProcessContext& ctx, const RenderTarget& sceneColor);
void shutdown();
/**
* @brief Recreate the extent-sized targets/framebuffers (render passes kept)
*
* Device must be idle. Returns false on failure (caller should shut the
* subsystem down).
*/
bool resize();
/**
* @brief Run the full bloom chain and composite it onto the scene color
* @param cmd Active command buffer (must be outside a render pass)
*/
void execute(vk::CommandBuffer cmd);
bool isInitialized() const { return m_initialized; }
private:
bool createTargets();
void destroyTargets();
struct BloomTarget {
vk::Image image;
VulkanAllocation allocation;
vk::ImageView fullView; // All mips (textureLod sampling)
std::array<vk::ImageView, MAX_MIP_BLUR_LEVELS> mipViews = {}; // Per-mip views (framebuffer attachment)
std::array<vk::Framebuffer, MAX_MIP_BLUR_LEVELS> mipFramebuffers = {};
};
PostProcessContext* m_ctx = nullptr;
const RenderTarget* m_sceneColor = nullptr;
std::array<BloomTarget, 2> m_tex; // Half-res RGBA16F, 4 mip levels
uint32_t m_width = 0; // Half of scene width
uint32_t m_height = 0; // Half of scene height
vk::RenderPass m_renderPass; // Color-only RGBA16F, loadOp=eDontCare
vk::RenderPass m_compositeRenderPass; // Color-only RGBA16F, loadOp=eLoad (additive to scene)
vk::Framebuffer m_sceneColorFB; // Scene color as attachment for bloom composite
bool m_initialized = false;
};
/**
* @brief Deferred geometry buffer (G-buffer) + optional MSAA G-buffer & resolve
*
* Cohesive subsystem owning the single-sample G-buffer targets (position, normal,
* specular, emissive, composite + a samplable normal copy) and, when MSAA is
* enabled, the multisample G-buffer that resolves into those single-sample
* targets. Also owns the render-pass / framebuffer builders shared by both paths.
* Renders into the scene color (attachment 0) and scene depth, both owned by the
* post-processor and referenced here.
*/
class VulkanDeferredGBuffer {
public:
// ===== G-Buffer Layout Constants =====
// Full layout: [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=composite, [6]=depth
// MSAA layout: [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth (no composite)
static constexpr uint32_t GBUF_ATT_COLOR = 0;
static constexpr uint32_t GBUF_ATT_POSITION = 1;
static constexpr uint32_t GBUF_ATT_NORMAL = 2;
static constexpr uint32_t GBUF_ATT_SPECULAR = 3;
static constexpr uint32_t GBUF_ATT_EMISSIVE = 4;
static constexpr uint32_t GBUF_ATT_COMPOSITE = 5; // Full layout only
static constexpr uint32_t GBUF_COLOR_ATTACHMENT_COUNT = 6; // Full layout (with composite)
static constexpr uint32_t MSAA_COLOR_ATTACHMENT_COUNT = 5; // MSAA layout (without composite)
static constexpr vk::Format GBUF_FORMAT_COLOR = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format GBUF_FORMAT_POSITION = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format GBUF_FORMAT_NORMAL = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format GBUF_FORMAT_SPECULAR = vk::Format::eR8G8B8A8Unorm;
static constexpr vk::Format GBUF_FORMAT_EMISSIVE = vk::Format::eR16G16B16A16Sfloat;
static constexpr vk::Format GBUF_FORMAT_COMPOSITE = vk::Format::eR16G16B16A16Sfloat;
/**
* @brief Create the single-sample G-buffer (sized to the scene extent)
* @param sceneColor Scene HDR color (attachment 0); must outlive this
* @param sceneDepth Scene depth attachment; must outlive this
*/
bool init(PostProcessContext& ctx, const RenderTarget& sceneColor, const RenderTarget& sceneDepth);
void shutdown();
/**
* @brief Recreate all extent-sized G-buffer (and, if active, MSAA)
* targets/framebuffers; render passes are kept. Device must be idle.
*/
bool resize();
/**
* @brief Create the multisample G-buffer + resolve resources (lazy)
* @return false if MSAA is disabled or creation failed
*/
bool initMsaa();
void shutdownMsaa();
bool isInitialized() const { return m_gbufInitialized; }
bool isMsaaInitialized() const { return m_msaaInitialized; }
// Mid-frame layout transitions / copies (all outside a render pass)
void transitionForResume(vk::CommandBuffer cmd);
void copyNormal(vk::CommandBuffer cmd);
void transitionMsaaForBegin(vk::CommandBuffer cmd);
void transitionMsaaForResume(vk::CommandBuffer cmd);
// G-buffer accessors
vk::RenderPass renderPass() const { return m_gbufRenderPass; }
vk::RenderPass renderPassLoad() const { return m_gbufRenderPassLoad; }
vk::Framebuffer framebuffer() const { return m_gbufFramebuffer; }
vk::ImageView positionView() const { return m_gbufPosition.view; }
vk::ImageView normalView() const { return m_gbufNormal.view; }
vk::ImageView specularView() const { return m_gbufSpecular.view; }
vk::ImageView emissiveView() const { return m_gbufEmissive.view; }
vk::ImageView compositeView() const { return m_gbufComposite.view; }
vk::ImageView normalCopyView() const { return m_gbufNormalCopy.view; }
vk::Image emissiveImage() const { return m_gbufEmissive.image; }
vk::Image compositeImage() const { return m_gbufComposite.image; }
vk::Image normalImage() const { return m_gbufNormal.image; }
// MSAA accessors
vk::RenderPass msaaRenderPass() const { return m_msaaGbufRenderPass; }
vk::RenderPass msaaRenderPassLoad() const { return m_msaaGbufRenderPassLoad; }
vk::Framebuffer msaaFramebuffer() const { return m_msaaGbufFramebuffer; }
vk::RenderPass msaaResolveRenderPass() const { return m_msaaResolveRenderPass; }
vk::Framebuffer msaaResolveFramebuffer() const { return m_msaaResolveFramebuffer; }
vk::RenderPass msaaEmissiveCopyRenderPass() const { return m_msaaEmissiveCopyRenderPass; }
vk::Framebuffer msaaEmissiveCopyFramebuffer() const { return m_msaaEmissiveCopyFramebuffer; }
vk::ImageView msaaColorView() const { return m_msaaColor.view; }
vk::ImageView msaaPositionView() const { return m_msaaPosition.view; }
vk::ImageView msaaNormalView() const { return m_msaaNormal.view; }
vk::ImageView msaaSpecularView() const { return m_msaaSpecular.view; }
vk::ImageView msaaEmissiveView() const { return m_msaaEmissive.view; }
vk::ImageView msaaDepthView() const { return m_msaaDepthView; }
vk::Image msaaColorImage() const { return m_msaaColor.image; }
vk::Image msaaPositionImage() const { return m_msaaPosition.image; }
vk::Image msaaNormalImage() const { return m_msaaNormal.image; }
vk::Image msaaSpecularImage() const { return m_msaaSpecular.image; }
vk::Image msaaEmissiveImage() const { return m_msaaEmissive.image; }
vk::Image msaaDepthImage() const { return m_msaaDepthImage; }
private:
struct GbufRenderPassConfig {
bool includeComposite;
vk::SampleCountFlagBits samples;
vk::AttachmentLoadOp colorLoadOp;
vk::AttachmentLoadOp depthLoadOp;
vk::ImageLayout colorInitialLayout;
vk::ImageLayout colorFinalLayout;
vk::ImageLayout depthInitialLayout;
bool useResolveDependency = false;
SCP_unordered_map<uint32_t, vk::AttachmentLoadOp> attachmentLoadOpOverrides = {};
};
vk::RenderPass createGbufRenderPass(const GbufRenderPassConfig& config);
vk::Framebuffer createGbufFramebuffer(vk::RenderPass renderPass, bool includeComposite,
bool useMsaaImages);
bool createTargets();
void destroyTargets();
bool createMsaaTargets();
void destroyMsaaTargets();
PostProcessContext* m_ctx = nullptr;
const RenderTarget* m_sceneColor = nullptr;
const RenderTarget* m_sceneDepth = nullptr;
// ---- G-Buffer (single-sample) ----
RenderTarget m_gbufPosition; // RGBA16F - view-space position (xyz) + AO (w)
RenderTarget m_gbufNormal; // RGBA16F - view-space normal (xyz) + gloss (w)
RenderTarget m_gbufNormalCopy; // RGBA16F - samplable copy of G-buffer normal (for decals)
RenderTarget m_gbufSpecular; // RGBA8 - specular color (rgb) + fresnel (a)
RenderTarget m_gbufEmissive; // RGBA16F - emissive / pre-lit color
RenderTarget m_gbufComposite; // RGBA16F - light accumulation scratch buffer
vk::RenderPass m_gbufRenderPass; // loadOp=eClear (initial)
vk::RenderPass m_gbufRenderPassLoad; // loadOp=eLoad (resume after mid-pass copy)
vk::Framebuffer m_gbufFramebuffer;
bool m_gbufInitialized = false;
// ---- MSAA G-buffer (multisample) + resolve ----
RenderTarget m_msaaColor; // RGBA16F (MS)
RenderTarget m_msaaPosition; // RGBA16F (MS)
RenderTarget m_msaaNormal; // RGBA16F (MS)
RenderTarget m_msaaSpecular; // RGBA8 (MS)
RenderTarget m_msaaEmissive; // RGBA16F (MS)
vk::Image m_msaaDepthImage;
vk::ImageView m_msaaDepthView;
VulkanAllocation m_msaaDepthAlloc;
vk::RenderPass m_msaaGbufRenderPass; // eClear, 5 MS color + MS depth
vk::RenderPass m_msaaGbufRenderPassLoad; // eLoad (emissive preserved)
vk::Framebuffer m_msaaGbufFramebuffer;
vk::RenderPass m_msaaResolveRenderPass; // 5 non-MSAA color + depth (via gl_FragDepth)
vk::Framebuffer m_msaaResolveFramebuffer;
vk::RenderPass m_msaaEmissiveCopyRenderPass; // 1 MS color att (for upsample)
vk::Framebuffer m_msaaEmissiveCopyFramebuffer;
bool m_msaaInitialized = false;
};
/**
* @brief Deferred light accumulation (light volumes → G-buffer composite)
*
* Lazily-initialized subsystem owning the light-volume meshes (sphere/cylinder),
* the per-frame deferred light UBO, and the light-accumulation render pass that
* additively blends every scene light into the G-buffer composite. Consumes the
* G-buffer, the shadow map, and the scene color as read-only inputs.
*/
class VulkanDeferredLighting {
public:
/**
* @brief Wire dependencies (GPU resources are created lazily on first render)
*/
void init(PostProcessContext& ctx, const RenderTarget& sceneColor,
const VulkanDeferredGBuffer& gbuffer, const VulkanShadowMap& shadow);
void shutdown();
/**
* @brief Recreate the light-accumulation framebuffer after a resize
*
* The framebuffer attaches the G-buffer composite view, which the G-buffer
* resize just recreated. No-op while the lazy resources don't exist yet.
*/
bool onResize();
/**
* @brief Accumulate all scene lights into the G-buffer composite
* @param cmd Active command buffer (must be outside a render pass)
*/
void render(vk::CommandBuffer cmd);
private:
bool createLightAccumFramebuffer();
// Light volume meshes (sphere + cylinder for positional lights)
struct LightVolumeMesh {
vk::Buffer vbo;
VulkanAllocation vboAlloc;
vk::Buffer ibo;
VulkanAllocation iboAlloc;
uint32_t vertexCount = 0;
uint32_t indexCount = 0;
};
bool initLightVolumes();
bool initLightAccumPass();
// Sized for a generous simultaneous-light budget so a large battle (many ships'
// weapons fire, thrusters, explosions, muzzle flashes) can't silently exceed
// it -- render()'s overflow check skips the ENTIRE light-accumulation pass for
// the frame (every light, including directional/ambient, not just whichever
// light pushed it over) when this is undersized. Confirmed in the wild: a
// mission log showed "Deferred UBO overflow (270464 > 262144)" repeatedly at
// the old 256KB size, i.e. the light count was hovering right at capacity and
// tipping over as dynamic lights came and went, blacking out lighting for
// those frames entirely.
//
// Per-light cost is (sizeof(deferred_light_data) + sizeof(matrix_uniforms)),
// each independently rounded up to minUniformBufferOffsetAlignment (device-
// dependent, queried at runtime in render()) -- 256 bytes is a conservative
// upper bound seen in practice, so budget worst-case 2x that per light.
//
// Sized well above the ~5000-object limit because particles, glowpoints, etc.
// can each contribute lights, so the effective ceiling is much higher than the
// object count. At ~8MB the buffer is cheap next to the frame's render targets,
// so we over-provision rather than risk the all-or-nothing overflow above.
static constexpr uint32_t DEFERRED_MAX_LIGHTS = 16384;
static constexpr uint32_t DEFERRED_UBO_SIZE = 64 * 1024 + DEFERRED_MAX_LIGHTS * 512; // ~8.06MB
// DEFERRED_UBO_SIZE is the budget for ONE pass; the buffer holds a full-size
// region per (frame-in-flight, lighting mode) so that no two passes ever pack
// into the same bytes. Eventually sharing one region between the scene and
// cockpit passes is desired, but it would first require moving the cockpit intensity/
// radius modifiers, etc. into the shader.
static constexpr uint32_t DEFERRED_UBO_REGIONS = MAX_FRAMES_IN_FLIGHT * static_cast<uint32_t>(lighting_mode::MAX_LIGHTING_MODES);
PostProcessContext* m_ctx = nullptr;
const RenderTarget* m_sceneColor = nullptr;
const VulkanDeferredGBuffer* m_gbuffer = nullptr;
const VulkanShadowMap* m_shadow = nullptr;
LightVolumeMesh m_sphereMesh;
LightVolumeMesh m_cylinderMesh;
// UBO for deferred light data (globals + lights + matrices), DEFERRED_UBO_REGIONS
// full-size regions wide.
vk::Buffer m_deferredUBO;
VulkanAllocation m_deferredUBOAlloc;
vk::RenderPass m_lightAccumRenderPass; // Single RGBA16F color, loadOp=eLoad, additive blend
vk::Framebuffer m_lightAccumFramebuffer; // Composite image as attachment 0
bool m_lightVolumesInitialized = false;
};
/**
* @brief Scene fog + volumetric nebula passes
*
* Lazily-initialized subsystem owning the fog render pass/framebuffer (which
* targets the scene color) and the mipmapped emissive copy used for volumetric
* LOD sampling. Consumes the G-buffer composite/emissive and the scene depth
* copy, performs its own scene-depth copy, and writes into the scene color.
*/
class VulkanFog {
public:
/**
* @brief Wire dependencies (GPU resources are created lazily on first render)
*/
void init(PostProcessContext& ctx, const RenderTarget& sceneColor,
const RenderTarget& sceneDepth, const RenderTarget& sceneDepthCopy,
const VulkanDeferredGBuffer& gbuffer);
void shutdown();
/**
* @brief Distance fog pass (composite + depth → scene color)
* @param cmd Active command buffer (must be outside a render pass)
*/
void renderScene(vk::CommandBuffer cmd);
/**
* @brief Volumetric nebula pass (composite + emissive mips + depth + 3D vol → scene color)
* @param cmd Active command buffer (must be outside a render pass)
*/
void renderVolumetric(vk::CommandBuffer cmd);
/**
* @brief Rewire extent-sized lazy resources after a resize
*
* Recreates the fog framebuffer (attaches the recreated scene color view)
* and drops the emissive mip chain so renderVolumetric() lazily rebuilds it
* at the new extent. No-op for resources that don't exist yet.
*/
bool onResize();
private:
bool initFogPass();
bool createFogFramebuffer();
void copySceneDepth(vk::CommandBuffer cmd);
void destroyEmissiveMipmapped();
PostProcessContext* m_ctx = nullptr;
const RenderTarget* m_sceneColor = nullptr;
const RenderTarget* m_sceneDepth = nullptr;
const RenderTarget* m_sceneDepthCopy = nullptr;
const VulkanDeferredGBuffer* m_gbuffer = nullptr;
vk::RenderPass m_fogRenderPass; // Color-only RGBA16F, loadOp=eDontCare, finalLayout=eColorAttachmentOptimal
vk::Framebuffer m_fogFramebuffer; // Scene color as color attachment
bool m_fogInitialized = false;
// Mipmapped emissive copy for volumetric fog LOD sampling
RenderTarget m_emissiveMipmapped; // RGBA16F with full mip chain
uint32_t m_emissiveMipLevels = 0;
vk::ImageView m_emissiveMipmappedFullView; // View with all mip levels
bool m_emissiveMipmappedInitialized = false;
};
/**
* @brief Tonemapping + FXAA + post-effects + lightshafts (the LDR pass chain)
*
* Self-contained subsystem owning the full-resolution LDR targets (tonemapped
* output, luma-for-FXAA scratch, and a [0,1]-clamped detection proxy used by
* FXAA/SMAA edge detection while HDR output is active) plus their render
* passes/framebuffers. Reads the HDR scene color (tonemap) and scene depth
* (lightshafts) as read-only inputs owned by the post-processor, and the
* bloom subsystem's init state (to decide whether to reset the shared
* scratch-UBO cursor, since bloom itself resets it when it runs).
*/
class VulkanLDR {
public:
/**
* @brief Create LDR targets (sized to the scene extent)
*/
bool init(PostProcessContext& ctx, const RenderTarget& sceneColor, const RenderTarget& sceneDepth,
const VulkanBloom& bloom);
void shutdown();
/**
* @brief Recreate the extent-sized targets/framebuffers (render passes kept)
*
* Device must be idle. Returns false on failure (caller should shut the
* subsystem down).
*/
bool resize();
bool isInitialized() const { return m_initialized; }
/**
* @brief HDR scene → Scene_ldr via the tonemapping shader
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeTonemap(vk::CommandBuffer cmd);
/**
* @brief FXAA prepass (luma) + main pass, both operating on Scene_ldr
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeFXAA(vk::CommandBuffer cmd);
/**
* @brief Saturation/brightness/etc. post effects: Scene_ldr -> Scene_luminance
* @param cmd Active command buffer (must be outside a render pass)
* @return true if effects were applied (blit should read the luminance target)
*/
bool executePostEffects(vk::CommandBuffer cmd);
/**
* @brief Lightshafts (god rays), additively blended onto Scene_ldr
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeLightshafts(vk::CommandBuffer cmd);
/**
* @brief Buffer FXAA/SMAA edge detection should read for luma computation:
* the real Scene_ldr when it's already bounded to [0,1], or the compressed
* tonemap proxy when HDR output has left it in extended range.
*/
vk::ImageView getAADetectionView() const;
bool postEffectsApplied() const { return m_postEffectsApplied; }
// Accessors for VulkanSMAA (reuses this render pass/targets) and
// VulkanPostProcessor::blitToSwapChain (reads the final LDR result).
vk::RenderPass renderPass() const { return m_ldrRenderPass; }
vk::RenderPass loadRenderPass() const { return m_ldrLoadRenderPass; }
vk::ImageView ldrView() const { return m_sceneLdr.view; }
vk::Framebuffer ldrFramebuffer() const { return m_sceneLdrFB; }
vk::ImageView luminanceView() const { return m_sceneLuminance.view; }
vk::Framebuffer luminanceFramebuffer() const { return m_sceneLuminanceFB; }
private:
bool createTargets();
void destroyTargets();
PostProcessContext* m_ctx = nullptr;
const RenderTarget* m_sceneColor = nullptr;
const RenderTarget* m_sceneDepth = nullptr;
const VulkanBloom* m_bloom = nullptr;
RenderTarget m_sceneLdr; // RGBA8 LDR after tonemapping
RenderTarget m_sceneLuminance; // RGBA8 LDR with luma in alpha (for FXAA)
// RGBA8, a [0,1]-clamped copy of Scene_ldr (see executeTonemap); used only
// as an edge-detection input for FXAA/SMAA (see getAADetectionView) so
// their fixed luma thresholds stay valid even though Scene_ldr itself can
// carry values above 1.0 while HDR is active. Unused (and left as a
// duplicate of Scene_ldr's own data) when HDR is inactive.
RenderTarget m_sceneLdrCompressed;
vk::RenderPass m_ldrRenderPass; // Color-only RGBA8, loadOp=eDontCare
vk::RenderPass m_ldrLoadRenderPass; // Color-only RGBA8, loadOp=eLoad (for additive blending)
vk::Framebuffer m_sceneLdrFB;
vk::Framebuffer m_sceneLuminanceFB;
vk::Framebuffer m_sceneLdrCompressedFB;
bool m_initialized = false;
bool m_postEffectsApplied = false; // Set per-frame by executePostEffects
};
/**
* @brief SMAA anti-aliasing (edge detection -> blending weights -> neighborhood blend -> resolve)
*
* Self-contained subsystem owning the static SMAA area/search lookup textures
* and the edge/blend-weight intermediate targets. Reuses VulkanLDR's render
* pass and Scene_ldr/Scene_luminance targets (same format/loadOp/finalLayout),
* so it depends on VulkanLDR rather than owning its own LDR-shaped state.
*/
class VulkanSMAA {
public:
/**
* @brief Upload SMAA lookup textures and create edge/blend intermediate targets
* @param ldr Must already be initialized; its render pass and targets are reused.
*/
bool init(PostProcessContext& ctx, const VulkanLDR& ldr);
void shutdown();
/**
* @brief Recreate the extent-sized edge/blend targets and framebuffers
*
* The static area/search lookup textures are extent-independent and kept.
* Device must be idle. Returns false on failure (caller should shut the
* subsystem down).
*/
bool resize();
bool isInitialized() const { return m_initialized; }
/**
* @brief Run the full SMAA chain, resolving the result back into Scene_ldr
* @param cmd Active command buffer (must be outside a render pass)
*/
void execute(vk::CommandBuffer cmd);
private:
bool createTargets();
void destroyTargets();
PostProcessContext* m_ctx = nullptr;
const VulkanLDR* m_ldr = nullptr;
// Static lookup textures (uploaded once at init, never change)
vk::Image m_smaaAreaTexImage;
vk::ImageView m_smaaAreaTexView;
VulkanAllocation m_smaaAreaTexAlloc;
vk::Image m_smaaSearchTexImage;
vk::ImageView m_smaaSearchTexView;
VulkanAllocation m_smaaSearchTexAlloc;
// Intermediate targets (reuse VulkanLDR's render pass: same format/loadOp/finalLayout)
RenderTarget m_smaaEdges; // RGBA8 edge detection output
RenderTarget m_smaaBlend; // RGBA8 blending weight output
vk::Framebuffer m_smaaEdgesFB;
vk::Framebuffer m_smaaBlendFB;
bool m_initialized = false;
};
/**
* @brief Manages Vulkan post-processing pipeline
*
* Owns offscreen render targets (HDR scene color + depth), render passes,
* and executes post-processing passes (tonemapping, bloom, FXAA, etc.)
* between the 3D scene rendering and the final swap chain presentation.
*/
class VulkanPostProcessor {
public:
VulkanPostProcessor() = default;
~VulkanPostProcessor() = default;
// Non-copyable
VulkanPostProcessor(const VulkanPostProcessor&) = delete;
VulkanPostProcessor& operator=(const VulkanPostProcessor&) = delete;
/**
* @brief Initialize post-processing resources
* @param device Vulkan logical device
* @param physDevice Physical device (for format checks)
* @param memMgr Memory manager for allocations
* @param extent Scene rendering resolution
* @param depthFormat Depth format (matches main depth buffer)
* @return true on success
*/
bool init(vk::Device device, vk::PhysicalDevice physDevice,
VulkanMemoryManager* memMgr, vk::Extent2D extent,
vk::Format depthFormat, bool hdrActive = false);
/**
* @brief Shutdown and free all post-processing resources
*/
void shutdown();
/**
* @brief Recreate all extent-sized targets/framebuffers for a new resolution
*
* Called from VulkanRenderer::recreateSwapChain() after the device-idle
* wait. Render passes, samplers, and the scratch UBO ring are extent-
* independent and kept alive, so every cached pipeline stays valid.
* Individual subsystem failures are non-fatal (the subsystem is disabled,
* matching init()); returns false only if the core scene targets could not
* be recreated.
*/
bool resize(vk::Extent2D newExtent);
/**
* @brief Per-frame reset (called from VulkanRenderer::setupFrame after the frame fence wait)
*
* Resets the scratch UBO ring cursor for this frame-in-flight index. This is
* the ONLY place the cursor is reset -- the per-frame slot budget covers every
* fullscreen pass of the frame (mid-scene fog included), so subsystems must
* not reset it themselves.
*/
void beginFrame(uint32_t frameIndex) { m_ctx.scratchRing.resetCursor(frameIndex); }
/**
* @brief Get the HDR scene render pass (for 3D scene rendering)
*
* This render pass has RGBA16F color + depth attachments with loadOp=eClear.
* Used between scene_texture_begin() and scene_texture_end().
*/
vk::RenderPass getSceneRenderPass() const { return m_sceneRenderPass; }
/**
* @brief Get the HDR scene render pass with loadOp=eLoad
*
* Compatible with getSceneRenderPass() (same formats/samples) so uses
* the same framebuffer. Used to resume scene rendering after
* copy_effect_texture interrupts the pass.
*/
vk::RenderPass getSceneRenderPassLoad() const { return m_sceneRenderPassLoad; }
/**
* @brief Get the HDR scene framebuffer
*/
vk::Framebuffer getSceneFramebuffer() const { return m_sceneFramebuffer; }
/**
* @brief Get the scene rendering extent
*/
vk::Extent2D getSceneExtent() const { return m_ctx.sceneExtent; }
/**
* @brief Execute post-processing passes and draw result to swap chain
*
* Called after the HDR scene render pass ends and before the resumed
* swap chain render pass begins. Runs tonemapping (and later bloom,
* FXAA, etc.) then draws a fullscreen triangle to blit the result
* into the swap chain.
*
* The caller is responsible for:
* 1. Ending the HDR scene render pass before calling this
* 2. Beginning the resumed swap chain render pass before calling this
* (the blit draws INTO the resumed pass)
*
* @param cmd Active command buffer
*/
void blitToSwapChain(vk::CommandBuffer cmd);
/**
* @brief Final output-encode pass: composition image -> swap chain image (HDR10 leg)
*
* Begins/ends its own render pass on the swap chain framebuffer and draws a
* fullscreen triangle that samples the fp16 composition image, applying the
* user gamma/brightness slider followed by the PQ / BT.2020 (HDR10)
* transfer. Only called when the swap chain negotiated HDR10 output --
* the SDR leg uses encodeOutputSdr() instead. See
* VulkanRenderer::encodeToSwapChain().
*/
void encodeOutput(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer,
vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler,
float paperwhiteNits, float peakNits, float gamma);
/**
* @brief Final output-encode pass: composition image -> swap chain image (SDR leg)
*
* Begins/ends its own render pass on the swap chain framebuffer and draws a
* fullscreen triangle (SDR_TYPE_GAMMA_BLIT) that samples the fp16
* composition image -- which already carries the final display-ready
* sRGB-encoded frame (3D scene + menus/HUD alike) -- and applies the user
* gamma/brightness slider before writing it into the swap chain image.
* Always used for the SDR leg; there is no shader-less fast path, so the
* slider works uniformly regardless of device blit support.
*/
void encodeOutputSdr(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer,
vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler, float gamma);
private:
/**
* @brief Shared body of encodeOutput()/encodeOutputSdr()
*
* Begins/ends the given render pass on the swap chain framebuffer and draws a
* fullscreen triangle that samples sourceView, binding uboData into the
* per-draw GenericData slot from the scratch ring. The two public legs share
* the gamma-correct shader and differ only in shaderFlags (SDR vs
* HDR10_OUTPUT) + the UBO values.
*/
void encodeToSwapChainPass(vk::CommandBuffer cmd, vk::RenderPass renderPass, vk::Framebuffer framebuffer,
vk::Extent2D extent, vk::ImageView sourceView, vk::Sampler sampler,
int shaderType, unsigned int shaderFlags, const void* uboData, size_t uboSize);
public:
/**
* @brief Execute bloom post-processing passes
*
* Called after the HDR scene render pass ends and before the resumed
* swap chain render pass begins. Manages its own render passes internally.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeBloom(vk::CommandBuffer cmd) { m_bloom.execute(cmd); }
/**
* @brief Execute tonemapping pass (HDR scene → LDR)
*
* Called after bloom and before FXAA. Renders to Scene_ldr (RGBA8).
* Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeTonemap(vk::CommandBuffer cmd) { m_ldr.executeTonemap(cmd); }
/**
* @brief Execute FXAA anti-aliasing passes
*
* Called after tonemapping. Runs prepass (LDR→luminance) then
* FXAA main pass (luminance→LDR). Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeFXAA(vk::CommandBuffer cmd) { m_ldr.executeFXAA(cmd); }
/**
* @brief Execute SMAA anti-aliasing passes
*
* Called after tonemapping (mutually exclusive with FXAA, selected via
* Gr_aa_mode). Runs edge detection, blending weight calculation, and
* neighborhood blending, then resolves the result back into Scene_ldr.
* Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeSMAA(vk::CommandBuffer cmd) { m_smaa.execute(cmd); }
/**
* @brief Execute post-processing effects (saturation, brightness, etc.)
*
* Called after FXAA and before the final blit. Reads Scene_ldr, writes
* Scene_luminance (reused as temp target). Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
* @return true if effects were applied (blit should read Scene_luminance)
*/
bool executePostEffects(vk::CommandBuffer cmd) { return m_ldr.executePostEffects(cmd); }
/**
* @brief Execute lightshafts (god rays) pass
*
* Called after FXAA and before post-effects. Additively blends god rays
* onto Scene_ldr based on sun position and depth buffer sampling.
* Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void executeLightshafts(vk::CommandBuffer cmd) { m_ldr.executeLightshafts(cmd); }
/**
* @brief Update distortion ping-pong textures
*
* Called every frame from endSceneRendering(). Internally tracks a ~30ms
* timer. When triggered, scrolls old distortion data right by 1 pixel and
* injects random noise at the left edge (matching OpenGL's
* gr_opengl_update_distortion()). Must be called outside a render pass.
*
* @param cmd Active command buffer (must be outside a render pass)
* @param frametime Time since last frame in seconds
*/
void updateDistortion(vk::CommandBuffer cmd, float frametime) { m_distortion.update(cmd, frametime); }
/**
* @brief Get a ready-to-use DescriptorImageInfo for the current distortion texture
*
* Returns the most recently written distortion texture (the one thrusters
* should read from). Returns a default-constructed info if not initialized.
*/
vk::DescriptorImageInfo getDistortionTextureInfo() const { return m_distortion.getTextureInfo(); }
/**
* @brief Copy scene color to effect texture for distortion/soft particle sampling
*
* Must be called outside a render pass. Transitions scene color through
* eTransferSrcOptimal and back to eColorAttachmentOptimal (ready for resumed
* scene render pass). Transitions effect texture to eShaderReadOnlyOptimal.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void copyEffectTexture(vk::CommandBuffer cmd) const;
/**
* @brief Copy scene depth to samplable depth copy for soft particle rendering
*
* Must be called outside a render pass. Transitions scene depth through
* eTransferSrcOptimal and back to eDepthStencilAttachmentOptimal. Transitions
* depth copy to eShaderReadOnlyOptimal for fragment shader sampling.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void copySceneDepth(vk::CommandBuffer cmd) const;
/**
* @brief Copy scene depth aside so the cockpit can render on a cleared depth buffer
*
* The cockpit model shares the world with the ship hull around it, so it needs a
* depth buffer of its own -- OpenGL gets one by swapping the depth attachment to
* Cockpit_depth_texture. Here the scene depth stays the attachment and its content
* is parked in a backup image instead, which restoreSceneDepth() puts back.
*
* Must be called outside a render pass. Leaves scene depth in
* eDepthStencilAttachmentOptimal and the backup in eTransferSrcOptimal.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void saveSceneDepth(vk::CommandBuffer cmd) const;
/**
* @brief Put the depth that saveSceneDepth() parked back into the scene depth buffer
*
* Discards whatever the cockpit wrote, which is the point: the post-processing
* passes that sample scene depth (lightshafts) must see the scene, not the cockpit.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void restoreSceneDepth(vk::CommandBuffer cmd) const;
/**
* @brief Check if LDR targets are available (tonemapping + FXAA ready)
*/
bool hasLDRTargets() const { return m_ldr.isInitialized(); }
/**
* @brief Get the scene color image (for layout transitions outside post-processor)
*/
vk::Image getSceneColorImage() const { return m_sceneColor.image; }
/**
* @brief Get the scene color image view (for post-processing texture binding)
*/
vk::ImageView getSceneColorView() const { return m_sceneColor.view; }
/**
* @brief Get the scene color sampler
*/
vk::Sampler getSceneColorSampler() const { return m_ctx.linearSampler; }
/**
* @brief Get a ready-to-use DescriptorImageInfo for the scene effect texture
*
* Available for sampling after copyEffectTexture() has been called.
* Used by distortion and soft particle shaders.
* Returns default-constructed info if the effect texture doesn't exist.
*/
vk::DescriptorImageInfo getSceneEffectTextureInfo() const {
if (!m_sceneEffect.view) return {};
return {m_ctx.linearSampler, m_sceneEffect.view, vk::ImageLayout::eShaderReadOnlyOptimal};
}
/**
* @brief Get the scene depth copy view (for soft particle depth sampling)
*
* Available for sampling after copySceneDepth() has been called.
*/
vk::ImageView getSceneDepthCopyView() const { return m_sceneDepthCopy.view; }
vk::Format getDepthFormat() const { return m_ctx.depthFormat; }
/**
* @brief Check if post-processing is initialized
*/
bool isInitialized() const { return m_initialized; }
// ========== Subsystem access ==========
// Consumers that drive the deferred / shadow passes operate directly on the
// owning subsystem (its resources, render passes, and mid-frame transitions)
// rather than through dozens of per-resource forwarding accessors.
VulkanDeferredGBuffer& deferred() { return m_deferred; }
const VulkanDeferredGBuffer& deferred() const { return m_deferred; }
VulkanShadowMap& shadow() { return m_shadow; }
const VulkanShadowMap& shadow() const { return m_shadow; }
// Shared draw/image helpers (fullscreen-triangle draws, image creation) for
// free-function callers outside VulkanPostProcessor itself, e.g. the deferred
// lighting pass driver in VulkanDeferred.cpp.
PostProcessContext& context() { return m_ctx; }
// ========== Deferred Light Accumulation ==========
/**
* @brief Render deferred lights into the composite buffer
*
* Reads G-buffer textures, renders light volumes (fullscreen, sphere, cylinder)
* with additive blending into the composite attachment. Manages its own render
* pass internally.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void renderDeferredLights(vk::CommandBuffer cmd) { m_lighting.render(cmd); }
// ========== Shadow Map ==========
/**
* @brief Initialize shadow map resources (lazy, called on first use)
* @return true on success
*/
bool initShadowPass() { return m_shadow.init(m_ctx); }
/**
* @brief Shutdown shadow map resources
*/
void shutdownShadowPass() { m_shadow.shutdown(); }
/**
* @brief Get a ready-to-use DescriptorImageInfo for the shadow map texture
*
* Uses the shadow map's depth-compare sampler so the shader can declare
* this binding as sampler2DArrayShadow and get hardware PCF.
*/
vk::DescriptorImageInfo getShadowTextureInfo() const {
return {m_shadow.compareSampler(), m_shadow.depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
// ========== Fog / Volumetric Nebula ==========
/**
* @brief Render scene fog into scene color
*
* Reads composite (lit result) + depth copy -> writes scene color.
* Must be called outside a render pass. After return, scene color
* is in eColorAttachmentOptimal.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void renderSceneFog(vk::CommandBuffer cmd) { m_fog.renderScene(cmd); }
/**
* @brief Render volumetric nebula fog into scene color
*
* Reads composite + mipmapped emissive + depth copy + 3D volume textures
* -> writes scene color. Must be called outside a render pass.
* After return, scene color is in eColorAttachmentOptimal.
*
* @param cmd Active command buffer (must be outside a render pass)
*/
void renderVolumetricFog(vk::CommandBuffer cmd) { m_fog.renderVolumetric(cmd); }
private:
bool createSceneTargets(vk::Extent2D extent);
void destroySceneTargets();
bool createSceneFramebuffer();
bool createImage(uint32_t width, uint32_t height, vk::Format format,
vk::ImageUsageFlags usage, vk::ImageAspectFlags aspect,
vk::Image& outImage, vk::ImageView& outView,
VulkanAllocation& outAllocation,
vk::SampleCountFlagBits sampleCount = vk::SampleCountFlagBits::e1)
const
{
return m_ctx.createImage(width, height, format, usage, aspect,
outImage, outView, outAllocation, sampleCount);
}
// G-buffer / MSAA (forwards to the VulkanDeferredGBuffer subsystem)
bool initGBuffer() { return m_deferred.init(m_ctx, m_sceneColor, m_sceneDepth); }
void shutdownGBuffer() { m_deferred.shutdown(); }
bool initMSAA() { return m_deferred.initMsaa(); }
void shutdownMSAA() { m_deferred.shutdownMsaa(); }
// Bloom pipeline (forwards to the VulkanBloom subsystem)
bool initBloom() { return m_bloom.init(m_ctx, m_sceneColor); }
void shutdownBloom() { m_bloom.shutdown(); }
static void generateMipmaps(vk::CommandBuffer cmd, vk::Image image,
uint32_t width, uint32_t height, uint32_t mipLevels)
{
PostProcessContext::generateMipmaps(cmd, image, width, height, mipLevels);
}
void drawFullscreenTriangle(vk::CommandBuffer cmd, vk::RenderPass renderPass,
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
vk::ImageView textureView, vk::Sampler sampler,
const void* uboData, size_t uboSize,
int blendMode,
unsigned int shaderFlags = 0)
{
m_ctx.drawFullscreenTriangle(cmd, renderPass, framebuffer, extent, shaderType,
textureView, sampler, uboData, uboSize, blendMode, shaderFlags);
}
void drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::RenderPass renderPass,
vk::Framebuffer framebuffer, vk::Extent2D extent,
int shaderType,
const vk::ImageView* views, uint32_t viewCount,
const void* uboData, size_t uboSize)
{
m_ctx.drawFullscreenTriangleMulti(cmd, renderPass, framebuffer, extent, shaderType,
views, viewCount, uboData, uboSize);
}
RenderTarget m_sceneColor; // RGBA16F HDR scene color
RenderTarget m_sceneDepth; // Depth buffer for scene
RenderTarget m_sceneDepthCopy; // Samplable copy of scene depth (for soft particles)
RenderTarget m_sceneDepthBackup; // Scene depth parked across the cockpit render (transfer only)
RenderTarget m_sceneEffect; // RGBA16F effect/composite (snapshot of scene color)
// Scene render pass and framebuffer
vk::RenderPass m_sceneRenderPass; // loadOp=eClear (initial scene begin)
vk::RenderPass m_sceneRenderPassLoad; // loadOp=eLoad (resume after copy_effect_texture)
vk::Framebuffer m_sceneFramebuffer; // Shared by both scene render passes (compatible)
// ---- Bloom (self-contained subsystem) ----
VulkanBloom m_bloom;
// ---- LDR / FXAA / post-effects / lightshafts (self-contained subsystem) ----
VulkanLDR m_ldr;
// ---- SMAA (self-contained subsystem, depends on VulkanLDR) ----
VulkanSMAA m_smaa;
// ---- Deferred G-buffer + MSAA (self-contained subsystem) ----
VulkanDeferredGBuffer m_deferred;
// ---- Deferred light accumulation (self-contained subsystem) ----
VulkanDeferredLighting m_lighting;
// ---- Shadow map (cascaded VSM, self-contained subsystem) ----
VulkanShadowMap m_shadow;
// ---- Fog / volumetric nebula (self-contained subsystem) ----
VulkanFog m_fog;
// ---- Distortion (ping-pong textures, self-contained subsystem) ----
VulkanDistortion m_distortion;
PostProcessContext m_ctx;
bool m_initialized = false;
};
// Global post-processor access
VulkanPostProcessor* getPostProcessor();
void setPostProcessor(VulkanPostProcessor* pp);
// gr_screen function pointer implementations for post-processing
void vulkan_post_process_begin();
void vulkan_post_process_end();
void vulkan_post_process_save_zbuffer();
void vulkan_post_process_restore_zbuffer();
void vulkan_post_process_set_effect(const char* name, int value, const vec3d* rgb);
void vulkan_post_process_set_defaults();
/**
* @brief Copy one image to another with automatic barrier management
*
* Handles pre-barriers (src→eTransferSrcOptimal, dst→eTransferDstOptimal),
* the copy command, and post-barriers (eTransferSrc→srcNewLayout, eTransferDst→dstNewLayout).
* Access masks and pipeline stages are derived from the layouts automatically.
*
* Skip rule: if srcNewLayout == eTransferSrcOptimal, the src post-barrier is skipped
* (image stays in transfer source layout). Same for dst + eTransferDstOptimal.
*
* @param cmd Active command buffer (must be outside a render pass)
* @param src Source image
* @param srcOldLayout Current layout of source image
* @param srcNewLayout Desired layout of source image after copy
* @param dst Destination image
* @param dstOldLayout Current layout of destination image
* @param dstNewLayout Desired layout of destination image after copy
* @param extent Copy region (width x height)
* @param aspect Image aspect (eColor or eDepth)
* @param dstMipLevels Number of mip levels in dst subresource range (for pre-barrier)
*/
void copyImageToImage(
vk::CommandBuffer cmd,
vk::Image src, vk::ImageLayout srcOldLayout, vk::ImageLayout srcNewLayout,
vk::Image dst, vk::ImageLayout dstOldLayout, vk::ImageLayout dstNewLayout,
vk::Extent2D extent,
vk::ImageAspectFlags aspect = vk::ImageAspectFlagBits::eColor,
uint32_t dstMipLevels = 1);
} // namespace graphics::vulkan