#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::render::shader_programs::metal::bindless_textures;
use core::marker::PhantomData;
use core::ptr::NonNull;
use objc2::Message as _;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer, MTLRenderCommandEncoder, MTLRenderStages, MTLResource, MTLResourceID,
MTLResourceUsage, MTLTexture,
};
const RESOURCE_ID_BYTES: usize = size_of::<MTLResourceID>();
const fn resource_id_offset(id: usize) -> usize {
id * RESOURCE_ID_BYTES
}
pub(super) const fn bindless_block_len(pool_len: usize) -> usize {
resource_id_offset(bindless_textures::pool(pool_len))
}
pub(super) const BINDLESS_POOL_OFFSET: usize = resource_id_offset(bindless_textures::pool(0));
pub(super) struct ResourceIdWriter<'a> {
base: NonNull<MTLResourceID>,
ids: usize,
_buffer: PhantomData<&'a ProtocolObject<dyn MTLBuffer>>,
}
impl<'a> ResourceIdWriter<'a> {
pub(super) fn new(buffer: &'a ProtocolObject<dyn MTLBuffer>) -> Self {
Self {
base: buffer.contents().cast::<MTLResourceID>(),
ids: buffer.length() / RESOURCE_ID_BYTES,
_buffer: PhantomData,
}
}
pub(super) fn set(&mut self, id: usize, texture: &ProtocolObject<dyn MTLTexture>) {
assert!(
id < self.ids,
"argument id {id} past a block of {} ids",
self.ids
);
unsafe { self.base.add(id).write(texture.gpuResourceID()) };
}
}
pub(super) struct Signature(std::collections::hash_map::DefaultHasher);
impl Signature {
pub(super) fn new() -> Self {
Self(std::collections::hash_map::DefaultHasher::new())
}
pub(super) fn push_u64(&mut self, v: u64) {
use std::hash::Hasher as _;
self.0.write_u64(v);
}
pub(super) fn push_texture(&mut self, tex: &ProtocolObject<dyn MTLTexture>) {
use std::hash::Hasher as _;
self.0.write_usize(tex as *const _ as *const () as usize);
}
pub(super) fn finish(&self) -> u64 {
use std::hash::Hasher as _;
self.0.finish()
}
}
pub(super) struct SlotGates {
accepted: Vec<Option<u64>>,
}
impl SlotGates {
pub(super) fn new(depth: usize) -> Self {
Self {
accepted: vec![None; depth.max(1)],
}
}
pub(super) fn stale(&mut self, slot: usize, sig: u64) -> bool {
let idx = slot % self.accepted.len();
if self.accepted[idx] == Some(sig) {
return false;
}
self.accepted[idx] = Some(sig);
true
}
pub(super) fn invalidate(&mut self, slot: usize) {
let idx = slot % self.accepted.len();
self.accepted[idx] = None;
}
}
pub(super) struct ResidencySet {
accepted: Option<u64>,
owned: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
ptrs: Vec<NonNull<ProtocolObject<dyn MTLResource>>>,
}
impl ResidencySet {
pub(super) fn new() -> Self {
Self {
accepted: None,
owned: Vec::new(),
ptrs: Vec::new(),
}
}
pub(super) fn refresh<'a>(
&mut self,
sig: u64,
textures: impl Iterator<Item = &'a ProtocolObject<dyn MTLTexture>>,
) {
if self.accepted == Some(sig) {
return;
}
self.accepted = Some(sig);
self.owned.clear();
self.owned.extend(textures.map(|t| t.retain()));
self.ptrs.clear();
self.ptrs.extend(
self.owned
.iter()
.map(|t| NonNull::from(ProtocolObject::from_ref(&**t))),
);
}
pub(super) fn declare_fragment(&self, enc: &ProtocolObject<dyn MTLRenderCommandEncoder>) {
if self.ptrs.is_empty() {
return;
}
let Some(first) = NonNull::new(self.ptrs.as_ptr().cast_mut()) else {
return;
};
unsafe {
enc.useResources_count_usage_stages(
first,
self.ptrs.len(),
MTLResourceUsage::Read,
MTLRenderStages::Fragment,
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
BINDLESS_POOL_OFFSET, RESOURCE_ID_BYTES, Signature, SlotGates, bindless_block_len,
bindless_textures, resource_id_offset,
};
#[test]
fn signature_is_order_sensitive_over_counters() {
let sig = |vals: &[u64]| {
let mut s = Signature::new();
for v in vals {
s.push_u64(*v);
}
s.finish()
};
assert_eq!(sig(&[1, 2, 3]), sig(&[1, 2, 3]));
assert_ne!(sig(&[1, 2, 3]), sig(&[3, 2, 1]));
assert_ne!(sig(&[1, 2, 3]), sig(&[1, 2, 4]));
}
#[test]
fn a_fresh_gate_is_stale_for_every_slot() {
let mut gates = SlotGates::new(3);
for slot in 0..3 {
assert!(gates.stale(slot, 7));
}
}
#[test]
fn an_accepted_signature_is_not_stale_again() {
let mut gates = SlotGates::new(2);
assert!(gates.stale(0, 7));
assert!(!gates.stale(0, 7));
assert!(!gates.stale(0, 7));
}
#[test]
fn a_change_is_stale_once_per_slot() {
let mut gates = SlotGates::new(3);
for slot in 0..3 {
assert!(gates.stale(slot, 7));
}
for slot in 0..3 {
assert!(gates.stale(slot, 8));
assert!(!gates.stale(slot, 8));
}
}
#[test]
fn slots_wrap_modulo_the_gate_count() {
let mut gates = SlotGates::new(2);
assert!(gates.stale(0, 7));
assert!(!gates.stale(2, 7));
assert!(gates.stale(3, 7));
}
#[test]
fn invalidate_forces_one_re_encode() {
let mut gates = SlotGates::new(2);
assert!(gates.stale(1, 7));
gates.invalidate(1);
assert!(gates.stale(1, 7));
assert!(!gates.stale(1, 7));
}
#[test]
fn a_resource_id_block_is_one_id_per_member() {
assert_eq!(RESOURCE_ID_BYTES, 8);
assert_eq!(resource_id_offset(bindless_textures::PROBE_CUBES), 32);
assert_eq!(BINDLESS_POOL_OFFSET, bindless_textures::FIXED * 8);
assert_eq!(bindless_block_len(0), BINDLESS_POOL_OFFSET);
assert_eq!(
bindless_block_len(1024),
(bindless_textures::FIXED + 1024) * 8
);
assert_eq!(BINDLESS_POOL_OFFSET % RESOURCE_ID_BYTES, 0);
}
}