#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::gfx::render_types;
use concinnity_core::gfx::render_types::{
ClusterParams, DrawIndex, InstancedCluster, LightUniforms, NUM_SHADOW_CASCADES, ShadowUniforms,
};
use concinnity_core::profile;
use concinnity_core::render::backend;
use concinnity_core::render::backend_init;
use concinnity_core::render::cluster_range::ClusterReach;
use concinnity_core::render::decal;
use concinnity_core::render::error;
use concinnity_core::render::hdr_output;
use concinnity_core::render::particles;
use concinnity_core::render::probe_book::ProbeBook;
use concinnity_core::render::render_graph;
use concinnity_core::render::scene_flow;
use concinnity_core::render::scene_state::SceneState;
use concinnity_core::render::shadow_schedule;
use concinnity_core::render::spot_shadow;
use concinnity_core::render::view_history::ViewHistory;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLArgumentEncoder as _, MTLBuffer, MTLCommandBuffer as _, MTLCommandQueue,
MTLDepthStencilState, MTLDevice as _, MTLIndirectCommandBuffer,
MTLIndirectCommandBufferDescriptor, MTLIndirectCommandType, MTLPixelFormat,
MTLRenderPipelineState, MTLResourceOptions, MTLSamplerState, MTLTexture,
};
use objc2_metal_kit::MTKView;
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::{
GBufferState, MtlBloomPass, 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 BINDLESS_TEXTURE_COUNT: usize =
concinnity_core::render::uniforms::BINDLESS_POOL_SIZE;
pub(super) const BINDLESS_TEXTURE_ARG_BUFFER_INDEX: usize = 7;
pub(super) const BINDLESS_SAMPLER_ARG_BUFFER_INDEX: usize = 10;
pub(super) struct InstancedState {
pub clusters: Vec<InstancedCluster>,
pub records: Vec<render_types::GpuObjectData>,
pub draw_args: Vec<render_types::GpuDrawArgs>,
pub any_lod: bool,
}
pub(super) struct ProbeState {
pub book: ProbeBook,
pub cubes: super::probe_set::ProbeCubeArray,
pub records_buf: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub bake: super::probe::MtlProbeBake,
pub prefilter: Option<super::probe_prefilter::ProbePrefilterPipelines>,
pub retire_pool: concinnity_core::render::retire_pool::RetirePool<super::probe::RetiredBake>,
}
pub(super) struct ShadowState {
pub enabled: bool,
pub map: Retained<ProtocolObject<dyn MTLTexture>>,
pub map_size: u32,
pub cadence: backend_init::ShadowCadence,
pub scheduler: 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 frusta: Vec<Frustum>,
pub scheduler: spot_shadow::SpotShadowScheduler,
pub render_mask: u32,
}
impl SpotShadowState {
pub(super) fn refreshed_slices(&self) -> impl Iterator<Item = u32> {
spot_shadow::refreshed_slices(self.render_mask, self.count)
}
}
pub(super) struct FrameRings {
pub object: super::frame_rings::TransientRing,
pub draw_args: super::frame_rings::TransientRing,
pub model_history: super::frame_rings::TransientRing,
pub bindless_tex: super::frame_rings::TransientRing,
pub probe_records: super::frame_rings::TransientRing,
pub joint: super::frame_rings::JointRing,
pub object_scratch: Vec<render_types::GpuObjectData>,
pub draw_args_scratch: Vec<render_types::GpuDrawArgs>,
pub material_params: super::material_params::MaterialParamRing,
}
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<super::glass::TracedGlassPipelines>,
pub pipeline_rt_textured: Option<super::glass::TracedGlassPipelines>,
pub mesh_pipeline_rt: Option<super::glass::TracedGlassPipelines>,
pub mesh_pipeline_rt_textured: Option<super::glass::TracedGlassPipelines>,
pub seethrough_mesh_indices: Vec<usize>,
pub panels: Vec<super::glass::GlassPanelRecord>,
pub reflection_targets: Option<super::glass::GlassReflectionTargets>,
}
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>>,
pub generation: u64,
}
impl HotReloadState {
pub(super) fn new(enabled: bool) -> Self {
Self {
enabled,
reload_pending: enabled
.then(|| std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false))),
generation: 0,
}
}
}
pub(super) struct WindowState {
pub appkit: crate::appkit::AppKitWindow,
pub view: Retained<MTKView>,
}
impl Drop for WindowState {
fn drop(&mut self) {
self.appkit.release_cursor();
if let Some(window) = self.appkit.window() {
window.close();
} else {
self.view.removeFromSuperview();
}
}
}
pub(super) struct Diagnostics {
pub frame_stats: profile::RenderStats,
pub gpu_time_us: std::sync::Arc<std::sync::atomic::AtomicU32>,
pub device_error: std::sync::Arc<std::sync::Mutex<Option<error::RenderError>>>,
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 MtlHardware {
pub device: Retained<ProtocolObject<dyn objc2_metal::MTLDevice>>,
pub allocator: DeviceAllocator,
pub command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
pub graph_queues: Option<super::graph_queues::GraphQueues>,
pub swap_pixel_format: MTLPixelFormat,
pub hdr_mode: hdr_output::HdrOutputMode,
pub swapchain_config: backend_init::SwapchainConfig,
pub window: Option<WindowState>,
}
impl MtlHardware {
pub(super) fn hand_over(&mut self) -> error::RenderResult<Self> {
Ok(Self {
window: Some(self.window.take().ok_or_else(|| {
error::RenderError::Other("apply_world_reload: window already taken".to_string())
})?),
graph_queues: self.graph_queues.take(),
allocator: DeviceAllocator::new(&self.device, self.swapchain_config.frames_in_flight),
device: self.device.clone(),
command_queue: self.command_queue.clone(),
swap_pixel_format: self.swap_pixel_format,
hdr_mode: self.hdr_mode,
swapchain_config: self.swapchain_config,
})
}
pub(super) fn max_edr(&self) -> Option<f32> {
match self.hdr_mode {
hdr_output::HdrOutputMode::Hdr { max_edr, .. } => Some(max_edr),
hdr_output::HdrOutputMode::Sdr => None,
}
}
pub(super) fn hdr_encoding(&self) -> Option<hdr_output::HdrEncoding> {
match self.hdr_mode {
hdr_output::HdrOutputMode::Hdr { encoding, .. } => Some(encoding),
hdr_output::HdrOutputMode::Sdr => None,
}
}
}
pub(super) struct MtlTargets {
pub hdr: HdrTargets,
pub output: (u32, u32),
pub transient_pool: TransientTexturePool,
pub depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pub depth_state_inclusive: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pub depth_state_read_only: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
pub geometry_less: bool,
}
pub(super) struct MtlSceneAssets {
pub vertex_buffer: PooledBuffer,
pub index_buffer: PooledBuffer,
pub textures: Vec<PooledTexture>,
pub fallback_textures: Vec<PooledTexture>,
pub local_light_buffer: PooledBuffer,
pub cluster_reach: ClusterReach,
pub area_light_buffer: PooledBuffer,
pub ltc_matrix_texture: PooledTexture,
pub ltc_magnitude_texture: PooledTexture,
pub env_map: EnvironmentMapTextures,
pub color_lut: PooledTexture,
pub sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
pub cube_sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
}
pub(super) struct MtlArgumentBuffers {
pub bindless_tex_gates: super::bindless_args::SlotGates,
pub bindless_tail_gates: super::bindless_args::SlotGates,
pub bindless_residency: super::bindless_args::ResidencySet,
pub bindless_sampler_args: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub texture_epoch: u64,
}
pub(super) struct CompositeState {
pub pipeline: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
}
pub(crate) struct MtlContext {
pub(super) last_present_texture: Option<Retained<ProtocolObject<dyn MTLTexture>>>,
pub(super) cull: CullState,
pub(super) arg_buffers: MtlArgumentBuffers,
pub(super) state: SceneState,
pub(super) graph_cache: Option<(render_graph::FrameGraphInputs, render_graph::CompiledGraph)>,
pub(super) instanced: InstancedState,
pub(super) scene: MtlSceneAssets,
pub(super) light_uniforms: LightUniforms,
pub(super) shadow: ShadowState,
pub(super) spot_shadow: SpotShadowState,
pub(super) probe: ProbeState,
pub(super) text: TextState,
pub(super) targets: MtlTargets,
pub(super) composite: CompositeState,
pub(super) bloom: Option<MtlBloomPass>,
pub(super) post_process: render_types::PostProcessParams,
pub(super) taa: TaaState,
pub(super) view_history: ViewHistory,
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) world_shader: Option<concinnity_core::components::ShaderPrograms>,
pub(super) capture: bool,
pub(super) skinned: SkinnedState,
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,
pub(super) sky: super::sky::SkyState,
pub(super) hw: MtlHardware,
}
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 {
#[inline]
pub(super) fn window(&self) -> &WindowState {
self.hw
.window
.as_ref()
.expect("MtlContext window taken by reload_world")
}
#[inline]
pub(super) fn window_mut(&mut self) -> &mut WindowState {
self.hw
.window
.as_mut()
.expect("MtlContext window taken by reload_world")
}
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.state.draw.objects.len() + self.state.draw.n_instances + self.state.draw.n_skinned
}
pub(super) fn skinned_record_base(&self) -> usize {
self.state.draw.objects.len() + self.state.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.scene.index_buffer.as_ref(),
}
}
pub(super) fn ensure_icb_capacity(&mut self, count: usize) -> error::RenderResult<()> {
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
.hw
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| super::error::allocation_failed("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
.hw
.device
.newBufferWithLength_options(
new_cap * std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| super::error::allocation_failed("cull status buffer"))?;
self.cull.status_buffer = Some(status);
if self.cull.two_pass_occlusion {
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_encoder.encodedLength().max(16);
let buf = self
.hw
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| {
super::error::allocation_failed("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_encoder.setArgumentBuffer_offset(Some(arg_buf2), 0);
for (b, icb) in icbs_2.iter().enumerate() {
arg_encoder.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) -> error::RenderResult<()> {
if self.cull.shadow_pipeline.is_none() {
return Ok(());
}
let mut cascades = std::mem::take(&mut self.cull.shadow_views);
let grown = self.grow_shadow_view_icb(&mut cascades, NUM_SHADOW_CASCADES, count);
self.cull.shadow_views = cascades;
grown?;
let mut spots = std::mem::take(&mut self.cull.spot_views);
let grown = self.grow_shadow_view_icb(&mut spots, self.spot_shadow.count as usize, count);
self.cull.spot_views = spots;
grown
}
fn grow_shadow_view_icb(
&self,
set: &mut crate::metal::cull::ShadowViewIcb,
views: usize,
count: usize,
) -> error::RenderResult<()> {
let Some(arg_encoder) = &self.cull.icb_arg_encoder else {
return Ok(());
};
let needed = count.saturating_mul(views);
if needed == 0 || (set.icb.is_some() && needed <= set.capacity) {
return Ok(());
}
let new_cap = needed.next_power_of_two().max(64);
let icb = self.build_cull_icb(new_cap)?;
let arg_buf = match set.arg_buffer.take() {
Some(buf) => buf,
None => {
let len = arg_encoder.encodedLength().max(16);
self.hw
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| {
super::error::allocation_failed("shadow view ICB argument buffer")
})?
}
};
unsafe {
arg_encoder.setArgumentBuffer_offset(Some(&arg_buf), 0);
arg_encoder.setIndirectCommandBuffer_atIndex(Some(&icb), 0);
}
let status = self
.hw
.device
.newBufferWithLength_options(
new_cap * std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| super::error::allocation_failed("shadow view cull status buffer"))?;
set.arg_buffer = Some(arg_buf);
set.status = Some(status);
set.icb = Some(icb);
set.capacity = new_cap;
Ok(())
}
pub(super) fn ensure_mirror_icb_capacity(
&mut self,
slot_count: usize,
count: usize,
) -> error::RenderResult<()> {
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
.hw
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| super::error::allocation_failed("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
.hw
.device
.newBufferWithLength_options(
new_cap * std::mem::size_of::<u32>(),
MTLResourceOptions::StorageModePrivate,
)
.ok_or_else(|| super::error::allocation_failed("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,
) -> error::RenderResult<Retained<ProtocolObject<dyn MTLIndirectCommandBuffer>>> {
let desc = MTLIndirectCommandBufferDescriptor::new();
desc.setCommandTypes(MTLIndirectCommandType::DrawIndexed);
desc.setInheritBuffers(true);
desc.setInheritPipelineState(true);
desc.setMaxVertexBufferBindCount(0);
desc.setMaxFragmentBufferBindCount(0);
unsafe {
self.hw
.device
.newIndirectCommandBufferWithDescriptor_maxCommandCount_options(
&desc,
cap,
MTLResourceOptions::StorageModePrivate,
)
}
.ok_or_else(|| super::error::allocation_failed("indirect command buffer"))
}
pub(crate) fn capabilities(&self) -> backend::DeviceCapabilities {
backend::DeviceCapabilities {
ray_tracing: super::raytrace::raytracing_supported(&self.hw.device),
selectable_upscaler: false,
reuses_build_slots: true,
rewrites_draws: true,
}
}
pub(crate) fn gpu_profile(&self) -> backend::GpuProfile {
super::gpu_profile::device_profile(&self.hw.device)
}
pub(crate) fn render_stats(&self) -> 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
.adaptation
.as_ref()
.map(|a| a.current_ev());
stats.max_edr = self.hw.max_edr();
stats
}
pub(crate) fn set_material_params(
&mut self,
row: u32,
params: [f32; render_types::MATERIAL_PARAM_COUNT],
) {
self.rings.material_params.set(row, params);
}
pub(crate) fn add_decal(&mut self, record: decal::DecalRecord) -> error::RenderResult<usize> {
if self.decal.pipeline.is_none() {
let (ps, vbuf, ibuf, samp) = super::init::world_fx::build_decal_resources_for_runtime(
&self.hw.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);
}
self.decal
.set
.insert(record)
.map_err(|_| error::RenderError::Other("add_decal: decal set is full".to_string()))
}
pub(crate) fn remove_decal(&mut self, decal_id: usize) -> error::RenderResult<()> {
self.decal
.set
.remove(decal_id)
.map_err(|e| error::RenderError::Other(format!("remove_decal: id {decal_id} {e}")))
}
pub(crate) fn add_emitter(
&mut self,
record: particles::ParticleEmitterRecord,
) -> error::RenderResult<usize> {
if self.particle.pipelines.is_none() {
let pipelines = super::particle::build_particle_pipelines(
&self.hw.device,
self.hot_reload.enabled,
)?;
self.particle.pipelines = Some(pipelines);
}
let gpu_state = super::particle::build_emitter_gpu_state(&self.hw.device, &record)?;
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) -> error::RenderResult<()> {
let rec_slot = self.particle.records.get_mut(emitter_id).ok_or_else(|| {
error::RenderError::Other(format!("remove_emitter: id {emitter_id} out of range"))
})?;
if rec_slot.is_none() {
return Err(error::RenderError::Other(format!(
"remove_emitter: id {emitter_id} already removed"
)));
}
*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 {
self.window().appkit.closed()
}
pub(crate) fn wait_idle(&self) {
if let Some(cmd_buf) = self.hw.command_queue.commandBuffer() {
cmd_buf.commit();
cmd_buf.waitUntilCompleted();
}
}
pub(super) fn take_device_error(&self) -> Option<error::RenderError> {
self.diagnostics
.device_error
.lock()
.ok()
.and_then(|mut slot| slot.take())
}
}
impl scene_flow::SceneControl for MtlContext {
fn update_visibility(&mut self, draw_idx: DrawIndex, visible: bool) {
self.state.update_visibility(draw_idx, visible);
}
fn set_fade(&mut self, fade: f32) {
self.state.set_fade(fade);
}
}
impl Drop for MtlContext {
fn drop(&mut self) {
self.wait_idle();
crate::shader::runtime_cache::checkpoint();
}
}
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],
) -> error::RenderResult<()> {
let bytes = std::mem::size_of_val(data);
if bytes == 0 {
return Ok(());
}
let len = buffer.length();
if bytes > len {
return Err(error::RenderError::Other(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],
) -> error::RenderResult<()> {
let len = buffer.length();
if offset.checked_add(src.len()).is_none_or(|end| end > len) {
return Err(error::RenderError::Other(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,
) -> error::RenderResult<()> {
let buf_len = buffer.length();
if offset.checked_add(len).is_none_or(|end| end > buf_len) {
return Err(error::RenderError::Other(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);
}
}