use ash::vk;
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedFramebuffer, OwnedPipeline, OwnedPipelineLayout, OwnedRenderPass,
OwnedSampler, OwnedSetLayout, VkDevice,
};
use crate::gfx::fullscreen::{FullscreenPass, encode_fullscreen};
use crate::gfx::render_types::SsgiParams;
use crate::gfx::ssgi::SsgiSettings;
use super::super::allocator::DeviceAllocator;
use super::super::context::{HDR_FORMAT, VkContext};
use super::super::pipeline::*;
use super::super::resources::{alloc_descriptor_sets, create_descriptor_set_layout};
use super::super::texture::*;
use crate::vulkan::slang_builtins::SlangCompile;
pub(in crate::vulkan) struct SsgiShaders {
pub vs: Vec<u8>,
pub gather_fs: Vec<u8>,
pub composite_fs: Vec<u8>,
}
pub(in crate::vulkan) fn compile_ssgi_shaders(hot_reload: bool) -> Result<SsgiShaders, String> {
use super::super::{builtins, slang_builtins};
let ctx = builtins::Ctx::plain(hot_reload);
Ok(SsgiShaders {
vs: slang_builtins::FULLSCREEN_VERT.compile(&ctx)?,
gather_fs: slang_builtins::SSGI_GATHER.compile(&ctx)?,
composite_fs: slang_builtins::SSGI_COMPOSITE.compile(&ctx)?,
})
}
pub(in crate::vulkan) struct SsgiResources {
pub(in crate::vulkan) settings: SsgiSettings,
gather_render_pass: OwnedRenderPass,
composite_render_pass: OwnedRenderPass,
_set_layout: OwnedSetLayout,
pipeline_layout: OwnedPipelineLayout,
gather_pso: OwnedPipeline,
composite_pso: OwnedPipeline,
_descriptor_pool: OwnedDescriptorPool,
gather_sets: Vec<vk::DescriptorSet>,
composite_sets: Vec<vk::DescriptorSet>,
sampler: OwnedSampler,
gi: GpuImage,
gi_extent: vk::Extent2D,
gather_framebuffer: OwnedFramebuffer,
composite_framebuffers: Vec<OwnedFramebuffer>,
}
fn ssgi_external_deps() -> [vk::SubpassDependency; 2] {
let dep_in = vk::SubpassDependency::default()
.src_subpass(vk::SUBPASS_EXTERNAL)
.dst_subpass(0)
.src_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::SHADER_READ)
.dst_stage_mask(
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
)
.dst_access_mask(
vk::AccessFlags::COLOR_ATTACHMENT_WRITE
| vk::AccessFlags::COLOR_ATTACHMENT_READ
| vk::AccessFlags::SHADER_READ,
);
let dep_out = vk::SubpassDependency::default()
.src_subpass(0)
.dst_subpass(vk::SUBPASS_EXTERNAL)
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
.dst_stage_mask(vk::PipelineStageFlags::FRAGMENT_SHADER)
.dst_access_mask(vk::AccessFlags::SHADER_READ);
[dep_in, dep_out]
}
fn create_gather_render_pass(device: &VkDevice) -> Result<OwnedRenderPass, String> {
let attachment = vk::AttachmentDescription::default()
.format(HDR_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 subpass = vk::SubpassDescription::default()
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
.color_attachments(std::slice::from_ref(&color_ref));
let deps = ssgi_external_deps();
let info = vk::RenderPassCreateInfo::default()
.attachments(std::slice::from_ref(&attachment))
.subpasses(std::slice::from_ref(&subpass))
.dependencies(&deps);
device
.create_render_pass(&info)
.map_err(|e| format!("SSGI gather render pass: {e}"))
}
fn create_composite_render_pass(device: &VkDevice) -> Result<OwnedRenderPass, String> {
let attachment = vk::AttachmentDescription::default()
.format(HDR_FORMAT)
.samples(vk::SampleCountFlags::TYPE_1)
.load_op(vk::AttachmentLoadOp::LOAD)
.store_op(vk::AttachmentStoreOp::STORE)
.stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
.stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
.initial_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.final_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL);
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));
let deps = ssgi_external_deps();
let info = vk::RenderPassCreateInfo::default()
.attachments(std::slice::from_ref(&attachment))
.subpasses(std::slice::from_ref(&subpass))
.dependencies(&deps);
device
.create_render_pass(&info)
.map_err(|e| format!("SSGI composite render pass: {e}"))
}
fn create_gi_target(
alloc: &DeviceAllocator,
device: &VkDevice,
width: u32,
height: u32,
) -> Result<GpuImage, String> {
let pooled = create_image(
alloc,
&ImageSpec {
width,
height,
format: HDR_FORMAT,
tiling: vk::ImageTiling::OPTIMAL,
usage: vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED,
mem_props: vk::MemoryPropertyFlags::DEVICE_LOCAL,
samples: vk::SampleCountFlags::TYPE_1,
},
)?;
let image = pooled.image();
let view = create_image_view(device, image, HDR_FORMAT, vk::ImageAspectFlags::COLOR)?;
Ok(GpuImage::from_pooled(pooled, view))
}
fn create_ssgi_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
additive: bool,
) -> Result<OwnedPipeline, String> {
let vert_mod = spv_module(device, vert_spv)?;
let frag_mod = spv_module(device, frag_spv)?;
let entry = std::ffi::CString::new("main").unwrap();
let stages = [
vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vert_mod.handle())
.name(&entry),
vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(frag_mod.handle())
.name(&entry),
];
let vert_input = vk::PipelineVertexInputStateCreateInfo::default();
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewport_count(1)
.scissor_count(1);
let raster = vk::PipelineRasterizationStateCreateInfo::default()
.polygon_mode(vk::PolygonMode::FILL)
.line_width(1.0)
.cull_mode(vk::CullModeFlags::NONE)
.front_face(vk::FrontFace::COUNTER_CLOCKWISE);
let multisample = vk::PipelineMultisampleStateCreateInfo::default()
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
let depth = vk::PipelineDepthStencilStateCreateInfo::default()
.depth_test_enable(false)
.depth_write_enable(false)
.depth_compare_op(vk::CompareOp::ALWAYS);
let blend_attach = if additive {
vk::PipelineColorBlendAttachmentState::default()
.color_write_mask(vk::ColorComponentFlags::RGBA)
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::ONE)
.dst_color_blend_factor(vk::BlendFactor::ONE)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::ONE)
.dst_alpha_blend_factor(vk::BlendFactor::ONE)
.alpha_blend_op(vk::BlendOp::ADD)
} else {
vk::PipelineColorBlendAttachmentState::default()
.color_write_mask(vk::ColorComponentFlags::RGBA)
.blend_enable(false)
};
let blend = vk::PipelineColorBlendStateCreateInfo::default()
.attachments(std::slice::from_ref(&blend_attach));
let dyn_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dyn_states);
let info = vk::GraphicsPipelineCreateInfo::default()
.stages(&stages)
.vertex_input_state(&vert_input)
.input_assembly_state(&input_assembly)
.viewport_state(&viewport_state)
.rasterization_state(&raster)
.multisample_state(&multisample)
.depth_stencil_state(&depth)
.color_blend_state(&blend)
.dynamic_state(&dynamic)
.layout(layout)
.render_pass(render_pass)
.subpass(0);
let pipeline = crate::vulkan::pipeline_cache::create_graphics_pipeline(device, &info)
.map_err(|e| format!("create ssgi pso: {e}"))?;
Ok(pipeline)
}
pub(in crate::vulkan) struct RebuiltSsgiPipelines {
pub gather: OwnedPipeline,
pub composite: OwnedPipeline,
}
pub(in crate::vulkan) fn rebuild_ssgi_pipelines(
device: &VkDevice,
ssgi: &SsgiResources,
hot_reload: bool,
) -> Result<RebuiltSsgiPipelines, String> {
let shaders = compile_ssgi_shaders(hot_reload)?;
let gather = create_ssgi_pipeline(
device,
ssgi.gather_render_pass.handle(),
ssgi.pipeline_layout.handle(),
&shaders.vs,
&shaders.gather_fs,
false,
)?;
let composite = create_ssgi_pipeline(
device,
ssgi.composite_render_pass.handle(),
ssgi.pipeline_layout.handle(),
&shaders.vs,
&shaders.composite_fs,
true,
)?;
Ok(RebuiltSsgiPipelines { gather, composite })
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct SsgiDevice<'a> {
pub alloc: &'a DeviceAllocator,
pub device: &'a VkDevice,
}
#[derive(Clone, Copy)]
pub(in crate::vulkan) struct SsgiInputViews<'a> {
pub hdr_resolve_views: &'a [vk::ImageView],
pub gbuffer_view: vk::ImageView,
}
impl SsgiResources {
pub(in crate::vulkan) fn new(
dev: SsgiDevice<'_>,
width: u32,
height: u32,
frames: usize,
settings: SsgiSettings,
views: SsgiInputViews<'_>,
hot_reload: bool,
) -> Result<Self, String> {
let SsgiDevice { alloc, device } = dev;
let SsgiInputViews {
hdr_resolve_views,
gbuffer_view,
} = views;
let gather_render_pass = create_gather_render_pass(device)?;
let composite_render_pass = create_composite_render_pass(device)?;
let set_layout = create_descriptor_set_layout(
device,
&[
(
0,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
vk::ShaderStageFlags::FRAGMENT,
),
(
1,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
vk::ShaderStageFlags::FRAGMENT,
),
],
)?;
let push = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::FRAGMENT)
.offset(0)
.size(std::mem::size_of::<SsgiParams>() as u32);
let set_layouts = [set_layout.handle()];
let pipeline_layout = device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(std::slice::from_ref(&push)),
)
.map_err(|e| format!("ssgi pipeline layout: {e}"))?;
let shaders = compile_ssgi_shaders(hot_reload)?;
let gather_pso = create_ssgi_pipeline(
device,
gather_render_pass.handle(),
pipeline_layout.handle(),
&shaders.vs,
&shaders.gather_fs,
false,
)?;
let composite_pso = create_ssgi_pipeline(
device,
composite_render_pass.handle(),
pipeline_layout.handle(),
&shaders.vs,
&shaders.composite_fs,
true,
)?;
let sampler_count = frames as u32 * 2 * 2;
let pool_size = vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(sampler_count);
let descriptor_pool = device
.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.pool_sizes(std::slice::from_ref(&pool_size))
.max_sets(frames as u32 * 2),
)
.map_err(|e| format!("ssgi descriptor pool: {e}"))?;
let gather_layouts: Vec<_> = (0..frames).map(|_| set_layout.handle()).collect();
let gather_sets = alloc_descriptor_sets(device, descriptor_pool.handle(), &gather_layouts)?;
let composite_layouts: Vec<_> = (0..frames).map(|_| set_layout.handle()).collect();
let composite_sets =
alloc_descriptor_sets(device, descriptor_pool.handle(), &composite_layouts)?;
let sampler = create_sampler_linear_clamp(device)?;
let mut me = Self {
settings,
gather_render_pass,
composite_render_pass,
_set_layout: set_layout,
pipeline_layout,
gather_pso,
composite_pso,
_descriptor_pool: descriptor_pool,
gather_sets,
composite_sets,
sampler,
gi: GpuImage::null(),
gi_extent: vk::Extent2D {
width: 1,
height: 1,
},
gather_framebuffer: OwnedFramebuffer::null(),
composite_framebuffers: Vec::new(),
};
me.build_targets(alloc, device, width, height, hdr_resolve_views)?;
me.wire_sets(
device,
hdr_resolve_views,
std::slice::from_ref(&gbuffer_view),
);
Ok(me)
}
fn build_targets(
&mut self,
alloc: &DeviceAllocator,
device: &VkDevice,
width: u32,
height: u32,
hdr_resolve_views: &[vk::ImageView],
) -> Result<(), String> {
let w = width.max(1);
let h = height.max(1);
let (gw, gh) = self.settings.gi_dimensions(w, h);
self.gi_extent = vk::Extent2D {
width: gw,
height: gh,
};
self.gi = create_gi_target(alloc, device, gw, gh)?;
self.gather_framebuffer = device
.create_framebuffer(
&vk::FramebufferCreateInfo::default()
.render_pass(self.gather_render_pass.handle())
.attachments(std::slice::from_ref(&self.gi.view))
.width(gw)
.height(gh)
.layers(1),
)
.map_err(|e| format!("ssgi gather framebuffer: {e}"))?;
let mut fbs = Vec::with_capacity(hdr_resolve_views.len());
for &view in hdr_resolve_views {
let fb = device
.create_framebuffer(
&vk::FramebufferCreateInfo::default()
.render_pass(self.composite_render_pass.handle())
.attachments(std::slice::from_ref(&view))
.width(w)
.height(h)
.layers(1),
)
.map_err(|e| format!("ssgi composite framebuffer: {e}"))?;
fbs.push(fb);
}
self.composite_framebuffers = fbs;
Ok(())
}
pub(in crate::vulkan) fn wire_sets(
&self,
device: &VkDevice,
hdr_resolve_views: &[vk::ImageView],
gbuffer_views: &[vk::ImageView],
) {
let gb_view = |i: usize| gbuffer_views[i % gbuffer_views.len().max(1)];
for (i, &set) in self.gather_sets.iter().enumerate() {
let gb_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(gb_view(i))
.sampler(self.sampler.handle());
let scene_view = hdr_resolve_views[i % hdr_resolve_views.len().max(1)];
let scene_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(scene_view)
.sampler(self.sampler.handle());
let writes = [
vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&scene_info)),
vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&gb_info)),
];
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
let gi_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(self.gi.view)
.sampler(self.sampler.handle());
for (i, &set) in self.composite_sets.iter().enumerate() {
let gb_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(gb_view(i))
.sampler(self.sampler.handle());
let writes = [
vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&gi_info)),
vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&gb_info)),
];
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
}
pub(in crate::vulkan) fn wire_sets_gbuffer(
&self,
device: &VkDevice,
hdr_resolve_views: &[vk::ImageView],
gbuffer_views: &[vk::ImageView],
) {
self.wire_sets(device, hdr_resolve_views, gbuffer_views);
}
fn destroy_targets(&mut self, _device: &VkDevice) {
self.gather_framebuffer = OwnedFramebuffer::null();
self.composite_framebuffers.clear();
if self.gi.image != vk::Image::null() {
self.gi = GpuImage::null();
}
}
pub(in crate::vulkan) fn rebuild(
&mut self,
dev: SsgiDevice<'_>,
width: u32,
height: u32,
hdr_resolve_views: &[vk::ImageView],
gbuffer_views: &[vk::ImageView],
) -> Result<(), String> {
let SsgiDevice { alloc, device } = dev;
self.destroy_targets(device);
self.build_targets(alloc, device, width, height, hdr_resolve_views)?;
self.wire_sets(device, hdr_resolve_views, gbuffer_views);
Ok(())
}
pub(in crate::vulkan) fn swap_pipelines(&mut self, rebuilt: RebuiltSsgiPipelines) {
self.gather_pso = rebuilt.gather;
self.composite_pso = rebuilt.composite;
}
pub(in crate::vulkan) fn destroy(&mut self, device: &VkDevice) {
self.destroy_targets(device);
}
}
impl VkContext {
pub(in crate::vulkan) fn encode_ssgi(
&self,
cmd: vk::CommandBuffer,
frame_idx: usize,
fov_y_radians: f32,
aspect: f32,
) {
let Some(ssgi) = &self.ssgi else { return };
if self.ssr.is_none() {
return;
}
let params = ssgi.settings.params(fov_y_radians, aspect);
encode_fullscreen(
&SsgiFullscreenPass {
ctx: self,
ssgi,
render_pass: ssgi.gather_render_pass.handle(),
framebuffer: ssgi.gather_framebuffer.handle(),
extent: ssgi.gi_extent,
pso: &ssgi.gather_pso.handle(),
set: ssgi.gather_sets[frame_idx],
params: ¶ms,
},
&cmd,
);
encode_fullscreen(
&SsgiFullscreenPass {
ctx: self,
ssgi,
render_pass: ssgi.composite_render_pass.handle(),
framebuffer: ssgi.composite_framebuffers[frame_idx].handle(),
extent: self.render_extent,
pso: &ssgi.composite_pso.handle(),
set: ssgi.composite_sets[frame_idx],
params: ¶ms,
},
&cmd,
);
}
}
struct SsgiFullscreenPass<'a> {
ctx: &'a VkContext,
ssgi: &'a SsgiResources,
render_pass: vk::RenderPass,
framebuffer: vk::Framebuffer,
extent: vk::Extent2D,
pso: &'a vk::Pipeline,
set: vk::DescriptorSet,
params: &'a SsgiParams,
}
impl FullscreenPass for SsgiFullscreenPass<'_> {
type Rec = vk::CommandBuffer;
fn begin(&self, cmd: &Self::Rec) {
self.ctx
.begin_fullscreen_pass_sized(*cmd, self.render_pass, self.framebuffer, self.extent);
}
fn draw(&self, cmd: &Self::Rec) {
let cmd = *cmd;
let device = &self.ctx.device;
let push = unsafe {
std::slice::from_raw_parts(
self.params as *const SsgiParams as *const u8,
std::mem::size_of::<SsgiParams>(),
)
};
unsafe {
device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, *self.pso);
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
self.ssgi.pipeline_layout.handle(),
0,
std::slice::from_ref(&self.set),
&[],
);
device.cmd_push_constants(
cmd,
self.ssgi.pipeline_layout.handle(),
vk::ShaderStageFlags::FRAGMENT,
0,
push,
);
device.cmd_draw(cmd, 3, 1, 0, 0);
}
}
fn end(&self, cmd: &Self::Rec) {
self.ctx.end_fullscreen_pass(*cmd);
}
}
#[cfg(test)]
mod tests {
#[test]
fn ssgi_shaders_compile() {
if !crate::slangc_gate::slangc_available() {
return;
}
super::compile_ssgi_shaders(false).expect("ssgi shaders compile");
}
}