use super::capabilities::{GpuBackend, GpuCapabilities, GpuLimits};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct GpuDetectionResult {
pub available: bool,
pub capabilities: GpuCapabilities,
pub recommended_backend: GpuBackend,
pub detection_method: DetectionMethod,
}
impl GpuDetectionResult {
#[must_use]
pub fn unavailable() -> Self {
Self {
available: false,
capabilities: GpuCapabilities::default(),
recommended_backend: GpuBackend::None,
detection_method: DetectionMethod::NoGpu,
}
}
#[must_use]
pub fn suitable_for_inference(&self) -> bool {
self.available && self.capabilities.suitable_for_inference()
}
#[must_use]
pub fn summary(&self) -> String {
if self.available {
format!(
"GPU Available: {} via {} ({})",
self.capabilities.name, self.recommended_backend, self.detection_method
)
} else {
"No GPU available".to_string()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectionMethod {
WgpuNative,
WebGpuBrowser,
Simulated,
NoGpu,
}
impl std::fmt::Display for DetectionMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WgpuNative => write!(f, "wgpu native"),
Self::WebGpuBrowser => write!(f, "WebGPU browser"),
Self::Simulated => write!(f, "simulated"),
Self::NoGpu => write!(f, "none"),
}
}
}
#[derive(Debug, Clone)]
pub struct DetectionOptions {
pub prefer_high_performance: bool,
pub require_compute: bool,
pub min_vram: u64,
pub preferred_backend: Option<GpuBackend>,
pub timeout_ms: u32,
}
impl Default for DetectionOptions {
fn default() -> Self {
Self {
prefer_high_performance: true,
require_compute: true,
min_vram: 0,
preferred_backend: None,
timeout_ms: 5000,
}
}
}
impl DetectionOptions {
#[must_use]
pub fn for_inference() -> Self {
Self {
prefer_high_performance: true,
require_compute: true,
min_vram: 256 * 1024 * 1024, preferred_backend: None,
timeout_ms: 5000,
}
}
#[must_use]
pub fn for_development() -> Self {
Self {
prefer_high_performance: false,
require_compute: false,
min_vram: 0,
preferred_backend: None,
timeout_ms: 10000,
}
}
#[must_use]
pub fn with_backend(mut self, backend: GpuBackend) -> Self {
self.preferred_backend = Some(backend);
self
}
#[must_use]
pub fn with_min_vram(mut self, vram: u64) -> Self {
self.min_vram = vram;
self
}
#[must_use]
pub fn without_compute_requirement(mut self) -> Self {
self.require_compute = false;
self
}
}
pub fn detect_gpu(options: &DetectionOptions) -> GpuDetectionResult {
#[cfg(not(feature = "webgpu"))]
{
let _ = options; GpuDetectionResult::unavailable()
}
#[cfg(feature = "webgpu")]
{
detect_gpu_wgpu(options)
}
}
#[cfg(feature = "webgpu")]
fn detect_gpu_wgpu(options: &DetectionOptions) -> GpuDetectionResult {
#[cfg(not(target_arch = "wasm32"))]
{
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
let timeout = Duration::from_millis(options.timeout_ms as u64);
let prefer_high_perf = options.prefer_high_performance;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = pollster::block_on(async {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let power_preference = if prefer_high_perf {
wgpu::PowerPreference::HighPerformance
} else {
wgpu::PowerPreference::LowPower
};
if let Some(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface: None,
force_fallback_adapter: false,
})
.await
{
let info = adapter.get_info();
let limits = adapter.limits();
let backend = match info.backend {
wgpu::Backend::Vulkan => GpuBackend::Vulkan,
wgpu::Backend::Metal => GpuBackend::Metal,
wgpu::Backend::Dx12 => GpuBackend::Dx12,
wgpu::Backend::Gl => GpuBackend::OpenGl,
wgpu::Backend::BrowserWebGpu => GpuBackend::BrowserWebGpu,
wgpu::Backend::Empty => GpuBackend::None,
};
let capabilities = GpuCapabilities {
name: info.name.clone(),
vendor: format!("{:?}", info.vendor),
backend,
limits: GpuLimits {
max_buffer_size: limits.max_buffer_size,
max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size,
max_uniform_buffer_binding_size: limits.max_uniform_buffer_binding_size,
max_compute_workgroup_size_x: limits.max_compute_workgroup_size_x,
max_compute_workgroup_size_y: limits.max_compute_workgroup_size_y,
max_compute_workgroup_size_z: limits.max_compute_workgroup_size_z,
max_compute_invocations_per_workgroup: limits
.max_compute_invocations_per_workgroup,
max_compute_workgroups_per_dimension: limits
.max_compute_workgroups_per_dimension,
max_bind_groups: limits.max_bind_groups,
},
supports_f16: adapter.features().contains(wgpu::Features::SHADER_F16),
supports_timestamp_query: adapter
.features()
.contains(wgpu::Features::TIMESTAMP_QUERY),
vram_bytes: 0, };
GpuDetectionResult {
available: true,
capabilities,
recommended_backend: backend,
detection_method: DetectionMethod::WgpuNative,
}
} else {
GpuDetectionResult::unavailable()
}
});
let _ = tx.send(result);
});
rx.recv_timeout(timeout)
.unwrap_or_else(|_| GpuDetectionResult::unavailable())
}
#[cfg(target_arch = "wasm32")]
{
let _ = options;
GpuDetectionResult::unavailable()
}
}
#[must_use]
pub fn detect_gpu_simulated(config: SimulatedGpuConfig) -> GpuDetectionResult {
let capabilities = GpuCapabilities {
name: config.name,
vendor: config.vendor,
backend: config.backend,
limits: config.limits,
supports_f16: config.supports_f16,
supports_timestamp_query: config.supports_timestamp_query,
vram_bytes: config.vram_bytes,
};
GpuDetectionResult {
available: config.backend != GpuBackend::None,
capabilities,
recommended_backend: config.backend,
detection_method: DetectionMethod::Simulated,
}
}
#[derive(Debug, Clone)]
pub struct SimulatedGpuConfig {
pub name: String,
pub vendor: String,
pub backend: GpuBackend,
pub limits: GpuLimits,
pub supports_f16: bool,
pub supports_timestamp_query: bool,
pub vram_bytes: u64,
}
impl Default for SimulatedGpuConfig {
fn default() -> Self {
Self {
name: "Simulated GPU".to_string(),
vendor: "Test".to_string(),
backend: GpuBackend::Vulkan,
limits: GpuLimits::default(),
supports_f16: true,
supports_timestamp_query: true,
vram_bytes: 4 * 1024 * 1024 * 1024, }
}
}
impl SimulatedGpuConfig {
#[must_use]
pub fn high_end_desktop() -> Self {
Self {
name: "Simulated RTX 4090".to_string(),
vendor: "NVIDIA".to_string(),
backend: GpuBackend::Vulkan,
limits: GpuLimits::desktop_high_end(),
supports_f16: true,
supports_timestamp_query: true,
vram_bytes: 24 * 1024 * 1024 * 1024, }
}
#[must_use]
pub fn apple_silicon() -> Self {
Self {
name: "Simulated Apple M2".to_string(),
vendor: "Apple".to_string(),
backend: GpuBackend::Metal,
limits: GpuLimits::default(),
supports_f16: true,
supports_timestamp_query: false,
vram_bytes: 16 * 1024 * 1024 * 1024, }
}
#[must_use]
pub fn mobile() -> Self {
Self {
name: "Simulated Adreno 730".to_string(),
vendor: "Qualcomm".to_string(),
backend: GpuBackend::Vulkan,
limits: GpuLimits::mobile(),
supports_f16: true,
supports_timestamp_query: false,
vram_bytes: 512 * 1024 * 1024, }
}
#[must_use]
pub fn browser_webgpu() -> Self {
Self {
name: "Browser GPU".to_string(),
vendor: "Unknown".to_string(),
backend: GpuBackend::BrowserWebGpu,
limits: GpuLimits::default(),
supports_f16: false, supports_timestamp_query: false,
vram_bytes: 0, }
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn with_vram(mut self, vram_bytes: u64) -> Self {
self.vram_bytes = vram_bytes;
self
}
#[must_use]
pub fn with_backend(mut self, backend: GpuBackend) -> Self {
self.backend = backend;
self
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GpuFeatureQuery {
pub compute: bool,
pub f16: bool,
pub timestamp_query: bool,
pub min_buffer_size: u64,
pub min_vram: u64,
}
impl GpuFeatureQuery {
#[must_use]
pub fn for_inference() -> Self {
Self {
compute: true,
f16: false, timestamp_query: false,
min_buffer_size: 256 * 1024 * 1024,
min_vram: 256 * 1024 * 1024,
}
}
#[must_use]
pub fn for_profiling() -> Self {
Self {
compute: true,
f16: false,
timestamp_query: true,
min_buffer_size: 64 * 1024 * 1024,
min_vram: 0,
}
}
#[must_use]
pub fn with_compute(mut self) -> Self {
self.compute = true;
self
}
#[must_use]
pub fn with_f16(mut self) -> Self {
self.f16 = true;
self
}
#[must_use]
pub fn with_timestamp_query(mut self) -> Self {
self.timestamp_query = true;
self
}
#[must_use]
pub fn satisfied_by(&self, caps: &GpuCapabilities) -> bool {
if self.compute && !caps.supports_compute() {
return false;
}
if self.f16 && !caps.supports_f16 {
return false;
}
if self.timestamp_query && !caps.supports_timestamp_query {
return false;
}
if self.min_buffer_size > caps.limits.max_buffer_size {
return false;
}
if self.min_vram > 0 && caps.vram_bytes > 0 && self.min_vram > caps.vram_bytes {
return false;
}
true
}
#[must_use]
pub fn unsatisfied_requirements(&self, caps: &GpuCapabilities) -> Vec<String> {
let mut reqs = Vec::new();
if self.compute && !caps.supports_compute() {
reqs.push("compute shaders".to_string());
}
if self.f16 && !caps.supports_f16 {
reqs.push("F16 support".to_string());
}
if self.timestamp_query && !caps.supports_timestamp_query {
reqs.push("timestamp queries".to_string());
}
if self.min_buffer_size > caps.limits.max_buffer_size {
reqs.push(format!(
"buffer size (need {} MB, have {} MB)",
self.min_buffer_size / 1024 / 1024,
caps.limits.max_buffer_size / 1024 / 1024
));
}
if self.min_vram > 0 && caps.vram_bytes > 0 && self.min_vram > caps.vram_bytes {
reqs.push(format!(
"VRAM (need {} MB, have {} MB)",
self.min_vram / 1024 / 1024,
caps.vram_bytes / 1024 / 1024
));
}
reqs
}
}
#[must_use]
pub fn recommend_backend() -> GpuBackend {
#[cfg(target_os = "macos")]
{
GpuBackend::Metal
}
#[cfg(target_os = "windows")]
{
GpuBackend::Dx12
}
#[cfg(target_os = "linux")]
{
GpuBackend::Vulkan
}
#[cfg(target_arch = "wasm32")]
{
GpuBackend::BrowserWebGpu
}
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
target_os = "linux",
target_arch = "wasm32"
)))]
{
GpuBackend::Vulkan }
}
#[must_use]
pub fn should_use_gpu(caps: &GpuCapabilities, workload_elements: usize) -> GpuRecommendation {
const GPU_THRESHOLD: usize = 10_000; const GPU_STRONGLY_RECOMMENDED: usize = 100_000;
if !caps.is_available() {
return GpuRecommendation::CpuOnly {
reason: "No GPU available".to_string(),
};
}
if !caps.supports_compute() {
return GpuRecommendation::CpuOnly {
reason: "GPU doesn't support compute shaders".to_string(),
};
}
if workload_elements < GPU_THRESHOLD {
return GpuRecommendation::CpuPreferred {
reason: format!(
"Workload size ({workload_elements} elements) is small; CPU may be faster due to GPU overhead"
),
};
}
if workload_elements >= GPU_STRONGLY_RECOMMENDED {
return GpuRecommendation::GpuStronglyRecommended {
speedup_estimate: estimate_speedup(caps, workload_elements),
};
}
GpuRecommendation::GpuRecommended {
speedup_estimate: estimate_speedup(caps, workload_elements),
}
}
#[derive(Debug, Clone)]
pub enum GpuRecommendation {
CpuOnly {
reason: String,
},
CpuPreferred {
reason: String,
},
GpuRecommended {
speedup_estimate: f32,
},
GpuStronglyRecommended {
speedup_estimate: f32,
},
}
impl GpuRecommendation {
#[must_use]
pub fn use_gpu(&self) -> bool {
matches!(
self,
Self::GpuRecommended { .. } | Self::GpuStronglyRecommended { .. }
)
}
#[must_use]
pub fn speedup(&self) -> Option<f32> {
match self {
Self::GpuRecommended { speedup_estimate }
| Self::GpuStronglyRecommended { speedup_estimate } => Some(*speedup_estimate),
_ => None,
}
}
}
fn estimate_speedup(caps: &GpuCapabilities, elements: usize) -> f32 {
let base_speedup = if caps.backend.is_high_performance() {
10.0
} else {
5.0
};
let scale = (elements as f32 / 10_000.0).ln().max(1.0);
(base_speedup * scale).min(100.0)
}