use std::sync::Arc;
use crate::{GpuContext, GpuError, GpuResult};
#[derive(Debug, Clone)]
pub struct FallbackConfig {
pub enabled: bool,
pub trigger_on_device_lost: bool,
pub trigger_on_no_backend: bool,
pub timeout_ms: Option<u64>,
pub log_on_fallback: bool,
}
impl Default for FallbackConfig {
fn default() -> Self {
Self {
enabled: true,
trigger_on_device_lost: true,
trigger_on_no_backend: true,
timeout_ms: None,
log_on_fallback: false,
}
}
}
impl FallbackConfig {
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn with_logging(mut self) -> Self {
self.log_on_fallback = true;
self
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = Some(timeout_ms);
self
}
pub fn should_fallback(&self, err: &GpuError) -> bool {
if !self.enabled {
return false;
}
if self.trigger_on_device_lost && matches!(err, GpuError::DeviceLost { .. }) {
return true;
}
if self.trigger_on_no_backend {
let is_no_adapter = matches!(err, GpuError::NoAdapter { .. });
let is_backend_na = matches!(err, GpuError::BackendNotAvailable { .. });
let msg = err.to_string();
let is_msg_match = msg.contains("No wgpu backend")
|| msg.contains("no backend")
|| msg.contains("no adapter");
if is_no_adapter || is_backend_na || is_msg_match {
return true;
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExecutionPath {
Gpu,
Cpu,
}
#[derive(Debug)]
pub struct FallbackResult<T> {
pub value: T,
pub path: ExecutionPath,
}
impl<T: Clone> Clone for FallbackResult<T> {
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
path: self.path,
}
}
}
pub fn execute_with_fallback<T, GpuFn, CpuFn>(
config: &FallbackConfig,
gpu_op: GpuFn,
cpu_op: CpuFn,
) -> GpuResult<FallbackResult<T>>
where
GpuFn: FnOnce() -> GpuResult<T>,
CpuFn: FnOnce() -> T,
{
match gpu_op() {
Ok(value) => Ok(FallbackResult {
value,
path: ExecutionPath::Gpu,
}),
Err(err) if config.should_fallback(&err) => {
if config.log_on_fallback {
eprintln!("[oxigdal-gpu] GPU op failed ({err}), falling back to CPU");
}
Ok(FallbackResult {
value: cpu_op(),
path: ExecutionPath::Cpu,
})
}
Err(err) => Err(err),
}
}
pub fn execute_with_fallback_timed<T, GpuFn, CpuFn>(
config: &FallbackConfig,
gpu_op: GpuFn,
cpu_op: CpuFn,
) -> GpuResult<FallbackResult<T>>
where
T: Send + 'static,
GpuFn: FnOnce() -> GpuResult<T> + Send + 'static,
CpuFn: FnOnce() -> T,
{
let timeout = match config.timeout_ms {
None => return execute_with_fallback(config, gpu_op, cpu_op),
Some(ms) => std::time::Duration::from_millis(ms),
};
let (tx, rx) = std::sync::mpsc::channel::<GpuResult<T>>();
std::thread::spawn(move || {
let _ = tx.send(gpu_op());
});
match rx.recv_timeout(timeout) {
Ok(Ok(value)) => Ok(FallbackResult {
value,
path: ExecutionPath::Gpu,
}),
Ok(Err(err)) if config.should_fallback(&err) => {
if config.log_on_fallback {
eprintln!("[oxigdal-gpu] GPU op failed ({err}), falling back to CPU");
}
Ok(FallbackResult {
value: cpu_op(),
path: ExecutionPath::Cpu,
})
}
Ok(Err(err)) => Err(err),
Err(_recv_err) => {
if config.log_on_fallback {
eprintln!(
"[oxigdal-gpu] GPU op timed out after {}ms, falling back to CPU",
timeout.as_millis()
);
}
Ok(FallbackResult {
value: cpu_op(),
path: ExecutionPath::Cpu,
})
}
}
}
pub struct FallbackContext {
inner: Arc<GpuContext>,
config: FallbackConfig,
}
impl FallbackContext {
pub fn new(ctx: GpuContext, config: FallbackConfig) -> Self {
Self {
inner: Arc::new(ctx),
config,
}
}
pub fn with_default_config(ctx: GpuContext) -> Self {
Self::new(ctx, FallbackConfig::default())
}
pub fn gpu_context(&self) -> &GpuContext {
&self.inner
}
pub fn fallback_config(&self) -> &FallbackConfig {
&self.config
}
pub fn is_device_lost(&self) -> bool {
self.inner.is_device_lost()
}
pub fn execute<T, GpuFn, CpuFn>(
&self,
gpu_op: GpuFn,
cpu_op: CpuFn,
) -> GpuResult<FallbackResult<T>>
where
GpuFn: FnOnce() -> GpuResult<T>,
CpuFn: FnOnce() -> T,
{
if self.inner.is_device_lost() {
if self.config.enabled {
if self.config.log_on_fallback {
eprintln!("[oxigdal-gpu] Device already lost, running CPU directly");
}
return Ok(FallbackResult {
value: cpu_op(),
path: ExecutionPath::Cpu,
});
}
return Err(GpuError::device_lost("device was lost"));
}
execute_with_fallback(&self.config, gpu_op, cpu_op)
}
}
impl std::fmt::Debug for FallbackContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FallbackContext")
.field("config", &self.config)
.field("device_lost", &self.inner.is_device_lost())
.finish()
}
}
pub mod cpu {
pub fn add_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}
pub fn sub_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
}
pub fn mul_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x * y).collect()
}
pub fn div_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x / y).collect()
}
pub fn max_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x.max(*y)).collect()
}
pub fn min_slices(a: &[f32], b: &[f32]) -> Vec<f32> {
a.iter().zip(b.iter()).map(|(x, y)| x.min(*y)).collect()
}
pub fn add_scalar(data: &[f32], scalar: f32) -> Vec<f32> {
data.iter().map(|x| x + scalar).collect()
}
pub fn sub_scalar(data: &[f32], scalar: f32) -> Vec<f32> {
data.iter().map(|x| x - scalar).collect()
}
pub fn mul_scalar(data: &[f32], scalar: f32) -> Vec<f32> {
data.iter().map(|x| x * scalar).collect()
}
pub fn div_scalar(data: &[f32], scalar: f32) -> Vec<f32> {
data.iter().map(|x| x / scalar).collect()
}
pub fn clamp(data: &[f32], lo: f32, hi: f32) -> Vec<f32> {
data.iter().map(|x| x.clamp(lo, hi)).collect()
}
pub fn abs(data: &[f32]) -> Vec<f32> {
data.iter().map(|x| x.abs()).collect()
}
pub fn sqrt(data: &[f32]) -> Vec<f32> {
data.iter().map(|x| x.sqrt()).collect()
}
pub fn powf(data: &[f32], exponent: f32) -> Vec<f32> {
data.iter().map(|x| x.powf(exponent)).collect()
}
pub fn mean(data: &[f32]) -> f32 {
if data.is_empty() {
return 0.0;
}
data.iter().sum::<f32>() / data.len() as f32
}
pub fn sum(data: &[f32]) -> f32 {
data.iter().sum()
}
pub fn min_value(data: &[f32]) -> f32 {
data.iter().cloned().fold(f32::MAX, f32::min)
}
pub fn max_value(data: &[f32]) -> f32 {
data.iter().cloned().fold(f32::MIN, f32::max)
}
pub fn variance(data: &[f32]) -> f32 {
if data.len() < 2 {
return 0.0;
}
let m = mean(data);
data.iter().map(|x| (x - m) * (x - m)).sum::<f32>() / data.len() as f32
}
pub fn std_dev(data: &[f32]) -> f32 {
variance(data).sqrt()
}
pub fn ndvi(red: &[f32], nir: &[f32]) -> Vec<f32> {
red.iter()
.zip(nir.iter())
.map(|(r, n)| {
let denom = n + r;
if denom == 0.0 { 0.0 } else { (n - r) / denom }
})
.collect()
}
}