concinnity-device 0.19.119

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
//! Vulkan render-pass construction for the main, shadow, and composite
//! passes.

use ash::vk;
use concinnity_core::render::error::RenderResult;

use crate::vulkan::owned::{OwnedRenderPass, VkDevice};

// The external dependency of every render pass that begins on the main
// framebuffers. Render-pass compatibility counts dependencies, so the main
// pass, both two-pass phases and the raymarch pass must share this one. It
// orders the LOAD reads too, since a pass may load after a layout transition.
pub(super) fn main_framebuffer_dependency() -> vk::SubpassDependency {
    vk::SubpassDependency::default()
        .src_subpass(vk::SUBPASS_EXTERNAL)
        .dst_subpass(0)
        .src_stage_mask(
            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
                | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS,
        )
        .src_access_mask(vk::AccessFlags::empty())
        .dst_stage_mask(
            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
                | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS,
        )
        .dst_access_mask(
            vk::AccessFlags::COLOR_ATTACHMENT_READ
                | vk::AccessFlags::COLOR_ATTACHMENT_WRITE
                | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ
                | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
        )
}

pub(super) fn create_main_render_pass(
    device: &VkDevice,
    format: vk::Format,
    msaa: vk::SampleCountFlags,
) -> RenderResult<OwnedRenderPass> {
    let multisampled = msaa != vk::SampleCountFlags::TYPE_1;

    // When multisampled the resolve attachment ends shader-readable; the MSAA
    // color image is transient. When single-sampled, attachment [0] is itself
    // the resolve image, so it ends shader-readable.
    let color_final_layout = if multisampled {
        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL
    } else {
        vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL
    };

    let mut attachments = vec![
        // [0] color (MSAA HDR, or the single-sample HDR resolve image)
        vk::AttachmentDescription::default()
            .format(format)
            .samples(msaa)
            .load_op(vk::AttachmentLoadOp::CLEAR)
            .store_op(if multisampled {
                vk::AttachmentStoreOp::DONT_CARE
            } else {
                vk::AttachmentStoreOp::STORE
            })
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(vk::ImageLayout::UNDEFINED)
            .final_layout(color_final_layout),
        // [1] depth: STORE because the projected-decal and volumetric-fog
        // passes sample this attachment after the main pass ends. Without
        // STORE the contents are spec-undefined post-pass: tilers may
        // discard them and the depth-read in fog returns garbage values
        // that bias toward the `depth >= 1.0` "no scene hit" branch,
        // producing per-pixel bright sparkles after bloom.
        vk::AttachmentDescription::default()
            .format(vk::Format::D32_SFLOAT)
            .samples(msaa)
            .load_op(vk::AttachmentLoadOp::CLEAR)
            .store_op(vk::AttachmentStoreOp::STORE)
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(vk::ImageLayout::UNDEFINED)
            .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL),
    ];
    if multisampled {
        // [2] resolve target (single-sample HDR resolve image)
        attachments.push(
            vk::AttachmentDescription::default()
                .format(format)
                .samples(vk::SampleCountFlags::TYPE_1)
                .load_op(vk::AttachmentLoadOp::DONT_CARE)
                .store_op(vk::AttachmentStoreOp::STORE)
                .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
                .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
                .initial_layout(vk::ImageLayout::UNDEFINED)
                .final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
        );
    }

    let color_ref = vk::AttachmentReference::default()
        .attachment(0)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
    let depth_ref = vk::AttachmentReference::default()
        .attachment(1)
        .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
    let resolve_ref = vk::AttachmentReference::default()
        .attachment(2)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let mut subpass = vk::SubpassDescription::default()
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .color_attachments(std::slice::from_ref(&color_ref))
        .depth_stencil_attachment(&depth_ref);
    if multisampled {
        subpass = subpass.resolve_attachments(std::slice::from_ref(&resolve_ref));
    }

    let dependency = main_framebuffer_dependency();

    let rp_info = vk::RenderPassCreateInfo::default()
        .attachments(&attachments)
        .subpasses(std::slice::from_ref(&subpass))
        .dependencies(std::slice::from_ref(&dependency));

    device
        .create_render_pass(&rp_info)
        .map_err(|e| super::error::map_vk_result(e, "main render pass"))
}

// Main-pass render pass for two-pass occlusion culling. Two variants share
// this builder, selected by `load`:
//
//   * `load = false` (phase 1, `Main`): same as `create_main_render_pass` but
//     the MSAA color is STORE'd (not DONT_CARE) so the phase-2 pass can load
//     the samples back and composite onto them. Color ends
//     COLOR_ATTACHMENT_OPTIMAL so `Main2` loads it directly.
//   * `load = true` (phase 2, `Main2`): loads (does not clear) the phase-1
//     color + depth, redraws the disoccluded statics, and resolves the
//     combined scene into `hdr_resolve` for the post stack.
//
// Both are render-pass-compatible with the existing main framebuffers (same
// attachment count / formats / sample counts), so no extra framebuffers are
// needed. `Main` (phase 1) still resolves into `hdr_resolve`; that resolve is
// overwritten by `Main2`'s and nothing reads it in between (HizBuild / Cull2
// are compute), so the combined result is what the post stack sees.
pub(super) fn create_main_render_pass_two_pass(
    device: &VkDevice,
    format: vk::Format,
    msaa: vk::SampleCountFlags,
    load: bool,
) -> RenderResult<OwnedRenderPass> {
    let multisampled = msaa != vk::SampleCountFlags::TYPE_1;

    // Phase 1 leaves the color in COLOR_ATTACHMENT_OPTIMAL for phase 2 to
    // load. Phase 2 ends shader-readable when single-sampled (the color image
    // is itself the resolve target the post stack samples); when multisampled
    // the resolve attachment carries that role and the MSAA color stays
    // transient in COLOR_ATTACHMENT_OPTIMAL.
    let color_final_layout = if multisampled || !load {
        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL
    } else {
        vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL
    };
    let color_load_op = if load {
        vk::AttachmentLoadOp::LOAD
    } else {
        vk::AttachmentLoadOp::CLEAR
    };
    let color_initial_layout = if load {
        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL
    } else {
        vk::ImageLayout::UNDEFINED
    };
    let depth_load_op = if load {
        vk::AttachmentLoadOp::LOAD
    } else {
        vk::AttachmentLoadOp::CLEAR
    };
    let depth_initial_layout = if load {
        vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL
    } else {
        vk::ImageLayout::UNDEFINED
    };

    let mut attachments = vec![
        // [0] color (MSAA HDR, or the single-sample HDR resolve image). STORE
        // unconditionally: phase 1 must keep the MSAA samples for phase 2's
        // load, and the single-sample image is the post-stack input.
        vk::AttachmentDescription::default()
            .format(format)
            .samples(msaa)
            .load_op(color_load_op)
            .store_op(vk::AttachmentStoreOp::STORE)
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(color_initial_layout)
            .final_layout(color_final_layout),
        // [1] depth: STORE (HizBuild reduces it, the post decals/fog sample it,
        // and phase 2 depth-tests against it).
        vk::AttachmentDescription::default()
            .format(vk::Format::D32_SFLOAT)
            .samples(msaa)
            .load_op(depth_load_op)
            .store_op(vk::AttachmentStoreOp::STORE)
            .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
            .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
            .initial_layout(depth_initial_layout)
            .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL),
    ];
    if multisampled {
        // [2] resolve target (single-sample HDR resolve image). Always
        // overwritten by the resolve, so its initial layout is irrelevant.
        attachments.push(
            vk::AttachmentDescription::default()
                .format(format)
                .samples(vk::SampleCountFlags::TYPE_1)
                .load_op(vk::AttachmentLoadOp::DONT_CARE)
                .store_op(vk::AttachmentStoreOp::STORE)
                .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
                .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
                .initial_layout(vk::ImageLayout::UNDEFINED)
                .final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL),
        );
    }

    let color_ref = vk::AttachmentReference::default()
        .attachment(0)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
    let depth_ref = vk::AttachmentReference::default()
        .attachment(1)
        .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
    let resolve_ref = vk::AttachmentReference::default()
        .attachment(2)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let mut subpass = vk::SubpassDescription::default()
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .color_attachments(std::slice::from_ref(&color_ref))
        .depth_stencil_attachment(&depth_ref);
    if multisampled {
        subpass = subpass.resolve_attachments(std::slice::from_ref(&resolve_ref));
    }

    // Phase 2's LOAD of the phase-1 color + depth is ordered by an explicit
    // `vkCmdPipelineBarrier` in `encode_main_pass_phase2`.
    let dependency = main_framebuffer_dependency();

    let rp_info = vk::RenderPassCreateInfo::default()
        .attachments(&attachments)
        .subpasses(std::slice::from_ref(&subpass))
        .dependencies(std::slice::from_ref(&dependency));

    device
        .create_render_pass(&rp_info)
        .map_err(|e| super::error::map_vk_result(e, "two-pass main render pass"))
}

pub(super) fn create_shadow_render_pass(device: &VkDevice) -> RenderResult<OwnedRenderPass> {
    let attachment = vk::AttachmentDescription::default()
        .format(vk::Format::D32_SFLOAT)
        .samples(vk::SampleCountFlags::TYPE_1)
        .load_op(vk::AttachmentLoadOp::CLEAR)
        .store_op(vk::AttachmentStoreOp::STORE)
        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
        .initial_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
        .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

    let depth_ref = vk::AttachmentReference::default()
        .attachment(0)
        .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

    let subpass = vk::SubpassDescription::default()
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .depth_stencil_attachment(&depth_ref);

    let rp_info = vk::RenderPassCreateInfo::default()
        .attachments(std::slice::from_ref(&attachment))
        .subpasses(std::slice::from_ref(&subpass));

    device
        .create_render_pass(&rp_info)
        .map_err(|e| super::error::map_vk_result(e, "shadow render pass"))
}

// Composite render pass. A single subpass renders the fullscreen tonemap +
// FXAA triangle (and the text overlay) into the swapchain backbuffer.
pub(super) fn create_composite_render_pass(
    device: &VkDevice,
    swapchain_format: vk::Format,
) -> RenderResult<OwnedRenderPass> {
    // The fullscreen triangle overwrites every pixel, so the backbuffer is
    // not cleared or loaded.
    let attachment = vk::AttachmentDescription::default()
        .format(swapchain_format)
        .samples(vk::SampleCountFlags::TYPE_1)
        .load_op(vk::AttachmentLoadOp::DONT_CARE)
        .store_op(vk::AttachmentStoreOp::STORE)
        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
        .initial_layout(vk::ImageLayout::UNDEFINED)
        .final_layout(vk::ImageLayout::PRESENT_SRC_KHR);

    let color_ref = vk::AttachmentReference::default()
        .attachment(0)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let subpass = vk::SubpassDescription::default()
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .color_attachments(std::slice::from_ref(&color_ref));

    // External dependency: the composite fragment shader must wait for the
    // main pass to finish writing (and resolving into) the HDR image, and the
    // backbuffer write must wait for the acquire semaphore's stage.
    let dependency = vk::SubpassDependency::default()
        .src_subpass(vk::SUBPASS_EXTERNAL)
        .dst_subpass(0)
        .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
        .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
        .dst_stage_mask(
            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
                | vk::PipelineStageFlags::FRAGMENT_SHADER,
        )
        .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::SHADER_READ);

    let rp_info = vk::RenderPassCreateInfo::default()
        .attachments(std::slice::from_ref(&attachment))
        .subpasses(std::slice::from_ref(&subpass))
        .dependencies(std::slice::from_ref(&dependency));

    device
        .create_render_pass(&rp_info)
        .map_err(|e| super::error::map_vk_result(e, "composite render pass"))
}

// Create the off-screen HDR attachment set, `count` slots deep (one per
// frame-in-flight). Returns `(msaa_color, depth, hdr_resolve)`; `msaa_color`
// is empty when MSAA is disabled, in which case the main pass renders
// straight into the resolve image.