#![deny(unsafe_op_in_unsafe_fn)]
use std::cell::Cell;
use concinnity_core::gfx::render_types;
use concinnity_core::gfx::render_types::TextDrawCall;
use concinnity_core::render::error::RenderError;
use concinnity_core::render::fullscreen;
use concinnity_core::render::fullscreen::TextBindCache;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::ns_string;
use objc2_metal::{
MTLCommandBuffer as _, MTLIndexType, MTLLoadAction, MTLPrimitiveType,
MTLRenderCommandEncoder as _, MTLScissorRect, MTLStoreAction, MTLTexture,
};
use crate::metal::context::MtlContext;
use crate::metal::encode::RenderEncode;
use crate::metal::scoped_encoder::ScopedEncoder;
struct CompositePass<'a> {
ctx: &'a MtlContext,
scene_color: &'a ProtocolObject<dyn MTLTexture>,
bloom_top: &'a ProtocolObject<dyn MTLTexture>,
logical: (f32, f32),
framebuffer: (u32, u32),
channel_view: u32,
reactive_valid: bool,
draws: Cell<u32>,
}
impl fullscreen::CompositeEncoder for CompositePass<'_> {
type Rec = ScopedEncoder<dyn objc2_metal::MTLRenderCommandEncoder>;
type Args = ();
fn begin_composite(&self, _enc: &Self::Rec, _args: &()) {}
fn composite_draw(&self, enc: &Self::Rec, _args: &()) {
enc.set_pipeline(&self.ctx.composite.pipeline);
enc.set_fragment_texture(self.scene_color, 0);
enc.set_fragment_texture(self.bloom_top, 1);
enc.set_fragment_texture(self.ctx.scene.color_lut.as_ref(), 2);
if self.channel_view != 0 {
let nd = self
.ctx
.gbuffer_normal_depth()
.unwrap_or_else(|| self.ctx.ssao.white.as_ref());
let rough = self
.ctx
.gbuffer_roughness()
.unwrap_or_else(|| self.ctx.ssao.white.as_ref());
enc.set_fragment_texture(nd, 3);
enc.set_fragment_texture(rough, 4);
enc.set_fragment_texture(self.ctx.ao_output_texture(), 5);
let motion = self
.ctx
.gbuffer_velocity()
.unwrap_or_else(|| self.ctx.ssao.white.as_ref());
enc.set_fragment_texture(motion, 6);
enc.set_fragment_texture(self.ctx.targets.hdr.reactive_mask.as_ref(), 7);
}
crate::metal::post::fullscreen::set_fragment_sampler_range(
enc,
&self.ctx.composite.sampler,
0,
8,
);
let (depth_near, depth_far) = render_types::CompositeParams::depth_view_range(
self.ctx.state.view.near,
self.ctx.state.view.view_distance,
);
let composite = render_types::CompositeParams {
post: self.ctx.post_process,
fade: self.ctx.state.view.scene_fade,
view_mode: self.channel_view,
depth_near,
depth_far,
reactive_valid: if self.reactive_valid { 1.0 } else { 0.0 },
};
enc.set_fragment_value(&composite, 0);
unsafe {
enc.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveType::Triangle, 0, 3);
}
self.draws.set(self.draws.get() + 1);
}
fn begin_text(&self, enc: &Self::Rec, _args: &()) -> bool {
let Some(text_ps) = self.ctx.text.pipeline_state.clone() else {
return false;
};
if self.ctx.text.upload.binding().is_none() || self.ctx.text.atlas_textures.is_empty() {
return false;
}
let (win_w, win_h) = self.logical;
let text_uniforms = render_types::TextUniforms {
win_width: win_w,
win_height: win_h,
_pad: [0.0; 2],
};
enc.set_pipeline(&text_ps);
enc.set_fragment_sampler(&self.ctx.text.sampler, 0);
enc.set_vertex_value(&text_uniforms, 0);
true
}
fn text_draw(
&self,
enc: &Self::Rec,
_args: &(),
idx: usize,
call: &TextDrawCall,
binds: &mut TextBindCache,
) -> concinnity_core::render::error::RenderResult<()> {
if call.vertices.is_empty() {
return Ok(());
}
let Some((text_buffer, ranges)) = self.ctx.text.upload.binding() else {
return Ok(());
};
let Some(range) = ranges.get(idx) else {
return Ok(());
};
let (win_w, win_h) = self.logical;
let (fb_w, fb_h) = self.framebuffer;
let scissor = match call.clip_rect {
Some(clip) => {
let Some(rect) =
fullscreen::clip_rect_to_scissor(clip, (win_w, win_h), (fb_w, fb_h))
else {
return Ok(());
};
rect
}
None => (0, 0, fb_w, fb_h),
};
if binds.scissor_changed(scissor) {
let (x, y, w, h) = scissor;
enc.setScissorRect(MTLScissorRect {
x: x as usize,
y: y as usize,
width: w as usize,
height: h as usize,
});
}
let atlas_idx = call.atlas_slot.min(self.ctx.text.atlas_textures.len() - 1);
if binds.atlas_changed(atlas_idx) {
enc.set_fragment_texture(self.ctx.text.atlas_textures[atlas_idx].as_ref(), 0);
}
enc.set_vertex_buffer(text_buffer, range.vertex_offset, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
call.indices.len(),
MTLIndexType::UInt16,
text_buffer,
range.index_offset,
);
}
self.draws.set(self.draws.get() + 1);
Ok(())
}
fn end_composite(&self, _enc: &Self::Rec, _args: &()) {}
}
impl MtlContext {
pub(in crate::metal) fn encode_composite_and_text(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
scene_color: &Retained<ProtocolObject<dyn MTLTexture>>,
text_calls: &[TextDrawCall],
reactive_valid: bool,
) -> concinnity_core::render::error::RenderResult<u32> {
let composite_pass_desc = self
.window()
.view
.currentRenderPassDescriptor()
.ok_or_else(|| RenderError::Other("no current render pass descriptor".to_string()))?;
unsafe {
let ca = composite_pass_desc
.colorAttachments()
.objectAtIndexedSubscript(0);
ca.setLoadAction(MTLLoadAction::DontCare);
ca.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(
&composite_pass_desc,
super::super::pass_timing::PassId::Composite,
);
}
let size = self.window().view.bounds().size;
let logical = (size.width as f32, size.height as f32);
let framebuffer = unsafe {
match composite_pass_desc
.colorAttachments()
.objectAtIndexedSubscript(0)
.texture()
{
Some(t) => (t.width() as u32, t.height() as u32),
None => (logical.0.max(0.0) as u32, logical.1.max(0.0) as u32),
}
};
let bloom_top = self.targets.transient_pool.bloom_top()?;
let post_encoder = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&composite_pass_desc)
.ok_or_else(|| {
RenderError::Other("failed to get post-process render encoder".to_string())
})?,
ns_string!("composite"),
);
let pass = CompositePass {
ctx: self,
scene_color: scene_color.as_ref(),
bloom_top,
logical,
framebuffer,
channel_view: if self.state.view.mode.is_gbuffer_channel() {
self.state.view.mode as u32
} else {
0
},
reactive_valid,
draws: Cell::new(0),
};
fullscreen::encode_composite_chain(&pass, &post_encoder, &(), text_calls)?;
Ok(pass.draws.get())
}
}