use ghi::command_buffer::{
BoundComputePipelineMode as _, BoundPipelineLayoutMode as _, BoundRasterizationPipelineMode as _,
CommandBufferRecording as _, CommonCommandBufferMode as _, RasterizationRenderPassMode as _,
};
use ghi::context::{Context as _, ContextCreate as _};
use ghi::frame::Frame as _;
use ghi::implementation::Frame;
use resource_management::{resource::resource_manager::ResourceManager, types::ShaderTypes as ResourceShaderTypes};
use utils::{Box, Extent, RGBA};
use crate::rendering::pipelines::visibility::pipeline_manager::Instance;
use crate::rendering::pipelines::visibility::skinning::{SkinningDispatch, SkinningPass};
use crate::rendering::pipelines::visibility::{
INSTANCE_ID_BINDING, MATERIAL_COUNT_BINDING, MATERIAL_EVALUATION_DISPATCHES_BINDING, MATERIAL_OFFSET_BINDING,
MATERIAL_OFFSET_SCRATCH_BINDING, MATERIAL_XY_BINDING, MAX_INSTANCES, MAX_LIGHTS, MAX_MATERIALS, MAX_MESHLETS,
MAX_PIXEL_MAPPING_ENTRIES, MAX_PRIMITIVE_TRIANGLES, MAX_TRIANGLES, MAX_VERTICES, MESHLET_CULLING_TASK_GROUP_SIZE,
MESHLET_DATA_BINDING, MESH_DATA_BINDING, PRIMITIVE_INDICES_BINDING, SHADOW_CASCADE_COUNT, SHADOW_MAP_RESOLUTION,
TEXTURES_BINDING, TRIANGLE_INDEX_BINDING, VERTEX_INDICES_BINDING, VERTEX_NORMALS_BINDING, VERTEX_POSITIONS_BINDING,
VERTEX_UV_BINDING, VIEWS_DATA_BINDING,
};
use crate::rendering::render_pass::RenderPassFunction;
use crate::rendering::{render_pass::RenderPassReturn, RenderPass, Sink};
const GTAO_DEPTH_BINDING: ghi::ShaderResourceDescriptor = ghi::ShaderResourceDescriptor::single(
ghi::ResourceSlot::new(1033),
ghi::ResourceKind::CombinedImageSampler,
ghi::AccessPolicies::READ,
);
const GTAO_OUTPUT_BINDING: ghi::ShaderResourceDescriptor = ghi::ShaderResourceDescriptor::single(
ghi::ResourceSlot::new(1034),
ghi::ResourceKind::StorageImage,
ghi::AccessPolicies::WRITE,
);
const GTAO_BLUR_DEPTH_BINDING: ghi::ShaderResourceDescriptor = ghi::ShaderResourceDescriptor::single(
ghi::ResourceSlot::new(1033),
ghi::ResourceKind::CombinedImageSampler,
ghi::AccessPolicies::READ,
);
const GTAO_BLUR_SOURCE_BINDING: ghi::ShaderResourceDescriptor = ghi::ShaderResourceDescriptor::single(
ghi::ResourceSlot::new(1034),
ghi::ResourceKind::CombinedImageSampler,
ghi::AccessPolicies::READ,
);
const GTAO_BLUR_OUTPUT_BINDING: ghi::ShaderResourceDescriptor = ghi::ShaderResourceDescriptor::single(
ghi::ResourceSlot::new(1035),
ghi::ResourceKind::StorageImage,
ghi::AccessPolicies::WRITE,
);
fn load_visibility_shader(
context: &mut ghi::implementation::Context,
resources: &ResourceManager,
id: &str,
name: &str,
expected_stage: ResourceShaderTypes,
) -> ghi::ShaderHandle {
let loaded = crate::rendering::shader_store::load_shader_resource(context, resources, id, name)
.unwrap_or_else(|error| panic!("Failed to load visibility shader '{id}': {error}"));
assert_eq!(
loaded.stage, expected_stage,
"Visibility shader stage mismatch for '{id}'. The most likely cause is incorrect shader sidecar metadata."
);
loaded.handle
}
fn mesh_dispatch_count(meshlet_count: u32) -> u32 {
meshlet_count.div_ceil(MESHLET_CULLING_TASK_GROUP_SIZE)
}
#[derive(Clone)]
pub(crate) struct VisibilityPass {
descriptor_set: ghi::DescriptorSetHandle,
opaque_pipeline: ghi::PipelineHandle,
transparent_pipeline: ghi::PipelineHandle,
opaque_attachments: [ghi::AttachmentInformation; 3],
transparent_attachments: [ghi::AttachmentInformation; 3],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum VisibilityPhase {
Opaque,
Transparent,
}
impl VisibilityPhase {
fn label(self) -> &'static str {
match self {
Self::Opaque => "Opaque",
Self::Transparent => "Transparent",
}
}
fn blend_flag(self) -> u32 {
match self {
Self::Opaque => 0,
Self::Transparent => 1,
}
}
}
impl VisibilityPass {
pub(crate) fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
descriptor_set: ghi::DescriptorSetHandle,
primitive_index: ghi::BaseImageHandle,
instance_id: ghi::BaseImageHandle,
depth_target: ghi::BaseImageHandle,
) -> Self {
let visibility_pass_task_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/visibility-task.besl",
"Visibility Pass Task Shader",
ResourceShaderTypes::Task,
);
let visibility_pass_mesh_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/visibility-mesh.besl",
"Visibility Pass Mesh Shader",
ResourceShaderTypes::Mesh,
);
let visibility_pass_fragment_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/visibility-fragment.besl",
"Visibility Pass Fragment Shader",
ResourceShaderTypes::Fragment,
);
let mut visibility_pass_shaders = Vec::with_capacity(3);
visibility_pass_shaders.push(ghi::ShaderParameter::new(
&visibility_pass_task_shader,
ghi::ShaderTypes::Task,
));
visibility_pass_shaders.push(ghi::ShaderParameter::new(
&visibility_pass_mesh_shader,
ghi::ShaderTypes::Mesh,
));
visibility_pass_shaders.push(ghi::ShaderParameter::new(
&visibility_pass_fragment_shader,
ghi::ShaderTypes::Fragment,
));
let attachments = [
ghi::pipelines::raster::AttachmentDescriptor::new(ghi::Formats::U32),
ghi::pipelines::raster::AttachmentDescriptor::new(ghi::Formats::U32),
ghi::pipelines::raster::AttachmentDescriptor::new(ghi::Formats::Depth32),
];
let vertex_layout = [
ghi::pipelines::VertexElement::new("POSITION", ghi::DataTypes::Float3, 0),
ghi::pipelines::VertexElement::new("NORMAL", ghi::DataTypes::Float3, 1),
];
let opaque_pipeline = context.create_raster_pipeline(ghi::pipelines::raster::Builder::new(
&[ghi::pipelines::PushConstantRange::new(0, 4)],
&vertex_layout,
&visibility_pass_shaders,
&attachments,
));
let transparent_pipeline = context.create_raster_pipeline(
ghi::pipelines::raster::Builder::new(
&[ghi::pipelines::PushConstantRange::new(0, 4)],
&vertex_layout,
&visibility_pass_shaders,
&attachments,
)
.depth_write(false),
);
VisibilityPass {
descriptor_set,
opaque_pipeline,
transparent_pipeline,
opaque_attachments: [
ghi::AttachmentInformation::new(
primitive_index,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Integer(u32::MAX, 0, 0, 0),
false,
true,
),
ghi::AttachmentInformation::new(
instance_id,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Integer(u32::MAX, 0, 0, 0),
false,
true,
),
ghi::AttachmentInformation::new(
depth_target,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Depth(0.0),
false,
true,
),
],
transparent_attachments: [
ghi::AttachmentInformation::new(
primitive_index,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Integer(u32::MAX, 0, 0, 0),
false,
true,
),
ghi::AttachmentInformation::new(
instance_id,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Integer(u32::MAX, 0, 0, 0),
false,
true,
),
ghi::AttachmentInformation::new(
depth_target,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Depth(0.0),
true,
true,
),
],
}
}
fn record(
&self,
c: &mut ghi::implementation::CommandBufferRecording,
extent: Extent,
instances: &[Instance],
phase: VisibilityPhase,
) {
let (pipeline, attachments) = match phase {
VisibilityPhase::Opaque => (self.opaque_pipeline, self.opaque_attachments),
VisibilityPhase::Transparent => (self.transparent_pipeline, self.transparent_attachments),
};
let drawable_instances = instances.iter().filter(|instance| instance.meshlet_count > 0).count();
let meshlet_count = instances.iter().map(|instance| instance.meshlet_count).sum::<u32>();
log::debug!(
"{} visibility pass executing: extent={}x{}, active_primitives={}, drawable_primitives={}, meshlets={}",
phase.label(),
extent.width(),
extent.height(),
instances.len(),
drawable_instances,
meshlet_count,
);
c.start_region(|label| {
label.write_str(phase.label())?;
label.write_str(" Visibility Buffer")
});
let c = c.start_render_pass(extent, &attachments);
let c = c.bind_raster_pipeline(pipeline);
c.bind_descriptor_sets(&[self.descriptor_set]);
for instance in instances {
if instance.meshlet_count == 0 {
continue;
}
c.write_push_constant(0, instance.shader_mesh_index);
c.dispatch_meshes(mesh_dispatch_count(instance.meshlet_count), 1, 1);
}
c.end_render_pass();
c.end_region();
}
}
pub struct ShadowPass {
descriptor_set: ghi::DescriptorSetHandle,
shadow_pass_pipeline: ghi::PipelineHandle,
shadow_map: ghi::BaseImageHandle,
}
impl ShadowPass {
fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
descriptor_set: ghi::DescriptorSetHandle,
shadow_map: ghi::BaseImageHandle,
) -> Self {
let shadow_pass_task_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/shadow-task.besl",
"Shadow Pass Task Shader",
ResourceShaderTypes::Task,
);
let shadow_pass_mesh_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/shadow-mesh.besl",
"Shadow Pass Mesh Shader",
ResourceShaderTypes::Mesh,
);
let attachments = [ghi::pipelines::raster::AttachmentDescriptor::new(ghi::Formats::Depth32)];
let vertex_layout = [
ghi::pipelines::VertexElement::new("POSITION", ghi::DataTypes::Float3, 0),
ghi::pipelines::VertexElement::new("NORMAL", ghi::DataTypes::Float3, 1),
];
let mut shadow_pass_shaders = Vec::with_capacity(2);
shadow_pass_shaders.push(ghi::ShaderParameter::new(&shadow_pass_task_shader, ghi::ShaderTypes::Task));
shadow_pass_shaders.push(ghi::ShaderParameter::new(&shadow_pass_mesh_shader, ghi::ShaderTypes::Mesh));
let shadow_pass_pipeline = context.create_raster_pipeline(ghi::pipelines::raster::Builder::new(
&[ghi::pipelines::PushConstantRange::new(0, 8)],
&vertex_layout,
&shadow_pass_shaders,
&attachments,
));
Self {
descriptor_set,
shadow_pass_pipeline,
shadow_map,
}
}
fn prepare<'a>(
&self,
frame: &mut ghi::implementation::Frame,
instances: &'a [Instance],
shadow_enabled: bool,
) -> impl RenderPassFunction + use<'a> {
let descriptor_set = self.descriptor_set;
let pipeline = self.shadow_pass_pipeline;
let shadow_map = self.shadow_map;
let extent = Extent::square(SHADOW_MAP_RESOLUTION);
let drawable_instances = instances.iter().filter(|instance| instance.meshlet_count > 0).count();
let meshlet_count = instances.iter().map(|instance| instance.meshlet_count).sum::<u32>();
if shadow_enabled {
frame.resize_image(shadow_map, extent);
}
move |c, _| {
if !shadow_enabled {
log::debug!("Visibility shadow pass skipped: no directional shadow light");
return;
}
log::debug!(
"Visibility shadow pass executing: cascades={}, active_primitives={}, drawable_primitives={}, meshlets={}",
SHADOW_CASCADE_COUNT,
instances.len(),
drawable_instances,
meshlet_count,
);
c.start_region(|label| label.write_str("Shadow Map"));
for cascade in 0..SHADOW_CASCADE_COUNT {
c.start_region(|label| label.write_str("Cascade"));
let attachments = [ghi::AttachmentInformation::new(
shadow_map,
ghi::Layouts::RenderTarget,
ghi::ClearValue::Depth(0.0),
false,
true,
)
.layer(cascade as u32)];
let c = c.start_render_pass(extent, &attachments);
let c = c.bind_raster_pipeline(pipeline);
c.bind_descriptor_sets(&[descriptor_set]);
c.write_push_constant(4, (cascade + 1) as u32);
for instance in instances {
if instance.meshlet_count == 0 {
continue;
}
c.write_push_constant(0, instance.shader_mesh_index);
c.dispatch_meshes(mesh_dispatch_count(instance.meshlet_count), 1, 1);
}
c.end_render_pass();
c.end_region();
}
c.end_region();
}
}
}
pub struct MaterialCountPass {
descriptor_set: ghi::DescriptorSetHandle,
visibility_pass_descriptor_set: ghi::DescriptorSetHandle,
material_count_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
pipeline: ghi::PipelineHandle,
}
impl MaterialCountPass {
fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
descriptor_set: ghi::DescriptorSetHandle,
visibility_pass_descriptor_set: ghi::DescriptorSetHandle,
material_count_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
) -> Self {
let material_count_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/material-count.besl",
"Material Count Pass Compute Shader",
ResourceShaderTypes::Compute,
);
let material_count_pipeline = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(&material_count_shader, ghi::ShaderTypes::Compute),
));
MaterialCountPass {
descriptor_set,
material_count_buffer,
visibility_pass_descriptor_set,
pipeline: material_count_pipeline,
}
}
fn prepare(&self, frame: &ghi::implementation::Frame, sink: &Sink) -> impl RenderPassFunction {
let descriptor_set = self.descriptor_set;
let visibility_pass_descriptor_set = self.visibility_pass_descriptor_set;
let pipeline = self.pipeline;
let material_count_buffer = self.material_count_buffer;
let extent = sink.extent();
move |c, _| {
log::debug!(
"Visibility material count pass executing: extent={}x{}",
extent.width(),
extent.height()
);
c.start_region(|label| label.write_str("Material Count"));
c.clear_buffers(&[material_count_buffer.into()]);
let compute_pipeline_command = c.bind_compute_pipeline(pipeline);
compute_pipeline_command.bind_descriptor_sets(&[descriptor_set, visibility_pass_descriptor_set]);
compute_pipeline_command.dispatch(ghi::DispatchExtent::new(extent, Extent::square(32)));
c.end_region();
}
}
fn get_material_count_buffer(&self) -> ghi::BaseBufferHandle {
self.material_count_buffer.into()
}
}
pub struct MaterialOffsetPass {
descriptor_set: ghi::DescriptorSetHandle,
visibility_pass_descriptor_set: ghi::DescriptorSetHandle,
material_offset_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_offset_scratch_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_evaluation_dispatches: ghi::BufferHandle<[[u32; 4]; MAX_MATERIALS]>,
material_offset_pipeline: ghi::PipelineHandle,
}
impl MaterialOffsetPass {
fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
descriptor_set: ghi::DescriptorSetHandle,
visibility_pass_descriptor_set: ghi::DescriptorSetHandle,
material_offset_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_offset_scratch_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_evaluation_dispatches: ghi::BufferHandle<[[u32; 4]; MAX_MATERIALS]>,
) -> Self {
let material_offset_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/material-offset.besl",
"Material Offset Pass Compute Shader",
ResourceShaderTypes::Compute,
);
let material_offset_pipeline = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(&material_offset_shader, ghi::ShaderTypes::Compute),
));
MaterialOffsetPass {
material_offset_buffer,
material_offset_scratch_buffer,
material_evaluation_dispatches,
descriptor_set,
visibility_pass_descriptor_set,
material_offset_pipeline,
}
}
fn prepare(&self) -> impl RenderPassFunction {
let descriptor_set = self.descriptor_set;
let visibility_passes_descriptor_set = self.visibility_pass_descriptor_set;
let pipeline = self.material_offset_pipeline;
let material_offset_buffer = self.material_offset_buffer;
let material_offset_scratch_buffer = self.material_offset_scratch_buffer;
let material_evaluation_dispatches = self.material_evaluation_dispatches;
move |c, _| {
log::debug!("Visibility material offset pass executing");
c.start_region(|label| label.write_str("Material Offset"));
c.clear_buffers(&[
material_offset_buffer.into(),
material_offset_scratch_buffer.into(),
material_evaluation_dispatches.into(),
]);
let compute_pipeline_command = c.bind_compute_pipeline(pipeline);
compute_pipeline_command.bind_descriptor_sets(&[descriptor_set, visibility_passes_descriptor_set]);
compute_pipeline_command.dispatch(ghi::DispatchExtent::new(Extent::line(1), Extent::line(1)));
c.end_region();
}
}
fn get_material_offset_buffer(&self) -> ghi::BaseBufferHandle {
self.material_offset_buffer.into()
}
fn get_material_offset_scratch_buffer(&self) -> ghi::BaseBufferHandle {
self.material_offset_scratch_buffer.into()
}
}
pub struct PixelMappingPass {
material_xy: ghi::BufferHandle<[(u16, u16); MAX_PIXEL_MAPPING_ENTRIES]>,
descriptor_set: ghi::DescriptorSetHandle,
visibility_passes_descriptor_set: ghi::DescriptorSetHandle,
pixel_mapping_pipeline: ghi::PipelineHandle,
}
impl PixelMappingPass {
fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
descriptor_set: ghi::DescriptorSetHandle,
visibility_passes_descriptor_set: ghi::DescriptorSetHandle,
material_xy: ghi::BufferHandle<[(u16, u16); MAX_PIXEL_MAPPING_ENTRIES]>,
) -> Self {
let pixel_mapping_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/pixel-mapping.besl",
"Pixel Mapping Pass Compute Shader",
ResourceShaderTypes::Compute,
);
let pixel_mapping_pipeline = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(&pixel_mapping_shader, ghi::ShaderTypes::Compute),
));
PixelMappingPass {
material_xy,
descriptor_set,
visibility_passes_descriptor_set,
pixel_mapping_pipeline,
}
}
pub(super) fn prepare(&self, frame: &mut ghi::implementation::Frame, sink: &Sink) -> impl RenderPassFunction {
let descriptor_set = self.descriptor_set;
let pipeline = self.pixel_mapping_pipeline;
let visibility_passes_descriptor_set = self.visibility_passes_descriptor_set;
let material_xy = self.material_xy;
let extent = sink.extent();
move |c, _| {
log::debug!(
"Visibility pixel mapping pass executing: extent={}x{}",
extent.width(),
extent.height()
);
c.start_region(|label| label.write_str("Pixel Mapping"));
c.clear_buffers(&[material_xy.into()]);
let compute_pipeline_command = c.bind_compute_pipeline(pipeline);
compute_pipeline_command.bind_descriptor_sets(&[descriptor_set, visibility_passes_descriptor_set]);
compute_pipeline_command.dispatch(ghi::DispatchExtent::new(extent, Extent::square(32)));
c.end_region();
}
}
}
pub struct GtaoPass {
base_descriptor_set: ghi::DescriptorSetHandle,
gtao_descriptor_set: ghi::DescriptorSetHandle,
blur_descriptor_set_x: ghi::DescriptorSetHandle,
blur_descriptor_set_y: ghi::DescriptorSetHandle,
gtao_pipeline: ghi::PipelineHandle,
blur_pipeline_x: ghi::PipelineHandle,
blur_pipeline_y: ghi::PipelineHandle,
ao_map: ghi::BaseImageHandle,
temp_ao_map: ghi::DynamicImageHandle,
}
impl GtaoPass {
fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
base_descriptor_set: ghi::DescriptorSetHandle,
depth: ghi::BaseImageHandle,
ao_map: ghi::BaseImageHandle,
) -> Self {
let gtao_descriptor_set = context.create_descriptor_set(Some("GTAO Descriptor Set"));
let blur_descriptor_set_x = context.create_descriptor_set(Some("GTAO Blur X Descriptor Set"));
let blur_descriptor_set_y = context.create_descriptor_set(Some("GTAO Blur Y Descriptor Set"));
let depth_sampler = context.build_sampler(
ghi::sampler::Builder::new()
.filtering_mode(ghi::FilteringModes::Closest)
.reduction_mode(ghi::SamplingReductionModes::WeightedAverage)
.mip_map_mode(ghi::FilteringModes::Closest)
.addressing_mode(ghi::SamplerAddressingModes::Border {})
.min_lod(0f32)
.max_lod(0f32),
);
let ao_sampler = context.build_sampler(
ghi::sampler::Builder::new()
.filtering_mode(ghi::FilteringModes::Closest)
.mip_map_mode(ghi::FilteringModes::Closest)
.addressing_mode(ghi::SamplerAddressingModes::Border {})
.min_lod(0f32)
.max_lod(0f32),
);
let temp_ao_map = context.build_dynamic_image(
ghi::image::Builder::new(ghi::Formats::R8UNORM, ghi::Uses::Storage | ghi::Uses::Image)
.name("GTAO Blur Intermediate")
.device_accesses(ghi::DeviceAccesses::DeviceOnly),
);
context.write(&[
ghi::DescriptorWrite::combined_image_sampler(
gtao_descriptor_set,
GTAO_DEPTH_BINDING.slot(),
depth,
depth_sampler,
ghi::Layouts::Read,
),
ghi::DescriptorWrite::image(gtao_descriptor_set, GTAO_OUTPUT_BINDING.slot(), ao_map, ghi::Layouts::General),
ghi::DescriptorWrite::combined_image_sampler(
blur_descriptor_set_x,
GTAO_BLUR_DEPTH_BINDING.slot(),
depth,
depth_sampler,
ghi::Layouts::Read,
),
ghi::DescriptorWrite::combined_image_sampler(
blur_descriptor_set_x,
GTAO_BLUR_SOURCE_BINDING.slot(),
ao_map,
ao_sampler,
ghi::Layouts::Read,
),
ghi::DescriptorWrite::image(
blur_descriptor_set_x,
GTAO_BLUR_OUTPUT_BINDING.slot(),
temp_ao_map,
ghi::Layouts::General,
),
ghi::DescriptorWrite::combined_image_sampler(
blur_descriptor_set_y,
GTAO_BLUR_DEPTH_BINDING.slot(),
depth,
depth_sampler,
ghi::Layouts::Read,
),
ghi::DescriptorWrite::combined_image_sampler(
blur_descriptor_set_y,
GTAO_BLUR_SOURCE_BINDING.slot(),
temp_ao_map,
ao_sampler,
ghi::Layouts::Read,
),
ghi::DescriptorWrite::image(
blur_descriptor_set_y,
GTAO_BLUR_OUTPUT_BINDING.slot(),
ao_map,
ghi::Layouts::General,
),
]);
let gtao_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/gtao.besl",
"GTAO Pass Compute Shader",
ResourceShaderTypes::Compute,
);
let gtao_pipeline = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(>ao_shader, ghi::ShaderTypes::Compute),
));
let blur_x_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/gtao-blur-x.besl",
"GTAO Blur X Compute Shader",
ResourceShaderTypes::Compute,
);
let blur_y_shader = load_visibility_shader(
context,
shader_resources,
"byte-engine/rendering/visibility/gtao-blur-y.besl",
"GTAO Blur Y Compute Shader",
ResourceShaderTypes::Compute,
);
let blur_pipeline_x = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(&blur_x_shader, ghi::ShaderTypes::Compute),
));
let blur_pipeline_y = context.create_compute_pipeline(ghi::pipelines::compute::Builder::new(
&[],
ghi::ShaderParameter::new(&blur_y_shader, ghi::ShaderTypes::Compute),
));
Self {
base_descriptor_set,
gtao_descriptor_set,
blur_descriptor_set_x,
blur_descriptor_set_y,
gtao_pipeline,
blur_pipeline_x,
blur_pipeline_y,
ao_map,
temp_ao_map,
}
}
fn prepare(&self, frame: &mut ghi::implementation::Frame, sink: &Sink) -> impl RenderPassFunction {
let base_descriptor_set = self.base_descriptor_set;
let gtao_descriptor_set = self.gtao_descriptor_set;
let blur_descriptor_set_x = self.blur_descriptor_set_x;
let blur_descriptor_set_y = self.blur_descriptor_set_y;
let gtao_pipeline = self.gtao_pipeline;
let blur_pipeline_x = self.blur_pipeline_x;
let blur_pipeline_y = self.blur_pipeline_y;
let ao_map = self.ao_map;
let temp_ao_map = self.temp_ao_map;
let extent = sink.extent();
frame.resize_image(ao_map, extent);
frame.resize_image(temp_ao_map.into(), extent);
move |c, _| {
c.start_region(|label| label.write_str("GTAO"));
c.clear_images(&[(ao_map, ghi::ClearValue::Color(RGBA::white()))]);
{
let c = c.bind_compute_pipeline(gtao_pipeline);
c.bind_descriptor_sets(&[base_descriptor_set, gtao_descriptor_set]);
c.dispatch(ghi::DispatchExtent::new(extent, Extent::new(8, 8, 1)));
}
{
let c = c.bind_compute_pipeline(blur_pipeline_x);
c.bind_descriptor_sets(&[base_descriptor_set, blur_descriptor_set_x]);
c.dispatch(ghi::DispatchExtent::new(extent, Extent::new(8, 8, 1)));
}
{
let c = c.bind_compute_pipeline(blur_pipeline_y);
c.bind_descriptor_sets(&[base_descriptor_set, blur_descriptor_set_y]);
c.dispatch(ghi::DispatchExtent::new(extent, Extent::new(8, 8, 1)));
}
c.end_region();
}
}
}
pub struct MaterialEvaluationPass {
lit: ghi::BaseImageHandle,
ao_map: ghi::BaseImageHandle,
base_descriptor_set: ghi::DescriptorSetHandle,
visibility_descriptor_set: ghi::DescriptorSetHandle,
descriptor_set: ghi::DescriptorSetHandle,
material_evaluation_dispatches: ghi::BufferHandle<[[u32; 4]; MAX_MATERIALS]>,
}
impl MaterialEvaluationPass {
fn new(
lit: ghi::BaseImageHandle,
ao_map: ghi::BaseImageHandle,
_shadow_map: ghi::BaseImageHandle,
base_descriptor_set: ghi::DescriptorSetHandle,
visibility_descriptor_set: ghi::DescriptorSetHandle,
descriptor_set: ghi::DescriptorSetHandle,
material_evaluation_dispatches: ghi::BufferHandle<[[u32; 4]; MAX_MATERIALS]>,
) -> Self {
MaterialEvaluationPass {
lit,
ao_map,
base_descriptor_set,
visibility_descriptor_set,
descriptor_set,
material_evaluation_dispatches,
}
}
fn prepare<'a>(
&'a self,
frame: &mut ghi::implementation::Frame,
sink: &Sink,
materials: &'a [(String, u32, ghi::PipelineHandle)],
phase: VisibilityPhase,
) -> impl RenderPassFunction + 'a {
let lit = self.lit;
let ao_map = self.ao_map;
let base_descriptor_set = self.base_descriptor_set;
let material_evaluation_dispatches = self.material_evaluation_dispatches;
let visibility_descriptor_set = self.visibility_descriptor_set;
let material_evaluation_descriptor_set = self.descriptor_set;
let extent = sink.extent();
if phase == VisibilityPhase::Opaque {
frame.resize_image(ao_map, extent);
}
move |c, t| {
log::debug!(
"{} visibility material evaluation executing: extent={}x{}, materials={}",
phase.label(),
extent.width(),
extent.height(),
materials.len(),
);
if phase == VisibilityPhase::Opaque {
c.clear_images(&[(lit, ghi::ClearValue::Color(RGBA::new(0.0, 0.0, 0.0, 0.0)))]);
}
c.start_region(|label| label.write_str("Material Evaluation"));
c.start_region(|label| label.write_str(phase.label()));
for (name, index, pipeline) in materials {
c.start_region(|label| label.write_str(name));
let c = c.bind_compute_pipeline(*pipeline);
c.bind_descriptor_sets(&[
base_descriptor_set,
visibility_descriptor_set,
material_evaluation_descriptor_set,
]);
c.write_push_constant(0, *index);
c.write_push_constant(4, phase.blend_flag());
c.indirect_dispatch(material_evaluation_dispatches, *index as usize);
c.end_region();
}
c.end_region();
c.end_region();
}
}
}
pub(crate) struct VisibilityPipelineRenderPass {
shadow_pass: ShadowPass,
visibility_pass: VisibilityPass,
material_count_pass: MaterialCountPass,
material_offset_pass: MaterialOffsetPass,
pixel_mapping_pass: PixelMappingPass,
gtao_pass: GtaoPass,
material_evaluation_pass: MaterialEvaluationPass,
}
impl VisibilityPipelineRenderPass {
pub(super) fn material_evaluation_descriptor_set(&self) -> ghi::DescriptorSetHandle {
self.material_evaluation_pass.descriptor_set
}
pub(crate) fn new(
context: &mut ghi::implementation::Context,
shader_resources: &ResourceManager,
base_descriptor_set: ghi::DescriptorSetHandle,
visibility_descriptor_set: ghi::DescriptorSetHandle,
material_evaluation_descriptor_set: ghi::DescriptorSetHandle,
material_count_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
lit: ghi::BaseImageHandle,
ao_map: ghi::BaseImageHandle,
shadow_map: ghi::BaseImageHandle,
depth: ghi::BaseImageHandle,
primitive_index: ghi::BaseImageHandle,
instance_id: ghi::BaseImageHandle,
material_xy: ghi::BufferHandle<[(u16, u16); MAX_PIXEL_MAPPING_ENTRIES]>,
material_offset_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_offset_scratch_buffer: ghi::BufferHandle<[u32; MAX_MATERIALS]>,
material_evaluation_dispatches: ghi::BufferHandle<[[u32; 4]; MAX_MATERIALS]>,
) -> Self {
let shadow_pass = ShadowPass::new(context, shader_resources, base_descriptor_set, shadow_map);
let visibility_pass = VisibilityPass::new(
context,
shader_resources,
base_descriptor_set,
primitive_index,
instance_id,
depth,
);
let material_count_pass = MaterialCountPass::new(
context,
shader_resources,
base_descriptor_set,
visibility_descriptor_set,
material_count_buffer,
);
let material_offset_pass = MaterialOffsetPass::new(
context,
shader_resources,
base_descriptor_set,
visibility_descriptor_set,
material_offset_buffer,
material_offset_scratch_buffer,
material_evaluation_dispatches,
);
let pixel_mapping_pass = PixelMappingPass::new(
context,
shader_resources,
base_descriptor_set,
visibility_descriptor_set,
material_xy,
);
let gtao_pass = GtaoPass::new(context, shader_resources, base_descriptor_set, depth, ao_map);
let material_evaluation_dispatches = material_offset_pass.material_evaluation_dispatches;
let material_evaluation_pass = MaterialEvaluationPass::new(
lit,
ao_map,
shadow_map,
base_descriptor_set,
visibility_descriptor_set,
material_evaluation_descriptor_set,
material_evaluation_dispatches,
);
Self {
shadow_pass,
visibility_pass,
material_count_pass,
material_offset_pass,
pixel_mapping_pass,
gtao_pass,
material_evaluation_pass,
}
}
pub(super) fn prepare<'a>(
&'a self,
frame: &mut ghi::implementation::Frame,
sink: &Sink,
skinning_pass: Option<&'a SkinningPass>,
skinning_dispatches: &'a [SkinningDispatch],
opaque_instances: &'a [Instance],
transparent_instances: &'a [Instance],
opaque_materials: &'a [(String, u32, ghi::PipelineHandle)],
transparent_materials: &'a [(String, u32, ghi::PipelineHandle)],
shadow_enabled: bool,
) -> impl RenderPassFunction + 'a {
let shadow_pass = self.shadow_pass.prepare(frame, opaque_instances, shadow_enabled);
let visibility_pass = &self.visibility_pass;
let material_count_pass = self.material_count_pass.prepare(frame, sink);
let material_offset_pass = self.material_offset_pass.prepare();
let pixel_mapping_pass = self.pixel_mapping_pass.prepare(frame, sink);
let gtao_pass = self.gtao_pass.prepare(frame, sink);
let opaque_material_evaluation_pass =
self.material_evaluation_pass
.prepare(frame, sink, opaque_materials, VisibilityPhase::Opaque);
let transparent_material_evaluation_pass =
self.material_evaluation_pass
.prepare(frame, sink, transparent_materials, VisibilityPhase::Transparent);
let extent = sink.extent();
let instance_count = opaque_instances.len() + transparent_instances.len();
let meshlet_count = opaque_instances
.iter()
.chain(transparent_instances)
.map(|instance| instance.meshlet_count)
.sum::<u32>();
let opaque_count = opaque_materials.len();
let transparent_count = transparent_materials.len();
move |c, t| {
log::debug!(
"Visibility render model executing: primitives={}, opaque_primitives={}, transparent_primitives={}, meshlets={}, opaque_materials={}, transparent_materials={}, shadow_enabled={}",
instance_count,
opaque_instances.len(),
transparent_instances.len(),
meshlet_count,
opaque_count,
transparent_count,
shadow_enabled,
);
c.start_region(|label| label.write_str("Visibility Render Model"));
if let Some(skinning_pass) = skinning_pass {
skinning_pass.record(c, skinning_dispatches);
}
shadow_pass(c, t);
visibility_pass.record(c, extent, opaque_instances, VisibilityPhase::Opaque);
material_count_pass(c, t);
material_offset_pass(c, t);
pixel_mapping_pass(c, t);
gtao_pass(c, t);
opaque_material_evaluation_pass(c, t);
for instance in transparent_instances {
visibility_pass.record(c, extent, std::slice::from_ref(instance), VisibilityPhase::Transparent);
material_count_pass(c, t);
material_offset_pass(c, t);
pixel_mapping_pass(c, t);
transparent_material_evaluation_pass(c, t);
}
c.end_region();
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn gtao_view_space_reconstruction_z_is_positive() {
use math::{mat::MatInverse as _, Matrix4, Vector3, Vector4};
let near = 0.1f32;
let far = 100.0f32;
let fov = 45.0f32;
let aspect = 16.0 / 9.0;
let extent_x = 1920i32;
let extent_y = 1080i32;
let proj = math::projection_matrix(fov, aspect, near, far);
let inv_proj = proj.inverse();
let reconstruct = |px: i32, py: i32, depth: f32| -> Vector3 {
let uv_x = (px as f32 + 0.5) / extent_x as f32;
let uv_y = (py as f32 + 0.5) / extent_y as f32;
let ndc_x = uv_x * 2.0 - 1.0;
let ndc_y = 1.0 - uv_y * 2.0;
let clip = Vector4::new(ndc_x, ndc_y, depth, 1.0);
let view = inv_proj * clip;
let w = view.w;
Vector3::new(view.x / w, view.y / w, view.z / w)
};
let project_to_depth = |vx: f32, vy: f32, vz: f32| -> f32 {
let clip = proj * Vector4::new(vx, vy, vz, 1.0);
clip.z / clip.w };
for vz in [0.5f32, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0] {
let depth = project_to_depth(0.0, 0.0, vz);
let center_px = extent_x / 2;
let center_py = extent_y / 2;
let center = reconstruct(center_px, center_py, depth);
let right = reconstruct(center_px + 1, center_py, depth);
let left = reconstruct(center_px - 1, center_py, depth);
let top = reconstruct(center_px, center_py - 1, depth);
let bottom = reconstruct(center_px, center_py + 1, depth);
let ap_h = Vector3::new(right.x - center.x, right.y - center.y, right.z - center.z);
let bp_h = Vector3::new(center.x - left.x, center.y - left.y, center.z - left.z);
let h_diff = if math::dot(ap_h, ap_h) < math::dot(bp_h, bp_h) {
ap_h
} else {
bp_h
};
let ap_v = Vector3::new(top.x - center.x, top.y - center.y, top.z - center.z);
let bp_v = Vector3::new(center.x - bottom.x, center.y - bottom.y, center.z - bottom.z);
let v_diff = if math::dot(ap_v, ap_v) < math::dot(bp_v, bp_v) {
ap_v
} else {
bp_v
};
let normal = math::cross(h_diff, v_diff);
let normal_len = math::length(normal);
let normal = if normal_len > 1e-8 {
Vector3::new(normal.x / normal_len, normal.y / normal_len, normal.z / normal_len)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let dot_n_p = normal.x * center.x + normal.y * center.y + normal.z * center.z;
let normal = if dot_n_p > 0.0 {
Vector3::new(-normal.x, -normal.y, -normal.z)
} else {
normal
};
eprintln!(
"vz={:.1}: center=({:.4},{:.4},{:.4}), normal=({:.4},{:.4},{:.4}), depth={:.6}",
vz, center.x, center.y, center.z, normal.x, normal.y, normal.z, depth
);
let dot_check = normal.x * center.x + normal.y * center.y + normal.z * center.z;
assert!(
dot_check <= 0.0,
"Normal should face camera (dot(normal, center_position) <= 0) at vz={}, got dot={}",
vz,
dot_check
);
assert!(
normal.z.abs() > 0.99,
"Normal Z should be dominant for flat surface perpendicular to Z at vz={}, got normal.z={}",
vz,
normal.z
);
}
}
#[test]
fn gtao_normal_on_floor_plane() {
use math::{mat::MatInverse as _, Matrix4, Vector3, Vector4};
let near = 0.1f32;
let far = 100.0f32;
let fov = 45.0f32;
let aspect = 16.0 / 9.0;
let extent_x = 1920i32;
let extent_y = 1080i32;
let proj = math::projection_matrix(fov, aspect, near, far);
let inv_proj = proj.inverse();
let reconstruct = |px: i32, py: i32, depth: f32| -> Vector3 {
let uv_x = (px as f32 + 0.5) / extent_x as f32;
let uv_y = (py as f32 + 0.5) / extent_y as f32;
let ndc_x = uv_x * 2.0 - 1.0;
let ndc_y = 1.0 - uv_y * 2.0;
let clip = Vector4::new(ndc_x, ndc_y, depth, 1.0);
let view = inv_proj * clip;
Vector3::new(view.x / view.w, view.y / view.w, view.z / view.w)
};
let project = |vx: f32, vy: f32, vz: f32| -> (f32, f32, f32) {
let clip = proj * Vector4::new(vx, vy, vz, 1.0);
let ndc_x = clip.x / clip.w;
let ndc_y = clip.y / clip.w;
let depth = clip.z / clip.w;
let uv_x = (ndc_x + 1.0) / 2.0;
let uv_y = (1.0 - ndc_y) / 2.0;
let px = uv_x * extent_x as f32 - 0.5;
let py = uv_y * extent_y as f32 - 0.5;
(px, py, depth)
};
let floor_y = -1.0f32;
let ray_hit_floor = |px: i32, py: i32| -> Option<(f32, f32)> {
let p = reconstruct(px, py, 0.5);
if p.y.abs() < 1e-8 {
return None;
} let t = floor_y / p.y;
if t <= 0.0 {
return None;
} let hit_z = p.z * t;
if hit_z < near || hit_z > far {
return None;
} let hit_x = p.x * t;
let clip = proj * Vector4::new(hit_x, floor_y, hit_z, 1.0);
Some((hit_z, clip.z / clip.w))
};
let min_diff = |p: Vector3, a: Vector3, b: Vector3| -> Vector3 {
let ap = Vector3::new(a.x - p.x, a.y - p.y, a.z - p.z);
let bp = Vector3::new(p.x - b.x, p.y - b.y, p.z - b.z);
if math::dot(ap, ap) < math::dot(bp, bp) {
ap
} else {
bp
}
};
eprintln!("\n--- Floor plane normal reconstruction ---");
eprintln!("Testing at various screen Y positions (floor at Y={}):", floor_y);
let mut found_flip = false;
for py in (extent_y / 2 + 50..extent_y - 10).step_by(50) {
let px = extent_x / 2;
let Some((center_vz, center_depth)) = ray_hit_floor(px, py) else {
continue;
};
let Some((_, left_depth)) = ray_hit_floor(px - 1, py) else {
continue;
};
let Some((_, right_depth)) = ray_hit_floor(px + 1, py) else {
continue;
};
let Some((_, top_depth)) = ray_hit_floor(px, py - 1) else {
continue;
};
let Some((_, bottom_depth)) = ray_hit_floor(px, py + 1) else {
continue;
};
let center = reconstruct(px, py, center_depth);
let left = reconstruct(px - 1, py, left_depth);
let right = reconstruct(px + 1, py, right_depth);
let top = reconstruct(px, py - 1, top_depth);
let bottom = reconstruct(px, py + 1, bottom_depth);
let h_diff = min_diff(center, right, left);
let v_diff = min_diff(center, top, bottom);
let normal = math::cross(h_diff, v_diff);
let normal_len = math::length(normal);
let normal = if normal_len > 1e-8 {
Vector3::new(normal.x / normal_len, normal.y / normal_len, normal.z / normal_len)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let dot_n_p = normal.x * center.x + normal.y * center.y + normal.z * center.z;
let normal = if dot_n_p > 0.0 {
Vector3::new(-normal.x, -normal.y, -normal.z)
} else {
normal
};
eprintln!(
"py={:4}, vz={:6.2}: h_diff=({:+.6},{:+.6},{:+.6}), v_diff=({:+.6},{:+.6},{:+.6}), normal=({:+.4},{:+.4},{:+.4})",
py, center_vz, h_diff.x, h_diff.y, h_diff.z, v_diff.x, v_diff.y, v_diff.z, normal.x, normal.y, normal.z,
);
if normal.y < 0.0 {
found_flip = true;
eprintln!(" ^^^ FLIPPED! Normal Y is negative (pointing into floor)");
}
}
if found_flip {
eprintln!("\nWARNING: Normal flipped at some distances! This explains the hard boundary.");
} else {
eprintln!("\nAll normals consistent (no flip detected in tested range).");
}
}
}