use ash::vk;
use concinnity_core::render::error::RenderResult;
use super::allocator::{DeviceAllocator, PooledBuffer, PooledImage};
use super::descriptor_layout::{Binding, PoolSizes};
use super::pipeline_desc::compute_pipeline;
use super::resources::{alloc_descriptor_sets, create_descriptor_set_layout};
use super::set_writes::SetWrites;
use super::texture::{
LayoutTransition, SubresourceRange, one_shot_submit, transition_image_layout_range,
};
use crate::vulkan::owned::{
OwnedDescriptorPool, OwnedPipeline, OwnedPipelineLayout, OwnedSetLayout, VkDevice,
};
use crate::vulkan::record::Recorder;
const MAX_HIZ_MIPS: usize = 16;
use crate::vulkan::builtin_shaders::CompileProgram;
use concinnity_core::render::hiz_spd::{self, Plan};
use concinnity_core::render::uniforms::HizSpdParams;
use concinnity_core::render::uniforms::vulkan::CullHizParams;
pub(super) fn hiz_mip_count(width: u32, height: u32) -> u32 {
let m = width.max(height).max(1);
32 - m.leading_zeros()
}
pub(super) struct HiZResources {
spd_pipeline: OwnedPipeline,
spd_tail_pipeline: OwnedPipeline,
spd_pipeline_layout: OwnedPipelineLayout,
spd_tail_pipeline_layout: OwnedPipelineLayout,
spd_set_layout: OwnedSetLayout,
spd_tail_set_layout: OwnedSetLayout,
pub(super) read_set_layout: OwnedSetLayout,
descriptor_pool: OwnedDescriptorPool,
pub(super) pyramid: PooledImage,
sampled_view: vk::ImageView,
mip_views: Vec<vk::ImageView>,
spd_sets: Vec<vk::DescriptorSet>,
spd_tail_sets: Vec<vk::DescriptorSet>,
pub(super) read_sets: Vec<vk::DescriptorSet>,
pub(super) cull_ubos: Vec<PooledBuffer>,
pub(super) read_sets2: Vec<vk::DescriptorSet>,
pub(super) cull_ubos2: Vec<PooledBuffer>,
pub(super) width: u32,
pub(super) height: u32,
pub(super) mip_count: u32,
sample_count: u32,
}
fn create_hiz_image(
alloc: &DeviceAllocator,
width: u32,
height: u32,
mip_count: u32,
) -> RenderResult<PooledImage> {
let img_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: width.max(1),
height: height.max(1),
depth: 1,
})
.mip_levels(mip_count.max(1))
.array_layers(1)
.format(vk::Format::R32_SFLOAT)
.tiling(vk::ImageTiling::OPTIMAL)
.initial_layout(vk::ImageLayout::UNDEFINED)
.usage(vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.samples(vk::SampleCountFlags::TYPE_1);
alloc
.create_image(&img_info, vk::MemoryPropertyFlags::DEVICE_LOCAL)
.map_err(|e| e.context("hiz image"))
}
fn create_hiz_view(
device: &VkDevice,
image: vk::Image,
base_mip: u32,
level_count: u32,
) -> RenderResult<vk::ImageView> {
let info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R32_SFLOAT)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: base_mip,
level_count,
base_array_layer: 0,
layer_count: 1,
});
unsafe { device.create_image_view(&info, None) }
.map_err(|e| super::error::map_vk_result(e, "hiz view"))
}
fn build_hiz_pipelines(
device: &VkDevice,
spd_layout: vk::PipelineLayout,
spd_tail_layout: vk::PipelineLayout,
sample_count: u32,
hot_reload: bool,
) -> RenderResult<(OwnedPipeline, OwnedPipeline)> {
let phase1_spv = if sample_count > 1 {
super::builtin_shaders::HIZ_SPD_MSAA.compile(hot_reload)?
} else {
super::builtin_shaders::HIZ_SPD_SINGLE.compile(hot_reload)?
};
let tail_spv = super::builtin_shaders::HIZ_SPD_TAIL.compile(hot_reload)?;
let phase1 = compute_pipeline(device, spd_layout, &phase1_spv, "hiz")?;
let tail = compute_pipeline(device, spd_tail_layout, &tail_spv, "hiz")?;
Ok((phase1, tail))
}
#[derive(Clone, Copy)]
pub(super) struct HiZDeviceCtx<'a> {
pub(super) alloc: &'a DeviceAllocator,
pub(super) device: &'a VkDevice,
pub(super) command_pool: vk::CommandPool,
pub(super) queue: vk::Queue,
}
#[derive(Clone, Copy)]
pub(super) struct HiZTarget<'a> {
pub(super) width: u32,
pub(super) height: u32,
pub(super) depth_views: &'a [vk::ImageView],
}
impl HiZResources {
pub(super) fn read_set_view(&self) -> vk::ImageView {
self.sampled_view
}
pub(super) fn new(
ctx: HiZDeviceCtx,
target: HiZTarget,
sample_count: u32,
frames: usize,
two_pass: bool,
hot_reload: bool,
) -> RenderResult<Self> {
let HiZDeviceCtx { alloc, device, .. } = ctx;
let HiZTarget { width, height, .. } = target;
let spd_set_layout = create_set_layout_counted(
device,
&[
(0, vk::DescriptorType::SAMPLED_IMAGE, 1),
(1, vk::DescriptorType::STORAGE_IMAGE, hiz_spd::LEVELS),
],
)?;
let spd_tail_set_layout = create_set_layout_counted(
device,
&[(0, vk::DescriptorType::STORAGE_IMAGE, hiz_spd::LEVELS)],
)?;
let read_set_layout = create_descriptor_set_layout(device, &read_set_bindings())?;
let push_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::COMPUTE)
.offset(0)
.size(std::mem::size_of::<HizSpdParams>() as u32);
let spd_pipeline_layout =
create_pipeline_layout(device, spd_set_layout.handle(), push_range)?;
let spd_tail_pipeline_layout =
create_pipeline_layout(device, spd_tail_set_layout.handle(), push_range)?;
let (spd_pipeline, spd_tail_pipeline) = build_hiz_pipelines(
device,
spd_pipeline_layout.handle(),
spd_tail_pipeline_layout.handle(),
sample_count,
hot_reload,
)?;
let descriptor_pool = create_pool(device, frames, two_pass)?;
let ubo_size = std::mem::size_of::<CullHizParams>() as u64;
let alloc_ubo_ring = |count: usize| -> RenderResult<Vec<PooledBuffer>> {
(0..count)
.map(|_| {
alloc.create_buffer(
ubo_size,
vk::BufferUsageFlags::UNIFORM_BUFFER,
vk::MemoryPropertyFlags::HOST_VISIBLE
| vk::MemoryPropertyFlags::HOST_COHERENT,
)
})
.collect()
};
let cull_ubos = alloc_ubo_ring(frames)?;
let cull_ubos2 = alloc_ubo_ring(if two_pass { frames } else { 0 })?;
let mut res = Self {
spd_pipeline,
spd_tail_pipeline,
spd_pipeline_layout,
spd_tail_pipeline_layout,
spd_set_layout,
spd_tail_set_layout,
read_set_layout,
descriptor_pool,
pyramid: PooledImage::null(),
sampled_view: vk::ImageView::null(),
mip_views: Vec::new(),
spd_sets: Vec::new(),
spd_tail_sets: Vec::new(),
read_sets: Vec::new(),
cull_ubos,
read_sets2: Vec::new(),
cull_ubos2,
width,
height,
mip_count: 0,
sample_count,
};
res.create_image_and_sets(ctx, target)?;
Ok(res)
}
fn create_image_and_sets(&mut self, ctx: HiZDeviceCtx, target: HiZTarget) -> RenderResult<()> {
let HiZDeviceCtx {
alloc,
device,
command_pool,
queue,
} = ctx;
let HiZTarget {
width,
height,
depth_views,
} = target;
let requested = hiz_mip_count(width, height).min(MAX_HIZ_MIPS as u32).max(1);
let mip_count = Plan::new(width, height, requested, 1).mip_count();
let pyramid = create_hiz_image(alloc, width, height, mip_count)?;
one_shot_submit(device, command_pool, queue, |cmd| {
transition_image_layout_range(
device,
cmd,
pyramid.image(),
LayoutTransition {
old_layout: vk::ImageLayout::UNDEFINED,
new_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
aspect: vk::ImageAspectFlags::COLOR,
},
SubresourceRange {
base_layer: 0,
layer_count: 1,
base_mip: 0,
mip_count,
},
);
})?;
let sampled_view = create_hiz_view(device, pyramid.image(), 0, mip_count)?;
pyramid.attach_view(sampled_view);
let mut mip_views = Vec::with_capacity(mip_count as usize);
for mip in 0..mip_count {
let view = create_hiz_view(device, pyramid.image(), mip, 1)?;
pyramid.attach_view(view);
mip_views.push(view);
}
unsafe {
device
.reset_descriptor_pool(
self.descriptor_pool.handle(),
vk::DescriptorPoolResetFlags::empty(),
)
.map_err(|e| super::error::map_vk_result(e, "reset hiz pool"))?;
}
let frames = self.cull_ubos.len();
let spd_layouts: Vec<_> = (0..frames).map(|_| self.spd_set_layout.handle()).collect();
let spd_sets = alloc_descriptor_sets(device, self.descriptor_pool.handle(), &spd_layouts)?;
let spd_tail_layouts = [self.spd_tail_set_layout.handle()];
let spd_tail_sets =
alloc_descriptor_sets(device, self.descriptor_pool.handle(), &spd_tail_layouts)?;
let read_layouts: Vec<_> = (0..frames).map(|_| self.read_set_layout.handle()).collect();
let read_sets =
alloc_descriptor_sets(device, self.descriptor_pool.handle(), &read_layouts)?;
let two_pass = !self.cull_ubos2.is_empty();
let read_layouts2: Vec<_> = (0..if two_pass { frames } else { 0 })
.map(|_| self.read_set_layout.handle())
.collect();
let read_sets2 =
alloc_descriptor_sets(device, self.descriptor_pool.handle(), &read_layouts2)?;
let bound = |base_mip: u32| -> Vec<vk::DescriptorImageInfo> {
Plan::bound_mips(base_mip, mip_count)
.map(|m| {
vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::GENERAL)
.image_view(mip_views[m as usize])
})
.collect()
};
let phase1_mips = bound(0);
for (i, &set) in spd_sets.iter().enumerate() {
let depth = depth_views[i.min(depth_views.len().saturating_sub(1))];
SetWrites::new(set)
.sampled_image(0, depth)
.images(1, vk::DescriptorType::STORAGE_IMAGE, &phase1_mips)
.apply(device);
}
let tail_mips = bound(hiz_spd::LEVELS - 1);
for &set in &spd_tail_sets {
SetWrites::new(set)
.images(0, vk::DescriptorType::STORAGE_IMAGE, &tail_mips)
.apply(device);
}
let ubo_size = std::mem::size_of::<CullHizParams>() as u64;
for (&set, ubo) in read_sets.iter().zip(&self.cull_ubos) {
write_read_set(device, set, sampled_view, ubo.buffer(), ubo_size);
}
for (&set, ubo) in read_sets2.iter().zip(&self.cull_ubos2) {
write_read_set(device, set, sampled_view, ubo.buffer(), ubo_size);
}
self.pyramid = pyramid;
self.sampled_view = sampled_view;
self.mip_views = mip_views;
self.spd_sets = spd_sets;
self.spd_tail_sets = spd_tail_sets;
self.read_sets = read_sets;
self.read_sets2 = read_sets2;
self.width = width;
self.height = height;
self.mip_count = mip_count;
Ok(())
}
pub(super) fn resize_to(&mut self, ctx: HiZDeviceCtx, target: HiZTarget) -> RenderResult<()> {
self.create_image_and_sets(ctx, target)
}
pub(super) fn swap_pipelines(&mut self, spd: OwnedPipeline, tail: OwnedPipeline) {
self.spd_pipeline = spd;
self.spd_tail_pipeline = tail;
}
pub(super) fn recompile_pipelines(
&self,
device: &VkDevice,
hot_reload: bool,
) -> RenderResult<(OwnedPipeline, OwnedPipeline)> {
build_hiz_pipelines(
device,
self.spd_pipeline_layout.handle(),
self.spd_tail_pipeline_layout.handle(),
self.sample_count,
hot_reload,
)
}
pub(super) fn destroy(&mut self, _device: &VkDevice) {
self.pyramid = PooledImage::null();
self.sampled_view = vk::ImageView::null();
self.mip_views.clear();
self.cull_ubos.clear();
self.cull_ubos2.clear();
}
}
impl crate::vulkan::context::VkContext {
pub(in crate::vulkan) fn encode_hiz_build(&self, rec: &Recorder<'_>, frame_idx: usize) {
let Some(hiz) = self.cull.hiz.as_ref() else {
return;
};
if hiz.mip_count == 0 || hiz.mip_views.is_empty() {
return;
}
let plan = Plan::new(
hiz.width,
hiz.height,
hiz.mip_count,
hiz.sample_count.max(1),
);
let spd_set = hiz.spd_sets[frame_idx.min(hiz.spd_sets.len().saturating_sub(1))];
rec.bind_pipeline(vk::PipelineBindPoint::COMPUTE, &hiz.spd_pipeline);
rec.bind_descriptor_sets(
vk::PipelineBindPoint::COMPUTE,
&hiz.spd_pipeline_layout,
0,
std::slice::from_ref(&spd_set),
&[],
);
rec.push_constants(
&hiz.spd_pipeline_layout,
vk::ShaderStageFlags::COMPUTE,
0,
&plan.phase1.params,
);
rec.dispatch(plan.phase1.groups.0, plan.phase1.groups.1, 1);
let (Some(tail), Some(&tail_set)) = (plan.tail, hiz.spd_tail_sets.first()) else {
return;
};
rec.pipeline_barrier(
vk::PipelineStageFlags::COMPUTE_SHADER,
vk::PipelineStageFlags::COMPUTE_SHADER,
&[vk::MemoryBarrier::default()
.src_access_mask(vk::AccessFlags::SHADER_WRITE)
.dst_access_mask(vk::AccessFlags::SHADER_READ)],
&[],
&[],
);
rec.bind_pipeline(vk::PipelineBindPoint::COMPUTE, &hiz.spd_tail_pipeline);
rec.bind_descriptor_sets(
vk::PipelineBindPoint::COMPUTE,
&hiz.spd_tail_pipeline_layout,
0,
std::slice::from_ref(&tail_set),
&[],
);
rec.push_constants(
&hiz.spd_tail_pipeline_layout,
vk::ShaderStageFlags::COMPUTE,
0,
&tail.params,
);
rec.dispatch(tail.groups.0, tail.groups.1, 1);
}
}
fn read_set_bindings() -> [Binding; 2] {
let compute = vk::ShaderStageFlags::COMPUTE;
[
(0, vk::DescriptorType::SAMPLED_IMAGE, compute),
(1, vk::DescriptorType::UNIFORM_BUFFER, compute),
]
}
fn create_set_layout_counted(
device: &VkDevice,
bindings: &[(u32, vk::DescriptorType, u32)],
) -> RenderResult<OwnedSetLayout> {
let binds: Vec<_> = bindings
.iter()
.map(|&(b, ty, count)| {
vk::DescriptorSetLayoutBinding::default()
.binding(b)
.descriptor_type(ty)
.descriptor_count(count)
.stage_flags(vk::ShaderStageFlags::COMPUTE)
})
.collect();
device
.create_descriptor_set_layout(
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&binds),
)
.map_err(|e| super::error::map_vk_result(e, "hiz set layout"))
}
fn create_pipeline_layout(
device: &VkDevice,
set_layout: vk::DescriptorSetLayout,
push_range: vk::PushConstantRange,
) -> RenderResult<OwnedPipelineLayout> {
let layouts = [set_layout];
device
.create_pipeline_layout(
&vk::PipelineLayoutCreateInfo::default()
.set_layouts(&layouts)
.push_constant_ranges(std::slice::from_ref(&push_range)),
)
.map_err(|e| super::error::map_vk_result(e, "hiz pipeline layout"))
}
fn create_pool(
device: &VkDevice,
frames: usize,
two_pass: bool,
) -> RenderResult<OwnedDescriptorPool> {
let f = frames as u32;
let read_rings = if two_pass { 2 } else { 1 };
let sizes = PoolSizes::default()
.sets(&read_set_bindings(), read_rings * f)
.add(vk::DescriptorType::SAMPLED_IMAGE, f)
.add(vk::DescriptorType::STORAGE_IMAGE, (f + 1) * hiz_spd::LEVELS)
.build();
let max_sets = (1 + read_rings) * f + 1;
device
.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&sizes)
.max_sets(max_sets),
)
.map_err(|e| super::error::map_vk_result(e, "hiz descriptor pool"))
}
pub(super) fn off_camera_read_set(
alloc: &DeviceAllocator,
device: &VkDevice,
pool: vk::DescriptorPool,
layout: vk::DescriptorSetLayout,
view: vk::ImageView,
) -> RenderResult<(vk::DescriptorSet, PooledBuffer)> {
let params = CullHizParams {
prev_view_proj: [[0.0; 4]; 4],
hiz_size: [1.0, 1.0],
hiz_mip_count: 1,
hiz_enabled: 0,
};
let size = std::mem::size_of::<CullHizParams>() as u64;
let ubo = alloc.create_buffer(
size,
vk::BufferUsageFlags::UNIFORM_BUFFER,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)?;
ubo.write_val(0, ¶ms);
let set = alloc_descriptor_sets(device, pool, std::slice::from_ref(&layout))?[0];
write_read_set(device, set, view, ubo.buffer(), size);
Ok((set, ubo))
}
pub(super) fn rewrite_read_set_view(
device: &VkDevice,
set: vk::DescriptorSet,
view: vk::ImageView,
) {
SetWrites::new(set).sampled_image(0, view).apply(device);
}
fn write_read_set(
device: &VkDevice,
set: vk::DescriptorSet,
view: vk::ImageView,
ubo: vk::Buffer,
ubo_size: u64,
) {
SetWrites::new(set)
.sampled_image(0, view)
.uniform_buffer(1, ubo, ubo_size)
.apply(device);
}
#[cfg(test)]
mod tests {
use super::hiz_mip_count;
#[test]
fn mip_count_power_of_two() {
assert_eq!(hiz_mip_count(1, 1), 1);
assert_eq!(hiz_mip_count(2, 2), 2);
assert_eq!(hiz_mip_count(256, 256), 9);
assert_eq!(hiz_mip_count(1024, 1024), 11);
}
#[test]
fn mip_count_uses_larger_dimension() {
assert_eq!(hiz_mip_count(1920, 1080), hiz_mip_count(1920, 1920));
assert_eq!(hiz_mip_count(1920, 1080), 11);
assert_eq!(hiz_mip_count(1280, 720), 11);
}
#[test]
fn mip_count_clamps_zero() {
assert_eq!(hiz_mip_count(0, 0), 1);
assert_eq!(hiz_mip_count(0, 8), 4);
}
}