#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer, MTLClearColor, MTLCommandBuffer as _, MTLCommandEncoder as _, MTLLoadAction,
MTLRenderCommandEncoder as _, MTLRenderPassDescriptor, MTLStoreAction,
};
use crate::metal::context::{BINDLESS_TEXTURE_ARG_BUFFER_INDEX, MtlContext};
use crate::metal::encode::RenderEncode;
use crate::metal::scoped_encoder::ScopedEncoder;
use crate::metal::uniforms::ModelUniforms;
use concinnity_core::render::uniforms::ViewUniforms;
#[derive(Clone, Copy)]
pub(in crate::metal) struct MainPassCamera {
pub elapsed: f32,
pub vp: [[f32; 4]; 4],
pub view: [[f32; 4]; 4],
pub cam_pos: [f32; 3],
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct GpuFrameBuffers<'a> {
pub object_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub bindless_tex_args: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub deformed_skinned: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub counts: crate::metal::context::DrawRecordCounts,
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct DrawInputs<'a> {
pub visible: &'a [u32],
pub prepared_instances: &'a super::super::instanced::PreparedInstances,
pub skinned_joint_bufs: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct FaceTargets<'a> {
pub color_msaa: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
pub depth_msaa: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
pub resolve: &'a ProtocolObject<dyn objc2_metal::MTLTexture>,
}
impl MtlContext {
fn reflection_resolve_active(&self) -> f32 {
if self.ssr.settings.is_some() || self.rt.accel.is_some() {
1.0
} else {
0.0
}
}
fn shade_mode(&self) -> f32 {
if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Unlit {
1.0
} else {
0.0
}
}
pub(in crate::metal) fn encode_main_pass(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
camera: MainPassCamera,
draw_inputs: DrawInputs,
gpu: GpuFrameBuffers,
world_hidden: bool,
) -> Result<u32, String> {
let MainPassCamera {
elapsed,
vp,
view: _,
cam_pos,
} = camera;
let DrawInputs {
visible,
prepared_instances,
skinned_joint_bufs,
} = draw_inputs;
let object_buffer = gpu.object_buffer;
let store_msaa_color = self.cull.two_pass_occlusion
&& object_buffer.is_some()
&& self.cull.pipeline_phase2.is_some();
let main_pass_desc = MTLRenderPassDescriptor::new();
let [r, g, b, a] = self.view.clear_color;
unsafe {
let ca = main_pass_desc
.colorAttachments()
.objectAtIndexedSubscript(0);
ca.setTexture(Some(self.hdr_targets.hdr_color.as_ref()));
ca.setResolveTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
ca.setLoadAction(MTLLoadAction::Clear);
ca.setStoreAction(if store_msaa_color {
MTLStoreAction::StoreAndMultisampleResolve
} else {
MTLStoreAction::MultisampleResolve
});
ca.setClearColor(MTLClearColor {
red: r as f64,
green: g as f64,
blue: b as f64,
alpha: a as f64,
});
let da = main_pass_desc.depthAttachment();
da.setTexture(Some(self.hdr_targets.depth.as_ref()));
da.setLoadAction(MTLLoadAction::Clear);
da.setClearDepth(1.0);
da.setResolveTexture(Some(self.hdr_targets.depth_resolve.as_ref()));
da.setDepthResolveFilter(objc2_metal::MTLMultisampleDepthResolveFilter::Sample0);
da.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&main_pass_desc, super::super::pass_timing::PassId::Main);
}
let encoder = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&main_pass_desc)
.ok_or("failed to get render encoder")?,
"main pass",
);
if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Wireframe {
encoder.setTriangleFillMode(objc2_metal::MTLTriangleFillMode::Lines);
}
let view_uniforms = ViewUniforms {
vp,
view: self.view.matrix,
elapsed,
reflections_enabled: self.reflection_resolve_active(),
cam_pos,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
shade_mode: self.shade_mode(),
_end_pad: 0.0,
};
if world_hidden || self.pipeline_state.is_none() {
return Ok(0);
}
self.bind_clusters(&encoder, true);
let count_static = self.encode_main_static_into(
&encoder,
&view_uniforms,
cam_pos,
visible,
gpu,
None,
);
let count_instanced =
self.encode_main_instanced_into(&encoder, &view_uniforms, prepared_instances);
let count_skinned =
self.encode_main_skinned_into(&encoder, &view_uniforms, cam_pos, skinned_joint_bufs);
Ok(count_static + count_instanced + count_skinned)
}
pub(in crate::metal) fn encode_main_into_face(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
face_targets: FaceTargets,
camera: MainPassCamera,
draw_inputs: DrawInputs,
gpu: GpuFrameBuffers,
icb_override: Option<&ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>,
) -> Result<u32, String> {
let FaceTargets {
color_msaa: face_color_msaa,
depth_msaa: face_depth_msaa,
resolve: face_resolve,
} = face_targets;
let MainPassCamera {
elapsed,
vp,
view,
cam_pos,
} = camera;
let DrawInputs {
visible,
prepared_instances,
skinned_joint_bufs,
} = draw_inputs;
let desc = MTLRenderPassDescriptor::new();
let [r, g, b, a] = self.view.clear_color;
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setTexture(Some(face_color_msaa));
ca.setResolveTexture(Some(face_resolve));
ca.setLoadAction(MTLLoadAction::Clear);
ca.setStoreAction(MTLStoreAction::MultisampleResolve);
ca.setClearColor(MTLClearColor {
red: r as f64,
green: g as f64,
blue: b as f64,
alpha: a as f64,
});
let da = desc.depthAttachment();
da.setTexture(Some(face_depth_msaa));
da.setLoadAction(MTLLoadAction::Clear);
da.setClearDepth(1.0);
da.setStoreAction(MTLStoreAction::DontCare);
}
let encoder = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&desc)
.ok_or("failed to get probe render encoder")?,
"probe face",
);
let view_uniforms = ViewUniforms {
vp,
view,
elapsed,
reflections_enabled: 0.0,
cam_pos,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
shade_mode: 0.0,
_end_pad: 0.0,
};
self.bind_clusters(&encoder, false);
let count_static = self.encode_main_static_into(
&encoder,
&view_uniforms,
cam_pos,
visible,
gpu,
icb_override,
);
let count_instanced =
self.encode_main_instanced_into(&encoder, &view_uniforms, prepared_instances);
let count_skinned =
self.encode_main_skinned_into(&encoder, &view_uniforms, cam_pos, skinned_joint_bufs);
Ok(count_static + count_instanced + count_skinned)
}
pub(in crate::metal) fn encode_main_pass_phase2(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
camera: MainPassCamera,
gpu: GpuFrameBuffers,
) -> Result<u32, String> {
let MainPassCamera {
elapsed,
vp,
view: _,
cam_pos,
} = camera;
let GpuFrameBuffers {
object_buffer,
bindless_tex_args,
deformed_skinned,
counts,
} = gpu;
let (Some(obj_buf), Some(tex_args), false) = (
object_buffer,
bindless_tex_args,
self.cull.icbs_2.is_empty(),
) else {
return Ok(0);
};
let main_pass_desc = MTLRenderPassDescriptor::new();
unsafe {
let ca = main_pass_desc
.colorAttachments()
.objectAtIndexedSubscript(0);
ca.setTexture(Some(self.hdr_targets.hdr_color.as_ref()));
ca.setResolveTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
ca.setLoadAction(MTLLoadAction::Load);
ca.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);
let da = main_pass_desc.depthAttachment();
da.setTexture(Some(self.hdr_targets.depth.as_ref()));
da.setLoadAction(MTLLoadAction::Load);
da.setResolveTexture(Some(self.hdr_targets.depth_resolve.as_ref()));
da.setDepthResolveFilter(objc2_metal::MTLMultisampleDepthResolveFilter::Sample0);
da.setStoreAction(MTLStoreAction::StoreAndMultisampleResolve);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&main_pass_desc, super::super::pass_timing::PassId::Main2);
}
let encoder = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&main_pass_desc)
.ok_or("failed to get render encoder")?,
"main2 pass",
);
if self.view.mode == concinnity_core::gfx::view_modes::ViewMode::Wireframe {
encoder.setTriangleFillMode(objc2_metal::MTLTriangleFillMode::Lines);
}
let view_uniforms = ViewUniforms {
vp,
view: self.view.matrix,
elapsed,
reflections_enabled: self.reflection_resolve_active(),
cam_pos,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
shade_mode: self.shade_mode(),
_end_pad: 0.0,
};
self.bind_main_pass_shared(&encoder, &view_uniforms);
self.bind_clusters(&encoder, true);
let draw_calls = self.execute_bindless_static_icb(
&encoder,
obj_buf,
tex_args,
&self.cull.icbs_2,
deformed_skinned,
counts,
);
Ok(draw_calls)
}
fn execute_bindless_static_icb(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
obj_buf: &Retained<ProtocolObject<dyn MTLBuffer>>,
tex_args: &Retained<ProtocolObject<dyn MTLBuffer>>,
icbs: &[Retained<ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>],
deformed_skinned: Option<&Retained<ProtocolObject<dyn MTLBuffer>>>,
counts: crate::metal::context::DrawRecordCounts,
) -> u32 {
use objc2_metal::{MTLRenderStages, MTLResourceUsage};
enc.set_vertex_buffer(obj_buf, 0, 9);
enc.set_fragment_buffer(obj_buf, 0, 9);
enc.set_fragment_buffer(tex_args, 0, BINDLESS_TEXTURE_ARG_BUFFER_INDEX);
if let Some(sampler_args) = &self.bindless_sampler_args {
enc.set_fragment_buffer(
sampler_args,
0,
crate::metal::context::BINDLESS_SAMPLER_ARG_BUFFER_INDEX,
);
}
self.use_bindless_textures(enc);
let mut draw_calls = 0u32;
if let Some(prefix) = counts.prefix(0) {
enc.useResource_usage_stages(
ProtocolObject::from_ref(&*self.index_buffer),
MTLResourceUsage::Read,
MTLRenderStages::Vertex,
);
let range = crate::metal::context::ns_range(prefix);
for (b, icb) in icbs.iter().enumerate() {
if b > 0 {
let Some(pso) = self.world_pipeline(b) else {
continue;
};
enc.set_pipeline(pso);
}
unsafe {
enc.executeCommandsInBuffer_withRange(icb, range);
}
draw_calls += 1;
}
if icbs.len() > 1
&& let Some(ps) = &self.pipeline_state
{
enc.set_pipeline(ps);
}
}
if let (Some(deformed), Some(icb0), Some(tail)) =
(deformed_skinned, icbs.first(), counts.skinned_tail(0))
{
enc.set_vertex_buffer(deformed, 0, 1);
if let Some(skinned_ib) = self.skinned.index_buffer.as_ref() {
enc.useResource_usage_stages(
ProtocolObject::from_ref(&**skinned_ib),
MTLResourceUsage::Read,
MTLRenderStages::Vertex,
);
}
unsafe {
enc.executeCommandsInBuffer_withRange(icb0, crate::metal::context::ns_range(tail));
}
draw_calls += 1;
}
draw_calls
}
fn bind_clusters(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
clustered: bool,
) {
let cluster_params = if clustered {
self.cluster_params
} else {
crate::gfx::render_types::ClusterParams {
use_clusters: 0,
..self.cluster_params
}
};
enc.set_fragment_value(&cluster_params, 11);
enc.set_fragment_buffer(&self.light_cull.cluster_buffer, 0, 12);
}
fn bind_main_pass_shared(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view_uniforms: &ViewUniforms,
) -> bool {
let Some(pipeline_state) = &self.pipeline_state else {
return false;
};
enc.set_pipeline(pipeline_state);
enc.set_depth_stencil(&self.depth_state);
enc.set_vertex_value(view_uniforms, 0);
enc.set_fragment_value(view_uniforms, 0);
enc.set_vertex_buffer(&self.vertex_buffer, 0, 1);
enc.set_fragment_value(&self.light_uniforms, 4);
enc.set_fragment_buffer(&self.local_light_buffer, 0, 8);
enc.set_fragment_buffer(&self.spot_shadow.buffer, 0, 13);
enc.set_fragment_buffer(&self.area_light_buffer, 0, 14);
enc.set_fragment_value(&self.shadow.uniforms, 5);
enc.set_fragment_texture(self.shadow.map.as_ref(), 2);
enc.set_fragment_texture(self.spot_shadow.map.as_ref(), 14);
enc.set_fragment_texture(self.ltc_matrix_texture.as_ref(), 15);
enc.set_fragment_texture(self.ltc_magnitude_texture.as_ref(), 16);
enc.set_fragment_sampler(self.shadow.sampler.as_ref(), 1);
enc.set_fragment_texture(self.env_map.irradiance.as_ref(), 3);
enc.set_fragment_texture(self.env_map.prefilter.as_ref(), 4);
enc.set_fragment_sampler(self.cube_sampler.as_ref(), 2);
enc.set_fragment_texture(self.ao_output_texture(), 5);
for i in 0..concinnity_core::render::uniforms::MAX_PROBES {
enc.set_fragment_texture(self.probe_cube_or_sky(i), 6 + i);
}
enc.set_fragment_value(&self.probe.set, 6);
true
}
fn encode_main_static_into(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view_uniforms: &ViewUniforms,
cam_pos: [f32; 3],
visible: &[u32],
gpu: GpuFrameBuffers,
icb_override: Option<&ProtocolObject<dyn objc2_metal::MTLIndirectCommandBuffer>>,
) -> u32 {
let GpuFrameBuffers {
object_buffer,
bindless_tex_args,
deformed_skinned,
counts,
} = gpu;
enc.pushDebugGroup(&objc2_foundation::NSString::from_str("main static"));
if !self.bind_main_pass_shared(enc, view_uniforms) {
enc.popDebugGroup();
return 0;
}
let mut draw_calls: u32 = 0;
let override_holder;
let bucket_icbs: &[Retained<_>] = match icb_override {
Some(icb) => {
override_holder = [objc2::Message::retain(icb)];
&override_holder
}
None => &self.cull.icbs,
};
if let (Some(obj_buf), Some(tex_args), false) =
(object_buffer, bindless_tex_args, bucket_icbs.is_empty())
{
draw_calls += self.execute_bindless_static_icb(
enc,
obj_buf,
tex_args,
bucket_icbs,
deformed_skinned,
counts,
);
} else {
draw_calls += self.draw_static_objects(enc, visible, cam_pos, |enc, obj, _| {
let model_uniforms = ModelUniforms { model: obj.model };
enc.set_vertex_value(&model_uniforms, 2);
enc.set_fragment_value(&obj.material, 3);
enc.set_fragment_texture(self.albedo_pool_texture(obj.texture_slot), 0);
enc.set_fragment_texture(self.normal_pool_texture(obj.normal_map_slot), 1);
enc.set_fragment_sampler(&self.sampler, 0);
});
}
enc.popDebugGroup();
draw_calls
}
fn encode_main_instanced_into(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view_uniforms: &ViewUniforms,
prepared: &super::super::instanced::PreparedInstances,
) -> u32 {
let Some(inst_ps) = self.instanced.pipeline_state.clone() else {
return 0;
};
if prepared.clusters.is_empty() {
return 0;
}
if self.bindless && !self.draw.objects.is_empty() {
return 0;
}
enc.pushDebugGroup(&objc2_foundation::NSString::from_str("main instanced"));
if !self.bind_main_pass_shared(enc, view_uniforms) {
enc.popDebugGroup();
return 0;
}
enc.set_pipeline(&inst_ps);
let draw_calls = self.draw_prepared_instances(enc, prepared, false, |enc, cluster| {
enc.set_fragment_value(&cluster.material, 3);
enc.set_fragment_texture(self.albedo_pool_texture(cluster.texture_slot), 0);
enc.set_fragment_texture(self.normal_pool_texture(cluster.normal_map_slot), 1);
enc.set_fragment_sampler(&self.sampler, 0);
});
if let Some(pipeline_state) = &self.pipeline_state {
enc.set_pipeline(pipeline_state);
}
enc.popDebugGroup();
draw_calls
}
fn encode_main_skinned_into(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view_uniforms: &ViewUniforms,
cam_pos: [f32; 3],
skinned_joint_bufs: &[Retained<ProtocolObject<dyn MTLBuffer>>],
) -> u32 {
let (Some(skinned_ps), Some(svb), Some(sib)) = (
self.skinned.pipeline_state.clone(),
self.skinned.vertex_buffer.clone(),
self.skinned.index_buffer.clone(),
) else {
return 0;
};
if self.skinned.draw_objects.is_empty() {
return 0;
}
if self.draw.n_skinned > 0 {
return 0;
}
enc.pushDebugGroup(&objc2_foundation::NSString::from_str("main skinned"));
if !self.bind_main_pass_shared(enc, view_uniforms) {
enc.popDebugGroup();
return 0;
}
enc.set_pipeline(&skinned_ps);
enc.set_vertex_buffer(&svb, 0, 1);
let draw_calls = self.draw_skinned_objects(enc, &sib, cam_pos, |enc, obj, i| {
let model_uniforms = ModelUniforms { model: obj.model };
let morph = self.skinned.morphs.get(i).and_then(|m| m.as_ref());
let mut morph_params = crate::metal::uniforms::VsMorphParams {
vertex_base: obj.vertex_base,
vertex_count: obj.vertex_count as u32,
target_count: morph.map_or(0, |m| m.target_count),
_pad: 0,
weights: [0.0; crate::metal::uniforms::MAX_MORPH_TARGETS],
};
if let Some(w) = self.skinned.morph_weights.get(i) {
for (dst, src) in morph_params.weights.iter_mut().zip(w.iter()) {
*dst = *src;
}
}
enc.set_vertex_value(&model_uniforms, 2);
enc.set_vertex_buffer(&skinned_joint_bufs[i], 0, 8);
let morph_buf = morph.map_or(svb.as_ref(), |m| m.buffer.as_ref());
enc.set_vertex_buffer(morph_buf, 0, 9);
enc.set_vertex_value(&morph_params, 10);
enc.set_fragment_value(&obj.material, 3);
enc.set_fragment_texture(self.albedo_pool_texture(obj.texture_slot), 0);
enc.set_fragment_texture(self.normal_pool_texture(obj.normal_map_slot), 1);
enc.set_fragment_sampler(&self.sampler, 0);
});
enc.popDebugGroup();
draw_calls
}
}