use crate::ThreadBound;
use crate::foundation::Error;
use crate::metal::generated_object_types::metal::{
Resource, SharedTextureHandle, TextureViewDescriptor,
};
use crate::metal::generated_struct_types::{ResourceID, TextureSwizzleChannels};
use crate::metal::generated_value_types::{
CPUCacheMode, HazardTrackingMode, SparsePageSize, TextureCompressionType, TextureSparseTier,
TextureSwizzle,
};
use crate::metal::{
Buffer, Device, PixelFormat, Region, ResourceOptions, StorageMode, TextureType, TextureUsage,
};
use objc2::msg_send;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2_foundation::NSRange;
use objc2_metal::{
MTLCPUCacheMode, MTLHazardTrackingMode, MTLResource, MTLSparsePageSize, MTLStorageMode,
MTLTexture, MTLTextureCompressionType, MTLTextureDescriptor, MTLTextureSwizzle,
MTLTextureSwizzleChannels,
};
use std::ffi::c_void;
use std::ptr::NonNull;
pub struct TextureDescriptor {
pub(super) inner: Retained<MTLTextureDescriptor>,
_thread_bound: ThreadBound,
}
impl TextureDescriptor {
pub(crate) fn as_any_object(&self) -> &objc2::runtime::AnyObject {
&self.inner
}
#[must_use]
pub fn new() -> Self {
Self {
inner: MTLTextureDescriptor::new(),
_thread_bound: ThreadBound::new(),
}
}
pub fn new_2d(
pixel_format: PixelFormat,
width: usize,
height: usize,
storage: ResourceOptions,
usage: TextureUsage,
) -> Result<Self, Error> {
if width == 0 || height == 0 {
return Err(Error::invalid_argument(
"texture dimensions must be greater than zero",
));
}
let inner = MTLTextureDescriptor::new();
inner.setTextureType(TextureType::D2.as_objc());
inner.setPixelFormat(pixel_format.as_objc());
unsafe {
inner.setWidth(width);
inner.setHeight(height);
}
inner.setResourceOptions(storage.as_objc());
inner.setUsage(usage.as_objc());
Ok(Self {
inner,
_thread_bound: ThreadBound::new(),
})
}
pub fn texture_2d(
pixel_format: PixelFormat,
width: usize,
height: usize,
mipmapped: bool,
) -> Result<Self, Error> {
if width == 0 || height == 0 {
return Err(Error::invalid_argument(
"texture dimensions must be greater than zero",
));
}
let inner = unsafe {
MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped(
pixel_format.as_objc(),
width,
height,
mipmapped,
)
};
Ok(Self {
inner,
_thread_bound: ThreadBound::new(),
})
}
pub fn texture_cube(
pixel_format: PixelFormat,
size: usize,
mipmapped: bool,
) -> Result<Self, Error> {
if size == 0 {
return Err(Error::invalid_argument(
"cube texture size must be greater than zero",
));
}
let inner = unsafe {
MTLTextureDescriptor::textureCubeDescriptorWithPixelFormat_size_mipmapped(
pixel_format.as_objc(),
size,
mipmapped,
)
};
Ok(Self {
inner,
_thread_bound: ThreadBound::new(),
})
}
pub fn texture_buffer(
pixel_format: PixelFormat,
width: usize,
resource_options: ResourceOptions,
usage: TextureUsage,
) -> Result<Self, Error> {
if width == 0 {
return Err(Error::invalid_argument(
"texture-buffer width must be greater than zero",
));
}
let inner = unsafe {
MTLTextureDescriptor::textureBufferDescriptorWithPixelFormat_width_resourceOptions_usage(
pixel_format.as_objc(),
width,
resource_options.as_objc(),
usage.as_objc(),
)
};
Ok(Self {
inner,
_thread_bound: ThreadBound::new(),
})
}
pub fn set_mipmap_level_count(&self, count: usize) -> Result<(), Error> {
if count == 0 {
return Err(Error::invalid_argument(
"mipmap level count must be greater than zero",
));
}
unsafe { self.inner.setMipmapLevelCount(count) };
Ok(())
}
#[must_use]
pub fn dimensions(&self) -> (usize, usize, usize) {
(self.inner.width(), self.inner.height(), self.inner.depth())
}
pub fn set_dimensions(&self, width: usize, height: usize, depth: usize) -> Result<(), Error> {
if width == 0 || height == 0 || depth == 0 {
return Err(Error::invalid_argument(
"texture dimensions must be greater than zero",
));
}
unsafe {
self.inner.setWidth(width);
self.inner.setHeight(height);
self.inner.setDepth(depth);
}
Ok(())
}
#[must_use]
pub fn array_length(&self) -> usize {
self.inner.arrayLength()
}
pub fn set_array_length(&self, value: usize) -> Result<(), Error> {
if value == 0 {
return Err(Error::invalid_argument("array length must be non-zero"));
}
unsafe { self.inner.setArrayLength(value) };
Ok(())
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.inner.sampleCount()
}
pub fn set_sample_count(&self, value: usize) -> Result<(), Error> {
if value == 0 {
return Err(Error::invalid_argument("sample count must be non-zero"));
}
unsafe { self.inner.setSampleCount(value) };
Ok(())
}
#[must_use]
pub fn mipmap_level_count(&self) -> usize {
self.inner.mipmapLevelCount()
}
#[must_use]
pub fn pixel_format(&self) -> PixelFormat {
PixelFormat::from_system_raw(self.inner.pixelFormat().0)
}
pub fn set_pixel_format(&self, value: PixelFormat) {
self.inner.setPixelFormat(value.as_objc());
}
pub fn texture_type(&self) -> Result<TextureType, Error> {
TextureType::try_from_system_raw(self.inner.textureType().0)
.ok_or_else(|| Error::unsupported("Metal returned an unknown texture type"))
}
pub fn set_texture_type(&self, value: TextureType) {
self.inner.setTextureType(value.as_objc());
}
#[must_use]
pub fn resource_options(&self) -> ResourceOptions {
ResourceOptions::from_system_raw(self.inner.resourceOptions().0)
}
pub fn set_resource_options(&self, value: ResourceOptions) {
self.inner.setResourceOptions(value.as_objc());
}
#[must_use]
pub fn usage(&self) -> TextureUsage {
TextureUsage::from_system_raw(self.inner.usage().0)
}
pub fn set_usage(&self, value: TextureUsage) {
self.inner.setUsage(value.as_objc());
}
pub fn storage_mode(&self) -> Result<StorageMode, Error> {
StorageMode::try_from_system_raw(self.inner.storageMode().0)
.ok_or_else(|| Error::unsupported("Metal returned an unknown storage mode"))
}
pub fn set_storage_mode(&self, value: StorageMode) {
self.inner.setStorageMode(MTLStorageMode(value.as_raw()));
}
#[must_use]
pub fn cpu_cache_mode(&self) -> CPUCacheMode {
CPUCacheMode::from_system_raw(self.inner.cpuCacheMode().0)
}
pub fn set_cpu_cache_mode(&self, value: CPUCacheMode) {
self.inner.setCpuCacheMode(MTLCPUCacheMode(value.as_raw()));
}
#[must_use]
pub fn hazard_tracking_mode(&self) -> HazardTrackingMode {
HazardTrackingMode::from_system_raw(self.inner.hazardTrackingMode().0)
}
pub fn set_hazard_tracking_mode(&self, value: HazardTrackingMode) {
self.inner
.setHazardTrackingMode(MTLHazardTrackingMode(value.as_raw()));
}
#[must_use]
pub fn compression_type(&self) -> TextureCompressionType {
TextureCompressionType::from_system_raw(self.inner.compressionType().0)
}
pub fn set_compression_type(&self, value: TextureCompressionType) {
self.inner
.setCompressionType(MTLTextureCompressionType(value.as_raw()));
}
#[must_use]
pub fn placement_sparse_page_size(&self) -> SparsePageSize {
SparsePageSize::from_system_raw(self.inner.placementSparsePageSize().0)
}
pub fn set_placement_sparse_page_size(&self, value: SparsePageSize) {
self.inner
.setPlacementSparsePageSize(MTLSparsePageSize(value.as_raw()));
}
#[must_use]
pub fn allows_gpu_optimized_contents(&self) -> bool {
self.inner.allowGPUOptimizedContents()
}
pub fn set_allows_gpu_optimized_contents(&self, value: bool) {
self.inner.setAllowGPUOptimizedContents(value);
}
#[must_use]
pub fn swizzle(&self) -> TextureSwizzleChannels {
swizzle_from_objc(self.inner.swizzle())
}
pub fn set_swizzle(&self, value: &TextureSwizzleChannels) -> Result<(), Error> {
self.inner.setSwizzle(swizzle_to_objc(value)?);
Ok(())
}
}
impl Default for TextureDescriptor {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct Texture {
pub(crate) inner: Retained<ProtocolObject<dyn MTLTexture>>,
_thread_bound: ThreadBound,
}
#[derive(Clone)]
pub struct IoSurface {
#[allow(dead_code)]
inner: Retained<AnyObject>,
_thread_bound: ThreadBound,
}
impl IoSurface {
pub(crate) fn as_inner(&self) -> &AnyObject {
&self.inner
}
}
impl Texture {
pub(crate) const fn new(inner: Retained<ProtocolObject<dyn MTLTexture>>) -> Self {
Self {
inner,
_thread_bound: ThreadBound::new(),
}
}
pub(crate) fn from_any_object(inner: Retained<AnyObject>) -> Result<Self, Error> {
let inner = unsafe { Retained::cast_unchecked(inner) };
Ok(Self::new(inner))
}
pub(crate) fn as_any_object(&self) -> &AnyObject {
unsafe { &*(std::ptr::from_ref(&*self.inner).cast::<AnyObject>()) }
}
#[must_use]
pub fn width(&self) -> usize {
self.inner.width()
}
#[must_use]
pub fn height(&self) -> usize {
self.inner.height()
}
#[must_use]
pub fn layout(&self) -> (usize, usize, usize, usize) {
(
self.inner.depth(),
self.inner.arrayLength(),
self.inner.mipmapLevelCount(),
self.inner.sampleCount(),
)
}
pub fn texture_type(&self) -> Result<TextureType, Error> {
TextureType::try_from_system_raw(self.inner.textureType().0)
.ok_or_else(|| Error::unsupported("Metal returned an unknown texture type"))
}
#[must_use]
pub fn usage(&self) -> TextureUsage {
TextureUsage::from_system_raw(self.inner.usage().0)
}
#[must_use]
pub fn is_framebuffer_only(&self) -> bool {
self.inner.isFramebufferOnly()
}
#[must_use]
pub fn is_shareable(&self) -> bool {
self.inner.isShareable()
}
#[must_use]
pub fn is_sparse(&self) -> bool {
self.inner.isSparse()
}
#[must_use]
pub fn allows_gpu_optimized_contents(&self) -> bool {
self.inner.allowGPUOptimizedContents()
}
#[must_use]
pub fn compression_type(&self) -> TextureCompressionType {
TextureCompressionType::from_system_raw(self.inner.compressionType().0)
}
#[must_use]
pub fn sparse_texture_tier(&self) -> TextureSparseTier {
TextureSparseTier::from_system_raw(self.inner.sparseTextureTier().0)
}
#[must_use]
pub fn parent_texture(&self) -> Option<Self> {
self.inner.parentTexture().map(Self::new)
}
#[must_use]
pub fn parent_relative_location(&self) -> (usize, usize) {
(
self.inner.parentRelativeLevel(),
self.inner.parentRelativeSlice(),
)
}
#[must_use]
pub fn buffer(&self) -> Option<Buffer> {
self.inner.buffer().map(|inner| {
let mode = match inner.storageMode().0 {
1 => StorageMode::Managed,
2 => StorageMode::Private,
3 => StorageMode::Memoryless,
_ => StorageMode::Shared,
};
Buffer::new(inner, mode)
})
}
#[must_use]
pub fn buffer_layout(&self) -> (usize, usize) {
(self.inner.bufferOffset(), self.inner.bufferBytesPerRow())
}
#[must_use]
pub fn sparse_tail(&self) -> (usize, usize) {
(self.inner.firstMipmapInTail(), self.inner.tailSizeInBytes())
}
#[must_use]
pub fn pixel_format(&self) -> PixelFormat {
PixelFormat::from_system_raw(self.inner.pixelFormat().0)
}
#[must_use]
pub fn storage_mode(&self) -> StorageMode {
match self.inner.storageMode().0 {
1 => StorageMode::Managed,
2 => StorageMode::Private,
3 => StorageMode::Memoryless,
_ => StorageMode::Shared,
}
}
#[must_use]
pub fn gpu_resource_id(&self) -> ResourceID {
let raw = self.inner.gpuResourceID();
let value = unsafe { std::mem::transmute::<objc2_metal::MTLResourceID, u64>(raw) };
ResourceID { _impl: value }
}
#[must_use]
pub fn iosurface_plane(&self) -> usize {
self.inner.iosurfacePlane()
}
#[must_use]
pub fn iosurface(&self) -> Option<IoSurface> {
let inner: Option<Retained<AnyObject>> = unsafe { msg_send![&*self.inner, iosurface] };
inner.map(|inner| IoSurface {
inner,
_thread_bound: ThreadBound::new(),
})
}
#[allow(deprecated)]
pub fn root_resource(&self) -> Option<Resource> {
self.inner.rootResource().map(|resource| {
let object = unsafe { Retained::cast_unchecked(resource) };
Resource::from_inner(object)
})
}
#[must_use]
pub fn remote_storage_texture(&self) -> Option<Self> {
self.inner.remoteStorageTexture().map(Self::new)
}
pub fn new_remote_view(&self, device: &Device) -> Result<Self, Error> {
if self.storage_mode() != StorageMode::Private && !self.is_shareable() {
return Err(Error::invalid_argument(
"remote texture views require private or shareable storage",
));
}
self.inner
.newRemoteTextureViewForDevice(&device.inner)
.map(Self::new)
.ok_or_else(|| Error::unsupported("Metal could not create a remote texture view"))
}
pub fn new_shared_texture_handle(&self) -> Result<SharedTextureHandle, Error> {
if !self.is_shareable() {
return Err(Error::invalid_argument(
"a shared texture handle requires a shareable texture",
));
}
self.inner
.newSharedTextureHandle()
.map(|handle| {
let object = unsafe { Retained::cast_unchecked(handle) };
SharedTextureHandle::from_inner(object)
})
.ok_or_else(|| Error::unsupported("Metal could not create a shared texture handle"))
}
pub fn view_with_pixel_format(&self, pixel_format: PixelFormat) -> Result<Self, Error> {
self.inner
.newTextureViewWithPixelFormat(pixel_format.as_objc())
.map(Self::new)
.ok_or_else(|| Error::unsupported("Metal rejected the texture view format"))
}
pub fn view(
&self,
pixel_format: PixelFormat,
texture_type: TextureType,
levels: std::ops::Range<usize>,
slices: std::ops::Range<usize>,
) -> Result<Self, Error> {
self.validate_view_ranges(&levels, &slices)?;
unsafe {
self.inner
.newTextureViewWithPixelFormat_textureType_levels_slices(
pixel_format.as_objc(),
texture_type.as_objc(),
NSRange::new(levels.start, levels.len()),
NSRange::new(slices.start, slices.len()),
)
}
.map(Self::new)
.ok_or_else(|| Error::unsupported("Metal rejected the texture view ranges"))
}
pub fn view_with_swizzle(
&self,
pixel_format: PixelFormat,
texture_type: TextureType,
levels: std::ops::Range<usize>,
slices: std::ops::Range<usize>,
swizzle: &TextureSwizzleChannels,
) -> Result<Self, Error> {
self.validate_view_ranges(&levels, &slices)?;
let swizzle = swizzle_to_objc(swizzle)?;
unsafe {
self.inner
.newTextureViewWithPixelFormat_textureType_levels_slices_swizzle(
pixel_format.as_objc(),
texture_type.as_objc(),
NSRange::new(levels.start, levels.len()),
NSRange::new(slices.start, slices.len()),
swizzle,
)
}
.map(Self::new)
.ok_or_else(|| Error::unsupported("Metal rejected the swizzled texture view"))
}
pub fn view_with_descriptor(&self, descriptor: &TextureViewDescriptor) -> Result<Self, Error> {
let descriptor: &objc2_metal::MTLTextureViewDescriptor = unsafe {
&*(std::ptr::from_ref(descriptor.as_inner())
.cast::<objc2_metal::MTLTextureViewDescriptor>())
};
self.inner
.newTextureViewWithDescriptor(descriptor)
.map(Self::new)
.ok_or_else(|| Error::unsupported("Metal rejected the texture view descriptor"))
}
pub fn replace_region_2d(
&self,
region: Region,
mipmap_level: usize,
bytes: &[u8],
bytes_per_row: usize,
) -> Result<(), Error> {
self.validate_cpu_write(region, mipmap_level, 0)?;
let required = texture_source_span(region, bytes_per_row, 0)?;
if bytes_per_row == 0 || bytes.len() < required || required == 0 {
return Err(Error::invalid_argument(
"texture source does not cover every requested row",
));
}
let source = NonNull::new(bytes.as_ptr().cast_mut().cast::<c_void>())
.ok_or_else(|| Error::invalid_argument("texture source is empty"))?;
unsafe {
self.inner.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region.into(),
mipmap_level,
source,
bytes_per_row,
);
}
Ok(())
}
pub fn replace_region(
&self,
region: Region,
mipmap_level: usize,
slice: usize,
bytes: &[u8],
bytes_per_row: usize,
bytes_per_image: usize,
) -> Result<(), Error> {
self.validate_cpu_write(region, mipmap_level, slice)?;
let required = texture_source_span(region, bytes_per_row, bytes_per_image)?;
if bytes_per_row == 0 || bytes_per_image == 0 || bytes.len() < required || required == 0 {
return Err(Error::invalid_argument(
"texture source does not cover every requested image",
));
}
let source = NonNull::new(bytes.as_ptr().cast_mut().cast::<c_void>())
.ok_or_else(|| Error::invalid_argument("texture source is empty"))?;
unsafe {
self.inner
.replaceRegion_mipmapLevel_slice_withBytes_bytesPerRow_bytesPerImage(
region.into(),
mipmap_level,
slice,
source,
bytes_per_row,
bytes_per_image,
);
}
Ok(())
}
#[must_use]
pub fn swizzle(&self) -> TextureSwizzleChannels {
swizzle_from_objc(self.inner.swizzle())
}
fn validate_view_ranges(
&self,
levels: &std::ops::Range<usize>,
slices: &std::ops::Range<usize>,
) -> Result<(), Error> {
if levels.start >= levels.end
|| levels.end > self.inner.mipmapLevelCount()
|| slices.start >= slices.end
|| slices.end > self.slice_count()?
{
return Err(Error::invalid_argument(
"texture view level or slice range is out of bounds",
));
}
Ok(())
}
fn validate_cpu_write(
&self,
region: Region,
mipmap_level: usize,
slice: usize,
) -> Result<(), Error> {
if matches!(
self.storage_mode(),
StorageMode::Private | StorageMode::Memoryless
) {
return Err(Error::unsupported(
"private and memoryless textures are not CPU writable",
));
}
if mipmap_level >= self.inner.mipmapLevelCount() || slice >= self.slice_count()? {
return Err(Error::invalid_argument(
"texture mip level or slice is out of bounds",
));
}
if region.size.width == 0 || region.size.height == 0 || region.size.depth == 0 {
return Err(Error::invalid_argument("texture region must be non-empty"));
}
let width = (self.width() >> mipmap_level.min(usize::BITS as usize - 1)).max(1);
let height = (self.height() >> mipmap_level.min(usize::BITS as usize - 1)).max(1);
let depth = (self.inner.depth() >> mipmap_level.min(usize::BITS as usize - 1)).max(1);
let end_x = region.origin.x.checked_add(region.size.width);
let end_y = region.origin.y.checked_add(region.size.height);
let end_z = region.origin.z.checked_add(region.size.depth);
if end_x.is_none_or(|end| end > width)
|| end_y.is_none_or(|end| end > height)
|| end_z.is_none_or(|end| end > depth)
{
return Err(Error::invalid_argument("texture region is out of bounds"));
}
Ok(())
}
fn slice_count(&self) -> Result<usize, Error> {
let count = match self.texture_type()? {
TextureType::Cube => 6,
TextureType::CubeArray => self
.inner
.arrayLength()
.checked_mul(6)
.ok_or_else(|| Error::invalid_argument("cube-array slice count overflow"))?,
TextureType::D1Array | TextureType::D2Array | TextureType::D2MultisampleArray => {
self.inner.arrayLength()
}
_ => 1,
};
Ok(count.max(1))
}
}
fn swizzle_to_objc(value: &TextureSwizzleChannels) -> Result<MTLTextureSwizzleChannels, Error> {
for channel in [&value.red, &value.green, &value.blue, &value.alpha] {
if !channel.is_valid() {
return Err(Error::invalid_argument(
"texture swizzle channel is not a declared value",
));
}
}
Ok(MTLTextureSwizzleChannels {
red: MTLTextureSwizzle(value.red.as_raw()),
green: MTLTextureSwizzle(value.green.as_raw()),
blue: MTLTextureSwizzle(value.blue.as_raw()),
alpha: MTLTextureSwizzle(value.alpha.as_raw()),
})
}
fn swizzle_from_objc(value: MTLTextureSwizzleChannels) -> TextureSwizzleChannels {
TextureSwizzleChannels {
red: TextureSwizzle::from_system_raw(value.red.0),
green: TextureSwizzle::from_system_raw(value.green.0),
blue: TextureSwizzle::from_system_raw(value.blue.0),
alpha: TextureSwizzle::from_system_raw(value.alpha.0),
}
}
fn texture_source_span(
region: Region,
bytes_per_row: usize,
bytes_per_image: usize,
) -> Result<usize, Error> {
const MAX_TEXEL_BYTES: usize = 16;
let final_texel_row = region
.size
.width
.checked_mul(MAX_TEXEL_BYTES)
.ok_or_else(|| Error::invalid_argument("texture row byte count overflow"))?;
let preceding_rows = region
.size
.height
.saturating_sub(1)
.checked_mul(bytes_per_row)
.ok_or_else(|| Error::invalid_argument("texture row layout overflow"))?;
let preceding_images = region
.size
.depth
.saturating_sub(1)
.checked_mul(bytes_per_image)
.ok_or_else(|| Error::invalid_argument("texture image layout overflow"))?;
preceding_images
.checked_add(preceding_rows)
.and_then(|value| value.checked_add(final_texel_row))
.ok_or_else(|| Error::invalid_argument("texture source layout overflow"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metal::{Origin, Size};
#[test]
fn source_span_covers_final_texel_row_and_image() {
let region = Region::new(Origin::new(0, 0, 0), Size::new(4, 3, 2));
assert_eq!(texture_source_span(region, 64, 192).unwrap(), 384);
}
#[test]
fn source_span_rejects_overflow() {
let region = Region::new(Origin::new(0, 0, 0), Size::new(usize::MAX, 1, 1));
assert!(texture_source_span(region, 1, 1).is_err());
}
}