use crate::{Result, SyphonError};
#[cfg(target_os = "macos")]
use objc::runtime::Object;
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
#[derive(Debug, Clone)]
pub struct MetalDeviceInfo {
#[cfg(target_os = "macos")]
pub raw_device: *mut Object,
pub name: String,
pub is_default: bool,
pub is_low_power: bool,
pub is_removable: bool,
pub has_unified_memory: bool,
pub recommended_max_working_set_size: Option<u64>,
pub gpu_family: Option<String>,
}
#[cfg(target_os = "macos")]
unsafe impl Send for MetalDeviceInfo {}
#[cfg(target_os = "macos")]
unsafe impl Sync for MetalDeviceInfo {}
impl MetalDeviceInfo {
pub fn is_high_performance(&self) -> bool {
if self.has_unified_memory && !self.is_low_power {
return true;
}
if !self.has_unified_memory && !self.is_low_power {
return true;
}
false
}
#[cfg(target_os = "macos")]
pub fn is_same_device(&self, other: &MetalDeviceInfo) -> bool {
self.raw_device == other.raw_device
}
pub fn is_compatible_with(&self, other: &MetalDeviceInfo) -> bool {
#[cfg(target_os = "macos")]
{
if self.is_same_device(other) {
return true;
}
if let (Some(ref self_family), Some(ref other_family)) =
(&self.gpu_family, &other.gpu_family) {
if self_family == other_family {
return true;
}
}
}
false
}
}
#[cfg(target_os = "macos")]
pub fn default_device() -> Option<MetalDeviceInfo> {
unsafe {
extern "C" {
fn MTLCreateSystemDefaultDevice() -> *mut Object;
}
let device = MTLCreateSystemDefaultDevice();
if device.is_null() {
return None;
}
get_device_info(device, true)
}
}
#[cfg(not(target_os = "macos"))]
pub fn default_device() -> Option<MetalDeviceInfo> {
None
}
#[cfg(target_os = "macos")]
pub fn get_device_info(device: *mut Object, is_default: bool) -> Option<MetalDeviceInfo> {
unsafe {
if device.is_null() {
return None;
}
let name_nsstring: *mut Object = msg_send![device, name];
let name = crate::utils::from_nsstring(name_nsstring);
let is_low_power: bool = msg_send![device, isLowPower];
let is_removable: bool = msg_send![device, isRemovable];
let has_unified_memory: bool = msg_send![device, hasUnifiedMemory];
let recommended_max_working_set_size: u64 =
msg_send![device, recommendedMaxWorkingSetSize];
let recommended_max_working_set_size = if recommended_max_working_set_size > 0 {
Some(recommended_max_working_set_size)
} else {
None
};
let gpu_family = get_gpu_family(device);
Some(MetalDeviceInfo {
raw_device: device,
name,
is_default,
is_low_power,
is_removable,
has_unified_memory,
recommended_max_working_set_size,
gpu_family,
})
}
}
#[cfg(target_os = "macos")]
unsafe fn get_gpu_family(device: *mut Object) -> Option<String> {
use cocoa::foundation::NSUInteger;
let family: NSUInteger = msg_send![device, supportsFamily: 1001u64]; if family != 0 {
let families = [
(1008, "Apple8"),
(1007, "Apple7"),
(1006, "Apple6"),
(1005, "Apple5"),
(1004, "Apple4"),
(1003, "Apple3"),
(1002, "Apple2"),
(1001, "Apple1"),
];
for (family_id, name) in &families {
let supported: bool = msg_send![device, supportsFamily: *family_id as u64];
if supported {
return Some(name.to_string());
}
}
}
let mac_families = [
(5001, "Mac2"),
(5002, "Mac1"),
];
for (family_id, name) in &mac_families {
let supported: bool = msg_send![device, supportsFamily: *family_id as u64];
if supported {
return Some(name.to_string());
}
}
None
}
#[cfg(target_os = "macos")]
pub fn available_devices() -> Vec<MetalDeviceInfo> {
unsafe {
extern "C" {
fn MTLCopyAllDevices() -> *mut Object; }
let devices_array = MTLCopyAllDevices();
if devices_array.is_null() {
return Vec::new();
}
let count: usize = msg_send![devices_array, count];
let default = default_device();
let default_ptr = default.as_ref().map(|d| d.raw_device);
let mut devices = Vec::with_capacity(count);
for i in 0..count {
let device: *mut Object = msg_send![devices_array, objectAtIndex: i];
let is_default = default_ptr.map(|d| d == device).unwrap_or(false);
if let Some(info) = get_device_info(device, is_default) {
devices.push(info);
}
}
let _: () = msg_send![devices_array, release];
devices
}
}
#[cfg(not(target_os = "macos"))]
pub fn available_devices() -> Vec<MetalDeviceInfo> {
Vec::new()
}
pub fn recommended_high_performance_device() -> Option<MetalDeviceInfo> {
let devices = available_devices();
if devices.is_empty() {
return None;
}
for device in &devices {
if !device.is_low_power && !device.has_unified_memory {
return Some(device.clone());
}
}
for device in &devices {
if device.has_unified_memory && !device.is_low_power {
return Some(device.clone());
}
}
for device in &devices {
if !device.is_low_power {
return Some(device.clone());
}
}
devices.into_iter().find(|d| d.is_default).or_else(default_device)
}
#[cfg(target_os = "macos")]
pub fn check_device_compatibility(device: *mut Object) -> Result<()> {
unsafe {
if device.is_null() {
return Err(SyphonError::InvalidParameter(
"Device pointer is null".to_string()
));
}
let format_supported: bool = msg_send![device, supportsTextureSampleCount: 1];
if !format_supported {
return Err(SyphonError::InvalidParameter(
"Device does not support required texture formats".to_string()
));
}
let name_nsstring: *mut Object = msg_send![device, name];
let name = crate::utils::from_nsstring(name_nsstring);
log::debug!("Device '{}' compatibility check passed", name);
Ok(())
}
}
#[cfg(not(target_os = "macos"))]
pub fn check_device_compatibility(_device: *mut Object) -> Result<()> {
Err(SyphonError::NotAvailable)
}
#[cfg(target_os = "macos")]
pub fn validate_device_match(
render_device: *mut Object,
syphon_device: *mut Object,
) -> Result<()> {
if render_device.is_null() || syphon_device.is_null() {
return Err(SyphonError::InvalidParameter(
"Device pointer is null".to_string()
));
}
if render_device == syphon_device {
return Ok(());
}
let render_info = get_device_info(render_device, false);
let syphon_info = get_device_info(syphon_device, false);
let render_name = render_info.as_ref().map(|d| d.name.as_str()).unwrap_or("Unknown");
let syphon_name = syphon_info.as_ref().map(|d| d.name.as_str()).unwrap_or("Unknown");
log::warn!(
"GPU mismatch detected: Render device '{}' does not match Syphon server device '{}'. \
This may cause performance penalties due to GPU-to-GPU transfers.",
render_name,
syphon_name
);
if let (Some(render_info), Some(syphon_info)) = (render_info, syphon_info) {
if !render_info.is_compatible_with(&syphon_info) {
log::warn!(
"Devices are from different GPU families and may not be fully compatible."
);
}
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
pub fn validate_device_match(
_render_device: *mut Object,
_syphon_device: *mut Object,
) -> Result<()> {
Err(SyphonError::NotAvailable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_device() {
#[cfg(target_os = "macos")]
{
if let Some(device) = default_device() {
println!("Default device: {:?}", device.name);
println!("Is low power: {:?}", device.is_low_power);
println!("Has unified memory: {:?}", device.has_unified_memory);
} else {
println!("No default Metal device found");
}
}
}
#[test]
fn test_available_devices() {
#[cfg(target_os = "macos")]
{
let devices = available_devices();
println!("Found {} Metal devices", devices.len());
for device in &devices {
println!(" - {} (default={}, low_power={}, unified={})",
device.name,
device.is_default,
device.is_low_power,
device.has_unified_memory
);
}
}
}
#[test]
fn test_recommended_device() {
#[cfg(target_os = "macos")]
{
if let Some(device) = recommended_high_performance_device() {
println!("Recommended high-performance device: {}", device.name);
println!("Is high performance: {}", device.is_high_performance());
} else {
println!("No high-performance device found");
}
}
}
}