#![deny(unsafe_op_in_unsafe_fn)]
use super::error::allocation_failed;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::render_graph::{
PixelFormat, TextureUsage, TransientSlot, TransientTexture,
};
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::metal::descriptors::TextureDesc;
use crate::metal::context::MtlContext;
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],
) -> RenderResult<Self> {
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_for(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(|| allocation_failed(format_args!("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) -> RenderResult<&ProtocolObject<dyn MTLTexture>> {
self.texture_for("bloom_top")
.ok_or_else(|| RenderError::Other("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],
) -> RenderResult<()> {
*self = Self::build(device, slots)?;
Ok(())
}
}
fn new_slot_heap(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
size: usize,
) -> RenderResult<Retained<ProtocolObject<dyn MTLHeap>>> {
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(|| allocation_failed(format_args!("{size}-byte transient slot heap")))
}
pub(in crate::metal) fn texture_descriptor_for(
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,
}
}
pub(in crate::metal) 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::R16Float => MTLPixelFormat::R16Float,
PixelFormat::R32Float => MTLPixelFormat::R32Float,
PixelFormat::Rg32Float => MTLPixelFormat::RG32Float,
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)
}
impl MtlContext {
pub(in crate::metal) fn ao_output_texture(&self) -> &ProtocolObject<dyn MTLTexture> {
self.targets
.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.targets
.transient_pool
.texture_for("gbuffer_normal_depth")
}
pub(in crate::metal) fn gbuffer_roughness(&self) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.targets.transient_pool.texture_for("gbuffer_roughness")
}
pub(in crate::metal) fn gbuffer_velocity(&self) -> Option<&ProtocolObject<dyn MTLTexture>> {
self.targets.transient_pool.texture_for("gbuffer_velocity")
}
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::render::post::device::PostExtent;
use concinnity_core::render::post::{bloom, ssao};
use concinnity_core::render::render_graph::{self, PoolGates, plan_pool_slots};
#[test]
fn translated_descriptors_match_the_feature_formats() {
let gates = PoolGates {
ssao: true,
gbuffer: true,
};
let slots = plan_pool_slots(gates, (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_for(member("ao_output"));
assert_eq!(ao.pixelFormat(), pixel_format(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_for(member("bloom_top"));
let chain = bloom::chain_desc(PostExtent {
width: 1024,
height: 768,
});
assert_eq!(bloom.pixelFormat(), pixel_format(chain.format));
assert_eq!((bloom.width(), bloom.height()), (512, 384));
}
#[test]
fn a_volume_translates_to_a_3d_descriptor() {
let desc = texture_descriptor_for(&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: 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
);
}
}