use crate::{
GpuAdapterInfo, GpuBackendType, GpuCaps, GpuContext, GpuDeviceType, GpuError, GpuResult,
TextureFormat,
};
#[cfg(feature = "metal")]
use metal::{Device, DeviceRef, MTLPixelFormat, MTLSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MetalFeatureSet {
IosGpuFamily1V1,
IosGpuFamily1V2,
IosGpuFamily2V1,
IosGpuFamily2V2,
IosGpuFamily3V1,
IosGpuFamily3V2,
IosGpuFamily4V1,
IosGpuFamily5V1,
MacosGpuFamily1V1,
MacosGpuFamily1V2,
MacosGpuFamily1V3,
MacosGpuFamily1V4,
MacosGpuFamily2V1,
CommonFamily1,
CommonFamily2,
CommonFamily3,
AppleFamily1,
AppleFamily2,
AppleFamily3,
AppleFamily4,
AppleFamily5,
AppleFamily6,
AppleFamily7,
AppleFamily8,
AppleFamily9,
}
impl std::fmt::Display for MetalFeatureSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IosGpuFamily1V1 => write!(f, "iOS GPU Family 1 v1"),
Self::IosGpuFamily1V2 => write!(f, "iOS GPU Family 1 v2"),
Self::IosGpuFamily2V1 => write!(f, "iOS GPU Family 2 v1"),
Self::IosGpuFamily2V2 => write!(f, "iOS GPU Family 2 v2"),
Self::IosGpuFamily3V1 => write!(f, "iOS GPU Family 3 v1"),
Self::IosGpuFamily3V2 => write!(f, "iOS GPU Family 3 v2"),
Self::IosGpuFamily4V1 => write!(f, "iOS GPU Family 4 v1"),
Self::IosGpuFamily5V1 => write!(f, "iOS GPU Family 5 v1"),
Self::MacosGpuFamily1V1 => write!(f, "macOS GPU Family 1 v1"),
Self::MacosGpuFamily1V2 => write!(f, "macOS GPU Family 1 v2"),
Self::MacosGpuFamily1V3 => write!(f, "macOS GPU Family 1 v3"),
Self::MacosGpuFamily1V4 => write!(f, "macOS GPU Family 1 v4"),
Self::MacosGpuFamily2V1 => write!(f, "macOS GPU Family 2 v1"),
Self::CommonFamily1 => write!(f, "Common Family 1"),
Self::CommonFamily2 => write!(f, "Common Family 2"),
Self::CommonFamily3 => write!(f, "Common Family 3"),
Self::AppleFamily1 => write!(f, "Apple Family 1"),
Self::AppleFamily2 => write!(f, "Apple Family 2"),
Self::AppleFamily3 => write!(f, "Apple Family 3"),
Self::AppleFamily4 => write!(f, "Apple Family 4"),
Self::AppleFamily5 => write!(f, "Apple Family 5"),
Self::AppleFamily6 => write!(f, "Apple Family 6"),
Self::AppleFamily7 => write!(f, "Apple Family 7"),
Self::AppleFamily8 => write!(f, "Apple Family 8"),
Self::AppleFamily9 => write!(f, "Apple Family 9"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MetalContextConfig {
pub prefer_low_power: bool,
pub prefer_headless: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MetalCaps {
pub base: GpuCaps,
pub feature_set: Option<MetalFeatureSet>,
pub is_low_power: bool,
pub is_headless: bool,
pub is_apple_silicon: bool,
pub max_buffer_length: u64,
pub max_threads_per_threadgroup: u64,
pub max_threadgroup_memory_length: u64,
pub max_texture_size: u32,
pub max_texture_3d_size: u32,
pub max_texture_cube_size: u32,
pub max_texture_array_layers: u32,
pub max_samplers_per_stage: u32,
pub max_textures_per_stage: u32,
pub max_buffers_per_stage: u32,
pub argument_buffers: bool,
pub raster_order_groups: bool,
pub float32_filtering: bool,
pub msaa_depth_resolve: bool,
pub sparse_textures: bool,
pub function_pointers: bool,
pub ray_tracing: bool,
pub mesh_shaders: bool,
pub argument_buffer_tier: u32,
pub read_write_texture_tier: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct MetalPixelFormatInfo {
pub format: u32,
pub bytes_per_pixel: u32,
pub is_depth: bool,
pub is_stencil: bool,
pub renderable: bool,
}
#[cfg(feature = "metal")]
pub fn texture_format_to_metal(format: TextureFormat) -> MTLPixelFormat {
match format {
TextureFormat::Rgba8Unorm => MTLPixelFormat::RGBA8Unorm,
TextureFormat::Rgba8UnormSrgb => MTLPixelFormat::RGBA8Unorm_sRGB,
TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm,
TextureFormat::Bgra8UnormSrgb => MTLPixelFormat::BGRA8Unorm_sRGB,
TextureFormat::R8Unorm => MTLPixelFormat::R8Unorm,
TextureFormat::Rg8Unorm => MTLPixelFormat::RG8Unorm,
TextureFormat::Rgba16Float => MTLPixelFormat::RGBA16Float,
TextureFormat::Rgba32Float => MTLPixelFormat::RGBA32Float,
TextureFormat::Depth24Stencil8 => MTLPixelFormat::Depth24Unorm_Stencil8,
TextureFormat::Depth32Float => MTLPixelFormat::Depth32Float,
}
}
#[cfg(feature = "metal")]
pub fn metal_to_texture_format(format: MTLPixelFormat) -> Option<TextureFormat> {
match format {
MTLPixelFormat::RGBA8Unorm => Some(TextureFormat::Rgba8Unorm),
MTLPixelFormat::RGBA8Unorm_sRGB => Some(TextureFormat::Rgba8UnormSrgb),
MTLPixelFormat::BGRA8Unorm => Some(TextureFormat::Bgra8Unorm),
MTLPixelFormat::BGRA8Unorm_sRGB => Some(TextureFormat::Bgra8UnormSrgb),
MTLPixelFormat::R8Unorm => Some(TextureFormat::R8Unorm),
MTLPixelFormat::RG8Unorm => Some(TextureFormat::Rg8Unorm),
MTLPixelFormat::RGBA16Float => Some(TextureFormat::Rgba16Float),
MTLPixelFormat::RGBA32Float => Some(TextureFormat::Rgba32Float),
MTLPixelFormat::Depth24Unorm_Stencil8 => Some(TextureFormat::Depth24Stencil8),
MTLPixelFormat::Depth32Float => Some(TextureFormat::Depth32Float),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalBlendFactor {
Zero,
#[default]
One,
SourceColor,
OneMinusSourceColor,
SourceAlpha,
OneMinusSourceAlpha,
DestinationColor,
OneMinusDestinationColor,
DestinationAlpha,
OneMinusDestinationAlpha,
SourceAlphaSaturated,
BlendColor,
OneMinusBlendColor,
BlendAlpha,
OneMinusBlendAlpha,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalBlendOperation {
#[default]
Add,
Subtract,
ReverseSubtract,
Min,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalCompareFunction {
Never,
Less,
Equal,
LessEqual,
Greater,
NotEqual,
GreaterEqual,
#[default]
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalStencilOperation {
#[default]
Keep,
Zero,
Replace,
IncrementClamp,
DecrementClamp,
Invert,
IncrementWrap,
DecrementWrap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalCullMode {
#[default]
None,
Front,
Back,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalWinding {
Clockwise,
#[default]
CounterClockwise,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalTriangleFillMode {
#[default]
Fill,
Lines,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalPrimitiveType {
Point,
Line,
LineStrip,
#[default]
Triangle,
TriangleStrip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalIndexType {
#[default]
UInt16,
UInt32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalSamplerAddressMode {
#[default]
ClampToEdge,
ClampToBorderColor,
ClampToZero,
Repeat,
MirrorClampToEdge,
MirrorRepeat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalSamplerMinMagFilter {
Nearest,
#[default]
Linear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalSamplerMipFilter {
#[default]
NotMipmapped,
Nearest,
Linear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalLoadAction {
#[default]
DontCare,
Load,
Clear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MetalStoreAction {
DontCare,
#[default]
Store,
MultisampleResolve,
StoreAndMultisampleResolve,
}
#[cfg(feature = "metal")]
pub struct MetalContext {
device: Device,
command_queue: metal::CommandQueue,
adapter_info: GpuAdapterInfo,
caps: MetalCaps,
}
#[cfg(feature = "metal")]
impl MetalContext {
pub fn new() -> GpuResult<Self> {
Self::with_config(MetalContextConfig::default())
}
pub fn with_config(config: MetalContextConfig) -> GpuResult<Self> {
let device = if config.prefer_low_power {
Device::all()
.into_iter()
.find(|d| d.is_low_power())
.or_else(Device::system_default)
} else if config.prefer_headless {
Device::all()
.into_iter()
.find(|d| d.is_headless())
.or_else(Device::system_default)
} else {
Device::system_default()
};
let device =
device.ok_or_else(|| GpuError::DeviceCreation("No Metal device found".into()))?;
Self::from_device(device)
}
pub fn from_device(device: Device) -> GpuResult<Self> {
let command_queue = device.new_command_queue();
let caps = Self::query_caps(&device);
let adapter_info = Self::build_adapter_info(&device, &caps);
Ok(Self {
device,
command_queue,
adapter_info,
caps,
})
}
fn query_caps(device: &DeviceRef) -> MetalCaps {
let is_apple_silicon = device.name().contains("Apple");
let max_buffer_length = device.max_buffer_length();
let max_threads = device.max_threads_per_threadgroup();
let max_threadgroup_memory = device.max_threadgroup_memory_length();
let feature_set = Self::detect_feature_set(device);
let (max_texture_size, max_3d_size, max_cube_size, max_array_layers) =
Self::get_texture_limits(&feature_set);
MetalCaps {
base: GpuCaps {
max_texture_size,
max_render_target_size: max_texture_size,
msaa_support: true,
max_msaa_samples: 8,
compute_support: true,
instancing_support: true,
},
feature_set: Some(feature_set),
is_low_power: device.is_low_power(),
is_headless: device.is_headless(),
is_apple_silicon,
max_buffer_length,
max_threads_per_threadgroup: max_threads.width * max_threads.height * max_threads.depth,
max_threadgroup_memory_length: max_threadgroup_memory,
max_texture_size,
max_texture_3d_size: max_3d_size,
max_texture_cube_size: max_cube_size,
max_texture_array_layers: max_array_layers,
max_samplers_per_stage: 16,
max_textures_per_stage: 128,
max_buffers_per_stage: 31,
argument_buffers: device.argument_buffers_support()
!= metal::MTLArgumentBuffersTier::Tier1,
raster_order_groups: device.are_raster_order_groups_supported(),
float32_filtering: true, msaa_depth_resolve: true,
sparse_textures: device.supports_sparse_textures(),
function_pointers: device.supports_function_pointers(),
ray_tracing: device.supports_raytracing(),
mesh_shaders: false, argument_buffer_tier: match device.argument_buffers_support() {
metal::MTLArgumentBuffersTier::Tier1 => 1,
metal::MTLArgumentBuffersTier::Tier2 => 2,
},
read_write_texture_tier: match device.read_write_texture_support() {
metal::MTLReadWriteTextureTier::TierNone => 0,
metal::MTLReadWriteTextureTier::Tier1 => 1,
metal::MTLReadWriteTextureTier::Tier2 => 2,
},
}
}
fn detect_feature_set(device: &DeviceRef) -> MetalFeatureSet {
if device.supports_family(metal::MTLGPUFamily::Apple9) {
return MetalFeatureSet::AppleFamily9;
}
if device.supports_family(metal::MTLGPUFamily::Apple8) {
return MetalFeatureSet::AppleFamily8;
}
if device.supports_family(metal::MTLGPUFamily::Apple7) {
return MetalFeatureSet::AppleFamily7;
}
if device.supports_family(metal::MTLGPUFamily::Apple6) {
return MetalFeatureSet::AppleFamily6;
}
if device.supports_family(metal::MTLGPUFamily::Apple5) {
return MetalFeatureSet::AppleFamily5;
}
if device.supports_family(metal::MTLGPUFamily::Apple4) {
return MetalFeatureSet::AppleFamily4;
}
if device.supports_family(metal::MTLGPUFamily::Apple3) {
return MetalFeatureSet::AppleFamily3;
}
if device.supports_family(metal::MTLGPUFamily::Apple2) {
return MetalFeatureSet::AppleFamily2;
}
if device.supports_family(metal::MTLGPUFamily::Apple1) {
return MetalFeatureSet::AppleFamily1;
}
if device.supports_family(metal::MTLGPUFamily::Common3) {
return MetalFeatureSet::CommonFamily3;
}
if device.supports_family(metal::MTLGPUFamily::Common2) {
return MetalFeatureSet::CommonFamily2;
}
if device.supports_family(metal::MTLGPUFamily::Common1) {
return MetalFeatureSet::CommonFamily1;
}
if device.supports_family(metal::MTLGPUFamily::Mac2) {
return MetalFeatureSet::MacosGpuFamily2V1;
}
if device.supports_family(metal::MTLGPUFamily::Mac1) {
return MetalFeatureSet::MacosGpuFamily1V4;
}
MetalFeatureSet::CommonFamily1
}
fn get_texture_limits(feature_set: &MetalFeatureSet) -> (u32, u32, u32, u32) {
match feature_set {
MetalFeatureSet::AppleFamily7
| MetalFeatureSet::AppleFamily8
| MetalFeatureSet::AppleFamily9
| MetalFeatureSet::MacosGpuFamily2V1 => (16384, 2048, 16384, 2048),
MetalFeatureSet::AppleFamily5
| MetalFeatureSet::AppleFamily6
| MetalFeatureSet::MacosGpuFamily1V4 => (16384, 2048, 16384, 2048),
MetalFeatureSet::AppleFamily3 | MetalFeatureSet::AppleFamily4 => {
(16384, 2048, 16384, 2048)
}
_ => (8192, 2048, 8192, 2048),
}
}
fn build_adapter_info(device: &DeviceRef, caps: &MetalCaps) -> GpuAdapterInfo {
let device_type = if device.is_low_power() {
GpuDeviceType::Integrated
} else if caps.is_apple_silicon {
GpuDeviceType::Integrated } else {
GpuDeviceType::Discrete
};
GpuAdapterInfo {
name: device.name().to_string(),
vendor: "Apple".to_string(),
backend: GpuBackendType::Metal,
device_type,
}
}
pub fn device(&self) -> &DeviceRef {
&self.device
}
pub fn command_queue(&self) -> &metal::CommandQueueRef {
&self.command_queue
}
pub fn metal_caps(&self) -> &MetalCaps {
&self.caps
}
pub fn new_command_buffer(&self) -> metal::CommandBuffer {
self.command_queue.new_command_buffer().to_owned()
}
pub fn new_render_pass_descriptor() -> metal::RenderPassDescriptor {
metal::RenderPassDescriptor::new().to_owned()
}
pub fn new_texture_descriptor() -> metal::TextureDescriptor {
metal::TextureDescriptor::new()
}
pub fn new_buffer_with_data(&self, data: &[u8]) -> metal::Buffer {
self.device.new_buffer_with_data(
data.as_ptr() as *const _,
data.len() as u64,
metal::MTLResourceOptions::StorageModeShared,
)
}
pub fn new_buffer(&self, size: u64) -> metal::Buffer {
self.device
.new_buffer(size, metal::MTLResourceOptions::StorageModeShared)
}
pub fn new_texture(&self, descriptor: &metal::TextureDescriptorRef) -> metal::Texture {
self.device.new_texture(descriptor)
}
pub fn new_sampler_state(
&self,
descriptor: &metal::SamplerDescriptorRef,
) -> metal::SamplerState {
self.device.new_sampler_state(descriptor)
}
pub fn new_depth_stencil_state(
&self,
descriptor: &metal::DepthStencilDescriptorRef,
) -> metal::DepthStencilState {
self.device.new_depth_stencil_state(descriptor)
}
pub fn new_library_with_source(&self, source: &str) -> Result<metal::Library, String> {
let options = metal::CompileOptions::new();
self.device
.new_library_with_source(source, &options)
.map_err(|e| e.to_string())
}
pub fn new_render_pipeline_state(
&self,
descriptor: &metal::RenderPipelineDescriptorRef,
) -> Result<metal::RenderPipelineState, String> {
self.device
.new_render_pipeline_state(descriptor)
.map_err(|e| e.to_string())
}
pub fn new_compute_pipeline_state(
&self,
function: &metal::FunctionRef,
) -> Result<metal::ComputePipelineState, String> {
self.device
.new_compute_pipeline_state_with_function(function)
.map_err(|e| e.to_string())
}
}
#[cfg(feature = "metal")]
impl GpuContext for MetalContext {
fn backend_type(&self) -> GpuBackendType {
GpuBackendType::Metal
}
fn adapter_info(&self) -> &GpuAdapterInfo {
&self.adapter_info
}
fn flush(&self) {
}
fn submit_and_wait(&self) {
let cmd = self.new_command_buffer();
cmd.commit();
cmd.wait_until_completed();
}
fn is_valid(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metal_feature_set_display() {
assert_eq!(
format!("{}", MetalFeatureSet::AppleFamily7),
"Apple Family 7"
);
assert_eq!(
format!("{}", MetalFeatureSet::CommonFamily1),
"Common Family 1"
);
}
#[test]
fn test_metal_config() {
let config = MetalContextConfig::default();
assert!(!config.prefer_low_power);
assert!(!config.prefer_headless);
}
#[test]
fn test_metal_caps_default() {
let caps = MetalCaps::default();
assert!(caps.feature_set.is_none());
assert!(!caps.is_apple_silicon);
}
#[test]
fn test_blend_factor() {
let factor = MetalBlendFactor::SourceAlpha;
assert_eq!(factor, MetalBlendFactor::SourceAlpha);
}
#[test]
fn test_compare_function() {
let func = MetalCompareFunction::Less;
assert_eq!(func, MetalCompareFunction::Less);
}
#[test]
fn test_primitive_type() {
let prim = MetalPrimitiveType::Triangle;
assert_eq!(prim, MetalPrimitiveType::Triangle);
}
#[test]
fn test_sampler_filter() {
let filter = MetalSamplerMinMagFilter::Linear;
assert_eq!(filter, MetalSamplerMinMagFilter::Linear);
}
}