#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLArgumentEncoder, MTLCompareFunction, MTLComputePipelineState, MTLDepthStencilDescriptor,
MTLDepthStencilState, MTLDevice, MTLFunction as _, MTLLibrary as _, MTLPixelFormat,
MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLVertexDescriptor, MTLVertexFormat,
MTLVertexStepFunction,
};
use crate::gfx::mesh_payload::Vertex;
use crate::metal::context::{
BINDLESS_SAMPLER_ARG_BUFFER_INDEX, BINDLESS_TEXTURE_ARG_BUFFER_INDEX, HDR_SAMPLE_COUNT,
};
use crate::metal::cull::build_cull_pipeline;
use crate::metal::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use crate::metal::pipeline::{load_library, ns_str, stage_library};
pub(crate) struct MainPipelineBundle {
pub pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub bindless: bool,
pub cull_pipeline: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub cull_icb_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
pub cull_pipeline_phase2: Option<Retained<ProtocolObject<dyn MTLComputePipelineState>>>,
pub cull_icb2_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
pub bindless_tex_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
pub bindless_sampler_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
}
pub(crate) fn make_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: 2,
format: MTLVertexFormat::Float3,
offset: 24,
buffer_index: 1,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 1,
},
VertexAttr {
index: 4,
format: MTLVertexFormat::Float2,
offset: 48,
buffer_index: 1,
},
],
&[VertexLayout {
buffer_index: 1,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
)
}
pub(crate) fn build_main_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
vert_lib_bytes: &[u8],
frag_lib_bytes: &[u8],
hot_reload: bool,
) -> Result<MainPipelineBundle, String> {
let engine_stages = vert_lib_bytes.is_empty() && frag_lib_bytes.is_empty();
let (vert_fn, main_frag_fn, bindless, engine_single_source) = if engine_stages {
let vert_library = super::super::slang_shaders::MAIN_BINDLESS_VERT
.library(device, hot_reload)
.map_err(|e| format!("failed to load engine vertex library: {e}"))?;
let frag_library = super::super::slang_shaders::MAIN_BINDLESS_FRAG
.library(device, hot_reload)
.map_err(|e| format!("failed to load engine fragment library: {e}"))?;
let vert_fn = vert_library
.newFunctionWithName(&ns_str("vertex_main_bindless"))
.ok_or("vertex_main_bindless not found in engine library")?;
let frag_fn = frag_library
.newFunctionWithName(&ns_str("fragment_main_bindless"))
.ok_or("fragment_main_bindless not found in engine library")?;
(vert_fn, frag_fn, true, true)
} else {
let vert_library = stage_library(device, hot_reload, vert_lib_bytes)
.map_err(|e| format!("failed to load vertex metallib: {}", e))?;
let frag_library = stage_library(device, hot_reload, frag_lib_bytes)
.map_err(|e| format!("failed to load fragment metallib: {}", e))?;
let vert_fn = vert_library
.newFunctionWithName(&ns_str("vertex_main"))
.ok_or("vertex_main not found in metallib")?;
let bindless_frag_fn = frag_library.newFunctionWithName(&ns_str("fragment_main_bindless"));
let bindless = bindless_frag_fn.is_some();
let frag_fn = match bindless_frag_fn {
Some(f) => f,
None => frag_library
.newFunctionWithName(&ns_str("fragment_main"))
.ok_or("fragment_main not found in metallib")?,
};
(vert_fn, frag_fn, bindless, false)
};
let pipeline_desc = MTLRenderPipelineDescriptor::new();
pipeline_desc.setVertexDescriptor(Some(vert_desc));
pipeline_desc.setVertexFunction(Some(&vert_fn));
pipeline_desc.setFragmentFunction(Some(&main_frag_fn));
pipeline_desc.setRasterSampleCount(HDR_SAMPLE_COUNT as usize);
unsafe {
pipeline_desc
.colorAttachments()
.objectAtIndexedSubscript(0)
.setPixelFormat(MTLPixelFormat::RGBA16Float);
}
pipeline_desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
if bindless {
pipeline_desc.setSupportIndirectCommandBuffers(true);
}
let pipeline_state = device
.newRenderPipelineStateWithDescriptor_error(&pipeline_desc)
.map_err(|e| format!("failed to create pipeline state: {:?}", e))?;
let (cull_pipeline, cull_icb_arg_encoder, cull_pipeline_phase2, cull_icb2_arg_encoder) =
if bindless {
let cull = build_cull_pipeline(device, hot_reload)?;
(
Some(cull.state),
Some(cull.icb_arg_encoder),
Some(cull.state_phase2),
Some(cull.icb2_arg_encoder),
)
} else {
(None, None, None, None)
};
let bindless_tex_arg_encoder = if bindless {
Some(unsafe {
main_frag_fn.newArgumentEncoderWithBufferIndex(BINDLESS_TEXTURE_ARG_BUFFER_INDEX)
})
} else {
None
};
let bindless_sampler_arg_encoder = if engine_single_source {
Some(unsafe {
main_frag_fn.newArgumentEncoderWithBufferIndex(BINDLESS_SAMPLER_ARG_BUFFER_INDEX)
})
} else {
None
};
Ok(MainPipelineBundle {
pipeline_state,
bindless,
cull_pipeline,
cull_icb_arg_encoder,
cull_pipeline_phase2,
cull_icb2_arg_encoder,
bindless_tex_arg_encoder,
bindless_sampler_arg_encoder,
})
}
pub(crate) fn build_bindless_sampler_args(
device: &ProtocolObject<dyn MTLDevice>,
encoder: &ProtocolObject<dyn MTLArgumentEncoder>,
tex_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
shadow_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
cube_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>, String> {
use objc2_metal::MTLResourceOptions;
let len = encoder.encodedLength().max(16);
let buf = device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate sampler argument buffer")?;
unsafe {
encoder.setArgumentBuffer_offset(Some(&buf), 0);
encoder.setSamplerState_atIndex(Some(tex_sampler), 0);
encoder.setSamplerState_atIndex(Some(shadow_sampler), 1);
encoder.setSamplerState_atIndex(Some(cube_sampler), 2);
}
Ok(buf)
}
pub(crate) type WorldPipelineTable =
Vec<Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>>;
pub(crate) fn build_world_pipeline_table(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
extra_shaders: &[crate::gfx::backend_init::ShaderBytes<'_>],
) -> Result<WorldPipelineTable, String> {
let mut table = Vec::with_capacity(extra_shaders.len());
for (i, shader) in extra_shaders.iter().enumerate() {
if shader.deferred {
table.push(None);
continue;
}
table.push(Some(build_bucket_pipeline(
device,
vert_desc,
i + 1,
shader.vert,
shader.frag,
)?));
}
Ok(table)
}
pub(crate) fn build_bucket_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
bucket: usize,
vert_bytes: &[u8],
frag_bytes: &[u8],
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let vert_library = load_library(device, vert_bytes)
.map_err(|e| format!("shader bucket {bucket}: failed to load vertex metallib: {e}"))?;
let frag_library = load_library(device, frag_bytes)
.map_err(|e| format!("shader bucket {bucket}: failed to load fragment metallib: {e}"))?;
let vert_fn = vert_library
.newFunctionWithName(&ns_str("vertex_main"))
.ok_or_else(|| format!("shader bucket {bucket}: vertex_main not found in metallib"))?;
let frag_fn = frag_library
.newFunctionWithName(&ns_str("fragment_main_bindless"))
.ok_or_else(|| {
format!(
"shader bucket {bucket}: fragment_main_bindless not found in metallib -- a \
material-referenced Shader must define the bindless entry points"
)
})?;
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(HDR_SAMPLE_COUNT as usize);
unsafe {
desc.colorAttachments()
.objectAtIndexedSubscript(0)
.setPixelFormat(MTLPixelFormat::RGBA16Float);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
desc.setSupportIndirectCommandBuffers(true);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| format!("shader bucket {bucket}: failed to create pipeline: {e:?}"))
}
pub(crate) fn build_instanced_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
vert_instanced_lib_bytes: &[u8],
frag_lib_bytes: &[u8],
has_clusters: bool,
hot_reload: bool,
) -> Result<Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>, String> {
if !has_clusters {
return Ok(None);
}
let inst_library = stage_library(device, hot_reload, vert_instanced_lib_bytes)
.map_err(|e| format!("failed to load instanced vertex metallib: {}", e))?;
let inst_vert_fn = inst_library
.newFunctionWithName(&ns_str("vertex_main_instanced"))
.ok_or("vertex_main_instanced not found in instanced metallib")?;
let frag_library = stage_library(device, hot_reload, frag_lib_bytes)
.map_err(|e| format!("failed to load fragment metallib: {}", e))?;
let frag_fn = frag_library
.newFunctionWithName(&ns_str("fragment_main"))
.ok_or("fragment_main not found in metallib")?;
let inst_pipeline_desc = MTLRenderPipelineDescriptor::new();
inst_pipeline_desc.setVertexDescriptor(Some(vert_desc));
inst_pipeline_desc.setVertexFunction(Some(&inst_vert_fn));
inst_pipeline_desc.setFragmentFunction(Some(&frag_fn));
inst_pipeline_desc.setRasterSampleCount(HDR_SAMPLE_COUNT as usize);
unsafe {
inst_pipeline_desc
.colorAttachments()
.objectAtIndexedSubscript(0)
.setPixelFormat(MTLPixelFormat::RGBA16Float);
}
inst_pipeline_desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
let ps = device
.newRenderPipelineStateWithDescriptor_error(&inst_pipeline_desc)
.map_err(|e| format!("failed to create instanced pipeline state: {:?}", e))?;
Ok(Some(ps))
}
pub(crate) fn build_shadow_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let shadow_fn = super::super::slang_shaders::entry_function(
device,
&super::super::slang_shaders::SHADOW_VERT,
hot_reload,
)?;
let shadow_pipeline_desc = MTLRenderPipelineDescriptor::new();
shadow_pipeline_desc.setVertexDescriptor(Some(vert_desc));
shadow_pipeline_desc.setVertexFunction(Some(&shadow_fn));
shadow_pipeline_desc.setRasterSampleCount(1);
shadow_pipeline_desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&shadow_pipeline_desc)
.map_err(|e| format!("failed to create shadow pipeline state: {:?}", e))
}
pub(crate) fn build_shadow_bindless_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let shadow_fn = super::super::slang_shaders::entry_function(
device,
&super::super::slang_shaders::SHADOW_VERT_BINDLESS,
hot_reload,
)?;
let shadow_pipeline_desc = MTLRenderPipelineDescriptor::new();
shadow_pipeline_desc.setVertexDescriptor(Some(vert_desc));
shadow_pipeline_desc.setVertexFunction(Some(&shadow_fn));
shadow_pipeline_desc.setRasterSampleCount(1);
shadow_pipeline_desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
shadow_pipeline_desc.setSupportIndirectCommandBuffers(true);
device
.newRenderPipelineStateWithDescriptor_error(&shadow_pipeline_desc)
.map_err(|e| format!("failed to create shadow bindless pipeline state: {:?}", e))
}
pub(crate) fn make_depth_state(
device: &ProtocolObject<dyn MTLDevice>,
) -> Result<Retained<ProtocolObject<dyn MTLDepthStencilState>>, String> {
let depth_desc = MTLDepthStencilDescriptor::new();
depth_desc.setDepthCompareFunction(MTLCompareFunction::Less);
depth_desc.setDepthWriteEnabled(true);
device
.newDepthStencilStateWithDescriptor(&depth_desc)
.ok_or_else(|| "failed to create depth stencil state".to_string())
}
pub(crate) fn make_depth_state_read_only(
device: &ProtocolObject<dyn MTLDevice>,
) -> Result<Retained<ProtocolObject<dyn MTLDepthStencilState>>, String> {
let depth_desc = MTLDepthStencilDescriptor::new();
depth_desc.setDepthCompareFunction(MTLCompareFunction::LessEqual);
depth_desc.setDepthWriteEnabled(false);
device
.newDepthStencilStateWithDescriptor(&depth_desc)
.ok_or_else(|| "failed to create read-only depth stencil state".to_string())
}