284 lines
9.5 KiB
Rust
284 lines
9.5 KiB
Rust
//! Compute backend detection and abstraction
|
|
//!
|
|
//! Detects available compute capabilities (WebGPU, WebGL2, WebWorkers)
|
|
//! and provides a unified interface for selecting the best backend.
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
/// Compute capabilities detected on the current device
|
|
#[derive(Clone, Debug)]
|
|
pub struct ComputeCapability {
|
|
/// WebGPU is available (best performance)
|
|
pub has_webgpu: bool,
|
|
/// WebGL2 is available (fallback for GPU compute)
|
|
pub has_webgl2: bool,
|
|
/// WebGL2 supports floating point textures
|
|
pub has_float_textures: bool,
|
|
/// Transform feedback is available (for GPU readback)
|
|
pub has_transform_feedback: bool,
|
|
/// WebWorkers are available
|
|
pub has_workers: bool,
|
|
/// SharedArrayBuffer is available (for shared memory)
|
|
pub has_shared_memory: bool,
|
|
/// Number of logical CPU cores
|
|
pub worker_count: usize,
|
|
/// Maximum texture size (for WebGL2)
|
|
pub max_texture_size: u32,
|
|
/// Estimated GPU memory (MB)
|
|
pub gpu_memory_mb: u32,
|
|
/// Device description
|
|
pub device_info: String,
|
|
}
|
|
|
|
impl ComputeCapability {
|
|
/// Convert to JavaScript object
|
|
pub fn to_js(&self) -> JsValue {
|
|
let obj = js_sys::Object::new();
|
|
|
|
js_sys::Reflect::set(&obj, &"hasWebGPU".into(), &self.has_webgpu.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"hasWebGL2".into(), &self.has_webgl2.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"hasFloatTextures".into(), &self.has_float_textures.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"hasTransformFeedback".into(), &self.has_transform_feedback.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"hasWorkers".into(), &self.has_workers.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"hasSharedMemory".into(), &self.has_shared_memory.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"workerCount".into(), &(self.worker_count as u32).into()).ok();
|
|
js_sys::Reflect::set(&obj, &"maxTextureSize".into(), &self.max_texture_size.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"gpuMemoryMB".into(), &self.gpu_memory_mb.into()).ok();
|
|
js_sys::Reflect::set(&obj, &"deviceInfo".into(), &self.device_info.clone().into()).ok();
|
|
|
|
obj.into()
|
|
}
|
|
|
|
/// Get recommended backend for a given operation size
|
|
pub fn recommend_backend(&self, operation_size: usize) -> ComputeBackend {
|
|
// WebGPU is always preferred if available
|
|
if self.has_webgpu {
|
|
return ComputeBackend::WebGPU;
|
|
}
|
|
|
|
// For large operations, prefer GPU
|
|
if operation_size > 4096 && self.has_webgl2 && self.has_float_textures {
|
|
return ComputeBackend::WebGL2;
|
|
}
|
|
|
|
// For medium operations with multiple cores, use workers
|
|
if operation_size > 1024 && self.has_workers && self.worker_count > 1 {
|
|
return ComputeBackend::WebWorkers;
|
|
}
|
|
|
|
// Fall back to single-threaded CPU
|
|
ComputeBackend::CPU
|
|
}
|
|
}
|
|
|
|
/// Available compute backends
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ComputeBackend {
|
|
/// WebGPU compute shaders (best performance)
|
|
WebGPU,
|
|
/// WebGL2 texture-based compute (fallback GPU)
|
|
WebGL2,
|
|
/// WebWorker pool (CPU parallelism)
|
|
WebWorkers,
|
|
/// Single-threaded CPU (last resort)
|
|
CPU,
|
|
}
|
|
|
|
impl ComputeBackend {
|
|
/// Get backend name
|
|
pub fn name(&self) -> &'static str {
|
|
match self {
|
|
ComputeBackend::WebGPU => "WebGPU",
|
|
ComputeBackend::WebGL2 => "WebGL2",
|
|
ComputeBackend::WebWorkers => "WebWorkers",
|
|
ComputeBackend::CPU => "CPU",
|
|
}
|
|
}
|
|
|
|
/// Get relative performance (higher is better)
|
|
pub fn relative_performance(&self) -> f32 {
|
|
match self {
|
|
ComputeBackend::WebGPU => 10.0,
|
|
ComputeBackend::WebGL2 => 5.0,
|
|
ComputeBackend::WebWorkers => 2.0,
|
|
ComputeBackend::CPU => 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Detect compute capabilities on the current device
|
|
pub fn detect_capabilities() -> Result<ComputeCapability, JsValue> {
|
|
let window = web_sys::window()
|
|
.ok_or_else(|| JsValue::from_str("No window object"))?;
|
|
|
|
let navigator = window.navigator();
|
|
|
|
// Detect WebGPU
|
|
let has_webgpu = js_sys::Reflect::has(&navigator, &"gpu".into())
|
|
.unwrap_or(false);
|
|
|
|
// Detect WebWorkers
|
|
let has_workers = js_sys::Reflect::has(&window, &"Worker".into())
|
|
.unwrap_or(false);
|
|
|
|
// Detect SharedArrayBuffer
|
|
let has_shared_memory = js_sys::Reflect::has(&window, &"SharedArrayBuffer".into())
|
|
.unwrap_or(false);
|
|
|
|
// Get hardware concurrency (CPU cores)
|
|
let worker_count = navigator.hardware_concurrency() as usize;
|
|
|
|
// Detect WebGL2 capabilities
|
|
let document = window.document()
|
|
.ok_or_else(|| JsValue::from_str("No document"))?;
|
|
|
|
let (has_webgl2, has_float_textures, has_transform_feedback, max_texture_size, gpu_memory_mb, device_info) =
|
|
detect_webgl2_capabilities(&document)?;
|
|
|
|
Ok(ComputeCapability {
|
|
has_webgpu,
|
|
has_webgl2,
|
|
has_float_textures,
|
|
has_transform_feedback,
|
|
has_workers,
|
|
has_shared_memory,
|
|
worker_count: worker_count.max(1),
|
|
max_texture_size,
|
|
gpu_memory_mb,
|
|
device_info,
|
|
})
|
|
}
|
|
|
|
/// Detect WebGL2-specific capabilities
|
|
fn detect_webgl2_capabilities(document: &web_sys::Document) -> Result<(bool, bool, bool, u32, u32, String), JsValue> {
|
|
// Create a temporary canvas to probe WebGL2
|
|
let canvas = document.create_element("canvas")?;
|
|
let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into()?;
|
|
|
|
// Try to get WebGL2 context
|
|
let context = match canvas.get_context("webgl2")? {
|
|
Some(ctx) => ctx,
|
|
None => return Ok((false, false, false, 0, 0, "No WebGL2".to_string())),
|
|
};
|
|
|
|
let gl: web_sys::WebGl2RenderingContext = context.dyn_into()?;
|
|
|
|
// Check for float texture support (required for compute)
|
|
let ext_color_buffer_float = gl.get_extension("EXT_color_buffer_float")?;
|
|
let has_float_textures = ext_color_buffer_float.is_some();
|
|
|
|
// Transform feedback is built into WebGL2
|
|
let has_transform_feedback = true;
|
|
|
|
// Get max texture size
|
|
let max_texture_size = gl.get_parameter(web_sys::WebGl2RenderingContext::MAX_TEXTURE_SIZE)?
|
|
.as_f64()
|
|
.unwrap_or(4096.0) as u32;
|
|
|
|
// Try to get GPU memory info (vendor-specific)
|
|
let gpu_memory_mb = get_gpu_memory_mb(&gl);
|
|
|
|
// Get renderer info
|
|
let renderer_info = gl.get_extension("WEBGL_debug_renderer_info")?;
|
|
let device_info = if renderer_info.is_some() {
|
|
// UNMASKED_RENDERER_WEBGL = 0x9246
|
|
let renderer = gl.get_parameter(0x9246)?;
|
|
renderer.as_string().unwrap_or_else(|| "Unknown GPU".to_string())
|
|
} else {
|
|
"Unknown GPU".to_string()
|
|
};
|
|
|
|
Ok((true, has_float_textures, has_transform_feedback, max_texture_size, gpu_memory_mb, device_info))
|
|
}
|
|
|
|
/// Try to get GPU memory size (vendor-specific extension)
|
|
fn get_gpu_memory_mb(gl: &web_sys::WebGl2RenderingContext) -> u32 {
|
|
// Try WEBGL_memory_info extension (available on some browsers)
|
|
if let Ok(Some(_ext)) = gl.get_extension("WEBGL_memory_info") {
|
|
// GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX = 0x9048
|
|
if let Ok(mem) = gl.get_parameter(0x9048) {
|
|
if let Some(kb) = mem.as_f64() {
|
|
return (kb / 1024.0) as u32;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default estimate based on typical mobile/desktop GPUs
|
|
// Most modern GPUs have at least 2GB
|
|
2048
|
|
}
|
|
|
|
/// Configuration for compute operations
|
|
#[derive(Clone, Debug)]
|
|
pub struct ComputeConfig {
|
|
/// Preferred backend (None = auto-select)
|
|
pub preferred_backend: Option<ComputeBackend>,
|
|
/// Maximum memory to use (bytes)
|
|
pub max_memory: usize,
|
|
/// Timeout for operations (ms)
|
|
pub timeout_ms: u32,
|
|
/// Enable profiling
|
|
pub profiling: bool,
|
|
}
|
|
|
|
impl Default for ComputeConfig {
|
|
fn default() -> Self {
|
|
ComputeConfig {
|
|
preferred_backend: None,
|
|
max_memory: 256 * 1024 * 1024, // 256MB
|
|
timeout_ms: 30_000, // 30 seconds
|
|
profiling: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_backend_recommendation() {
|
|
let caps = ComputeCapability {
|
|
has_webgpu: false,
|
|
has_webgl2: true,
|
|
has_float_textures: true,
|
|
has_transform_feedback: true,
|
|
has_workers: true,
|
|
has_shared_memory: true,
|
|
worker_count: 4,
|
|
max_texture_size: 4096,
|
|
gpu_memory_mb: 2048,
|
|
device_info: "Test GPU".to_string(),
|
|
};
|
|
|
|
// Large operations should use WebGL2
|
|
assert_eq!(caps.recommend_backend(10000), ComputeBackend::WebGL2);
|
|
|
|
// Medium operations with workers should use workers
|
|
assert_eq!(caps.recommend_backend(2000), ComputeBackend::WebWorkers);
|
|
|
|
// Small operations should use CPU
|
|
assert_eq!(caps.recommend_backend(100), ComputeBackend::CPU);
|
|
}
|
|
|
|
#[test]
|
|
fn test_backend_with_webgpu() {
|
|
let caps = ComputeCapability {
|
|
has_webgpu: true,
|
|
has_webgl2: true,
|
|
has_float_textures: true,
|
|
has_transform_feedback: true,
|
|
has_workers: true,
|
|
has_shared_memory: true,
|
|
worker_count: 4,
|
|
max_texture_size: 4096,
|
|
gpu_memory_mb: 2048,
|
|
device_info: "Test GPU".to_string(),
|
|
};
|
|
|
|
// WebGPU should always be preferred
|
|
assert_eq!(caps.recommend_backend(100), ComputeBackend::WebGPU);
|
|
assert_eq!(caps.recommend_backend(10000), ComputeBackend::WebGPU);
|
|
}
|
|
}
|