#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLDevice as _, MTLPixelFormat, MTLTexture, MTLTextureType, MTLTextureUsage};
use super::allocator::{DeviceAllocator, PooledTexture};
use super::descriptors::TextureDesc;
pub(super) fn upload_texture(
alloc: &DeviceAllocator,
width: u32,
height: u32,
pixels: &[u8],
) -> Result<PooledTexture, String> {
let base = (width as usize) * (height as usize) * 4;
if pixels.len() < base {
return Err(format!(
"pixel data too short for {}x{} RGBA texture ({} bytes, need {})",
width,
height,
pixels.len(),
base
));
}
let chain = crate::gfx::mipmap::generate_mip_chain(width, height, pixels);
let desc = TextureDesc {
width: width as usize,
height: height as usize,
mip_count: chain.len(),
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
for (mip, level) in chain.iter().enumerate() {
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: level.width as usize,
height: level.height as usize,
depth: 1,
},
};
let bytes_per_row = (level.width * 4) as usize;
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region,
mip,
std::ptr::NonNull::new(level.pixels.as_ptr() as *mut _)
.ok_or("pixel slice is empty")?,
bytes_per_row,
);
}
}
Ok(texture)
}
pub(super) fn upload_texture_image(
alloc: &DeviceAllocator,
image: &concinnity_core::build::texture::TextureImage,
) -> Result<PooledTexture, String> {
use concinnity_core::build::texture::TextureFormat;
if image.format == TextureFormat::Rgba8 {
let mip = image
.mips
.first()
.ok_or("RGBA8 texture image has no mip level")?;
return upload_texture(alloc, mip.width, mip.height, &mip.data);
}
let (pixel_format, block_bytes) = match image.format {
TextureFormat::Bc1 => (MTLPixelFormat::BC1_RGBA, 8usize),
TextureFormat::Bc3 => (MTLPixelFormat::BC3_RGBA, 16),
TextureFormat::Bc5 => (MTLPixelFormat::BC5_RGUnorm, 16),
TextureFormat::Bc7 => (MTLPixelFormat::BC7_RGBAUnorm, 16),
TextureFormat::Rgba8 => unreachable!("RGBA8 handled above"),
};
let base = image
.mips
.first()
.ok_or("compressed texture image has no mip level")?;
let desc = TextureDesc {
format: pixel_format,
width: base.width as usize,
height: base.height as usize,
mip_count: image.mips.len(),
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
for (mip, level) in image.mips.iter().enumerate() {
let blocks_x = level.width.div_ceil(4) as usize;
let blocks_y = level.height.div_ceil(4) as usize;
let bytes_per_row = blocks_x * block_bytes;
let needed = bytes_per_row * blocks_y;
if level.data.len() < needed {
return Err(format!(
"compressed mip {} ({}x{}) is {} bytes, need {}",
mip,
level.width,
level.height,
level.data.len(),
needed
));
}
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: level.width as usize,
height: level.height as usize,
depth: 1,
},
};
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region,
mip,
std::ptr::NonNull::new(level.data.as_ptr() as *mut _)
.ok_or("compressed mip data is empty")?,
bytes_per_row,
);
}
}
Ok(texture)
}
pub(super) fn create_fallback_texture(alloc: &DeviceAllocator) -> Result<PooledTexture, String> {
upload_texture(alloc, 1, 1, &[255u8, 255, 255, 255])
}
pub(super) fn create_shadow_map_fallback(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
) -> Result<Retained<ProtocolObject<dyn MTLTexture>>, String> {
let desc = TextureDesc {
kind: MTLTextureType::Type2DArray,
format: MTLPixelFormat::Depth32Float,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create shadow map fallback texture")?;
let depth: f32 = 1.0;
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: 1,
height: 1,
depth: 1,
},
};
texture.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region,
0,
0,
std::ptr::NonNull::new(std::ptr::addr_of!(depth) as *mut _)
.ok_or("depth ptr is null")?,
4,
4,
);
}
Ok(texture)
}
pub(super) fn upload_cubemap(
alloc: &DeviceAllocator,
face_size: u32,
bytes: &[u8],
) -> Result<PooledTexture, String> {
let face_bytes = (face_size as usize) * (face_size as usize) * 4 * 4;
let needed = 6 * face_bytes;
if bytes.len() < needed {
return Err(format!(
"cubemap data too short for face_size {}: {} bytes, need {}",
face_size,
bytes.len(),
needed
));
}
let desc = TextureDesc {
kind: MTLTextureType::TypeCube,
format: MTLPixelFormat::RGBA32Float,
width: face_size as usize,
height: face_size as usize,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
let bytes_per_row = (face_size as usize) * 4 * 4;
let bytes_per_image = bytes_per_row * (face_size as usize);
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: face_size as usize,
height: face_size as usize,
depth: 1,
},
};
for face in 0..6 {
let face_start = face * face_bytes;
let face_ptr = bytes.as_ptr().add(face_start) as *mut std::ffi::c_void;
texture.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region,
0,
face,
std::ptr::NonNull::new(face_ptr).ok_or("cube face pointer is null")?,
bytes_per_row,
bytes_per_image,
);
}
}
Ok(texture)
}
pub(super) struct EnvironmentMapTextures {
pub irradiance: PooledTexture,
pub prefilter: PooledTexture,
pub prefilter_mip_count: u32,
}
pub(super) fn create_fallback_cubemap(
alloc: &DeviceAllocator,
value: [f32; 4],
) -> Result<PooledTexture, String> {
let desc = TextureDesc {
kind: MTLTextureType::TypeCube,
format: MTLPixelFormat::RGBA32Float,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
let bytes_per_row = 4 * 4;
let bytes_per_image = bytes_per_row;
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: 1,
height: 1,
depth: 1,
},
};
for face in 0..6 {
texture.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region,
0,
face,
std::ptr::NonNull::new(value.as_ptr() as *mut _)
.ok_or("fallback cube value pointer null")?,
bytes_per_row,
bytes_per_image,
);
}
}
Ok(texture)
}
pub(super) fn upload_color_lut(
alloc: &DeviceAllocator,
size: u32,
bytes: &[u8],
) -> Result<PooledTexture, String> {
let n = size as usize;
let needed = n * n * n * 4;
if bytes.len() < needed {
return Err(format!(
"color LUT data too short for size {}: {} bytes, need {}",
size,
bytes.len(),
needed
));
}
let desc = TextureDesc {
kind: MTLTextureType::Type3D,
width: n,
height: n,
depth: n,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: n,
height: n,
depth: n,
},
};
let bytes_per_row = n * 4;
let bytes_per_image = bytes_per_row * n;
texture.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region,
0,
0,
std::ptr::NonNull::new(bytes.as_ptr() as *mut _).ok_or("color LUT pointer is null")?,
bytes_per_row,
bytes_per_image,
);
}
Ok(texture)
}
pub(super) fn create_fallback_color_lut(alloc: &DeviceAllocator) -> Result<PooledTexture, String> {
let mut data = Vec::with_capacity(2 * 2 * 2 * 4);
for b in 0..2u8 {
for g in 0..2u8 {
for r in 0..2u8 {
data.extend_from_slice(&[r * 255, g * 255, b * 255, 255]);
}
}
}
upload_color_lut(alloc, 2, &data)
}
pub(super) fn upload_environment_map(
alloc: &DeviceAllocator,
irradiance_face: u32,
irradiance_bytes: &[u8],
prefilter_face: u32,
mip_bytes: &[&[u8]],
) -> Result<EnvironmentMapTextures, String> {
if mip_bytes.is_empty() {
return Err("envmap upload: prefilter mip_bytes must not be empty".into());
}
let irradiance = upload_cubemap(alloc, irradiance_face, irradiance_bytes)
.map_err(|e| format!("envmap irradiance: {}", e))?;
let prefilter = upload_prefilter_cube(alloc, prefilter_face, mip_bytes)
.map_err(|e| format!("envmap prefilter: {}", e))?;
Ok(EnvironmentMapTextures {
irradiance,
prefilter,
prefilter_mip_count: mip_bytes.len() as u32,
})
}
fn upload_prefilter_cube(
alloc: &DeviceAllocator,
face_size: u32,
mip_bytes: &[&[u8]],
) -> Result<PooledTexture, String> {
let mip_count = mip_bytes.len() as u32;
let desc = TextureDesc {
kind: MTLTextureType::TypeCube,
format: MTLPixelFormat::RGBA32Float,
width: face_size as usize,
height: face_size as usize,
mip_count: mip_count as usize,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
for (mip, bytes) in mip_bytes.iter().enumerate() {
let mip_face_size = face_size >> mip;
if mip_face_size == 0 {
return Err(format!(
"prefilter mip {} would have zero face size (face_size {} too small)",
mip, face_size
));
}
let face_bytes = (mip_face_size as usize) * (mip_face_size as usize) * 4 * 4;
let needed = 6 * face_bytes;
if bytes.len() < needed {
return Err(format!(
"prefilter mip {} too short: {} bytes, need {}",
mip,
bytes.len(),
needed
));
}
let bytes_per_row = (mip_face_size as usize) * 4 * 4;
let bytes_per_image = bytes_per_row * (mip_face_size as usize);
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: mip_face_size as usize,
height: mip_face_size as usize,
depth: 1,
},
};
for face in 0..6 {
let face_start = face * face_bytes;
let face_ptr = bytes.as_ptr().add(face_start) as *mut std::ffi::c_void;
texture.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region,
mip,
face,
std::ptr::NonNull::new(face_ptr).ok_or("prefilter face pointer null")?,
bytes_per_row,
bytes_per_image,
);
}
}
}
Ok(texture)
}
pub(super) fn create_shadow_map_array(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
size: u32,
layers: u32,
) -> Result<Retained<ProtocolObject<dyn MTLTexture>>, String> {
let desc = TextureDesc {
kind: MTLTextureType::Type2DArray,
format: MTLPixelFormat::Depth32Float,
width: size as usize,
height: size as usize,
array_length: layers as usize,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create shadow map array texture".to_string())
}
pub(super) struct HdrTargets {
pub hdr_color: Retained<ProtocolObject<dyn MTLTexture>>,
pub hdr_resolve: Retained<ProtocolObject<dyn MTLTexture>>,
pub hdr_resolve_copy: Retained<ProtocolObject<dyn MTLTexture>>,
pub transparent_scene_copy: Retained<ProtocolObject<dyn MTLTexture>>,
pub depth: Retained<ProtocolObject<dyn MTLTexture>>,
pub depth_resolve: Retained<ProtocolObject<dyn MTLTexture>>,
pub width: u32,
pub height: u32,
}
pub(super) fn create_hdr_targets(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
sample_count: u32,
) -> Result<HdrTargets, String> {
let w = width.max(1) as usize;
let h = height.max(1) as usize;
let color_desc = TextureDesc {
kind: MTLTextureType::Type2DMultisample,
format: MTLPixelFormat::RGBA16Float,
width: w,
height: h,
sample_count: sample_count as usize,
usage: MTLTextureUsage::RenderTarget,
..Default::default()
}
.build();
let hdr_color = device
.newTextureWithDescriptor(&color_desc)
.ok_or("failed to create MSAA HDR color texture")?;
let resolve_desc = TextureDesc {
format: MTLPixelFormat::RGBA16Float,
width: w,
height: h,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
let hdr_resolve = device
.newTextureWithDescriptor(&resolve_desc)
.ok_or("failed to create HDR resolve texture")?;
let hdr_resolve_copy = device
.newTextureWithDescriptor(&resolve_desc)
.ok_or("failed to create HDR resolve-copy texture")?;
let transparent_scene_copy = device
.newTextureWithDescriptor(&resolve_desc)
.ok_or("failed to create transparent scene-copy texture")?;
let depth_desc = TextureDesc {
kind: MTLTextureType::Type2DMultisample,
format: MTLPixelFormat::Depth32Float,
width: w,
height: h,
sample_count: sample_count as usize,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
let depth = device
.newTextureWithDescriptor(&depth_desc)
.ok_or("failed to create MSAA depth texture")?;
let depth_resolve_desc = TextureDesc {
format: MTLPixelFormat::Depth32Float,
width: w,
height: h,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
let depth_resolve = device
.newTextureWithDescriptor(&depth_resolve_desc)
.ok_or("failed to create single-sample depth resolve texture")?;
Ok(HdrTargets {
hdr_color,
hdr_resolve,
hdr_resolve_copy,
transparent_scene_copy,
depth,
depth_resolve,
width: w as u32,
height: h as u32,
})
}
pub(super) fn create_lut_texture(
alloc: &DeviceAllocator,
texels: &[f32],
size: u32,
components: usize,
) -> Result<PooledTexture, String> {
let needed = (size as usize) * (size as usize) * components;
if texels.len() < needed {
return Err(format!(
"LUT data too short for {size}x{size}x{components}: {} values, need {needed}",
texels.len()
));
}
let format = match components {
2 => MTLPixelFormat::RG32Float,
4 => MTLPixelFormat::RGBA32Float,
n => return Err(format!("unsupported LUT component count {n}")),
};
let desc = TextureDesc {
format,
width: size as usize,
height: size as usize,
storage: objc2_metal::MTLStorageMode::Shared,
..Default::default()
}
.build();
let texture = alloc.alloc_texture(&desc)?;
let bytes_per_row = (size as usize) * components * 4;
unsafe {
use objc2_metal::MTLRegion;
let region = MTLRegion {
origin: objc2_metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: objc2_metal::MTLSize {
width: size as usize,
height: size as usize,
depth: 1,
},
};
let ptr = texels.as_ptr() as *mut std::ffi::c_void;
texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region,
0,
std::ptr::NonNull::new(ptr).ok_or("LUT texel pointer is null")?,
bytes_per_row,
);
}
Ok(texture)
}