#[cfg(feature = "cuda")]
pub mod cuda;
pub mod wgpu_backend;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock};
use crush_core::error::Result;
use tracing::{debug, info, trace, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GpuVendor {
Nvidia,
Amd,
Intel,
Apple,
Other,
}
impl std::fmt::Display for GpuVendor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Nvidia => write!(f, "NVIDIA"),
Self::Amd => write!(f, "AMD"),
Self::Intel => write!(f, "Intel"),
Self::Apple => write!(f, "Apple"),
Self::Other => write!(f, "Other"),
}
}
}
#[derive(Debug, Clone)]
pub struct GpuInfo {
pub name: String,
pub vendor: GpuVendor,
pub vram_bytes: u64,
pub api_backend: String,
}
#[derive(Debug, Clone)]
pub struct CompressedTile {
pub data: Vec<u8>,
pub uncompressed_size: u32,
pub sub_stream_count: u8,
pub checksum: u32,
}
pub trait ComputeBackend: Send + Sync {
fn name(&self) -> &str;
fn gpu_info(&self) -> &GpuInfo;
fn decompress_tiles(
&self,
tiles: &[CompressedTile],
cancel: &AtomicBool,
) -> Result<Vec<Vec<u8>>>;
fn decompress_tiles_gdeflate(
&self,
tiles: &[CompressedTile],
cancel: &AtomicBool,
) -> Result<Vec<Vec<u8>>>;
fn release(&self);
}
pub const MIN_VRAM_BYTES: u64 = 2 * 1024 * 1024 * 1024;
pub const GPU_MEMORY_BUDGET: u64 = 256 * 1024 * 1024;
pub const MAX_TILES_PER_BATCH: usize = 512;
#[must_use]
pub fn deinterleave(
raw_output: &[u8],
ss_lengths: &[u32],
sub_stream_count: u32,
uncompressed_size: u32,
) -> Vec<u8> {
let n = sub_stream_count as usize;
let max_per_ss = (uncompressed_size as usize).div_ceil(n);
let sub_streams: Vec<&[u8]> = (0..n)
.map(|i| {
let start = i * max_per_ss;
let len = ss_lengths[i] as usize;
let end = (start + len).min(raw_output.len());
let actual_start = start.min(raw_output.len());
&raw_output[actual_start..end]
})
.collect();
let mut output = Vec::with_capacity(uncompressed_size as usize);
let max_len = sub_streams.iter().map(|s| s.len()).max().unwrap_or(0);
for j in 0..max_len {
for ss in &sub_streams {
if j < ss.len() {
output.push(ss[j]);
}
if output.len() == uncompressed_size as usize {
return output;
}
}
}
output
}
static CACHED_BACKEND: OnceLock<Option<Arc<dyn ComputeBackend>>> = OnceLock::new();
#[cfg(feature = "cuda")]
fn try_cuda() -> std::result::Result<Arc<dyn ComputeBackend>, String> {
debug!("Probing CUDA backend...");
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(cuda::CudaBackend::try_new);
std::panic::set_hook(prev_hook);
match result {
Ok(Ok(Some(backend))) => {
let gi = backend.gpu_info();
info!(
gpu = %gi.name,
vram_mb = gi.vram_bytes / 1024 / 1024,
"CUDA backend ready: {}",
gi.name
);
Ok(Arc::new(backend) as Arc<dyn ComputeBackend>)
}
Ok(Ok(None)) => {
debug!("CUDA probe: no compatible NVIDIA GPU found");
Err("no compatible NVIDIA GPU found".to_owned())
}
Ok(Err(e)) => {
debug!("CUDA probe failed: {e}");
Err(format!("{e}"))
}
Err(panic_info) => {
let msg = panic_info
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic_info.downcast_ref::<&str>().copied())
.unwrap_or("unknown panic");
warn!("CUDA probe panicked (nvrtc likely missing): {msg}");
Err(format!(
"CUDA runtime compiler (nvrtc) not found. \
Install the CUDA Toolkit to enable CUDA decompression. \
Detail: {msg}"
))
}
}
}
fn try_wgpu() -> Option<Arc<dyn ComputeBackend>> {
debug!("Probing wgpu backend...");
match wgpu_backend::WgpuBackend::try_new() {
Ok(Some(backend)) => {
let gi = backend.gpu_info();
info!(
gpu = %gi.name,
api = %gi.api_backend,
vram_mb = gi.vram_bytes / 1024 / 1024,
"wgpu backend ready: {} ({})",
gi.name,
gi.api_backend
);
Some(Arc::new(backend) as Arc<dyn ComputeBackend>)
}
Ok(None) => {
debug!("wgpu probe: no compatible GPU found");
None
}
Err(e) => {
warn!("wgpu backend init failed: {e}");
None
}
}
}
pub fn discover_gpu() -> Result<Option<Arc<dyn ComputeBackend>>> {
use crush_core::error::{CrushError, PluginError};
use crate::BackendPreference;
if let Some(cached) = CACHED_BACKEND.get() {
trace!("Using cached GPU backend");
return Ok(cached.clone());
}
let pref = crate::get_config().backend;
info!(preference = ?pref, "Discovering GPU backend (preference: {pref:?})");
let backend: Option<Arc<dyn ComputeBackend>> = match pref {
BackendPreference::Auto => {
#[cfg(feature = "cuda")]
{
match try_cuda() {
Ok(backend) => Some(backend),
Err(msg) => {
info!("CUDA unavailable ({msg}), trying wgpu");
try_wgpu()
}
}
}
#[cfg(not(feature = "cuda"))]
{
debug!("CUDA feature not compiled in, trying wgpu only");
try_wgpu()
}
}
BackendPreference::Cuda => {
#[cfg(feature = "cuda")]
{
match try_cuda() {
Ok(backend) => Some(backend),
Err(msg) => {
return Err(CrushError::from(PluginError::OperationFailed(format!(
"CUDA backend requested but unavailable: {msg}"
))));
}
}
}
#[cfg(not(feature = "cuda"))]
{
return Err(CrushError::from(PluginError::OperationFailed(
"CUDA backend not included in this build. \
Reinstall with: cargo install crush-cli --features cuda"
.to_owned(),
)));
}
}
BackendPreference::Wgpu => try_wgpu(),
};
if let Some(ref b) = backend {
info!(backend = b.name(), "GPU backend selected: {}", b.name());
} else {
info!("No GPU backend available, will use CPU fallback");
}
let _ = CACHED_BACKEND.set(backend.clone());
Ok(CACHED_BACKEND.get().cloned().flatten())
}