#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::cull_status::CullStatus;
use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::gfx::lod;
use concinnity_core::gfx::render_types;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::model_history::HistoryMode;
use concinnity_core::render::shader_programs::metal::bindless_textures;
use concinnity_core::transform::IDENTITY;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::{NSString, ns_string};
use objc2_metal::{
MTLArgumentEncoder, MTLCommandBuffer as _, MTLComputePassDescriptor, MTLComputePipelineState,
MTLDevice as _, MTLFunction as _, MTLLibrary as _, MTLRenderPipelineState,
};
use super::builtin_shaders::compute_pipeline;
use super::context::*;
use super::encode::ComputeEncode;
use super::init::pipelines::BucketPipelines;
use super::pipeline::{cull_encode_library, ns_str};
use super::scoped_encoder::ScopedEncoder;
use concinnity_core::render::uniforms::metal::*;
#[derive(Default)]
pub(crate) struct ShadowViewIcb {
pub icb: Option<Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>>,
pub arg_buffer: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub status: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub capacity: usize,
}
struct ViewSet<'a> {
views: &'a [(usize, Frustum)],
region_count: usize,
region_mask: u32,
}
pub(crate) struct CullState {
pub bindless: bool,
pub main_pipeline: Option<BucketPipelines>,
pub world_pipelines: concinnity_core::render::world_pipelines::WorldPipelines<BucketPipelines>,
pub pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub encode_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub bucket_count: usize,
pub icbs: Vec<Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>>,
pub icb_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
pub icb_arg_buffer: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub icb_capacity: usize,
pub pipeline_phase2: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub icbs_2: Vec<Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>>,
pub icb_2_arg_buffer: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub status_buffer: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub two_pass_occlusion: bool,
pub hiz: Option<super::hiz::HiZResources>,
pub prev_view_proj: [[f32; 4]; 4],
pub cur_view_proj: [[f32; 4]; 4],
pub hiz_valid: bool,
pub shadow_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub shadow_bindless_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub shadow_views: ShadowViewIcb,
pub spot_views: ShadowViewIcb,
pub mirror_slots: Vec<MirrorCullSlot>,
pub mirror_status: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub mirror_icb_capacity: usize,
}
pub(crate) struct MirrorCullSlot {
pub icb: Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>,
pub arg_buffer: Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
}
pub(super) const CULL_ICB_BUFFER_INDEX: usize = 4;
const CULL_STATUS_BUFFER_INDEX: usize = 5;
const CULL_ENCODE_PARAMS_INDEX: usize = 7;
pub(super) struct FlatPoolIndices {
pub albedo: u32,
pub normal: u32,
pub emissive: u32,
pub orm: u32,
}
impl FlatPoolIndices {
fn onto(&self, mut rec: render_types::GpuObjectData) -> render_types::GpuObjectData {
rec.emissive_map_index = self.emissive;
rec.orm_map_index = self.orm;
rec
}
}
pub(super) fn metal_flat_pool_indices(
texture_count: usize,
texture_slot: usize,
normal_map_slot: usize,
material: &render_types::MaterialUniforms,
) -> FlatPoolIndices {
use concinnity_core::gfx::render_types::{albedo_pool_index, normal_pool_index};
let cap = (super::context::BINDLESS_TEXTURE_COUNT as u32).saturating_sub(1);
let tc = texture_count as u32;
let clamp = |i: u32| i.min(cap);
FlatPoolIndices {
albedo: clamp(albedo_pool_index(texture_slot, tc)),
normal: clamp(normal_pool_index(normal_map_slot, tc)),
emissive: clamp(material.emissive_map_index),
orm: clamp(material.orm_map_index),
}
}
pub(super) fn metal_instance_records(
clusters: &[render_types::InstancedCluster],
texture_count: usize,
) -> Vec<render_types::GpuObjectData> {
let total: usize = clusters.iter().map(|c| c.instances.len()).sum();
let mut records = Vec::with_capacity(total);
for cluster in clusters {
let idx = metal_flat_pool_indices(
texture_count,
cluster.texture_slot,
cluster.normal_map_slot,
&cluster.material,
);
for &model in &cluster.instances {
let rec = render_types::pack_instance_record(cluster, model, idx.albedo, idx.normal);
records.push(idx.onto(rec));
}
}
records
}
pub(super) fn metal_skinned_record(
obj: &render_types::SkinnedDrawObject,
texture_count: usize,
) -> render_types::GpuObjectData {
let idx = metal_flat_pool_indices(
texture_count,
obj.texture_slot,
obj.normal_map_slot,
&obj.material,
);
idx.onto(render_types::pack_skinned_record(
obj, idx.albedo, idx.normal,
))
}
struct CullSceneBuffers<'a> {
object_buffer: &'a ProtocolObject<dyn objc2_metal::MTLBuffer>,
draw_args_buffer: &'a ProtocolObject<dyn objc2_metal::MTLBuffer>,
counts: crate::metal::context::DrawRecordCounts,
}
struct CullView<'a> {
frustum: &'a Frustum,
cam_pos: [f32; 3],
}
struct CullOutputTarget<'a> {
icbs: &'a [Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>],
arg_buf: &'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
status: &'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
}
pub(in crate::metal) struct MirrorCull<'a> {
pub(in crate::metal) object_buffer: &'a ProtocolObject<dyn objc2_metal::MTLBuffer>,
pub(in crate::metal) draw_args_buffer: &'a ProtocolObject<dyn objc2_metal::MTLBuffer>,
pub(in crate::metal) frustum: &'a Frustum,
pub(in crate::metal) eye: [f32; 3],
pub(in crate::metal) slot: usize,
pub(in crate::metal) timer: super::pass_timing::PassTimer,
}
struct CullDispatchOptions<'a> {
use_hiz: bool,
timing: super::pass_timing::PassTimer,
label: &'a NSString,
}
fn dispatch_records(
enc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>,
pipeline: &ProtocolObject<dyn MTLComputePipelineState>,
slots: usize,
) {
use objc2_metal::{MTLComputeCommandEncoder as _, MTLComputePipelineState as _, MTLSize};
let tg = pipeline.maxTotalThreadsPerThreadgroup().clamp(1, 64);
enc.dispatchThreads_threadsPerThreadgroup(
MTLSize {
width: slots,
height: 1,
depth: 1,
},
MTLSize {
width: tg,
height: 1,
depth: 1,
},
);
}
impl MtlContext {
pub(super) fn build_joint_buffers(
&mut self,
ring_slot: usize,
) -> RenderResult<Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>> {
self.rings.joint.write_all(
&self.hw.device,
ring_slot,
&self.state.skinned.joint_matrices,
)
}
pub(super) fn build_morph_weight_buffers(
&mut self,
ring_slot: usize,
) -> RenderResult<Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>> {
self.rings.joint.write_weights(
&self.hw.device,
ring_slot,
&self.state.skinned.morph_weights,
)
}
pub(super) fn build_object_buffer(
&mut self,
ring_slot: usize,
) -> RenderResult<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>> {
if self.cull_count() == 0 {
return Ok(None);
}
let texture_count = self.scene.textures.len();
let mut objects = std::mem::take(&mut self.rings.object_scratch);
objects.clear();
for obj in &self.state.draw.objects {
let idx = metal_flat_pool_indices(
texture_count,
obj.texture_slot,
obj.normal_map_slot,
&obj.material,
);
objects.push(idx.onto(render_types::pack_object_record(
obj, idx.albedo, idx.normal,
)));
}
if self.state.draw.n_instances > 0 {
objects.extend_from_slice(&self.instanced.records);
}
if self.state.draw.n_skinned > 0 {
for obj in &self.state.skinned.draw_objects {
objects.push(metal_skinned_record(obj, texture_count));
}
}
let result = self.rings.object.write(
&self.hw.device,
ring_slot,
super::context::bytes_of_slice(&objects),
);
self.rings.object_scratch = objects;
result.map(Some)
}
pub(super) fn build_draw_args_buffer(
&mut self,
cam_pos: [f32; 3],
ring_slot: usize,
history: HistoryMode,
) -> RenderResult<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>> {
use concinnity_core::gfx::render_types::{GpuDrawArgs, draw_args_flags};
if self.cull_count() == 0 {
return Ok(None);
}
let n_cull = self.cull_count();
self.state.model_history.get_mut().begin(history, n_cull);
let mesh_glass_active = self.mesh_glass_active();
let mut args = std::mem::take(&mut self.rings.draw_args_scratch);
args.clear();
for (i, obj) in self.state.draw.objects.iter().enumerate() {
let d = lod::camera_distance(obj, cam_pos);
let (index_offset, index_count) = obj.active_lod(d);
let opaque_visible =
obj.visible && !(mesh_glass_active && obj.material.see_through != 0);
args.push(GpuDrawArgs {
index_count: index_count as u32,
index_offset: index_offset as u32,
base_vertex: obj.base_vertex as u32,
flags: draw_args_flags(opaque_visible, obj.resident, obj.cullable())
| render_types::draw_args_bucket_bits(obj.shader_bucket)
| self.state.model_history.get_mut().draw_flags(i, i),
});
}
if self.state.draw.n_instances > 0 {
let instance_base = args.len();
args.extend_from_slice(&self.instanced.draw_args);
if self.instanced.any_lod {
lod::for_each_instance_lod(
&self.instanced.clusters,
cam_pos,
|record, index_offset, index_count| {
let rec = &mut args[instance_base + record];
rec.index_offset = index_offset as u32;
rec.index_count = index_count as u32;
},
);
}
}
if self.state.draw.n_skinned > 0 {
let base = args.len();
for (k, obj) in self.state.skinned.draw_objects.iter().enumerate() {
let d = lod::skinned_camera_distance(obj, cam_pos);
let (index_offset, index_count) = obj.active_lod(d);
args.push(GpuDrawArgs {
index_count: index_count as u32,
index_offset: index_offset as u32,
base_vertex: 0,
flags: draw_args_flags(obj.visible, true, true)
| self
.state
.model_history
.get_mut()
.skinned_flags(base + k, k),
});
}
}
let result = self.rings.draw_args.write(
&self.hw.device,
ring_slot,
super::context::bytes_of_slice(&args),
);
self.rings.draw_args_scratch = args;
result.map(Some)
}
pub(in crate::metal) fn encode_cull(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
object_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
draw_args_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
frustum: &Frustum,
cam_pos: [f32; 3],
counts: crate::metal::context::DrawRecordCounts,
) -> RenderResult<()> {
let (Some(arg_buf), Some(status)) = (&self.cull.icb_arg_buffer, &self.cull.status_buffer)
else {
return Ok(());
};
if self.cull.icbs.is_empty() {
return Ok(());
}
self.encode_cull_into(
cmd_buf,
CullSceneBuffers {
object_buffer,
draw_args_buffer,
counts,
},
CullView { frustum, cam_pos },
CullOutputTarget {
icbs: &self.cull.icbs,
arg_buf,
status,
},
CullDispatchOptions {
use_hiz: true,
timing: super::pass_timing::PassTimer::Whole(super::pass_timing::PassId::Cull),
label: ns_string!("cull phase1"),
},
)?;
Ok(())
}
pub(in crate::metal) fn encode_mirror_cull(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
cull: MirrorCull<'_>,
) -> RenderResult<()> {
let MirrorCull {
object_buffer,
draw_args_buffer,
frustum,
eye,
slot,
timer,
} = cull;
let (Some(mirror), Some(status)) =
(self.cull.mirror_slots.get(slot), &self.cull.mirror_status)
else {
return Ok(());
};
self.encode_cull_into(
cmd_buf,
CullSceneBuffers {
object_buffer,
draw_args_buffer,
counts: self.draw_record_counts(),
},
CullView {
frustum,
cam_pos: eye,
},
CullOutputTarget {
icbs: std::slice::from_ref(&mirror.icb),
arg_buf: &mirror.arg_buffer,
status,
},
CullDispatchOptions {
use_hiz: false,
timing: timer,
label: ns_string!("mirror cull"),
},
)
}
fn encode_cull_into(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
scene: CullSceneBuffers,
view: CullView,
target: CullOutputTarget,
options: CullDispatchOptions,
) -> RenderResult<()> {
let CullSceneBuffers {
object_buffer,
draw_args_buffer,
counts,
} = scene;
let CullView { frustum, cam_pos } = view;
let CullOutputTarget {
icbs,
arg_buf,
status,
} = target;
let CullDispatchOptions {
use_hiz,
timing,
label,
} = options;
use objc2_metal::{MTLComputeCommandEncoder as _, MTLResourceUsage};
let (Some(pipeline), Some(encode)) = (&self.cull.pipeline, &self.cull.encode_pipeline)
else {
return Ok(());
};
let object_count = counts.total;
if object_count == 0 {
return Ok(());
}
let mut planes = [[0.0f32; 4]; 6];
for (i, p) in frustum.planes.iter().enumerate() {
planes[i] = [p.normal[0], p.normal[1], p.normal[2], p.d];
}
let (hiz_tex, hiz_size, hiz_mip_count, hiz_enabled) = match self.cull.hiz.as_ref() {
Some(h) => (
Some(h.texture.as_ref()),
[h.width as f32, h.height as f32],
h.mip_count,
if use_hiz && self.cull.hiz_valid {
1u32
} else {
0u32
},
),
None => (None, [1.0, 1.0], 1, 0u32),
};
let cull_uniforms = CullUniforms {
planes,
cam_pos: [cam_pos[0], cam_pos[1], cam_pos[2], 0.0],
prev_view_proj: self.cull.prev_view_proj,
hiz_size,
hiz_mip_count,
hiz_enabled,
object_count: object_count as u32,
skinned_base: counts.skinned_base as u32,
cascade_base: 0,
bucket_count: icbs.len() as u32,
};
let cull_pass_desc = MTLComputePassDescriptor::new();
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_compute_timer(&cull_pass_desc, timing);
}
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&cull_pass_desc)
.ok_or_else(|| RenderError::Other("failed to get compute encoder".to_string()))?,
label,
);
enc.set_pipeline(pipeline);
enc.set_buffer(object_buffer, 0, 0);
enc.set_buffer(draw_args_buffer, 0, 1);
enc.set_value(&cull_uniforms, 2);
enc.set_buffer(&self.scene.index_buffer, 0, 3);
enc.set_buffer(arg_buf, 0, CULL_ICB_BUFFER_INDEX);
enc.set_buffer(status, 0, CULL_STATUS_BUFFER_INDEX);
enc.set_buffer(self.skinned_index_or_placeholder(), 0, 6);
if let Some(tex) = hiz_tex {
enc.set_texture(tex, 0);
}
for icb in icbs {
enc.useResource_usage(ProtocolObject::from_ref(&**icb), MTLResourceUsage::Write);
}
dispatch_records(&enc, pipeline, object_count);
enc.set_pipeline(encode);
enc.set_value(
&EncodeParams {
object_count: object_count as u32,
region_count: 1,
region_mask: 1,
skinned_base: counts.skinned_base as u32,
bucket_count: icbs.len() as u32,
draw_status: CullStatus::DRAWN,
region_base: 0,
_pad: 0,
},
CULL_ENCODE_PARAMS_INDEX,
);
dispatch_records(&enc, encode, object_count);
Ok(())
}
pub(in crate::metal) fn encode_cull_phase2(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
object_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
draw_args_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
frustum: &Frustum,
cam_pos: [f32; 3],
) -> RenderResult<u32> {
use objc2_metal::{MTLComputeCommandEncoder as _, MTLResourceUsage};
let (Some(pipeline), Some(encode), Some(arg_buf), Some(status), Some(hiz)) = (
&self.cull.pipeline_phase2,
&self.cull.encode_pipeline,
&self.cull.icb_2_arg_buffer,
&self.cull.status_buffer,
self.cull.hiz.as_ref(),
) else {
return Ok(0);
};
if self.cull.icbs_2.is_empty() {
return Ok(0);
}
let object_count = self.cull_count();
if object_count == 0 {
return Ok(0);
}
let mut planes = [[0.0f32; 4]; 6];
for (i, p) in frustum.planes.iter().enumerate() {
planes[i] = [p.normal[0], p.normal[1], p.normal[2], p.d];
}
let skinned_base = self.skinned_record_base() as u32;
let cull_uniforms = CullUniforms {
planes,
cam_pos: [cam_pos[0], cam_pos[1], cam_pos[2], 0.0],
prev_view_proj: self.cull.cur_view_proj,
hiz_size: [hiz.width as f32, hiz.height as f32],
hiz_mip_count: hiz.mip_count,
hiz_enabled: 1,
object_count: object_count as u32,
skinned_base,
cascade_base: 0,
bucket_count: self.cull.icbs_2.len() as u32,
};
let cull_pass_desc = MTLComputePassDescriptor::new();
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_compute(&cull_pass_desc, super::pass_timing::PassId::Cull2);
}
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&cull_pass_desc)
.ok_or_else(|| RenderError::Other("failed to get compute encoder".to_string()))?,
ns_string!("cull phase2"),
);
enc.set_pipeline(pipeline);
enc.set_buffer(object_buffer, 0, 0);
enc.set_buffer(draw_args_buffer, 0, 1);
enc.set_value(&cull_uniforms, 2);
enc.set_buffer(&self.scene.index_buffer, 0, 3);
enc.set_buffer(arg_buf, 0, CULL_ICB_BUFFER_INDEX);
enc.set_buffer(status, 0, CULL_STATUS_BUFFER_INDEX);
enc.set_buffer(self.skinned_index_or_placeholder(), 0, 6);
enc.set_texture(hiz.texture.as_ref(), 0);
for icb in &self.cull.icbs_2 {
enc.useResource_usage(ProtocolObject::from_ref(&**icb), MTLResourceUsage::Write);
}
dispatch_records(&enc, pipeline, object_count);
enc.set_pipeline(encode);
enc.set_value(
&EncodeParams {
object_count: object_count as u32,
region_count: 1,
region_mask: 1,
skinned_base,
bucket_count: self.cull.icbs_2.len() as u32,
draw_status: CullStatus::REDRAW,
region_base: 0,
_pad: 0,
},
CULL_ENCODE_PARAMS_INDEX,
);
dispatch_records(&enc, encode, object_count);
Ok(0)
}
pub(in crate::metal) fn encode_shadow_culls(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
object_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
draw_args_buffer: &ProtocolObject<dyn objc2_metal::MTLBuffer>,
) -> RenderResult<()> {
use concinnity_core::gfx::render_types::{MAX_SHADOWED_SPOTS, NUM_SHADOW_CASCADES};
let Some(pipeline) = &self.cull.shadow_pipeline else {
return Ok(());
};
if self.cull_count() == 0
|| (self.cull.shadow_views.icb.is_none() && self.cull.spot_views.icb.is_none())
{
return Ok(());
}
let cull_pass_desc = MTLComputePassDescriptor::new();
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&cull_pass_desc)
.ok_or_else(|| {
RenderError::Other("failed to get shadow cull compute encoder".to_string())
})?,
ns_string!("shadow cull"),
);
enc.set_buffer(object_buffer, 0, 0);
enc.set_buffer(draw_args_buffer, 0, 1);
enc.set_buffer(&self.scene.index_buffer, 0, 3);
enc.set_buffer(self.skinned_index_or_placeholder(), 0, 6);
let all = (1u32 << NUM_SHADOW_CASCADES) - 1;
let mask = if self.shadow.render_mask == 0 {
all
} else {
self.shadow.render_mask
};
let mut cascades = [(0, Frustum::from_shadow(IDENTITY)); NUM_SHADOW_CASCADES];
let mut kept = 0;
for (c, light_vp) in self.shadow.uniforms.light_vps.iter().enumerate() {
if mask & (1u32 << c) == 0 {
continue;
}
cascades[kept] = (c, Frustum::from_shadow(*light_vp));
kept += 1;
}
self.encode_view_culls(
&enc,
pipeline,
&self.cull.shadow_views,
ViewSet {
views: &cascades[..kept],
region_count: NUM_SHADOW_CASCADES,
region_mask: mask,
},
);
let mut spots = [(0, Frustum::from_shadow(IDENTITY)); MAX_SHADOWED_SPOTS];
let mut kept = 0;
let mut spot_mask = 0u32;
for slice in self.spot_shadow.refreshed_slices() {
spots[kept] = (slice as usize, self.spot_shadow.frusta[slice as usize]);
spot_mask |= 1 << slice;
kept += 1;
}
self.encode_view_culls(
&enc,
pipeline,
&self.cull.spot_views,
ViewSet {
views: &spots[..kept],
region_count: self.spot_shadow.count as usize,
region_mask: spot_mask,
},
);
Ok(())
}
fn encode_view_culls(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>,
pipeline: &ProtocolObject<dyn MTLComputePipelineState>,
set: &ShadowViewIcb,
views: ViewSet<'_>,
) {
use objc2_metal::{MTLComputeCommandEncoder as _, MTLResourceUsage};
let (Some(encode), Some(icb), Some(arg_buf), Some(status)) = (
&self.cull.encode_pipeline,
&set.icb,
&set.arg_buffer,
&set.status,
) else {
return;
};
if views.views.is_empty() {
return;
}
let object_count = self.cull_count();
let skinned_base = self.skinned_record_base() as u32;
enc.set_pipeline(pipeline);
enc.set_buffer(arg_buf, 0, CULL_ICB_BUFFER_INDEX);
enc.set_buffer(status, 0, CULL_STATUS_BUFFER_INDEX);
enc.useResource_usage(ProtocolObject::from_ref(&**icb), MTLResourceUsage::Write);
for (region, frustum) in views.views {
let mut planes = [[0.0f32; 4]; 6];
for (i, p) in frustum.planes.iter().enumerate() {
planes[i] = [p.normal[0], p.normal[1], p.normal[2], p.d];
}
let cull_uniforms = CullUniforms {
planes,
cam_pos: [0.0; 4],
prev_view_proj: IDENTITY,
hiz_size: [1.0, 1.0],
hiz_mip_count: 1,
hiz_enabled: 0,
object_count: object_count as u32,
skinned_base,
cascade_base: (region * object_count) as u32,
bucket_count: 1,
};
enc.set_value(&cull_uniforms, 2);
dispatch_records(enc, pipeline, object_count);
}
let (region_base, span) =
EncodeParams::encoded_span(views.region_mask, views.region_count as u32);
if span == 0 {
return;
}
enc.set_pipeline(encode);
enc.set_value(
&EncodeParams {
object_count: object_count as u32,
region_count: views.region_count as u32,
region_mask: views.region_mask,
skinned_base,
bucket_count: 1,
draw_status: CullStatus::DRAWN,
region_base,
_pad: 0,
},
CULL_ENCODE_PARAMS_INDEX,
);
dispatch_records(enc, encode, span as usize * object_count);
}
fn bindless_fixed_members(
&self,
) -> [(usize, &ProtocolObject<dyn objc2_metal::MTLTexture>); bindless_textures::FIXED] {
use bindless_textures as ids;
[
(ids::SHADOW_MAP, &*self.shadow.map),
(ids::IRRADIANCE_CUBE, &*self.scene.env_map.irradiance),
(ids::PREFILTER_CUBE, &*self.scene.env_map.prefilter),
(ids::SSAO, self.ao_output_texture()),
(ids::PROBE_CUBES, self.probe.cubes.texture()),
(ids::SPOT_SHADOW_MAP, &*self.spot_shadow.map),
(ids::LTC_MATRIX, &*self.scene.ltc_matrix_texture),
(ids::LTC_MAGNITUDE, &*self.scene.ltc_magnitude_texture),
]
}
pub(super) fn bindless_texture_signature(&self) -> u64 {
let mut sig = super::bindless_args::Signature::new();
sig.push_u64(self.arg_buffers.texture_epoch);
sig.push_u64(self.scene.textures.len() as u64);
for tex in &self.scene.fallback_textures {
sig.push_texture(tex.as_ref());
}
for (_, tex) in self.bindless_fixed_members() {
sig.push_texture(tex);
}
sig.finish()
}
fn bindless_tail_signature(&self) -> u64 {
let mut sig = super::bindless_args::Signature::new();
sig.push_u64(self.scene.textures.len() as u64);
if let Some(tex) = self.scene.fallback_textures.get(1) {
sig.push_texture(tex.as_ref());
}
sig.finish()
}
pub(super) fn build_bindless_texture_args(
&mut self,
ring_slot: usize,
sig: u64,
) -> RenderResult<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>> {
use bindless_textures as ids;
if !self.cull.bindless {
return Ok(None);
}
let tail_sig = self.bindless_tail_signature();
let count = super::context::BINDLESS_TEXTURE_COUNT;
let len = super::bindless_args::bindless_block_len(count);
let (buf, allocated) =
self.rings
.bindless_tex
.slot_fresh(&self.hw.device, ring_slot, len)?;
if allocated {
self.arg_buffers.bindless_tex_gates.invalidate(ring_slot);
self.arg_buffers.bindless_tail_gates.invalidate(ring_slot);
}
let write_tail = self
.arg_buffers
.bindless_tail_gates
.stale(ring_slot, tail_sig);
let write_pool = self.arg_buffers.bindless_tex_gates.stale(ring_slot, sig);
if !write_pool && !write_tail {
return Ok(Some(buf));
}
let mut block = super::bindless_args::ResourceIdWriter::new(&buf);
let texture_count = self.scene.textures.len();
if write_pool {
for (id, tex) in self.bindless_fixed_members() {
block.set(id, tex);
}
for (i, tex) in self.scene.textures.iter().take(count).enumerate() {
block.set(ids::pool(i), tex);
}
for (i, tex) in (texture_count..count).zip(&self.scene.fallback_textures) {
block.set(ids::pool(i), tex);
}
}
if write_tail && let Some(white) = self.scene.fallback_textures.last() {
for i in (texture_count + self.scene.fallback_textures.len()).min(count)..count {
block.set(ids::pool(i), white);
}
}
Ok(Some(buf))
}
pub(super) fn refresh_bindless_residency(&mut self, sig: u64) {
let mut set = core::mem::replace(
&mut self.arg_buffers.bindless_residency,
super::bindless_args::ResidencySet::new(),
);
set.refresh(
sig,
self.scene
.textures
.iter()
.chain(self.scene.fallback_textures.iter())
.map(|t| t.as_ref())
.chain(self.bindless_fixed_members().map(|(_, tex)| tex)),
);
self.arg_buffers.bindless_residency = set;
}
pub(super) fn use_bindless_textures(
&self,
encoder: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
) {
self.arg_buffers
.bindless_residency
.declare_fragment(encoder);
}
}
pub(super) struct CullPipeline {
pub decide: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub decide_phase2: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub encode: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub icb_arg_encoder: Retained<ProtocolObject<dyn MTLArgumentEncoder>>,
}
pub(super) fn build_cull_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> RenderResult<CullPipeline> {
let decide = compute_pipeline(device, &super::builtin_shaders::CULL_PHASE1, hot_reload)?;
let decide_phase2 = compute_pipeline(device, &super::builtin_shaders::CULL_PHASE2, hot_reload)?;
let library = cull_encode_library(device, hot_reload)?;
let encode_fn = library
.newFunctionWithName(&ns_str("cull_encode"))
.ok_or_else(|| {
RenderError::ShaderCompile("cull_encode not found in cull_encode library".to_string())
})?;
let encode = device
.newComputePipelineStateWithFunction_error(&encode_fn)
.map_err(|e| RenderError::ShaderCompile(format!("cull encode pipeline: {e:?}")))?;
let icb_arg_encoder =
unsafe { encode_fn.newArgumentEncoderWithBufferIndex(CULL_ICB_BUFFER_INDEX) };
Ok(CullPipeline {
decide,
decide_phase2,
encode,
icb_arg_encoder,
})
}
pub(super) fn build_shadow_cull_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLComputePipelineState>>> {
compute_pipeline(device, &super::builtin_shaders::CULL_SHADOW, hot_reload)
}
#[cfg(test)]
mod tests {
use super::metal_flat_pool_indices;
use concinnity_core::gfx::render_types::{MaterialUniforms, NO_NORMAL_MAP_SLOT};
#[test]
fn flat_pool_indices_share_one_handle_indexed_pool() {
let material = MaterialUniforms {
emissive_map_index: 3,
orm_map_index: 0,
..MaterialUniforms::DEFAULT
};
let idx = metal_flat_pool_indices(8, 2, 1, &material);
assert_eq!(idx.albedo, 2);
assert_eq!(idx.normal, 1);
assert_eq!(idx.emissive, 3);
assert_eq!(idx.orm, 0);
}
#[test]
fn flat_pool_indices_map_a_missing_normal_to_the_fallback() {
let idx = metal_flat_pool_indices(8, 3, NO_NORMAL_MAP_SLOT, &MaterialUniforms::DEFAULT);
assert_eq!(idx.albedo, 3);
assert_eq!(idx.normal, 8);
}
#[test]
fn flat_pool_indices_clamp_out_of_range_and_cap() {
let idx = metal_flat_pool_indices(4, 99, 99, &MaterialUniforms::DEFAULT);
assert_eq!(idx.albedo, 3); assert_eq!(idx.normal, 3);
let cap = super::super::context::BINDLESS_TEXTURE_COUNT;
let idx = metal_flat_pool_indices(cap + 5, 9999, 9999, &MaterialUniforms::DEFAULT);
assert_eq!(idx.albedo, (cap - 1) as u32);
assert_eq!(idx.normal, (cap - 1) as u32);
}
}