#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLClearColor, MTLCommandBuffer as _, MTLDevice as _, MTLLoadAction, MTLPixelFormat,
MTLRenderCommandEncoder as _, MTLRenderPassDescriptor, MTLRenderPipelineDescriptor,
MTLRenderPipelineState, MTLStoreAction, MTLTexture, MTLTextureUsage, MTLVertexDescriptor,
MTLVertexFormat, MTLVertexStepFunction,
};
use crate::gfx::mesh_payload::Vertex;
use crate::metal::context::MtlContext;
use crate::metal::descriptors::{TextureDesc, VertexAttr, VertexLayout, vertex_descriptor};
use crate::metal::encode::RenderEncode;
use crate::metal::scoped_encoder::ScopedEncoder;
use crate::metal::slang_shaders::{self, SlangLib};
use crate::metal::uniforms::SsrPrepassMat;
use concinnity_render::uniforms::GBufferModel;
use concinnity_render::uniforms::GBufferView;
pub(crate) struct GBufferState {
pub targets: Option<GBufferTargets>,
pub prepass_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub instanced_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub skinned_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub bindless_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
}
pub(crate) struct GBufferTargets {
pub depth: Retained<ProtocolObject<dyn MTLTexture>>,
}
pub(crate) fn create_gbuffer_targets(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
) -> Result<GBufferTargets, String> {
let desc = TextureDesc {
format: MTLPixelFormat::Depth32Float,
width: width.max(1) as usize,
height: height.max(1) as usize,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
let depth = device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create G-buffer depth texture")?;
Ok(GBufferTargets { depth })
}
pub(crate) fn build_gbuffer_prepass_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
vert_desc: &MTLVertexDescriptor,
vertex: &SlangLib,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let vert_fn = slang_shaders::entry_function(device, vertex, hot_reload)?;
let frag_fn =
slang_shaders::entry_function(device, &slang_shaders::GBUFFER_PREPASS_FRAG, hot_reload)?;
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let ca0 = desc.colorAttachments().objectAtIndexedSubscript(0);
ca0.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca0.setBlendingEnabled(false);
let ca1 = desc.colorAttachments().objectAtIndexedSubscript(1);
ca1.setPixelFormat(MTLPixelFormat::R8Unorm);
ca1.setBlendingEnabled(false);
let ca2 = desc.colorAttachments().objectAtIndexedSubscript(2);
ca2.setPixelFormat(MTLPixelFormat::RG16Float);
ca2.setBlendingEnabled(false);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| format!("failed to create G-buffer pre-pass pipeline: {:?}", e))
}
pub(crate) fn gbuffer_bindless_vertex_descriptor() -> Retained<MTLVertexDescriptor> {
vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 1,
}, VertexAttr {
index: 1,
format: MTLVertexFormat::Float3,
offset: 12,
buffer_index: 1,
}, VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 1,
}, VertexAttr {
index: 5,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 2,
}, ],
&[
VertexLayout {
buffer_index: 1,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
},
VertexLayout {
buffer_index: 2,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
},
],
)
}
pub(crate) fn build_gbuffer_bindless_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let vert_fn = slang_shaders::entry_function(
device,
&slang_shaders::GBUFFER_PREPASS_VERT_BINDLESS,
hot_reload,
)?;
let frag_fn = slang_shaders::entry_function(
device,
&slang_shaders::GBUFFER_PREPASS_FRAG_BINDLESS,
hot_reload,
)?;
let vert_desc = gbuffer_bindless_vertex_descriptor();
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let ca0 = desc.colorAttachments().objectAtIndexedSubscript(0);
ca0.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca0.setBlendingEnabled(false);
let ca1 = desc.colorAttachments().objectAtIndexedSubscript(1);
ca1.setPixelFormat(MTLPixelFormat::R8Unorm);
ca1.setBlendingEnabled(false);
let ca2 = desc.colorAttachments().objectAtIndexedSubscript(2);
ca2.setPixelFormat(MTLPixelFormat::RG16Float);
ca2.setBlendingEnabled(false);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
desc.setSupportIndirectCommandBuffers(true);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| format!("failed to create G-buffer bindless pipeline: {:?}", e))
}
pub(in crate::metal) struct GbufferSceneInputs<'a> {
pub visible: &'a [u32],
pub cam_pos: [f32; 3],
pub prepared_instances: &'a super::super::instanced::PreparedInstances,
pub cur_joint_bufs: &'a [Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>],
pub prev_joint_bufs: &'a [Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>],
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct GbufferGpuBuffers<'a> {
pub object_buffer: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub prev_model_buffer: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub deformed_current: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub deformed_prev: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
}
impl MtlContext {
pub(in crate::metal) fn encode_gbuffer_prepass(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
view: &GBufferView,
scene: GbufferSceneInputs,
gpu: GbufferGpuBuffers,
velocity_active: bool,
) -> Result<u32, String> {
let GbufferSceneInputs {
visible,
cam_pos,
prepared_instances,
cur_joint_bufs,
prev_joint_bufs,
} = scene;
let (targets, static_ps) = match (&self.gbuffer.targets, &self.gbuffer.prepass_pipeline) {
(Some(t), Some(p)) => (t, p),
_ => return Ok(0),
};
let (normal_depth, roughness, velocity) = match (
self.gbuffer_normal_depth(),
self.gbuffer_roughness(),
self.gbuffer_velocity(),
) {
(Some(n), Some(r), Some(v)) => (n, r, v),
_ => {
return Err(
"G-buffer pre-pass: the transient pool is missing a colour channel; \
its build gate disagrees with the pre-pass's"
.to_string(),
);
}
};
let desc = MTLRenderPassDescriptor::new();
unsafe {
let ca0 = desc.colorAttachments().objectAtIndexedSubscript(0);
ca0.setTexture(Some(normal_depth));
ca0.setLoadAction(MTLLoadAction::Clear);
ca0.setStoreAction(MTLStoreAction::Store);
ca0.setClearColor(MTLClearColor {
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: 0.0,
});
let ca1 = desc.colorAttachments().objectAtIndexedSubscript(1);
ca1.setTexture(Some(roughness));
ca1.setLoadAction(MTLLoadAction::Clear);
ca1.setStoreAction(MTLStoreAction::Store);
ca1.setClearColor(MTLClearColor {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 0.0,
});
let ca2 = desc.colorAttachments().objectAtIndexedSubscript(2);
ca2.setTexture(Some(velocity));
ca2.setLoadAction(MTLLoadAction::Clear);
ca2.setStoreAction(MTLStoreAction::Store);
ca2.setClearColor(MTLClearColor {
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: 0.0,
});
let da = desc.depthAttachment();
da.setTexture(Some(targets.depth.as_ref()));
da.setLoadAction(MTLLoadAction::Clear);
da.setClearDepth(1.0);
da.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&desc, crate::metal::pass_timing::PassId::GBufferPrepass);
}
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&desc)
.ok_or("failed to get G-buffer pre-pass encoder")?,
"g-buffer prepass",
);
if self.gbuffer.bindless_pipeline.is_some() && gpu.object_buffer.is_some() {
let draws = self.encode_gbuffer_prepass_gpu_driven(&enc, view, gpu, velocity_active);
return Ok(draws);
}
enc.set_pipeline(static_ps);
enc.set_depth_stencil(&self.depth_state);
enc.set_vertex_value(view, 0);
enc.set_vertex_buffer(&self.vertex_buffer, 0, 1);
let mut draws = self.draw_static_objects(&enc, visible, cam_pos, |enc, obj, idx| {
let model = GBufferModel {
cur_model: obj.model,
prev_model: if velocity_active {
self.prev_draw_models[idx]
} else {
obj.model
},
};
let mat = SsrPrepassMat {
roughness: obj.material.roughness,
_pad: [0.0; 3],
};
enc.set_vertex_value(&model, 2);
enc.set_fragment_value(&mat, 0);
});
if let Some(inst_ps) = &self.gbuffer.instanced_pipeline
&& !prepared_instances.clusters.is_empty()
{
enc.set_pipeline(inst_ps);
draws +=
self.draw_prepared_instances(&enc, prepared_instances, true, |enc, cluster| {
let mat = SsrPrepassMat {
roughness: cluster.material.roughness,
_pad: [0.0; 3],
};
enc.set_fragment_value(&mat, 0);
});
}
if let (Some(skinned_ps), Some(svb), Some(sib)) = (
&self.gbuffer.skinned_pipeline,
&self.skinned.vertex_buffer,
&self.skinned.index_buffer,
) && !self.skinned.draw_objects.is_empty()
{
enc.set_pipeline(skinned_ps);
enc.set_vertex_buffer(svb, 0, 1);
draws += self.draw_skinned_objects(&enc, sib, cam_pos, |enc, obj, i| {
let model = GBufferModel {
cur_model: obj.model,
prev_model: obj.model,
};
let mat = SsrPrepassMat {
roughness: obj.material.roughness,
_pad: [0.0; 3],
};
let prev = prev_joint_bufs.get(i).unwrap_or(&cur_joint_bufs[i]);
enc.set_vertex_value(&model, 2);
enc.set_fragment_value(&mat, 0);
enc.set_vertex_buffer(&cur_joint_bufs[i], 0, 8);
enc.set_vertex_buffer(prev, 0, 9);
});
}
Ok(draws)
}
fn encode_gbuffer_prepass_gpu_driven(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view: &GBufferView,
gpu: GbufferGpuBuffers,
velocity_active: bool,
) -> u32 {
use objc2_metal::{MTLRenderStages, MTLResourceUsage};
use std::sync::atomic::Ordering;
let GbufferGpuBuffers {
object_buffer,
prev_model_buffer,
deformed_current,
deformed_prev,
} = gpu;
let (Some(pipeline), Some(object_buffer), Some(prev_models)) = (
self.gbuffer.bindless_pipeline.as_ref(),
object_buffer,
prev_model_buffer,
) else {
return 0;
};
if self.cull.icbs.is_empty() {
return 0;
}
enc.set_pipeline(pipeline);
enc.set_depth_stencil(&self.depth_state);
enc.set_vertex_value(view, 0);
enc.set_vertex_buffer(object_buffer, 0, 9);
enc.set_vertex_buffer(prev_models, 0, 10);
enc.set_vertex_buffer(&self.vertex_buffer, 0, 1);
enc.set_vertex_buffer(&self.vertex_buffer, 0, 2);
let counts = self.draw_record_counts();
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 self.cull.icbs.iter().enumerate() {
if !self.world_shader_resident(b) {
continue;
}
unsafe {
enc.executeCommandsInBuffer_withRange(icb, range);
}
draw_calls += 1;
}
}
if let (Some(deformed), Some(tail)) = (deformed_current, counts.skinned_tail(0)) {
let prev = if velocity_active
&& self.frames_in_flight >= 2
&& self.skinned.deformed_primed.load(Ordering::Relaxed)
{
deformed_prev.unwrap_or(deformed)
} else {
deformed
};
enc.set_vertex_buffer(deformed, 0, 1);
enc.set_vertex_buffer(prev, 0, 2);
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(
&self.cull.icbs[0],
crate::metal::context::ns_range(tail),
);
}
draw_calls += 1;
self.skinned.deformed_primed.store(true, Ordering::Relaxed);
}
draw_calls
}
pub(in crate::metal) fn build_gbuffer_prev_models(
&mut self,
ring_slot: usize,
velocity_active: bool,
) -> Result<Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>, String> {
if self.draw.objects.is_empty() {
return Ok(None);
}
let mut models = std::mem::take(&mut self.rings.prev_model_scratch);
models.clear();
for (i, obj) in self.draw.objects.iter().enumerate() {
models.push(if velocity_active {
self.prev_draw_models[i]
} else {
obj.model
});
}
if self.draw.n_instances > 0 {
models.extend(self.instanced.records.iter().map(|r| r.model));
}
if self.draw.n_skinned > 0 {
models.extend(self.skinned.draw_objects.iter().map(|o| o.model));
}
let result = self.rings.prev_model.write(
&self.device,
ring_slot,
crate::metal::context::bytes_of_slice(&models),
);
self.rings.prev_model_scratch = models;
result.map(Some)
}
}