ruviz 0.6.0

High-performance 2D plotting library for Rust
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::core::{PlottingError, Result};

pub(super) const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
pub(super) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;

pub(crate) struct GpuContext3D {
    pub(super) _instance: wgpu::Instance,
    pub(super) _adapter: wgpu::Adapter,
    pub(super) device: Arc<wgpu::Device>,
    pub(super) queue: Arc<wgpu::Queue>,
    pub(super) sample_count: u32,
    pub(super) adapter_name: String,
    lost: Arc<AtomicBool>,
}

impl GpuContext3D {
    #[cfg(not(target_arch = "wasm32"))]
    pub(super) fn new() -> Result<Self> {
        pollster::block_on(Self::from_instance_async(Self::create_instance(), None))
    }

    pub(crate) fn create_instance() -> wgpu::Instance {
        wgpu::Instance::new(wgpu::InstanceDescriptor {
            backends: wgpu::Backends::all(),
            flags: if cfg!(debug_assertions) {
                wgpu::InstanceFlags::DEBUG | wgpu::InstanceFlags::VALIDATION
            } else {
                wgpu::InstanceFlags::default()
            },
            memory_budget_thresholds: Default::default(),
            backend_options: Default::default(),
            display: None,
        })
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub(super) fn for_surface(
        instance: wgpu::Instance,
        surface: &wgpu::Surface<'_>,
    ) -> Result<Self> {
        pollster::block_on(Self::from_instance_async(instance, Some(surface)))
    }

    pub(crate) async fn from_instance_async(
        instance: wgpu::Instance,
        compatible_surface: Option<&wgpu::Surface<'_>>,
    ) -> Result<Self> {
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                force_fallback_adapter: false,
                compatible_surface,
            })
            .await
            .map_err(|error| PlottingError::GpuNotAvailable(error.to_string()))?;

        validate_format(
            &adapter,
            COLOR_FORMAT,
            wgpu::TextureUsages::RENDER_ATTACHMENT,
        )?;
        validate_format(
            &adapter,
            DEPTH_FORMAT,
            wgpu::TextureUsages::RENDER_ATTACHMENT,
        )?;

        let adapter_info = adapter.get_info();
        let required_limits = wgpu::Limits::default().using_resolution(adapter.limits());
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("ruviz direct 3d device"),
                required_features: wgpu::Features::empty(),
                required_limits,
                experimental_features: wgpu::ExperimentalFeatures::disabled(),
                memory_hints: wgpu::MemoryHints::Performance,
                trace: wgpu::Trace::Off,
            })
            .await
            .map_err(|error| PlottingError::GpuInitError {
                backend: format!("{:?}", adapter_info.backend),
                error: error.to_string(),
            })?;
        let device = Arc::new(device);
        let queue = Arc::new(queue);
        let lost = Arc::new(AtomicBool::new(false));
        let lost_callback = Arc::clone(&lost);
        device.set_device_lost_callback(move |reason, message| {
            lost_callback.store(true, Ordering::Release);
            log::error!("ruviz 3d GPU device lost ({reason:?}): {message}");
        });
        device.on_uncaptured_error(Arc::new(|error| {
            log::error!("ruviz 3d GPU validation error: {error}");
        }));

        let color_features = adapter.get_texture_format_features(COLOR_FORMAT);
        let depth_features = adapter.get_texture_format_features(DEPTH_FORMAT);
        let sample_count = if color_features
            .flags
            .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4)
            && color_features
                .flags
                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_RESOLVE)
            && depth_features
                .flags
                .contains(wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4)
        {
            4
        } else {
            1
        };

        Ok(Self {
            _instance: instance,
            _adapter: adapter,
            device,
            queue,
            sample_count,
            adapter_name: adapter_info.name,
            lost,
        })
    }

    pub(crate) fn adapter(&self) -> &wgpu::Adapter {
        &self._adapter
    }

    pub(crate) fn instance(&self) -> &wgpu::Instance {
        &self._instance
    }

    pub(crate) fn device(&self) -> &wgpu::Device {
        &self.device
    }

    pub(crate) fn ensure_available(&self) -> Result<()> {
        if self.is_lost() {
            Err(PlottingError::GpuNotAvailable(
                "the direct 3d wgpu device was lost".to_string(),
            ))
        } else {
            Ok(())
        }
    }

    pub(crate) fn is_lost(&self) -> bool {
        self.lost.load(Ordering::Acquire)
    }

    #[cfg(test)]
    pub(super) fn mark_lost_for_test(&self) {
        self.lost.store(true, Ordering::Release);
    }
}

pub(super) fn validate_format(
    adapter: &wgpu::Adapter,
    format: wgpu::TextureFormat,
    required_usage: wgpu::TextureUsages,
) -> Result<()> {
    let features = adapter.get_texture_format_features(format);
    if features.allowed_usages.contains(required_usage) {
        Ok(())
    } else {
        Err(PlottingError::UnsupportedGpuFeature(format!(
            "{format:?} does not support required usage {required_usage:?}"
        )))
    }
}