#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::render::backend_init;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::world_pipelines::WorldPipelines;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLArgumentBuffersTier, MTLArgumentEncoder, MTLDevice, MTLFunction, MTLPixelFormat,
MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLVertexDescriptor, MTLVertexFormat,
MTLVertexStepFunction,
};
pub(crate) use crate::metal::bucket_pipelines::{
BucketBuild, BucketPipelines, build_bucket_pipelines, replacement,
};
use crate::metal::context::{BINDLESS_SAMPLER_ARG_BUFFER_INDEX, BINDLESS_TEXTURE_ARG_BUFFER_INDEX};
use crate::metal::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use crate::metal::error::allocation_failed;
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_fn: &ProtocolObject<dyn MTLFunction>,
frag_fn: &ProtocolObject<dyn MTLFunction>,
sample_count: u32,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let pipeline_desc = MTLRenderPipelineDescriptor::new();
pipeline_desc.setVertexDescriptor(Some(vert_desc));
pipeline_desc.setVertexFunction(Some(vert_fn));
pipeline_desc.setFragmentFunction(Some(frag_fn));
pipeline_desc.setRasterSampleCount(sample_count as usize);
unsafe {
pipeline_desc
.colorAttachments()
.objectAtIndexedSubscript(0)
.setPixelFormat(MTLPixelFormat::RGBA16Float);
}
pipeline_desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
pipeline_desc.setSupportIndirectCommandBuffers(true);
device
.newRenderPipelineStateWithDescriptor_error(&pipeline_desc)
.map_err(|e| RenderError::ShaderCompile(format!("main pipeline state: {e:?}")))
}
fn check_bindless_support(
device: &ProtocolObject<dyn MTLDevice>,
fragment: &ProtocolObject<dyn MTLFunction>,
) -> RenderResult<()> {
use crate::metal::bindless_args::bindless_block_len;
if device.argumentBuffersSupport() != MTLArgumentBuffersTier::Tier2 {
return Err(RenderError::Other(
"the bindless main pass needs argument-buffer tier 2, which this device lacks".into(),
));
}
let texture =
unsafe { fragment.newArgumentEncoderWithBufferIndex(BINDLESS_TEXTURE_ARG_BUFFER_INDEX) };
let expected = bindless_block_len(1);
if texture.encodedLength() != expected {
return Err(RenderError::Other(format!(
"the bindless texture block encodes to {} bytes, not the {expected} its resource-id \
layout assumes",
texture.encodedLength()
)));
}
Ok(())
}
pub(crate) fn build_bindless_sampler_args(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
tex_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
shadow_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
cube_sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>> {
use objc2_metal::MTLResourceOptions;
let fragment = super::super::builtin_shaders::entry_function(
device,
&super::super::builtin_shaders::MAIN_BINDLESS_FRAG,
hot_reload,
)?;
check_bindless_support(device, &fragment)?;
let encoder =
unsafe { fragment.newArgumentEncoderWithBufferIndex(BINDLESS_SAMPLER_ARG_BUFFER_INDEX) };
let len = encoder.encodedLength().max(16);
let buf = device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("the 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) fn build_world_pipelines(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
extra_shaders: &[backend_init::WorldShader<'_>],
build: BucketBuild,
) -> RenderResult<WorldPipelines<BucketPipelines>> {
let mut table = Vec::with_capacity(extra_shaders.len());
for (i, shader) in extra_shaders.iter().enumerate() {
let Some(programs) = shader.programs.filter(|_| !shader.deferred) else {
table.push(None);
continue;
};
table.push(Some(build_bucket_pipelines(
device,
vert_desc,
i + 1,
Some(programs),
build,
)?));
}
Ok(WorldPipelines::new(table))
}
pub(crate) fn build_shadow_bindless_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_desc: &MTLVertexDescriptor,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let shadow_fn = super::super::builtin_shaders::entry_function(
device,
&super::super::builtin_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| RenderError::ShaderCompile(format!("shadow bindless pipeline state: {e:?}")))
}