use ash::vk;
use concinnity_core::render::error::RenderResult;
use super::super::context::{HDR_FORMAT, VkContext};
use super::super::descriptor_layout::PoolSizes;
use super::super::pipeline::*;
use super::super::resources::{
alloc_descriptor_sets, create_descriptor_set_layout, source_set_bindings, write_source_set,
};
use super::super::texture::*;
use super::gbuffer::GbufferResources;
use crate::vulkan::builtin_shaders::CompileProgram;
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedFramebuffer, OwnedPipeline, OwnedPipelineLayout, OwnedRenderPass,
OwnedSampler, OwnedSetLayout, VkDevice,
};
use crate::vulkan::wire_cache::WireCache;
pub(in crate::vulkan) struct ReflectionCompositeResources {
pub(in crate::vulkan) output: GpuImage,
output_framebuffer: OwnedFramebuffer,
blur: GpuImage,
blur_framebuffer: OwnedFramebuffer,
blur_extent: vk::Extent2D,
render_pass: OwnedRenderPass,
_blur_set_layout: OwnedSetLayout,
_composite_set_layout: OwnedSetLayout,
blur_pipeline_layout: OwnedPipelineLayout,
composite_pipeline_layout: OwnedPipelineLayout,
blur_pso: OwnedPipeline,
composite_pso: OwnedPipeline,
_descriptor_pool: OwnedDescriptorPool,
blur_sets: Vec<vk::DescriptorSet>,
composite_sets: Vec<vk::DescriptorSet>,
wired_reflection: WireCache<vk::ImageView>,
sampler: OwnedSampler,
blur_scale: u32,
}
unsafe impl Send for ReflectionCompositeResources {}
pub(in crate::vulkan) struct ReflectionCompositeShaders {
pub vs: Vec<u8>,
pub blur_fs: Vec<u8>,
pub composite_fs: Vec<u8>,
}
pub(in crate::vulkan) fn compile_reflection_composite_shaders(
hot_reload: bool,
) -> RenderResult<ReflectionCompositeShaders> {
use super::super::builtin_shaders;
Ok(ReflectionCompositeShaders {
vs: builtin_shaders::FULLSCREEN_VERT.compile(hot_reload)?,
blur_fs: builtin_shaders::REFLECTION_BLUR.compile(hot_reload)?,
composite_fs: builtin_shaders::REFLECTION_COMPOSITE.compile(hot_reload)?,
})
}
pub(in crate::vulkan) struct RebuiltReflectionComposite {
pub blur: OwnedPipeline,
pub composite: OwnedPipeline,
}
pub(in crate::vulkan) fn rebuild_reflection_composite_pipelines(
device: &VkDevice,
rc: &ReflectionCompositeResources,
hot_reload: bool,
) -> RenderResult<RebuiltReflectionComposite> {
let shaders = compile_reflection_composite_shaders(hot_reload)?;
let blur = create_composite_pipeline(
device,
rc.render_pass.handle(),
rc.blur_pipeline_layout.handle(),
&shaders.vs,
&shaders.blur_fs,
)?;
let composite = create_composite_pipeline(
device,
rc.render_pass.handle(),
rc.composite_pipeline_layout.handle(),
&shaders.vs,
&shaders.composite_fs,
)?;
Ok(RebuiltReflectionComposite { blur, composite })
}
const BLUR_SOURCES: u32 = 2;
const COMPOSITE_SOURCES: u32 = 5;
fn create_composite_render_pass(device: &VkDevice) -> RenderResult<OwnedRenderPass> {
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 dep = 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::SHADER_READ);
let info = vk::RenderPassCreateInfo::default()
.attachments(std::slice::from_ref(&attachment))
.subpasses(std::slice::from_ref(&subpass))
.dependencies(std::slice::from_ref(&dep));
device
.create_render_pass(&info)
.map_err(|e| crate::vulkan::error::map_vk_result(e, "reflection composite render pass"))
}
pub(in crate::vulkan) struct CompositeInputs {
hdr_resolve_views: Vec<vk::ImageView>,
normal_depth_views: Vec<vk::ImageView>,
roughness_views: Vec<vk::ImageView>,
}
impl CompositeInputs {
pub(in crate::vulkan) fn new(
hdr_resolve_images: &[GpuImage],
gbuffer: &GbufferResources,
) -> Self {
Self {
hdr_resolve_views: hdr_resolve_images.iter().map(|img| img.view).collect(),
normal_depth_views: gbuffer.normal_depth_views(),
roughness_views: gbuffer.roughness_views(),
}
}
}
fn create_target(ctx: &GpuUploadContext, width: u32, height: u32) -> RenderResult<GpuImage> {
let &GpuUploadContext {
alloc,
device,
command_pool,
queue,
} = ctx;
let pooled = create_image(
alloc,
&super::super::texture::ImageSpec {
width,
height,
format: HDR_FORMAT,
tiling: vk::ImageTiling::OPTIMAL,
usage: vk::ImageUsageFlags::COLOR_ATTACHMENT
| vk::ImageUsageFlags::SAMPLED
| vk::ImageUsageFlags::TRANSFER_SRC,
mem_props: vk::MemoryPropertyFlags::DEVICE_LOCAL,
samples: vk::SampleCountFlags::TYPE_1,
},
)?;
let image = pooled.image();
one_shot_submit(device, command_pool, queue, |cmd| {
transition_image_layout(
device,
cmd,
image,
vk::ImageLayout::UNDEFINED,
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
vk::ImageAspectFlags::COLOR,
);
})?;
let view = create_image_view(device, image, HDR_FORMAT, vk::ImageAspectFlags::COLOR)?;
Ok(GpuImage::from_pooled(pooled, view))
}
fn create_composite_pipeline(
device: &VkDevice,
render_pass: vk::RenderPass,
layout: vk::PipelineLayout,
vert_spv: &[u8],
frag_spv: &[u8],
) -> RenderResult<OwnedPipeline> {
let modules = GraphicsStages::new(device, vert_spv, frag_spv)?;
let stages = modules.infos();
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 = 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| crate::vulkan::error::map_vk_result(e, "create reflection composite pso"))?;
Ok(pipeline)
}
impl ReflectionCompositeResources {
pub(in crate::vulkan) fn new(
ctx: &GpuUploadContext,
width: u32,
height: u32,
frames: usize,
blur_scale: u32,
inputs: &CompositeInputs,
hot_reload: bool,
) -> RenderResult<Self> {
let device = ctx.device;
let blur_scale = blur_scale.max(1);
let render_pass = create_composite_render_pass(device)?;
let blur_bindings = source_set_bindings(BLUR_SOURCES);
let composite_bindings = source_set_bindings(COMPOSITE_SOURCES);
let blur_set_layout = create_descriptor_set_layout(device, &blur_bindings)?;
let composite_set_layout = create_descriptor_set_layout(device, &composite_bindings)?;
let make_layout = |set_layout: vk::DescriptorSetLayout, name: &str| -> RenderResult<_> {
let layouts = [set_layout];
device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default().set_layouts(&layouts),
)
.map_err(|e| crate::vulkan::error::map_vk_result(e, name))
};
let blur_pipeline_layout = make_layout(blur_set_layout.handle(), "reflection blur layout")?;
let composite_pipeline_layout =
make_layout(composite_set_layout.handle(), "reflection composite layout")?;
let shaders = compile_reflection_composite_shaders(hot_reload)?;
let blur_pso = create_composite_pipeline(
device,
render_pass.handle(),
blur_pipeline_layout.handle(),
&shaders.vs,
&shaders.blur_fs,
)?;
let composite_pso = create_composite_pipeline(
device,
render_pass.handle(),
composite_pipeline_layout.handle(),
&shaders.vs,
&shaders.composite_fs,
)?;
let f = frames as u32;
let pool_sizes = PoolSizes::default()
.sets(&blur_bindings, f)
.sets(&composite_bindings, f)
.build();
let descriptor_pool = device
.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&pool_sizes)
.max_sets(f * 2),
)
.map_err(|e| {
crate::vulkan::error::map_vk_result(e, "reflection composite descriptor pool")
})?;
let blur_layouts: Vec<_> = (0..frames).map(|_| blur_set_layout.handle()).collect();
let blur_sets = alloc_descriptor_sets(device, descriptor_pool.handle(), &blur_layouts)?;
let composite_layouts: Vec<_> =
(0..frames).map(|_| composite_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 {
output: GpuImage::null(),
output_framebuffer: OwnedFramebuffer::null(),
blur: GpuImage::null(),
blur_framebuffer: OwnedFramebuffer::null(),
blur_extent: vk::Extent2D::default(),
render_pass,
_blur_set_layout: blur_set_layout,
_composite_set_layout: composite_set_layout,
blur_pipeline_layout,
composite_pipeline_layout,
blur_pso,
composite_pso,
_descriptor_pool: descriptor_pool,
blur_sets,
composite_sets,
sampler,
blur_scale,
wired_reflection: WireCache::new(frames),
};
me.build_targets(ctx, width, height)?;
me.wire_sets(device, inputs);
Ok(me)
}
fn build_targets(
&mut self,
ctx: &GpuUploadContext,
width: u32,
height: u32,
) -> RenderResult<()> {
let device = ctx.device;
let w = width.max(1);
let h = height.max(1);
let bw = (w / self.blur_scale).max(1);
let bh = (h / self.blur_scale).max(1);
self.output = create_target(ctx, w, h)?;
self.blur = create_target(ctx, bw, bh)?;
self.blur_extent = vk::Extent2D {
width: bw,
height: bh,
};
let make_fb = |view: vk::ImageView, fw: u32, fh: u32| -> RenderResult<OwnedFramebuffer> {
device
.create_framebuffer(
&vk::FramebufferCreateInfo::default()
.render_pass(self.render_pass.handle())
.attachments(std::slice::from_ref(&view))
.width(fw)
.height(fh)
.layers(1),
)
.map_err(|e| {
crate::vulkan::error::map_vk_result(e, "reflection composite framebuffer")
})
};
self.output_framebuffer = make_fb(self.output.view, w, h)?;
self.blur_framebuffer = make_fb(self.blur.view, bw, bh)?;
Ok(())
}
fn wire_sets(&self, device: &VkDevice, inputs: &CompositeInputs) {
let hdr_resolve_views = inputs.hdr_resolve_views.as_slice();
let normal_depth_views = inputs.normal_depth_views.as_slice();
let roughness_views = inputs.roughness_views.as_slice();
let frames = self.blur_sets.len();
debug_assert_eq!(hdr_resolve_views.len(), frames);
debug_assert_eq!(normal_depth_views.len(), frames);
debug_assert_eq!(roughness_views.len(), frames);
let sampler = self.sampler.handle();
for i in 0..frames {
let placeholder = hdr_resolve_views[i];
let rough = roughness_views[i];
write_source_set(
device,
self.blur_sets[i],
&[(placeholder, sampler), (rough, sampler)],
);
write_source_set(
device,
self.composite_sets[i],
&[
(placeholder, sampler),
(hdr_resolve_views[i], sampler),
(normal_depth_views[i], sampler),
(rough, sampler),
(self.blur.view, sampler),
],
);
}
}
fn destroy_targets(&mut self, _device: &VkDevice) {
self.output_framebuffer = OwnedFramebuffer::null();
self.blur_framebuffer = OwnedFramebuffer::null();
self.output = GpuImage::null();
self.blur = GpuImage::null();
}
pub(in crate::vulkan) fn rebuild(
&mut self,
ctx: &GpuUploadContext,
width: u32,
height: u32,
inputs: &CompositeInputs,
) -> RenderResult<()> {
self.destroy_targets(ctx.device);
self.build_targets(ctx, width, height)?;
self.wire_sets(ctx.device, inputs);
self.wired_reflection.reset();
Ok(())
}
pub(in crate::vulkan) fn repoint_reflection(
&mut self,
device: &VkDevice,
frame_idx: usize,
view: vk::ImageView,
) {
if !self.wired_reflection.changed(frame_idx, view) {
return;
}
let refl = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.image_view(view);
let write = |set: vk::DescriptorSet| {
vk::WriteDescriptorSet::default()
.dst_set(set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
.image_info(std::slice::from_ref(&refl))
};
let writes = [
write(self.blur_sets[frame_idx]),
write(self.composite_sets[frame_idx]),
];
unsafe { device.update_descriptor_sets(&writes, &[]) };
}
pub(in crate::vulkan) fn swap_pipelines(&mut self, rebuilt: RebuiltReflectionComposite) {
self.blur_pso = rebuilt.blur;
self.composite_pso = rebuilt.composite;
}
pub(in crate::vulkan) fn destroy(&mut self, device: &VkDevice) {
self.destroy_targets(device);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::vulkan) struct ReflectionPath {
pub(in crate::vulkan) ssr_resolve: bool,
pub(in crate::vulkan) composite: bool,
}
impl ReflectionPath {
pub(in crate::vulkan) fn new(ssr_authored: bool, rt_active: bool) -> Self {
let ssr_resolve = ssr_authored && !rt_active;
Self {
ssr_resolve,
composite: rt_active || ssr_resolve,
}
}
}
impl VkContext {
fn reflection_path(&self) -> ReflectionPath {
ReflectionPath::new(self.ssr_authored(), self.rt_reflections_active())
}
pub(in crate::vulkan) fn ssr_resolve_active(&self) -> bool {
self.reflection_path().ssr_resolve
}
pub(in crate::vulkan) fn post_scene_image(&self, frame: usize) -> &GpuImage {
match self.reflection_composite.as_ref() {
Some(rc) => &rc.output,
None => &self.targets.hdr_resolve_images[frame % self.targets.hdr_resolve_images.len()],
}
}
pub(in crate::vulkan) fn release_unfed_reflection_composite(&mut self) -> bool {
if self.reflection_path().composite {
return false;
}
let Some(mut rc) = self.reflection_composite.take() else {
return false;
};
rc.destroy(&self.hw.device);
true
}
pub(in crate::vulkan) fn reconcile_reflection_composite(
&mut self,
blur_scale: u32,
) -> RenderResult<()> {
self.release_unfed_reflection_composite();
if !self.reflection_path().composite || self.reflection_composite.is_some() {
return Ok(());
}
let gb = self
.gbuffer
.as_ref()
.expect("a reflection path forces the unified G-buffer pre-pass");
let rc = ReflectionCompositeResources::new(
&GpuUploadContext {
alloc: &self.hw.alloc,
device: &self.hw.device,
command_pool: self.commands.command_pool,
queue: self.hw.graphics_queue,
},
self.targets.render_extent.width,
self.targets.render_extent.height,
self.frames_in_flight,
blur_scale,
&CompositeInputs::new(&self.targets.hdr_resolve_images, gb),
self.hot_reload.enabled,
)?;
self.reflection_composite = Some(rc);
Ok(())
}
pub(in crate::vulkan) fn prepare_reflection_composite(&mut self, frame_idx: usize) {
debug_assert_eq!(
self.reflection_composite.is_some(),
self.reflection_path().composite,
"the reflection composite is out of step with the resolves that feed it"
);
let view = if self.rt_reflections_active() {
self.rt_reflections.as_ref().map(|rt| rt.output.view)
} else {
self.ssr.as_ref().map(|ssr| ssr.output.view())
};
let Some(view) = view else {
return;
};
let device = self.hw.device.clone();
if let Some(rc) = self.reflection_composite.as_mut() {
rc.repoint_reflection(&device, frame_idx, view);
}
}
pub(in crate::vulkan) fn encode_reflection_composite(
&self,
cmd: vk::CommandBuffer,
reflection_view: vk::ImageView,
frame_idx: usize,
) {
let Some(rc) = &self.reflection_composite else {
return;
};
debug_assert_eq!(
rc.wired_reflection.current(frame_idx),
Some(reflection_view),
"reflection composite set wired for a different resolve target"
);
let device = &self.hw.device;
self.begin_fullscreen_pass_sized(
cmd,
rc.render_pass.handle(),
rc.blur_framebuffer.handle(),
rc.blur_extent,
);
unsafe {
device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, rc.blur_pso.handle());
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
rc.blur_pipeline_layout.handle(),
0,
std::slice::from_ref(&rc.blur_sets[frame_idx]),
&[],
);
device.cmd_draw(cmd, 3, 1, 0, 0);
}
self.end_fullscreen_pass(cmd);
self.begin_fullscreen_pass(cmd, rc.render_pass.handle(), rc.output_framebuffer.handle());
unsafe {
device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
rc.composite_pso.handle(),
);
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
rc.composite_pipeline_layout.handle(),
0,
std::slice::from_ref(&rc.composite_sets[frame_idx]),
&[],
);
device.cmd_draw(cmd, 3, 1, 0, 0);
}
self.end_fullscreen_pass(cmd);
}
}
#[cfg(test)]
mod tests {
use super::ReflectionPath;
#[test]
fn rt_takes_the_resolve_slot_from_authored_ssr() {
let path = ReflectionPath::new(true, true);
assert!(!path.ssr_resolve);
assert!(path.composite);
}
#[test]
fn authored_ssr_resolves_without_rt() {
let path = ReflectionPath::new(true, false);
assert!(path.ssr_resolve);
assert!(path.composite);
}
#[test]
fn rt_alone_keeps_the_composite() {
let path = ReflectionPath::new(false, true);
assert!(!path.ssr_resolve);
assert!(path.composite);
}
#[test]
fn no_resolve_leaves_no_composite() {
let path = ReflectionPath::new(false, false);
assert!(!path.ssr_resolve);
assert!(!path.composite);
}
#[test]
fn reflection_composite_shaders_compile() {
if !concinnity_shader::dxc_available() {
return;
}
let shaders = super::compile_reflection_composite_shaders(false)
.expect("reflection composite shaders compile");
assert!(super::is_spirv(&shaders.vs));
assert!(super::is_spirv(&shaders.blur_fs));
assert!(super::is_spirv(&shaders.composite_fs));
}
}