#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLDevice as _, MTLHazardTrackingMode, MTLHeap, MTLHeapDescriptor, MTLHeapType, MTLPixelFormat,
MTLStorageMode, MTLTexture, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage,
};
use crate::gfx::render_graph::{
PixelFormat, PoolGates, TextureUsage, TransientSlot, TransientTexture, plan_pool_slots,
};
use crate::metal::context::MtlContext;
use crate::metal::descriptors::TextureDesc;
struct PooledTexture {
label: &'static str,
texture: Retained<ProtocolObject<dyn MTLTexture>>,
}
pub(super) struct TransientTexturePool {
heaps: Vec<Retained<ProtocolObject<dyn MTLHeap>>>,
textures: Vec<PooledTexture>,
#[cfg(debug_assertions)]
slot_labels: Vec<Vec<&'static str>>,
}
impl TransientTexturePool {
pub(super) fn build(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
slots: &[TransientSlot],
) -> Result<Self, String> {
let mut heaps = Vec::with_capacity(slots.len());
let mut textures = Vec::new();
let mut unaliased_bytes: u64 = 0;
for slot in slots {
let mut slot_size: usize = 0;
let mut descs = Vec::with_capacity(slot.members.len());
for m in &slot.members {
let desc = texture_descriptor(m);
let size = device.heapTextureSizeAndAlignWithDescriptor(&desc).size;
unaliased_bytes += size as u64;
slot_size = slot_size.max(size);
descs.push((m.label, desc));
}
let heap = new_slot_heap(device, slot_size)?;
for (label, desc) in descs {
let texture = unsafe { heap.newTextureWithDescriptor_offset(&desc, 0) }
.ok_or_else(|| format!("failed to place transient texture {label}"))?;
textures.push(PooledTexture { label, texture });
}
heaps.push(heap);
}
let pool = Self {
heaps,
textures,
#[cfg(debug_assertions)]
slot_labels: slots.iter().map(|s| s.labels()).collect(),
};
let aliased_bytes = pool.heap_bytes();
tracing::info!(
"transient texture pool: {} slot heap(s), {} KiB ({} KiB saved by aliasing)",
pool.heaps.len(),
aliased_bytes / 1024,
unaliased_bytes.saturating_sub(aliased_bytes) / 1024,
);
Ok(pool)
}
pub(super) fn texture_for(&self, label: &str) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.lookup(label).map(|t| t.texture.as_ref())
}
pub(super) fn bloom_top(&self) -> Result<Retained<ProtocolObject<dyn MTLTexture>>, String> {
self.lookup("bloom_top")
.map(|t| t.texture.clone())
.ok_or_else(|| "bloom_top missing from transient pool".to_string())
}
pub(super) fn heap_bytes(&self) -> u64 {
self.heaps.iter().map(|h| h.size() as u64).sum()
}
#[cfg(debug_assertions)]
pub(super) fn slot_labels(&self) -> &[Vec<&'static str>] {
&self.slot_labels
}
fn lookup(&self, label: &str) -> Option<&PooledTexture> {
self.textures.iter().find(|t| t.label == label)
}
pub(super) fn rebuild(
&mut self,
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
slots: &[TransientSlot],
) -> Result<(), String> {
*self = Self::build(device, slots)?;
Ok(())
}
}
fn new_slot_heap(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
size: usize,
) -> Result<Retained<ProtocolObject<dyn MTLHeap>>, String> {
let desc = MTLHeapDescriptor::new();
desc.setType(MTLHeapType::Placement);
desc.setStorageMode(MTLStorageMode::Private);
desc.setHazardTrackingMode(MTLHazardTrackingMode::Tracked);
desc.setSize(size.max(1));
device
.newHeapWithDescriptor(&desc)
.ok_or_else(|| format!("failed to create {size}-byte transient slot heap"))
}
fn texture_descriptor(spec: &TransientTexture) -> Retained<MTLTextureDescriptor> {
TextureDesc {
kind: texture_type(spec),
format: pixel_format(spec.format),
width: spec.width.max(1) as usize,
height: spec.height.max(1) as usize,
depth: spec.depth.max(1) as usize,
array_length: spec.array_layers.max(1) as usize,
mip_count: spec.mip_levels.max(1) as usize,
sample_count: spec.sample_count.max(1) as usize,
usage: texture_usage(spec.usage),
..Default::default()
}
.build()
}
fn texture_type(spec: &TransientTexture) -> MTLTextureType {
match (
spec.depth.max(1) > 1,
spec.array_layers.max(1) > 1,
spec.sample_count.max(1) > 1,
) {
(true, _, _) => MTLTextureType::Type3D,
(_, true, _) => MTLTextureType::Type2DArray,
(_, _, true) => MTLTextureType::Type2DMultisample,
_ => MTLTextureType::Type2D,
}
}
fn pixel_format(format: PixelFormat) -> MTLPixelFormat {
match format {
PixelFormat::Rgba16Float => MTLPixelFormat::RGBA16Float,
PixelFormat::Rgba8Unorm => MTLPixelFormat::RGBA8Unorm,
PixelFormat::Rg16Float => MTLPixelFormat::RG16Float,
PixelFormat::R8Unorm => MTLPixelFormat::R8Unorm,
PixelFormat::R32Float => MTLPixelFormat::R32Float,
PixelFormat::Depth32Float => MTLPixelFormat::Depth32Float,
PixelFormat::BgraSwapchain => MTLPixelFormat::BGRA8Unorm,
}
}
fn texture_usage(usage: TextureUsage) -> MTLTextureUsage {
let mut bits = 0;
if usage.contains(TextureUsage::SHADER_READ) {
bits |= MTLTextureUsage::ShaderRead.0;
}
if usage.contains(TextureUsage::RENDER_TARGET) || usage.contains(TextureUsage::DEPTH_STENCIL) {
bits |= MTLTextureUsage::RenderTarget.0;
}
if usage.contains(TextureUsage::STORAGE) {
bits |= MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::ShaderWrite.0;
}
MTLTextureUsage(bits)
}
pub(super) fn transient_slots(
ssao_enabled: bool,
gbuffer_enabled: bool,
render_extent: (u32, u32),
output_extent: (u32, u32),
) -> Result<Vec<TransientSlot>, String> {
plan_pool_slots(
PoolGates {
ssao: ssao_enabled,
bloom: true,
gbuffer: gbuffer_enabled,
},
render_extent,
output_extent,
)
}
impl MtlContext {
pub(in crate::metal) fn ao_output_texture(&self) -> &ProtocolObject<dyn MTLTexture> {
self.transient_pool
.texture_for("ao_output")
.unwrap_or_else(|| self.ssao.white.as_ref())
}
pub(in crate::metal) fn gbuffer_normal_depth(&self) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.transient_pool.texture_for("gbuffer_normal_depth")
}
pub(in crate::metal) fn gbuffer_roughness(&self) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.transient_pool.texture_for("gbuffer_roughness")
}
pub(in crate::metal) fn gbuffer_velocity(&self) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.transient_pool.texture_for("gbuffer_velocity")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_late_bloom_target_aliases_an_early_one() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let shared: Vec<Vec<&str>> = slots
.iter()
.map(|s| s.labels())
.filter(|l| l.len() > 1)
.collect();
assert!(
shared.iter().any(|l| l.contains(&"bloom_top")),
"bloom_top should reuse an earlier member's heap: {:?}",
slots.iter().map(|s| s.labels()).collect::<Vec<_>>()
);
let pair = shared
.iter()
.find(|l| l.contains(&"bloom_top"))
.expect("checked above");
assert_ne!(pair[0], "bloom_top", "{pair:?}");
}
#[test]
fn the_gbuffer_colour_targets_are_pooled_and_depth_is_not() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let labels: Vec<&str> = slots.iter().flat_map(|s| s.labels()).collect();
for want in [
"gbuffer_normal_depth",
"gbuffer_roughness",
"gbuffer_velocity",
] {
assert!(labels.contains(&want), "{want} pooled: {labels:?}");
}
assert!(
!labels.contains(&"gbuffer_depth"),
"gbuffer_depth must stay feature-owned: {labels:?}"
);
}
#[test]
fn the_gbuffer_gate_is_what_places_them() {
let slots = transient_slots(true, false, (1024, 768), (1024, 768)).expect("plans");
let labels: Vec<&str> = slots.iter().flat_map(|s| s.labels()).collect();
assert!(!labels.contains(&"gbuffer_normal_depth"), "{labels:?}");
}
#[test]
fn bloom_top_alone_is_unshared() {
let slots = transient_slots(false, false, (1024, 768), (1024, 768)).expect("plans");
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].members.len(), 1);
assert_eq!(slots[0].members[0].label, "bloom_top");
}
#[test]
fn translated_descriptors_match_the_feature_formats() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let member = |label: &str| {
slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == label)
.expect("member present")
};
let ao = texture_descriptor(member("ao_output"));
assert_eq!(
ao.pixelFormat(),
super::super::post::ssao::SSAO_OCCLUSION_FORMAT
);
assert_eq!((ao.width(), ao.height()), (1024, 768));
assert_eq!(ao.textureType(), MTLTextureType::Type2D);
assert_eq!(ao.mipmapLevelCount(), 1);
assert_eq!(ao.sampleCount(), 1);
assert_eq!(
ao.usage().0,
MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0
);
let bloom = texture_descriptor(member("bloom_top"));
assert_eq!(bloom.pixelFormat(), super::super::post::bloom::BLOOM_FORMAT);
assert_eq!((bloom.width(), bloom.height()), (512, 384));
}
#[test]
fn a_volume_translates_to_a_3d_descriptor() {
let desc = texture_descriptor(&TransientTexture {
label: "probe_volume",
width: 80,
height: 45,
depth: 64,
format: PixelFormat::Rgba16Float,
sample_count: 1,
array_layers: 1,
mip_levels: 1,
usage: TextureUsage::STORAGE.union(TextureUsage::SHADER_READ),
clear: crate::gfx::render_graph::ClearValue::Color([0.0; 4]),
});
assert_eq!(desc.textureType(), MTLTextureType::Type3D);
assert_eq!(desc.depth(), 64);
assert_eq!(
desc.usage().0,
MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::ShaderWrite.0
);
}
}