#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLArgumentEncoder, MTLBuffer, MTLCommandBuffer as _, MTLCommandQueue, MTLDepthStencilState,
MTLDevice as _, MTLIndirectCommandBuffer, MTLIndirectCommandBufferDescriptor,
MTLIndirectCommandType, MTLPixelFormat, MTLRenderPipelineState, MTLResourceOptions,
MTLSamplerState, MTLTexture,
};
use objc2_metal_kit::MTKView;
use crate::gfx::render_types::{
ClusterParams, DrawObject, InstancedCluster, LightUniforms, NUM_SHADOW_CASCADES, ShadowUniforms,
};
use super::allocator::{DeviceAllocator, PooledBuffer, PooledTexture};
use super::auto_exposure::AutoExposureGpu;
use super::cull::CullState;
use super::decal::DecalState;
use super::fog::FogState;
use super::line::LineState;
use super::particle::ParticleState;
use super::post::{
BloomPipelines, BloomTargets, GBufferState, SsaoState, SsgiState, SsrState, TaaState,
UpscaleState,
};
use super::raytrace::RtState;
use super::resources::skinning::SkinnedState;
use super::texture::{EnvironmentMapTextures, HdrTargets};
use super::transient_pool::TransientTexturePool;
pub(super) const HDR_SAMPLE_COUNT: u32 = 4;
pub(super) const BINDLESS_TEXTURE_COUNT: usize = 1024;
pub(super) const BINDLESS_TEXTURE_ARG_BUFFER_INDEX: usize = 7;
pub(super) const BINDLESS_SAMPLER_ARG_BUFFER_INDEX: usize = 10;
static EMBEDDED_VIEW_PTR: std::sync::atomic::AtomicPtr<std::ffi::c_void> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
static EMBEDDED_PUMP_EVENTS: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn set_preview_view(ptr: *mut std::ffi::c_void) {
EMBEDDED_VIEW_PTR.store(ptr, std::sync::atomic::Ordering::SeqCst);
}
pub fn set_embedded_pump_events(v: bool) {
EMBEDDED_PUMP_EVENTS.store(v, std::sync::atomic::Ordering::SeqCst);
}
pub(super) fn take_embedded_view() -> *mut std::ffi::c_void {
EMBEDDED_VIEW_PTR.swap(std::ptr::null_mut(), std::sync::atomic::Ordering::SeqCst)
}
pub(super) fn take_embedded_pump_events() -> bool {
EMBEDDED_PUMP_EVENTS.swap(false, std::sync::atomic::Ordering::SeqCst)
}
pub(super) struct DrawState {
pub objects: Vec<DrawObject>,
pub bvh: crate::gfx::bvh::Bvh,
pub always: Vec<u32>,
pub always_member: Vec<bool>,
pub visible_scratch: Vec<u32>,
pub graph_cache: Option<(
crate::gfx::render_graph::FrameGraphInputs,
crate::gfx::render_graph::CompiledGraph,
)>,
pub n_instances: usize,
pub n_skinned: usize,
}
pub(super) struct InstancedState {
pub clusters: Vec<InstancedCluster>,
pub records: Vec<crate::gfx::render_types::GpuObjectData>,
pub draw_args: Vec<crate::gfx::render_types::GpuDrawArgs>,
pub pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
}
pub(super) struct ViewState {
pub clear_color: [f32; 4],
pub scene_fade: f32,
pub mode: concinnity_core::gfx::view_modes::ViewMode,
pub far: f32,
pub matrix: [[f32; 4]; 4],
}
pub(super) struct ProbeState {
pub placements: Vec<crate::gfx::reflection_probe::ProbePlacement>,
pub maps: Vec<ProbeCube>,
pub bake_queue: crate::gfx::reflection_probe::ProbeBakeQueue,
pub set: concinnity_core::render::uniforms::ProbeSet,
pub rendering: Option<super::probe::RenderingBake>,
pub prefiltering: Option<super::probe::PrefilteringBake>,
pub prefilter: Option<super::probe_prefilter::ProbePrefilterPipelines>,
pub retire_pool: super::transient::RetirePool<super::probe::RetiredBake>,
}
pub(super) struct ProbeCube {
pub prefilter: super::allocator::PooledTexture,
}
pub(super) struct ShadowState {
pub pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub map: Retained<ProtocolObject<dyn MTLTexture>>,
pub map_size: u32,
pub update: crate::components::ShadowUpdate,
pub distance: u32,
pub cascades: u32,
pub scheduler: crate::gfx::shadow_schedule::ShadowCascadeScheduler,
pub render_mask: u32,
pub sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub uniforms: ShadowUniforms,
pub light_dir: [f32; 3],
}
pub(super) struct SpotShadowState {
pub map: Retained<ProtocolObject<dyn MTLTexture>>,
pub buffer: PooledBuffer,
pub count: u32,
pub scheduler: crate::gfx::spot_shadow::SpotShadowScheduler,
pub render_mask: u32,
}
pub(super) struct FrameRings {
pub object: super::transient::TransientRing,
pub draw_args: super::transient::TransientRing,
pub prev_model: super::transient::TransientRing,
pub bindless_tex: super::transient::TransientRing,
pub joint: super::transient::JointRing,
pub prev_joint: super::transient::JointRing,
pub instance: super::transient::InstanceRing,
pub object_scratch: Vec<crate::gfx::render_types::GpuObjectData>,
pub draw_args_scratch: Vec<crate::gfx::render_types::GpuDrawArgs>,
pub prev_model_scratch: Vec<[[f32; 4]; 4]>,
}
pub(super) struct WaterState {
pub pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub pipeline_rt: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub pipeline_rt_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub surfaces: Vec<super::water::WaterSurfaceRecord>,
}
pub(super) struct GlassState {
pub pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub pipeline_rt: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub pipeline_rt_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub mesh_pipeline_rt: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub mesh_pipeline_rt_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub seethrough_mesh_indices: Vec<usize>,
pub panels: Vec<super::glass::GlassPanelRecord>,
}
pub(super) struct RaymarchState {
pub volumes: Vec<super::raymarch::RaymarchVolumeRecord>,
pub cube_vertex_buffer: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub cube_index_buffer: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
}
pub(super) struct TextState {
pub pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub atlas_textures: Vec<PooledTexture>,
pub sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub upload: super::text_upload::TextUploadRing,
}
pub(super) struct HotReloadState {
pub enabled: bool,
pub reload_pending: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
#[expect(
dead_code,
reason = "held so the watcher thread stays alive; events arrive through reload_pending"
)]
pub watcher: Option<crate::metal::hot_reload::WatcherHandle>,
}
pub(super) struct GeometryAllocators {
pub mesh_vtx: crate::suballoc::range_alloc::RangeAllocator,
pub mesh_idx: crate::suballoc::range_alloc::RangeAllocator,
pub chunk_vtx: crate::suballoc::range_alloc::RangeAllocator,
pub chunk_idx: crate::suballoc::range_alloc::RangeAllocator,
}
pub(super) struct WindowState {
pub appkit: crate::appkit::AppKitWindow,
pub view: Retained<MTKView>,
pub owns: bool,
pub was_visible: bool,
}
pub(super) struct Diagnostics {
pub frame_stats: crate::gfx::profile::RenderStats,
pub gpu_time_us: std::sync::Arc<std::sync::atomic::AtomicU32>,
pub render_fault_logged: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub device_error: std::sync::Arc<std::sync::Mutex<Option<crate::gfx::error::RenderError>>>,
pub pass_fault_count: std::sync::Arc<std::sync::atomic::AtomicU32>,
pub pass_timing: Option<super::pass_timing::PassTimingResources>,
pub pass_times_us:
std::sync::Arc<[std::sync::atomic::AtomicU32; super::pass_timing::PASS_COUNT]>,
pub draw_calls_accum: std::sync::atomic::AtomicU32,
}
pub(super) struct HdrState {
pub max_edr: Option<f32>,
pub encoding: Option<crate::gfx::hdr_output::HdrEncoding>,
pub display_requested: bool,
pub pq_requested: bool,
}
pub(crate) struct MtlContext {
pub(super) device: Retained<ProtocolObject<dyn objc2_metal::MTLDevice>>,
pub(super) allocator: DeviceAllocator,
pub(super) command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
pub(super) swap_pixel_format: MTLPixelFormat,
pub(super) hdr: HdrState,
pub(super) last_present_texture: Option<Retained<ProtocolObject<dyn MTLTexture>>>,
pub(super) pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(super) world_pipelines: super::init::pipelines::WorldPipelineTable,
pub(super) bindless: bool,
pub(super) cull: CullState,
pub(super) bindless_tex_arg_encoder: Option<Retained<ProtocolObject<dyn MTLArgumentEncoder>>>,
pub(super) bindless_sampler_args: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub(super) depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pub(super) depth_state_read_only: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pub(super) vertex_buffer: PooledBuffer,
pub(super) index_buffer: PooledBuffer,
pub(super) draw: DrawState,
pub(super) instanced: InstancedState,
pub(super) view: ViewState,
pub(super) geometry_less: bool,
pub(super) textures: Vec<PooledTexture>,
pub(super) fallback_textures: Vec<PooledTexture>,
pub(super) light_uniforms: LightUniforms,
pub(super) local_light_buffer: PooledBuffer,
pub(super) sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub(super) shadow: ShadowState,
pub(super) spot_shadow: SpotShadowState,
pub(super) area_light_buffer: PooledBuffer,
pub(super) ltc_matrix_texture: PooledTexture,
pub(super) ltc_magnitude_texture: PooledTexture,
pub(super) env_map: EnvironmentMapTextures,
pub(super) probe: ProbeState,
pub(super) cube_sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub(super) text: TextState,
pub(super) hdr_targets: HdrTargets,
pub(super) post_pipeline_state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(super) post_sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub(super) bloom_targets: BloomTargets,
pub(super) bloom_pipelines: Option<BloomPipelines>,
pub(super) transient_pool: TransientTexturePool,
pub(super) post_process: crate::gfx::render_types::PostProcessParams,
pub(super) color_lut: PooledTexture,
pub(super) taa: TaaState,
pub(super) prev_view_proj: [[f32; 4]; 4],
pub(super) upscale: UpscaleState,
pub(super) ssao: SsaoState,
pub(super) ssr: SsrState,
pub(super) gbuffer: GBufferState,
pub(super) ssgi: SsgiState,
pub(super) rt: RtState,
pub(super) lines: LineState,
pub(super) decal: DecalState,
pub(super) fog: FogState,
pub(super) light_cull: super::light_cull::LightCullState,
pub(super) cluster_params: ClusterParams,
pub(super) particle: ParticleState,
pub(super) auto_exposure: AutoExposureGpu,
pub(super) hot_reload: HotReloadState,
pub(super) capture: bool,
pub(super) prev_draw_models: Vec<[[f32; 4]; 4]>,
pub(super) skinned: SkinnedState,
pub(super) geometry_alloc: GeometryAllocators,
pub(super) window: WindowState,
pub(super) diagnostics: Diagnostics,
pub(super) frame_pacing: super::frame_pacing::FrameInFlight,
pub(super) frames_in_flight: usize,
pub(super) frame_ring_index: u64,
pub(super) rings: FrameRings,
pub(super) water: WaterState,
pub(super) planar_reflection: Option<super::planar::PlanarReflectionSet>,
pub(super) glass: GlassState,
pub(super) raymarch: RaymarchState,
}
unsafe impl Send for MtlContext {}
#[inline]
#[track_caller]
pub(super) fn debug_assert_main_thread(entry: &str) {
debug_assert!(
objc2::MainThreadMarker::new().is_some(),
"{entry} must be called from the main thread: MtlContext is main-thread-only \
(see `unsafe impl Send for MtlContext`); driving GraphicsSystem off the main \
thread races AppKit/Metal",
);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct DrawRecordCounts {
pub total: usize,
pub skinned_base: usize,
}
impl DrawRecordCounts {
pub(super) fn prefix(&self, block: usize) -> Option<std::ops::Range<usize>> {
(self.skinned_base > 0).then(|| block..block + self.skinned_base)
}
pub(super) fn skinned_tail(&self, block: usize) -> Option<std::ops::Range<usize>> {
(self.total > self.skinned_base).then(|| block + self.skinned_base..block + self.total)
}
}
pub(super) fn ns_range(r: std::ops::Range<usize>) -> objc2_foundation::NSRange {
objc2_foundation::NSRange {
location: r.start,
length: r.end - r.start,
}
}
impl MtlContext {
pub(super) fn draw_record_counts(&self) -> DrawRecordCounts {
DrawRecordCounts {
total: self.cull_count(),
skinned_base: self.skinned_record_base(),
}
}
pub(super) fn cull_count(&self) -> usize {
self.draw.objects.len() + self.draw.n_instances + self.draw.n_skinned
}
pub(super) fn probe_cube_or_sky(&self, i: usize) -> &ProtocolObject<dyn MTLTexture> {
match self.probe.maps.get(i) {
Some(p) => &p.prefilter,
None => self.env_map.prefilter.as_ref(),
}
}
pub(super) fn skinned_record_base(&self) -> usize {
self.draw.objects.len() + self.draw.n_instances
}
pub(super) fn skinned_index_or_placeholder(
&self,
) -> &ProtocolObject<dyn objc2_metal::MTLBuffer> {
match self.skinned.index_buffer.as_ref() {
Some(b) => b.as_ref(),
None => self.index_buffer.as_ref(),
}
}
pub(super) fn ensure_icb_capacity(&mut self, count: usize) -> Result<(), String> {
let arg_encoder = match &self.cull.icb_arg_encoder {
Some(e) => e.clone(),
None => return Ok(()),
};
if !self.cull.icbs.is_empty() && count <= self.cull.icb_capacity {
return Ok(());
}
let new_cap = count.next_power_of_two().max(64);
let bucket_count = self.cull.bucket_count.max(1);
let mut icbs = Vec::with_capacity(bucket_count);
for _ in 0..bucket_count {
icbs.push(self.build_cull_icb(new_cap)?);
}
if self.cull.icb_arg_buffer.is_none() {
let len = arg_encoder.encodedLength().max(16);
let buf = self
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or("failed to create ICB argument buffer")?;
self.cull.icb_arg_buffer = Some(buf);
}
let arg_buf = self
.cull
.icb_arg_buffer
.as_ref()
.expect("ICB argument buffer was just ensured");
unsafe {
arg_encoder.setArgumentBuffer_offset(Some(arg_buf), 0);
for (b, icb) in icbs.iter().enumerate() {
arg_encoder.setIndirectCommandBuffer_atIndex(Some(icb), b);
}
}
let status = self
.device
.newBufferWithLength_options(
new_cap * std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or("failed to create cull status buffer")?;
self.cull.status_buffer = Some(status);
if self.cull.two_pass_occlusion {
let arg_encoder2 = match &self.cull.icb_2_arg_encoder {
Some(e) => e.clone(),
None => {
return Err(
"two-pass occlusion on but phase-2 ICB argument encoder missing".into(),
);
}
};
let mut icbs_2 = Vec::with_capacity(bucket_count);
for _ in 0..bucket_count {
icbs_2.push(self.build_cull_icb(new_cap)?);
}
if self.cull.icb_2_arg_buffer.is_none() {
let len = arg_encoder2.encodedLength().max(16);
let buf = self
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or("failed to create phase-2 ICB argument buffer")?;
self.cull.icb_2_arg_buffer = Some(buf);
}
let arg_buf2 = self
.cull
.icb_2_arg_buffer
.as_ref()
.expect("phase-2 ICB argument buffer was just ensured");
unsafe {
arg_encoder2.setArgumentBuffer_offset(Some(arg_buf2), 0);
for (b, icb) in icbs_2.iter().enumerate() {
arg_encoder2.setIndirectCommandBuffer_atIndex(Some(icb), b);
}
}
self.cull.icbs_2 = icbs_2;
}
self.cull.icbs = icbs;
self.cull.icb_capacity = new_cap;
Ok(())
}
pub(super) fn ensure_shadow_icb_capacity(&mut self, count: usize) -> Result<(), String> {
let arg_encoder = match &self.cull.shadow_icb_arg_encoder {
Some(e) => e.clone(),
None => return Ok(()),
};
let needed = count.saturating_mul(NUM_SHADOW_CASCADES);
if self.cull.shadow_icb.is_some() && needed <= self.cull.shadow_icb_capacity {
return Ok(());
}
let new_cap = needed.next_power_of_two().max(64);
let icb = self.build_cull_icb(new_cap)?;
if self.cull.shadow_icb_arg_buffer.is_none() {
let len = arg_encoder.encodedLength().max(16);
let buf = self
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or("failed to create shadow ICB argument buffer")?;
self.cull.shadow_icb_arg_buffer = Some(buf);
}
let arg_buf = self
.cull
.shadow_icb_arg_buffer
.as_ref()
.expect("shadow ICB argument buffer was just ensured");
unsafe {
arg_encoder.setArgumentBuffer_offset(Some(arg_buf), 0);
arg_encoder.setIndirectCommandBuffer_atIndex(Some(&icb), 0);
}
self.cull.shadow_icb = Some(icb);
self.cull.shadow_icb_capacity = new_cap;
Ok(())
}
pub(super) fn ensure_mirror_icb_capacity(
&mut self,
slot_count: usize,
count: usize,
) -> Result<(), String> {
if slot_count == 0 {
self.cull.mirror_slots.clear();
self.cull.mirror_status = None;
self.cull.mirror_icb_capacity = 0;
return Ok(());
}
let arg_encoder = match &self.cull.icb_arg_encoder {
Some(e) => e.clone(),
None => return Ok(()),
};
if self.cull.mirror_slots.len() == slot_count && count <= self.cull.mirror_icb_capacity {
return Ok(());
}
let new_cap = count.next_power_of_two().max(64);
let mut slots = Vec::with_capacity(slot_count);
for _ in 0..slot_count {
let icb = self.build_cull_icb(new_cap)?;
let len = arg_encoder.encodedLength().max(16);
let arg_buffer = self
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or("failed to create mirror ICB argument buffer")?;
unsafe {
arg_encoder.setArgumentBuffer_offset(Some(&arg_buffer), 0);
arg_encoder.setIndirectCommandBuffer_atIndex(Some(&icb), 0);
}
slots.push(super::cull::MirrorCullSlot { icb, arg_buffer });
}
let status = self
.device
.newBufferWithLength_options(
new_cap * std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or("failed to create mirror cull status buffer")?;
self.cull.mirror_status = Some(status);
self.cull.mirror_slots = slots;
self.cull.mirror_icb_capacity = new_cap;
Ok(())
}
fn build_cull_icb(
&self,
cap: usize,
) -> Result<Retained<ProtocolObject<dyn MTLIndirectCommandBuffer>>, String> {
let desc = MTLIndirectCommandBufferDescriptor::new();
desc.setCommandTypes(MTLIndirectCommandType::DrawIndexed);
desc.setInheritBuffers(true);
desc.setInheritPipelineState(true);
desc.setMaxVertexBufferBindCount(0);
desc.setMaxFragmentBufferBindCount(0);
unsafe {
self.device
.newIndirectCommandBufferWithDescriptor_maxCommandCount_options(
&desc,
cap,
MTLResourceOptions::StorageModePrivate,
)
}
.ok_or_else(|| "failed to create indirect command buffer".to_string())
}
pub(crate) fn capabilities(&self) -> crate::gfx::backend::DeviceCapabilities {
crate::gfx::backend::DeviceCapabilities {
ray_tracing: super::raytrace::raytracing_supported(&self.device),
selectable_upscaler: false,
reuses_build_slots: true,
rewrites_draws: true,
}
}
pub(crate) fn gpu_profile(&self) -> crate::gfx::backend::GpuProfile {
super::gpu_profile::device_profile(&self.device)
}
pub(crate) fn render_stats(&self) -> crate::gfx::profile::RenderStats {
let mut stats = self.diagnostics.frame_stats;
stats.gpu_frame_us = self
.diagnostics
.gpu_time_us
.load(std::sync::atomic::Ordering::Relaxed);
for (i, name) in super::pass_timing::PASS_NAMES.iter().enumerate() {
let micros =
self.diagnostics.pass_times_us[i].load(std::sync::atomic::Ordering::Relaxed);
stats.pass_times_us[i] = (*name, micros);
}
stats.auto_exposure_ev = self.auto_exposure.state.as_ref().map(|s| s.current_ev);
stats.max_edr = self.hdr.max_edr;
stats
}
pub(crate) fn update_view(&mut self, matrix: [[f32; 4]; 4]) {
self.view.matrix = matrix;
}
pub(crate) fn update_models(&mut self, updates: &[(u32, [[f32; 4]; 4])]) {
for &(index, model) in updates {
if let Some(obj) = self.draw.objects.get_mut(index as usize) {
obj.model = model;
}
}
}
pub(crate) fn update_visibility(&mut self, index: usize, visible: bool) {
if let Some(obj) = self.draw.objects.get_mut(index) {
obj.visible = visible;
}
}
pub(crate) fn retire_draw_object(&mut self, index: usize) {
if let Some(obj) = self.draw.objects.get_mut(index) {
obj.visible = false;
obj.resident = false;
}
}
pub(super) fn place_draw_object(
&mut self,
obj: DrawObject,
model: [[f32; 4]; 4],
dst: crate::gfx::draw_slot::SlotAlloc,
) -> usize {
match dst {
crate::gfx::draw_slot::SlotAlloc::Reuse(slot) => {
self.draw.objects[slot] = obj;
self.prev_draw_models[slot] = model;
slot
}
crate::gfx::draw_slot::SlotAlloc::Append(slot) => {
debug_assert_eq!(
slot,
self.draw.objects.len(),
"appended draw slot must match the draw-object count"
);
self.draw.objects.push(obj);
self.prev_draw_models.push(model);
self.draw.always_member.push(false);
slot
}
}
}
pub(super) fn ensure_always_draw(&mut self, slot: usize) {
if !self.draw.always_member[slot] {
self.draw.always.push(slot as u32);
self.draw.always_member[slot] = true;
}
}
pub(crate) fn set_fade(&mut self, fade: f32) {
self.view.scene_fade = fade.clamp(0.0, 1.0);
}
pub(crate) fn clone_static_draw_object(
&mut self,
src_draw_idx: usize,
model: [[f32; 4]; 4],
dst: crate::gfx::draw_slot::SlotAlloc,
) -> Result<(), String> {
let src = self.draw.objects.get(src_draw_idx).ok_or_else(|| {
format!(
"clone_static_draw_object: src draw {} out of range",
src_draw_idx
)
})?;
let obj = DrawObject {
vertex_offset: src.vertex_offset,
vertex_count: src.vertex_count,
index_offset: src.index_offset,
index_count: src.index_count,
base_vertex: src.base_vertex,
geometry_generation: src.geometry_generation,
model,
texture_slot: src.texture_slot,
normal_map_slot: src.normal_map_slot,
material: src.material,
shader_bucket: src.shader_bucket,
visible: true,
resident: true,
bb_min: [f32::NAN; 3],
bb_max: [f32::NAN; 3],
cull_distance: src.cull_distance,
lod_alternates: src.lod_alternates.clone(),
};
let idx = self.place_draw_object(obj, model, dst);
self.ensure_always_draw(idx);
self.rt.topology_dirty = true;
Ok(())
}
pub(crate) fn set_draw_material(
&mut self,
draw_idx: usize,
material: crate::gfx::render_types::MaterialUniforms,
texture_slot: usize,
normal_map_slot: usize,
) {
if let Some(obj) = self.draw.objects.get_mut(draw_idx) {
obj.material = material;
obj.texture_slot = texture_slot;
obj.normal_map_slot = normal_map_slot;
self.rt.topology_dirty = true;
}
}
pub(crate) fn set_draw_cull_distance(&mut self, draw_idx: usize, cull_distance: f32) {
if let Some(obj) = self.draw.objects.get_mut(draw_idx) {
obj.cull_distance = cull_distance.max(0.0);
}
}
pub(crate) fn add_decal(
&mut self,
record: crate::gfx::decal::DecalRecord,
) -> Result<usize, String> {
if self.decal.pipeline.is_none() {
let (ps, vbuf, ibuf, samp) = super::init::effects::build_decal_resources_for_runtime(
&self.device,
self.hot_reload.enabled,
)?;
self.decal.pipeline = Some(ps);
self.decal.cube_vertex_buffer = Some(vbuf);
self.decal.cube_index_buffer = Some(ibuf);
self.decal.sampler = Some(samp);
}
let idx = if let Some(slot) = self.decal.free_slots.pop() {
self.decal.records[slot] = Some(record);
slot
} else {
self.decal.records.push(Some(record));
self.decal.records.len() - 1
};
Ok(idx)
}
pub(crate) fn remove_decal(&mut self, decal_id: usize) -> Result<(), String> {
let slot = self
.decal
.records
.get_mut(decal_id)
.ok_or_else(|| format!("remove_decal: id {} out of range", decal_id))?;
if slot.is_none() {
return Err(format!("remove_decal: id {} already removed", decal_id));
}
*slot = None;
self.decal.free_slots.push(decal_id);
Ok(())
}
pub(crate) fn add_emitter(
&mut self,
record: crate::gfx::particles::ParticleEmitterRecord,
) -> Result<usize, String> {
if self.particle.pipelines.is_none() {
let pipelines =
super::particle::build_particle_pipelines(&self.device, self.hot_reload.enabled)?;
self.particle.pipelines = Some(pipelines);
}
let gpu_state =
super::particle::build_emitter_gpu_state(&self.device, &record, self.frames_in_flight)?;
let idx = if let Some(slot) = self.particle.free_slots.pop() {
self.particle.records[slot] = Some(record);
self.particle.emitter_state[slot] = Some(gpu_state);
slot
} else {
self.particle.records.push(Some(record));
self.particle.emitter_state.push(Some(gpu_state));
self.particle.records.len() - 1
};
Ok(idx)
}
pub(crate) fn remove_emitter(&mut self, emitter_id: usize) -> Result<(), String> {
let rec_slot = self
.particle
.records
.get_mut(emitter_id)
.ok_or_else(|| format!("remove_emitter: id {} out of range", emitter_id))?;
if rec_slot.is_none() {
return Err(format!("remove_emitter: id {} already removed", emitter_id));
}
*rec_slot = None;
if let Some(gpu_slot) = self.particle.emitter_state.get_mut(emitter_id) {
*gpu_slot = None;
}
self.particle.free_slots.push(emitter_id);
Ok(())
}
pub(crate) fn window_closed(&self) -> bool {
if self.window.appkit.closed() {
return true;
}
self.window.was_visible && self.window.appkit.window().is_some_and(|w| !w.isVisible())
}
pub(crate) fn wait_idle(&self) {
if let Some(cmd_buf) = self.command_queue.commandBuffer() {
cmd_buf.commit();
cmd_buf.waitUntilCompleted();
}
}
pub(super) fn take_device_error(&self) -> Option<crate::gfx::error::RenderError> {
self.diagnostics
.device_error
.lock()
.ok()
.and_then(|mut slot| slot.take())
}
}
impl crate::gfx::scene_flow::SceneControl for MtlContext {
fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
self.update_visibility(draw_idx, visible);
}
fn set_fade(&mut self, fade: f32) {
self.set_fade(fade);
}
}
impl Drop for MtlContext {
fn drop(&mut self) {
self.wait_idle();
crate::runtime_cache::checkpoint();
if !self.window.owns {
return;
}
self.window.appkit.release_cursor();
if let Some(window) = self.window.appkit.window() {
window.close();
} else {
self.window.view.removeFromSuperview();
}
}
}
pub(super) fn copy_buffer_prefix(
src: &ProtocolObject<dyn MTLBuffer>,
dst: &ProtocolObject<dyn MTLBuffer>,
len: usize,
) {
if len == 0 {
return;
}
assert!(
len <= src.length() && len <= dst.length(),
"copy_buffer_prefix: {len} bytes exceeds src {} / dst {}",
src.length(),
dst.length()
);
unsafe {
let s = src.contents().as_ptr() as *const u8;
let d = dst.contents().as_ptr() as *mut u8;
std::ptr::copy_nonoverlapping(s, d, len);
}
}
pub(super) fn bytes_of_slice<T: bytemuck::NoUninit>(slice: &[T]) -> &[u8] {
bytemuck::cast_slice(slice)
}
pub(super) fn write_buffer_slice<T: Copy>(
buffer: &ProtocolObject<dyn MTLBuffer>,
data: &[T],
) -> Result<(), String> {
let bytes = std::mem::size_of_val(data);
if bytes == 0 {
return Ok(());
}
let len = buffer.length();
if bytes > len {
return Err(format!(
"buffer write of {bytes} bytes exceeds buffer length {len}"
));
}
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr().cast::<u8>(),
buffer.contents().as_ptr().cast::<u8>(),
bytes,
);
}
Ok(())
}
pub(super) fn write_buffer_region(
buffer: &ProtocolObject<dyn MTLBuffer>,
offset: usize,
src: &[u8],
) -> Result<(), String> {
let len = buffer.length();
if offset.checked_add(src.len()).is_none_or(|end| end > len) {
return Err(format!(
"buffer write [{}, {}) exceeds buffer length {}",
offset,
offset.saturating_add(src.len()),
len
));
}
if src.is_empty() {
return Ok(());
}
let dst = buffer.contents().as_ptr() as *mut u8;
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst.add(offset), src.len());
}
Ok(())
}
pub(super) fn zero_buffer_region(
buffer: &ProtocolObject<dyn MTLBuffer>,
offset: usize,
len: usize,
) -> Result<(), String> {
let buf_len = buffer.length();
if offset.checked_add(len).is_none_or(|end| end > buf_len) {
return Err(format!(
"buffer zero [{}, {}) exceeds buffer length {}",
offset,
offset.saturating_add(len),
buf_len
));
}
if len == 0 {
return Ok(());
}
let dst = buffer.contents().as_ptr() as *mut u8;
unsafe {
std::ptr::write_bytes(dst.add(offset), 0, len);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn counts(total: usize, skinned_base: usize) -> DrawRecordCounts {
DrawRecordCounts {
total,
skinned_base,
}
}
#[test]
fn static_only_set_is_all_prefix() {
let c = counts(12, 12);
assert_eq!(c.prefix(0), Some(0..12));
assert_eq!(c.skinned_tail(0), None);
}
#[test]
fn folded_skinned_set_splits_at_the_base() {
let c = counts(12, 9);
assert_eq!(c.prefix(0), Some(0..9));
assert_eq!(c.skinned_tail(0), Some(9..12));
}
#[test]
fn skinned_only_set_has_no_prefix() {
let c = counts(4, 0);
assert_eq!(c.prefix(0), None);
assert_eq!(c.skinned_tail(0), Some(0..4));
}
#[test]
fn empty_set_yields_no_ranges() {
let c = counts(0, 0);
assert_eq!(c.prefix(0), None);
assert_eq!(c.skinned_tail(0), None);
}
#[test]
fn block_offsets_both_ranges_into_a_cascade() {
let c = counts(12, 9);
assert_eq!(c.prefix(24), Some(24..33));
assert_eq!(c.skinned_tail(24), Some(33..36));
}
#[test]
fn ranges_stay_inside_the_block_they_start() {
let c = counts(12, 9);
for cascade in 0..4 {
let block = cascade * c.total;
let tail = c.skinned_tail(block).expect("skinned tail");
assert_eq!(c.prefix(block).expect("prefix").start, block);
assert_eq!(tail.end, block + c.total);
}
}
#[test]
fn ns_range_carries_start_and_length() {
let r = ns_range(9..12);
assert_eq!(r.location, 9);
assert_eq!(r.length, 3);
}
}