#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::render::error::{RenderError, RenderResult};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBlendFactor, MTLCommandBuffer as _, MTLCommandEncoder as _, MTLDevice as _, MTLFunction,
MTLLoadAction, MTLPixelFormat, MTLPrimitiveType, MTLRenderCommandEncoder as _,
MTLRenderPassDescriptor, MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLStoreAction,
MTLTexture,
};
use crate::metal::builtin_shaders::{FULLSCREEN_VERT, ShaderProgram, entry_function};
use crate::metal::encode::RenderEncode;
#[derive(Clone, Copy)]
pub(crate) enum FullscreenBlend {
Replace,
Additive,
PremultipliedOver,
}
fn build_fullscreen_pipeline_from(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
vert_fn: &ProtocolObject<dyn MTLFunction>,
frag_fn: &ProtocolObject<dyn MTLFunction>,
label: &str,
format: MTLPixelFormat,
blend: FullscreenBlend,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexFunction(Some(vert_fn));
desc.setFragmentFunction(Some(frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(format);
match blend {
FullscreenBlend::Replace => ca.setBlendingEnabled(false),
FullscreenBlend::Additive => {
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::One);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::One);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::One);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::One);
}
FullscreenBlend::PremultipliedOver => {
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::One);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::One);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
}
}
}
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| RenderError::ShaderCompile(format!("{label} pipeline: {e:?}")))
}
pub(in crate::metal) fn build_fullscreen_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
fragment: &ShaderProgram,
format: MTLPixelFormat,
blend: FullscreenBlend,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let vert_fn = entry_function(device, &FULLSCREEN_VERT, hot_reload)?;
let frag_fn = entry_function(device, fragment, hot_reload)?;
build_fullscreen_pipeline_from(device, &vert_fn, &frag_fn, fragment.label, format, blend)
}
pub(in crate::metal) fn set_fragment_sampler_range(
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
sampler: &ProtocolObject<dyn objc2_metal::MTLSamplerState>,
first: usize,
count: usize,
) {
for i in first..first + count {
enc.set_fragment_sampler(sampler, i);
}
}
pub(crate) use crate::metal::pass_timing::PassTimer;
pub(in crate::metal) struct FullscreenPass<'a> {
pub target: &'a ProtocolObject<dyn MTLTexture>,
pub load: MTLLoadAction,
pub timer: PassTimer,
pub pipeline: &'a ProtocolObject<dyn MTLRenderPipelineState>,
pub label: &'a str,
}
pub(in crate::metal) fn encode_fullscreen_pass(
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
timing: Option<&crate::metal::pass_timing::PassTimingResources>,
pass: FullscreenPass,
bind: impl FnOnce(&ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>),
) -> RenderResult<()> {
let FullscreenPass {
target,
load,
timer,
pipeline,
label,
} = pass;
let desc = MTLRenderPassDescriptor::new();
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setTexture(Some(target));
ca.setLoadAction(load);
ca.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = timing {
t.attach_render_timer(&desc, timer);
}
let enc = cmd_buf
.renderCommandEncoderWithDescriptor(&desc)
.ok_or_else(|| RenderError::Other(format!("failed to get {label} encoder")))?;
enc.set_pipeline(pipeline);
bind(&enc);
unsafe {
enc.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveType::Triangle, 0, 3);
}
enc.endEncoding();
Ok(())
}