use crate::core::error::PlottingError;
use crate::data::platform::get_platform_optimizer;
use std::sync::{Arc, Mutex, OnceLock};
#[allow(clippy::enum_variant_names)] #[derive(Debug, thiserror::Error)]
pub enum GpuError {
#[error("GPU initialization failed: {0}")]
InitializationFailed(String),
#[error("Buffer operation failed: {0}")]
BufferOperationFailed(String),
#[error("Operation failed: {0}")]
OperationFailed(String),
#[error("Buffer creation failed: {0}")]
BufferCreationFailed(String),
}
pub type GpuResult<T> = Result<T, GpuError>;
pub mod buffer;
pub mod compute;
pub mod device;
pub mod memory;
pub mod pipeline;
pub mod renderer;
pub use buffer::{BufferManager, BufferUsage, GpuBuffer};
pub use compute::{AggregationParams, ComputeManager, ComputeStats, TransformParams};
pub use device::{DeviceSelector, GpuDevice, GpuDeviceInfo};
pub use memory::{GpuMemoryPool, GpuMemoryStats, PooledGpuBuffer};
pub use pipeline::{ComputePipeline, PipelineCache, RenderPipeline};
pub use renderer::{GpuRenderer, GpuRendererStats, GpuVertex};
#[derive(Clone)]
pub struct GpuBackend {
device: Arc<GpuDevice>,
buffer_manager: Arc<Mutex<BufferManager>>,
pipeline_cache: Arc<Mutex<PipelineCache>>,
capabilities: GpuCapabilities,
config: GpuConfig,
}
#[derive(Debug, Clone)]
pub struct GpuCapabilities {
pub max_texture_size: u32,
pub max_buffer_size: u64,
pub max_compute_workgroups: [u32; 3],
pub memory_size: Option<u64>,
pub supports_compute: bool,
pub supports_storage_textures: bool,
pub supports_timestamps: bool,
pub max_render_targets: u32,
pub supported_formats: Vec<wgpu::TextureFormat>,
}
#[derive(Debug, Clone)]
pub struct GpuConfig {
pub enable_gpu: bool,
pub preferred_backend: Option<wgpu::Backends>,
pub memory_limit_fraction: f32,
pub enable_validation: bool,
pub enable_profiling: bool,
pub power_preference: wgpu::PowerPreference,
pub required_features: wgpu::Features,
pub required_limits: wgpu::Limits,
}
impl Default for GpuConfig {
fn default() -> Self {
Self {
enable_gpu: true,
preferred_backend: None, memory_limit_fraction: 0.8, enable_validation: cfg!(debug_assertions),
enable_profiling: false,
power_preference: wgpu::PowerPreference::HighPerformance,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
}
}
}
impl GpuBackend {
pub async fn new() -> Result<Self, PlottingError> {
Self::with_config(GpuConfig::default()).await
}
pub async fn with_config(config: GpuConfig) -> Result<Self, PlottingError> {
if !config.enable_gpu {
return Err(PlottingError::FeatureNotEnabled {
feature: "GPU acceleration".to_string(),
operation: "GPU backend initialization".to_string(),
});
}
let instance = Self::create_instance(&config)?;
let device = GpuDevice::new(&instance, &config).await?;
let capabilities = Self::detect_capabilities(&device)?;
Self::validate_capabilities(&capabilities, &config)?;
let platform_optimizer = get_platform_optimizer();
let hints = platform_optimizer.get_performance_hints();
let buffer_manager = BufferManager::new(&device, &capabilities, &hints)?;
let pipeline_cache = PipelineCache::new();
Ok(Self {
device: Arc::new(device),
buffer_manager: Arc::new(Mutex::new(buffer_manager)),
pipeline_cache: Arc::new(Mutex::new(pipeline_cache)),
capabilities,
config,
})
}
fn create_instance(config: &GpuConfig) -> Result<wgpu::Instance, PlottingError> {
let backends = config.preferred_backend.unwrap_or_else(|| {
#[cfg(target_os = "windows")]
return wgpu::Backends::DX12 | wgpu::Backends::VULKAN;
#[cfg(target_os = "macos")]
return wgpu::Backends::METAL;
#[cfg(target_os = "linux")]
return wgpu::Backends::VULKAN | wgpu::Backends::GL;
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
return wgpu::Backends::all();
});
let instance_desc = wgpu::InstanceDescriptor {
backends,
flags: if config.enable_validation {
wgpu::InstanceFlags::DEBUG | wgpu::InstanceFlags::VALIDATION
} else {
wgpu::InstanceFlags::default()
},
memory_budget_thresholds: Default::default(),
backend_options: Default::default(),
display: None,
};
Ok(wgpu::Instance::new(instance_desc))
}
fn detect_capabilities(device: &GpuDevice) -> Result<GpuCapabilities, PlottingError> {
let limits = device.limits();
let features = device.features();
Ok(GpuCapabilities {
max_texture_size: limits.max_texture_dimension_2d,
max_buffer_size: limits.max_buffer_size,
max_compute_workgroups: [
limits.max_compute_workgroups_per_dimension,
limits.max_compute_workgroups_per_dimension,
limits.max_compute_workgroups_per_dimension,
],
memory_size: None, supports_compute: true, supports_storage_textures: features
.contains(wgpu::Features::STORAGE_RESOURCE_BINDING_ARRAY),
supports_timestamps: features.contains(wgpu::Features::TIMESTAMP_QUERY),
max_render_targets: limits.max_color_attachments,
supported_formats: Self::get_supported_formats(device),
})
}
fn get_supported_formats(device: &GpuDevice) -> Vec<wgpu::TextureFormat> {
let common_formats = [
wgpu::TextureFormat::R8Unorm,
wgpu::TextureFormat::Rg8Unorm,
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Rgba8UnormSrgb,
wgpu::TextureFormat::R16Float,
wgpu::TextureFormat::Rg16Float,
wgpu::TextureFormat::Rgba16Float,
wgpu::TextureFormat::R32Float,
wgpu::TextureFormat::Rg32Float,
wgpu::TextureFormat::Rgba32Float,
wgpu::TextureFormat::Bgra8Unorm,
wgpu::TextureFormat::Bgra8UnormSrgb,
];
common_formats
.into_iter()
.filter(|&format| {
device
.adapter()
.get_texture_format_features(format)
.allowed_usages
.contains(
wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
)
})
.collect()
}
fn validate_capabilities(
capabilities: &GpuCapabilities,
config: &GpuConfig,
) -> Result<(), PlottingError> {
if capabilities.max_texture_size < 4096 {
return Err(PlottingError::UnsupportedGpuFeature(format!(
"Maximum texture size {} is too small (minimum 4096)",
capabilities.max_texture_size
)));
}
if !capabilities.supports_compute
&& config.required_features.contains(wgpu::Features::empty())
{
return Err(PlottingError::UnsupportedGpuFeature(
"Compute shaders required but not supported".to_string(),
));
}
const MIN_BUFFER_SIZE: u64 = 256 * 1024 * 1024; if capabilities.max_buffer_size < MIN_BUFFER_SIZE {
return Err(PlottingError::UnsupportedGpuFeature(format!(
"Maximum buffer size {} is too small (minimum {})",
capabilities.max_buffer_size, MIN_BUFFER_SIZE
)));
}
Ok(())
}
pub fn device(&self) -> &Arc<GpuDevice> {
&self.device
}
pub fn queue(&self) -> &Arc<wgpu::Queue> {
self.device.queue()
}
pub fn capabilities(&self) -> &GpuCapabilities {
&self.capabilities
}
pub fn config(&self) -> &GpuConfig {
&self.config
}
pub fn buffer_manager(&self) -> Arc<Mutex<BufferManager>> {
Arc::clone(&self.buffer_manager)
}
pub fn pipeline_cache(&self) -> Arc<Mutex<PipelineCache>> {
Arc::clone(&self.pipeline_cache)
}
pub fn create_render_pass(
&self,
width: u32,
height: u32,
format: wgpu::TextureFormat,
) -> Result<GpuRenderPass, PlottingError> {
GpuRenderPass::new(&self.device, width, height, format, &self.capabilities)
}
pub fn create_compute_manager(&self) -> Result<ComputeManager, PlottingError> {
if !self.capabilities.supports_compute {
return Err(PlottingError::UnsupportedGpuFeature(
"Compute shaders not supported".to_string(),
));
}
Ok(ComputeManager::new(
Arc::clone(self.device.device()),
Arc::clone(self.device.queue()),
))
}
pub fn is_available(&self) -> bool {
self.device.is_valid()
}
pub fn get_stats(&self) -> GpuStats {
let buffer_manager = self.buffer_manager.lock().unwrap();
let pipeline_cache = self.pipeline_cache.lock().unwrap();
GpuStats {
device_info: self.device.info().clone(),
buffer_stats: buffer_manager.get_stats(),
pipeline_stats: pipeline_cache.get_stats(),
memory_usage: buffer_manager.get_memory_usage(),
active_passes: 0, }
}
}
pub struct GpuRenderPass {
texture: wgpu::Texture,
view: wgpu::TextureView,
format: wgpu::TextureFormat,
width: u32,
height: u32,
depth_texture: Option<wgpu::Texture>,
depth_view: Option<wgpu::TextureView>,
}
impl GpuRenderPass {
fn new(
device: &GpuDevice,
width: u32,
height: u32,
format: wgpu::TextureFormat,
capabilities: &GpuCapabilities,
) -> Result<Self, PlottingError> {
if width > capabilities.max_texture_size || height > capabilities.max_texture_size {
return Err(PlottingError::GpuMemoryError {
requested: (width * height * 4) as usize, available: Some(
(capabilities.max_texture_size * capabilities.max_texture_size * 4) as usize,
),
});
}
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Render Target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let (depth_texture, depth_view) = if capabilities
.supported_formats
.contains(&wgpu::TextureFormat::Depth32Float)
{
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Depth Buffer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
(Some(depth_texture), Some(depth_view))
} else {
(None, None)
};
Ok(Self {
texture,
view,
format,
width,
height,
depth_texture,
depth_view,
})
}
pub fn color_view(&self) -> &wgpu::TextureView {
&self.view
}
pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
self.depth_view.as_ref()
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn format(&self) -> wgpu::TextureFormat {
self.format
}
}
#[derive(Debug, Clone)]
pub struct GpuStats {
pub device_info: GpuDeviceInfo,
pub buffer_stats: BufferStats,
pub pipeline_stats: PipelineStats,
pub memory_usage: u64,
pub active_passes: u32,
}
#[derive(Debug, Clone)]
pub struct BufferStats {
pub total_buffers: usize,
pub total_memory: u64,
pub active_buffers: usize,
pub reused_buffers: usize,
}
#[derive(Debug, Clone)]
pub struct PipelineStats {
pub total_pipelines: usize,
pub cache_hits: usize,
pub cache_misses: usize,
}
#[cfg(not(target_arch = "wasm32"))]
static GPU_BACKEND: OnceLock<Option<GpuBackend>> = OnceLock::new();
pub async fn create_gpu_backend() -> Result<GpuBackend, PlottingError> {
GpuBackend::new().await
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn initialize_gpu_backend() -> Result<(), PlottingError> {
let backend = match create_gpu_backend().await {
Ok(backend) => Some(backend),
Err(e) => {
log::warn!("Failed to initialize GPU backend: {}", e);
None
}
};
GPU_BACKEND
.set(backend)
.map_err(|_| PlottingError::GpuInitError {
backend: "wgpu".to_string(),
error: "Backend already initialized".to_string(),
})?;
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn initialize_gpu_backend() -> Result<(), PlottingError> {
if let Err(err) = create_gpu_backend().await {
log::warn!("Failed to initialize GPU backend: {}", err);
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_gpu_backend() -> Option<&'static GpuBackend> {
GPU_BACKEND.get().and_then(|backend| backend.as_ref())
}
#[cfg(target_arch = "wasm32")]
pub fn get_gpu_backend() -> Option<&'static GpuBackend> {
None
}
pub fn is_gpu_available() -> bool {
get_gpu_backend().is_some_and(|backend| backend.is_available())
}
pub fn get_gpu_capabilities() -> Option<&'static GpuCapabilities> {
get_gpu_backend().map(|backend| backend.capabilities())
}