use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Engine {
Cpu,
Amx,
Gpu(GpuPath),
Ane,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GpuPath {
Wgpu,
Aruminium,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NpuKind {
Ane,
Nnapi,
Xdna,
Hexagon,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryClass {
Uma,
UmaBandwidthLimited,
NonUma,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThermalBudget {
Desktop,
Laptop,
Tablet,
Phone,
Xr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FallbackPolicy {
Required,
DegradeTo(Engine),
Skip,
}
#[derive(Debug, Clone)]
pub struct PlatformCapabilities {
pub engines: HashSet<Engine>,
pub has_unimem: bool,
pub has_amx: bool,
pub has_ane: bool,
pub npu_kind: Option<NpuKind>,
pub gpu_paths: HashSet<GpuPath>,
pub memory_class: MemoryClass,
pub thermal_budget: ThermalBudget,
pub tick_rate_hz: u32,
}
impl PlatformCapabilities {
pub fn probe() -> Self {
let has_unimem = probe_unimem();
let has_amx = is_apple_silicon();
let has_ane = is_apple_silicon();
let mut gpu_paths = HashSet::new();
gpu_paths.insert(GpuPath::Wgpu);
if is_apple_silicon() {
gpu_paths.insert(GpuPath::Aruminium);
}
let mut engines = HashSet::new();
engines.insert(Engine::Cpu);
if has_amx {
engines.insert(Engine::Amx);
}
for &p in &gpu_paths {
engines.insert(Engine::Gpu(p));
}
if has_ane {
engines.insert(Engine::Ane);
}
let memory_class = if has_unimem {
MemoryClass::Uma
} else if cfg!(target_os = "android") {
MemoryClass::UmaBandwidthLimited
} else {
MemoryClass::NonUma
};
let thermal_budget = if cfg!(target_os = "android") {
ThermalBudget::Phone
} else if cfg!(target_os = "macos") {
ThermalBudget::Laptop
} else {
ThermalBudget::Desktop
};
let tick_rate_hz = match thermal_budget {
ThermalBudget::Desktop => 30,
ThermalBudget::Laptop => 20,
ThermalBudget::Tablet => 15,
ThermalBudget::Phone => 10,
ThermalBudget::Xr => 90, };
let npu_kind = if has_ane {
Some(NpuKind::Ane)
} else if cfg!(target_os = "android") {
Some(NpuKind::Nnapi)
} else {
None
};
Self {
engines,
has_unimem,
has_amx,
has_ane,
npu_kind,
gpu_paths,
memory_class,
thermal_budget,
tick_rate_hz,
}
}
pub fn supports(&self, engine: Engine) -> bool {
self.engines.contains(&engine)
}
pub fn resolve(&self, preferred: Engine, policy: FallbackPolicy) -> Option<Engine> {
if self.supports(preferred) {
return Some(preferred);
}
match policy {
FallbackPolicy::Required => None,
FallbackPolicy::DegradeTo(alt) => {
if self.supports(alt) {
Some(alt)
} else {
None
}
}
FallbackPolicy::Skip => None,
}
}
}
const fn is_apple_silicon() -> bool {
cfg!(all(target_os = "macos", target_arch = "aarch64"))
}
#[cfg(feature = "probe-unimem")]
fn probe_unimem() -> bool {
unimem::Block::open(8).is_ok()
}
#[cfg(not(feature = "probe-unimem"))]
fn probe_unimem() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_always_available() {
let caps = PlatformCapabilities::probe();
assert!(caps.supports(Engine::Cpu));
assert!(caps.gpu_paths.contains(&GpuPath::Wgpu));
}
#[test]
fn tick_rate_matches_thermal_class() {
let caps = PlatformCapabilities::probe();
let expected = match caps.thermal_budget {
ThermalBudget::Desktop => 30,
ThermalBudget::Laptop => 20,
ThermalBudget::Tablet => 15,
ThermalBudget::Phone => 10,
ThermalBudget::Xr => 90,
};
assert_eq!(caps.tick_rate_hz, expected);
}
#[test]
fn resolve_returns_preferred_when_available() {
let caps = PlatformCapabilities::probe();
assert_eq!(
caps.resolve(Engine::Cpu, FallbackPolicy::Required),
Some(Engine::Cpu)
);
}
#[test]
fn resolve_required_returns_none_when_missing() {
let caps = PlatformCapabilities {
engines: [Engine::Cpu].into_iter().collect(),
has_unimem: false,
has_amx: false,
has_ane: false,
npu_kind: None,
gpu_paths: [GpuPath::Wgpu].into_iter().collect(),
memory_class: MemoryClass::NonUma,
thermal_budget: ThermalBudget::Desktop,
tick_rate_hz: 30,
};
assert_eq!(caps.resolve(Engine::Amx, FallbackPolicy::Required), None);
}
#[test]
fn resolve_degrade_falls_back() {
let caps = PlatformCapabilities {
engines: [Engine::Cpu].into_iter().collect(),
has_unimem: false,
has_amx: false,
has_ane: false,
npu_kind: None,
gpu_paths: [GpuPath::Wgpu].into_iter().collect(),
memory_class: MemoryClass::NonUma,
thermal_budget: ThermalBudget::Desktop,
tick_rate_hz: 30,
};
assert_eq!(
caps.resolve(Engine::Amx, FallbackPolicy::DegradeTo(Engine::Cpu)),
Some(Engine::Cpu)
);
}
#[test]
fn resolve_skip_returns_none_for_missing() {
let caps = PlatformCapabilities {
engines: [Engine::Cpu].into_iter().collect(),
has_unimem: false,
has_amx: false,
has_ane: false,
npu_kind: None,
gpu_paths: [GpuPath::Wgpu].into_iter().collect(),
memory_class: MemoryClass::NonUma,
thermal_budget: ThermalBudget::Desktop,
tick_rate_hz: 30,
};
assert_eq!(caps.resolve(Engine::Ane, FallbackPolicy::Skip), None);
}
}