#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::transform::IDENTITY;
use concinnity_core::render::model_history::HistoryMode;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLArgumentEncoder, MTLCommandBuffer as _, MTLComputePassDescriptor, MTLComputePipelineState,
MTLDevice as _, MTLFunction as _, MTLLibrary as _, MTLRenderCommandEncoder as _,
MTLRenderPipelineState,
};
use concinnity_core::gfx::cull_status::CullStatus;
use super::context::*;
use super::encode::ComputeEncode;
use super::pipeline::{ns_str, shader_library};
use super::scoped_encoder::ScopedEncoder;
use super::uniforms::*;
use crate::gfx::lod::camera_distance as lod_camera_distance;
pub(crate) struct CullState {
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_icb: Option<Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>>,
pub shadow_icb_arg_buffer: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub shadow_status: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub shadow_icb_capacity: usize,
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,
}
pub(super) fn metal_flat_pool_indices(
texture_count: usize,
texture_slot: usize,
normal_map_slot: usize,
material: &crate::gfx::render_types::MaterialUniforms,
) -> FlatPoolIndices {
use crate::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: &[crate::gfx::render_types::InstancedCluster],
texture_count: usize,
) -> Vec<crate::gfx::render_types::GpuObjectData> {
use crate::gfx::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 (bb_min, bb_max) = crate::gfx::frustum::transform_aabb(
cluster.local_bb_min,
cluster.local_bb_max,
model,
);
records.push(GpuObjectData {
model,
tint: cluster.material.tint,
roughness: cluster.material.roughness,
emissive: cluster.material.emissive,
metallic: cluster.material.metallic,
albedo_index: idx.albedo,
normal_index: idx.normal,
emissive_map_index: idx.emissive,
orm_map_index: idx.orm,
bb_min,
cull_distance: cluster.cull_distance,
bb_max,
alpha_cutoff: cluster.material.alpha_cutoff,
});
}
}
records
}
pub(super) fn metal_skinned_record(
obj: &crate::gfx::render_types::SkinnedDrawObject,
texture_count: usize,
) -> crate::gfx::render_types::GpuObjectData {
let idx = metal_flat_pool_indices(
texture_count,
obj.texture_slot,
obj.normal_map_slot,
&obj.material,
);
let mut rec = crate::gfx::render_types::pack_skinned_record(obj, idx.albedo, idx.normal);
rec.emissive_map_index = idx.emissive;
rec.orm_map_index = idx.orm;
rec
}
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 crate::gfx::frustum::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>>,
}
struct CullDispatchOptions<'a> {
use_hiz: bool,
timing: Option<super::pass_timing::PassId>,
label: &'a str,
}
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,
) -> Result<Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
self.rings
.joint
.write_all(&self.device, ring_slot, &self.skinned.joint_matrices)
}
pub(super) fn build_morph_weight_buffers(
&mut self,
ring_slot: usize,
) -> Result<Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
self.rings
.joint
.write_weights(&self.device, ring_slot, &self.skinned.morph_weights)
}
pub(super) fn build_object_buffer(
&mut self,
ring_slot: usize,
) -> Result<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
use crate::gfx::render_types::GpuObjectData;
if self.cull_count() == 0 {
return Ok(None);
}
let texture_count = self.textures.len();
let mut objects = std::mem::take(&mut self.rings.object_scratch);
objects.clear();
for obj in &self.draw.objects {
let idx = metal_flat_pool_indices(
texture_count,
obj.texture_slot,
obj.normal_map_slot,
&obj.material,
);
objects.push(GpuObjectData {
model: obj.model,
tint: obj.material.tint,
roughness: obj.material.roughness,
emissive: obj.material.emissive,
metallic: obj.material.metallic,
albedo_index: idx.albedo,
normal_index: idx.normal,
emissive_map_index: idx.emissive,
orm_map_index: idx.orm,
bb_min: obj.bb_min,
cull_distance: obj.cull_distance,
bb_max: obj.bb_max,
alpha_cutoff: obj.material.alpha_cutoff,
});
}
if self.draw.n_instances > 0 {
objects.extend_from_slice(&self.instanced.records);
}
if self.draw.n_skinned > 0 {
for obj in &self.skinned.draw_objects {
objects.push(metal_skinned_record(obj, texture_count));
}
}
let result = self.rings.object.write(
&self.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,
) -> Result<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
use crate::gfx::render_types::{GpuDrawArgs, draw_args_flags};
if self.cull_count() == 0 {
return Ok(None);
}
self.model_history.begin(history, self.cull_count());
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.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())
| crate::gfx::render_types::draw_args_bucket_bits(obj.shader_bucket)
| self.model_history.draw_flags(i, i),
});
}
if self.draw.n_instances > 0 {
let instance_base = args.len();
args.extend_from_slice(&self.instanced.draw_args);
if self.instanced.any_lod {
crate::gfx::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.draw.n_skinned > 0 {
let base = args.len();
for (k, obj) in self.skinned.draw_objects.iter().enumerate() {
let d = crate::gfx::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.model_history.skinned_flags(base + k, k),
});
}
}
let result = self.rings.draw_args.write(
&self.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: &crate::gfx::frustum::Frustum,
cam_pos: [f32; 3],
counts: crate::metal::context::DrawRecordCounts,
) -> Result<(), String> {
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: Some(super::pass_timing::PassId::Cull),
label: "cull phase1",
},
)?;
Ok(())
}
pub(in crate::metal) fn encode_mirror_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: &crate::gfx::frustum::Frustum,
cam_pos: [f32; 3],
slot: usize,
) -> Result<(), String> {
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 },
CullOutputTarget {
icbs: std::slice::from_ref(&mirror.icb),
arg_buf: &mirror.arg_buffer,
status,
},
CullDispatchOptions {
use_hiz: false,
timing: None,
label: "mirror cull",
},
)
}
fn encode_cull_into(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
scene: CullSceneBuffers,
view: CullView,
target: CullOutputTarget,
options: CullDispatchOptions,
) -> Result<(), String> {
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), Some(id)) = (&self.diagnostics.pass_timing, timing) {
t.attach_compute(&cull_pass_desc, id);
}
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&cull_pass_desc)
.ok_or("failed to get compute encoder")?,
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.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,
_pad: [0; 2],
},
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: &crate::gfx::frustum::Frustum,
cam_pos: [f32; 3],
) -> Result<u32, String> {
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("failed to get compute encoder")?,
"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.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,
_pad: [0; 2],
},
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>,
) -> Result<(), String> {
use crate::gfx::render_types::NUM_SHADOW_CASCADES;
use objc2_metal::{MTLComputeCommandEncoder as _, MTLResourceUsage};
let (Some(pipeline), Some(encode), Some(icb), Some(arg_buf), Some(status)) = (
&self.cull.shadow_pipeline,
&self.cull.encode_pipeline,
&self.cull.shadow_icb,
&self.cull.shadow_icb_arg_buffer,
&self.cull.shadow_status,
) else {
return Ok(());
};
let object_count = self.cull_count();
if object_count == 0 {
return Ok(());
}
let all = (1u32 << NUM_SHADOW_CASCADES) - 1;
let mask = if self.shadow.render_mask == 0 {
all
} else {
self.shadow.render_mask
};
let cull_pass_desc = MTLComputePassDescriptor::new();
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&cull_pass_desc)
.ok_or("failed to get shadow cull compute encoder")?,
"shadow cull",
);
enc.set_pipeline(pipeline);
enc.set_buffer(object_buffer, 0, 0);
enc.set_buffer(draw_args_buffer, 0, 1);
enc.set_buffer(&self.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.useResource_usage(ProtocolObject::from_ref(&**icb), MTLResourceUsage::Write);
let skinned_base = self.skinned_record_base() as u32;
for c in 0..NUM_SHADOW_CASCADES {
if mask & (1u32 << c) == 0 {
continue;
}
let frustum = crate::gfx::frustum::Frustum::from_view_projection(
self.shadow.uniforms.light_vps[c],
);
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: (c * object_count) as u32,
bucket_count: 1,
};
enc.set_value(&cull_uniforms, 2);
dispatch_records(&enc, pipeline, object_count);
}
enc.set_pipeline(encode);
enc.set_value(
&EncodeParams {
object_count: object_count as u32,
region_count: NUM_SHADOW_CASCADES as u32,
region_mask: mask,
skinned_base,
bucket_count: 1,
draw_status: CullStatus::DRAWN,
_pad: [0; 2],
},
CULL_ENCODE_PARAMS_INDEX,
);
dispatch_records(&enc, encode, NUM_SHADOW_CASCADES * object_count);
Ok(())
}
pub(super) fn build_bindless_texture_args(
&mut self,
ring_slot: usize,
) -> Result<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
use objc2_metal::MTLArgumentEncoder as _;
let enc = match &self.bindless_tex_arg_encoder {
Some(e) => e.clone(),
None => return Ok(None),
};
let len = enc.encodedLength().max(16);
let buf = self.rings.bindless_tex.slot(&self.device, ring_slot, len)?;
unsafe {
enc.setArgumentBuffer_offset(Some(&buf), 0);
}
let count = super::context::BINDLESS_TEXTURE_COUNT;
let texture_count = self.textures.len();
for i in 0..count {
let tex = if i < texture_count {
self.textures[i].as_ref()
} else if i == texture_count {
self.fallback_textures[0].as_ref()
} else {
self.fallback_textures[1].as_ref()
};
unsafe {
enc.setTexture_atIndex(Some(tex), i);
}
}
unsafe {
enc.setTexture_atIndex(Some(self.shadow.map.as_ref()), count);
enc.setTexture_atIndex(Some(self.env_map.irradiance.as_ref()), count + 1);
enc.setTexture_atIndex(Some(self.env_map.prefilter.as_ref()), count + 2);
enc.setTexture_atIndex(Some(self.ao_output_texture()), count + 3);
for i in 0..concinnity_core::render::uniforms::MAX_PROBES {
enc.setTexture_atIndex(Some(self.probe_cube_or_sky(i)), count + 4 + i);
}
enc.setTexture_atIndex(
Some(self.spot_shadow.map.as_ref()),
count + 4 + concinnity_core::render::uniforms::MAX_PROBES,
);
enc.setTexture_atIndex(
Some(self.ltc_matrix_texture.as_ref()),
count + 5 + concinnity_core::render::uniforms::MAX_PROBES,
);
enc.setTexture_atIndex(
Some(self.ltc_magnitude_texture.as_ref()),
count + 6 + concinnity_core::render::uniforms::MAX_PROBES,
);
}
Ok(Some(buf))
}
pub(super) fn use_bindless_textures(
&self,
encoder: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
) {
use objc2_metal::{MTLRenderStages, MTLResourceUsage};
for tex in self.textures.iter().chain(self.fallback_textures.iter()) {
encoder.useResource_usage_stages(
ProtocolObject::from_ref(&**tex),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
}
for tex in [
self.shadow.map.as_ref(),
self.spot_shadow.map.as_ref(),
self.ltc_matrix_texture.as_ref(),
self.ltc_magnitude_texture.as_ref(),
self.env_map.irradiance.as_ref(),
self.env_map.prefilter.as_ref(),
] {
encoder.useResource_usage_stages(
ProtocolObject::from_ref(tex),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
}
encoder.useResource_usage_stages(
ProtocolObject::from_ref(self.ao_output_texture()),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
for i in 0..concinnity_core::render::uniforms::MAX_PROBES {
encoder.useResource_usage_stages(
ProtocolObject::from_ref(self.probe_cube_or_sky(i)),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
}
}
}
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>>,
}
fn compute_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
function: &ProtocolObject<dyn objc2_metal::MTLFunction>,
what: &str,
) -> Result<Retained<ProtocolObject<dyn MTLComputePipelineState>>, String> {
device
.newComputePipelineStateWithFunction_error(function)
.map_err(|e| format!("failed to create {what} pipeline state: {e:?}"))
}
fn decision_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
lib: &super::slang_shaders::SlangLib,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLComputePipelineState>>, String> {
let function = super::slang_shaders::entry_function(device, lib, hot_reload)?;
compute_pipeline(device, &function, lib.name)
}
pub(super) fn build_cull_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<CullPipeline, String> {
let decide = decision_pipeline(device, &super::slang_shaders::CULL_PHASE1, hot_reload)?;
let decide_phase2 = decision_pipeline(device, &super::slang_shaders::CULL_PHASE2, hot_reload)?;
let library = shader_library(device, hot_reload, "cull_encode.metal")?;
let encode_fn = library
.newFunctionWithName(&ns_str("cull_encode"))
.ok_or("cull_encode not found in cull_encode library")?;
let encode = compute_pipeline(device, &encode_fn, "cull encode")?;
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,
) -> Result<Retained<ProtocolObject<dyn MTLComputePipelineState>>, String> {
decision_pipeline(device, &super::slang_shaders::CULL_SHADOW, hot_reload)
}
#[cfg(test)]
mod tests {
use super::metal_flat_pool_indices;
use crate::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);
}
}