#![deny(unsafe_op_in_unsafe_fn)]
use crate::metal::depth::CLEAR_DEPTH;
use crate::metal::error::allocation_failed;
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::shader_programs::metal::prepass_buffers::{
DRAW_ARGS as PREPASS_DRAW_ARGS_INDEX, PREV_MODELS as PREPASS_PREV_MODELS_INDEX,
VIEW as PREPASS_VIEW_INDEX,
};
use concinnity_core::render::uniforms::{GBufferView, ViewUniforms};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::ns_string;
use objc2_metal::{
MTLClearColor, MTLCommandBuffer as _, MTLDevice as _, MTLLoadAction, MTLPixelFormat,
MTLRenderCommandEncoder as _, MTLRenderPassDescriptor, MTLRenderPipelineDescriptor,
MTLRenderPipelineState, MTLStoreAction, MTLTexture, MTLTextureUsage, MTLVertexDescriptor,
MTLVertexFormat, MTLVertexStepFunction,
};
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;
pub(crate) struct GBufferState {
pub targets: Option<GBufferTargets>,
pub history_pipeline:
Option<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>>,
}
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,
) -> RenderResult<GBufferTargets> {
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_else(|| allocation_failed("G-buffer depth texture"))?;
Ok(GBufferTargets { depth })
}
pub(in crate::metal) const GBUFFER_FORMATS: [MTLPixelFormat; 3] = [
MTLPixelFormat::RGBA16Float,
MTLPixelFormat::R8Unorm,
MTLPixelFormat::RG16Float,
];
fn gbuffer_prepass_vertex_descriptor() -> Retained<MTLVertexDescriptor> {
let attr = |index, format, offset, buffer_index| VertexAttr {
index,
format,
offset,
buffer_index,
};
let layout = |buffer_index| VertexLayout {
buffer_index,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
};
vertex_descriptor(
&[
attr(0, MTLVertexFormat::Float3, 0, 1), attr(1, MTLVertexFormat::Float3, 12, 1), attr(2, MTLVertexFormat::Float3, 24, 1), attr(3, MTLVertexFormat::Float3, 36, 1), attr(4, MTLVertexFormat::Float2, 48, 1), attr(5, MTLVertexFormat::Float3, 0, 2), ],
&[layout(1), layout(2)],
)
}
pub(crate) fn build_gbuffer_prepass_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
vert_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
frag_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let vert_desc = gbuffer_prepass_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 targets = desc.colorAttachments();
for (i, format) in GBUFFER_FORMATS.iter().enumerate() {
let target = targets.objectAtIndexedSubscript(i);
target.setPixelFormat(*format);
target.setBlendingEnabled(false);
}
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
desc.setSupportIndirectCommandBuffers(true);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
RenderError::ShaderCompile(format!(
"failed to create G-buffer pre-pass pipeline: {e:?}"
))
})
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct GbufferPrepassViews<'a> {
pub gbuffer: &'a GBufferView,
pub main: &'a ViewUniforms,
pub raymarch: &'a crate::metal::raymarch::RaymarchView,
pub frustum: &'a concinnity_core::gfx::frustum::Frustum,
}
#[derive(Clone, Copy)]
pub(in crate::metal) struct GbufferGpuBuffers<'a> {
pub object_buffer: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub material_params: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub bindless_tex_args: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub prev_model_buffer: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub draw_args_buffer: Option<&'a Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub history_targets: &'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 reads_motion(&self) -> bool {
self.taa.enabled
|| self.upscale.scaler.is_some()
|| (self.ssgi.pass.is_some() && self.ssgi.settings.is_some_and(|s| s.contributes()))
}
pub(in crate::metal) fn encode_gbuffer_prepass(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
views: GbufferPrepassViews<'_>,
gpu: GbufferGpuBuffers,
velocity_active: bool,
) -> RenderResult<u32> {
let Some(targets) = &self.gbuffer.targets else {
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(RenderError::Other(
"G-buffer pre-pass: the transient pool is missing a color 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(CLEAR_DEPTH);
da.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&desc, crate::metal::pass_timing::PassId::GBufferPrepass);
}
let snapshot = gpu.object_buffer.cloned();
let history_targets = gpu.history_targets;
let draw_calls = {
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&desc)
.ok_or_else(|| {
RenderError::Other("failed to get G-buffer pre-pass encoder".to_string())
})?,
ns_string!("g-buffer prepass"),
);
let draws = self.encode_gbuffer_prepass_gpu_driven(&enc, views, gpu, velocity_active)
+ self.encode_raymarch_prepass(&enc, views.raymarch, views.frustum);
if self.draws_sky(self.state.view.mode) {
self.encode_sky_velocity(&enc, views.gbuffer);
}
draws
};
if let Some(objects) = snapshot.as_ref() {
self.encode_model_history(cmd_buf, objects, history_targets, self.cull_count())?;
}
Ok(draw_calls)
}
fn encode_gbuffer_prepass_gpu_driven(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
views: GbufferPrepassViews<'_>,
gpu: GbufferGpuBuffers,
velocity_active: bool,
) -> u32 {
use objc2_metal::{MTLRenderStages, MTLResourceUsage};
use std::sync::atomic::Ordering;
let GbufferGpuBuffers {
object_buffer,
material_params,
bindless_tex_args,
prev_model_buffer,
draw_args_buffer,
history_targets: _,
deformed_current,
deformed_prev,
} = gpu;
let (
Some(default),
Some(object_buffer),
Some(tex_args),
Some(prev_models),
Some(draw_args),
) = (
self.cull.main_pipeline.as_ref(),
object_buffer,
bindless_tex_args,
prev_model_buffer,
draw_args_buffer,
)
else {
return 0;
};
if self.cull.icbs.is_empty() {
return 0;
}
enc.set_depth_stencil(&self.targets.depth_state);
enc.set_vertex_value(views.main, 0);
enc.set_vertex_value(views.gbuffer, PREPASS_VIEW_INDEX);
enc.set_fragment_value(views.gbuffer, PREPASS_VIEW_INDEX);
enc.set_vertex_buffer(object_buffer, 0, 9);
enc.set_fragment_buffer(object_buffer, 0, 9);
if let Some(params) = material_params {
enc.set_vertex_buffer(
params,
0,
crate::metal::material_params::MATERIAL_PARAMS_BUFFER_INDEX,
);
}
enc.set_vertex_buffer(prev_models, 0, PREPASS_PREV_MODELS_INDEX);
enc.set_vertex_buffer(draw_args, 0, PREPASS_DRAW_ARGS_INDEX);
enc.set_fragment_buffer(
tex_args,
0,
crate::metal::context::BINDLESS_TEXTURE_ARG_BUFFER_INDEX,
);
if let Some(sampler_args) = &self.arg_buffers.bindless_sampler_args {
enc.set_fragment_buffer(
sampler_args,
0,
crate::metal::context::BINDLESS_SAMPLER_ARG_BUFFER_INDEX,
);
}
self.use_bindless_textures(enc);
enc.set_vertex_buffer(&self.scene.vertex_buffer, 0, 1);
enc.set_vertex_buffer(&self.scene.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.scene.index_buffer),
MTLResourceUsage::Read,
MTLRenderStages::Vertex,
);
let range = crate::metal::context::ns_range(prefix);
for (b, icb) in self.cull.icbs.iter().enumerate() {
let pipelines = match b {
0 => Some(default),
b => self.cull.world_pipelines.get(b),
};
let Some(prepass) = pipelines.and_then(|p| p.prepass.as_ref()) else {
continue;
};
enc.set_pipeline(prepass);
unsafe {
enc.executeCommandsInBuffer_withRange(icb, range);
}
draw_calls += 1;
}
}
if let (Some(deformed), Some(tail), Some(prepass)) = (
deformed_current,
counts.skinned_tail(0),
default.prepass.as_ref(),
) {
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_pipeline(prepass);
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
}
}