487 lines · 17.1 KB · Rust
RawDownload
#[cfg(not(target_family = "wasm"))]use anyhow::Context as _;#[cfg(not(target_family = "wasm"))]use gpui_util::ResultExt;use std::sync::Arc;use std::sync::atomic::{AtomicBool, Ordering};use wgpu::TextureFormat; pub struct WgpuContext {    pub instance: wgpu::Instance,    pub adapter: wgpu::Adapter,    pub device: Arc<wgpu::Device>,    pub queue: Arc<wgpu::Queue>,    dual_source_blending: bool,    color_texture_format: wgpu::TextureFormat,    device_lost: Arc<AtomicBool>,} #[derive(Clone, Copy)]pub struct CompositorGpuHint {    pub vendor_id: u32,    pub device_id: u32,} impl WgpuContext {    #[cfg(not(target_family = "wasm"))]    pub fn new(        instance: wgpu::Instance,        surface: &wgpu::Surface<'_>,        compositor_gpu: Option<CompositorGpuHint>,    ) -> anyhow::Result<Self> {        Self::new_with_options(instance, surface, compositor_gpu, false)    }     #[cfg(not(target_family = "wasm"))]    pub fn new_rejecting_software(        instance: wgpu::Instance,        surface: &wgpu::Surface<'_>,        compositor_gpu: Option<CompositorGpuHint>,    ) -> anyhow::Result<Self> {        Self::new_with_options(instance, surface, compositor_gpu, true)    }     #[cfg(not(target_family = "wasm"))]    fn new_with_options(        instance: wgpu::Instance,        surface: &wgpu::Surface<'_>,        compositor_gpu: Option<CompositorGpuHint>,        reject_software: bool,    ) -> anyhow::Result<Self> {        let device_id_filter = match std::env::var("ZED_DEVICE_ID") {            Ok(val) => parse_pci_id(&val)                .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")                .log_err(),            Err(std::env::VarError::NotPresent) => None,            err => {                err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")                    .log_err();                None            }        };         // Select an adapter by actually testing surface configuration with the real device.        // This is the only reliable way to determine compatibility on hybrid GPU systems.        let (adapter, device, queue, dual_source_blending, color_texture_format) =            gpui::block_on(Self::select_adapter_and_device(                &instance,                device_id_filter,                surface,                compositor_gpu.as_ref(),                reject_software,            ))?;         let device_lost = Arc::new(AtomicBool::new(false));        device.set_device_lost_callback({            let device_lost = Arc::clone(&device_lost);            move |reason, message| {                log::error!("wgpu device lost: reason={reason:?}, message={message}");                if reason != wgpu::DeviceLostReason::Destroyed {                    device_lost.store(true, Ordering::Relaxed);                }            }        });         log::info!(            "Selected GPU adapter: {:?} ({:?})",            adapter.get_info().name,            adapter.get_info().backend        );         Ok(Self {            instance,            adapter,            device: Arc::new(device),            queue: Arc::new(queue),            dual_source_blending,            color_texture_format,            device_lost,        })    }     #[cfg(target_family = "wasm")]    pub async fn new_web() -> anyhow::Result<Self> {        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {            backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,            flags: wgpu::InstanceFlags::default(),            backend_options: wgpu::BackendOptions::default(),            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),            display: None,        });         let adapter = instance            .request_adapter(&wgpu::RequestAdapterOptions {                power_preference: wgpu::PowerPreference::HighPerformance,                compatible_surface: None,                force_fallback_adapter: false,            })            .await            .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?;         log::info!(            "Selected GPU adapter: {:?} ({:?})",            adapter.get_info().name,            adapter.get_info().backend        );         let device_lost = Arc::new(AtomicBool::new(false));        let (device, queue, dual_source_blending, color_texture_format) =            Self::create_device(&adapter).await?;         Ok(Self {            instance,            adapter,            device: Arc::new(device),            queue: Arc::new(queue),            dual_source_blending,            color_texture_format,            device_lost,        })    }     async fn create_device(        adapter: &wgpu::Adapter,    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {        let dual_source_blending = adapter            .features()            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);         let mut required_features = wgpu::Features::empty();        if dual_source_blending {            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;        } else {            log::warn!(                "Dual-source blending not available on this GPU. \                Subpixel text antialiasing will be disabled."            );        }         let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;         let (device, queue) = adapter            .request_device(&wgpu::DeviceDescriptor {                label: Some("gpui_device"),                required_features,                required_limits: wgpu::Limits::downlevel_defaults()                    .using_resolution(adapter.limits())                    .using_alignment(adapter.limits()),                memory_hints: wgpu::MemoryHints::MemoryUsage,                trace: wgpu::Trace::Off,                experimental_features: wgpu::ExperimentalFeatures::disabled(),            })            .await            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;         Ok((            device,            queue,            dual_source_blending,            color_atlas_texture_format,        ))    }     #[cfg(not(target_family = "wasm"))]    pub fn instance(display: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) -> wgpu::Instance {        wgpu::Instance::new(wgpu::InstanceDescriptor {            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,            flags: wgpu::InstanceFlags::default(),            backend_options: wgpu::BackendOptions::default(),            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),            display: Some(display),        })    }     pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {        let caps = surface.get_capabilities(&self.adapter);        if caps.formats.is_empty() {            let info = self.adapter.get_info();            anyhow::bail!(                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \                 display surface for this window.",                info.name,                info.backend,                info.device,            );        }        Ok(())    }     /// Select an adapter and create a device, testing that the surface can actually be configured.    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where    /// adapters may report surface compatibility via get_capabilities() but fail when actually    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the    /// Wayland compositor runs on the Intel GPU).    #[cfg(not(target_family = "wasm"))]    async fn select_adapter_and_device(        instance: &wgpu::Instance,        device_id_filter: Option<u32>,        surface: &wgpu::Surface<'_>,        compositor_gpu: Option<&CompositorGpuHint>,        reject_software: bool,    ) -> anyhow::Result<(        wgpu::Adapter,        wgpu::Device,        wgpu::Queue,        bool,        TextureFormat,    )> {        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;         if adapters.is_empty() {            anyhow::bail!("No GPU adapters found");        }         if let Some(device_id) = device_id_filter {            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);        }         // Sort adapters into a single priority order. Tiers (from highest to lowest):        //        // 1. ZED_DEVICE_ID match — explicit user override        // 2. Compositor GPU match — the GPU the display server is rendering on        // 3. Device type (Discrete > Integrated > Other > Virtual > Cpu).        //    "Other" ranks above "Virtual" because OpenGL seems to count as "Other".        // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.        adapters.sort_by_key(|adapter| {            let info = adapter.get_info();             // Backends like OpenGL report device=0 for all adapters, so            // device-based matching is only meaningful when non-zero.            let device_known = info.device != 0;             let user_override: u8 = match device_id_filter {                Some(id) if device_known && info.device == id => 0,                _ => 1,            };             let compositor_match: u8 = match compositor_gpu {                Some(hint)                    if device_known                        && info.vendor == hint.vendor_id                        && info.device == hint.device_id =>                {                    0                }                _ => 1,            };             let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu {                4            } else {                match info.device_type {                    wgpu::DeviceType::DiscreteGpu => 0,                    wgpu::DeviceType::IntegratedGpu => 1,                    wgpu::DeviceType::Other => 2,                    wgpu::DeviceType::VirtualGpu => 3,                    wgpu::DeviceType::Cpu => 4,                }            };             let backend_priority: u8 = match info.backend {                wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,                _ => 1,            };             (                user_override,                compositor_match,                type_priority,                backend_priority,            )        });         // Log all available adapters (in sorted order)        log::info!("Found {} GPU adapter(s):", adapters.len());        for adapter in &adapters {            let info = adapter.get_info();            log::info!(                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",                info.name,                info.vendor,                info.device,                info.backend,                info.device_type,            );        }         // Test each adapter by creating a device and configuring the surface        for adapter in adapters {            let info = adapter.get_info();             if reject_software && info.device_type == wgpu::DeviceType::Cpu {                log::info!(                    "Skipping software renderer: {} ({:?})",                    info.name,                    info.backend                );                continue;            }             log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);             match Self::try_adapter_with_surface(&adapter, surface).await {                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {                    log::info!(                        "Selected GPU (passed configuration test): {} ({:?})",                        info.name,                        info.backend                    );                    return Ok((                        adapter,                        device,                        queue,                        dual_source_blending,                        color_atlas_texture_format,                    ));                }                Err(e) => {                    log::info!(                        "  Adapter {} ({:?}) failed: {}, trying next...",                        info.name,                        info.backend,                        e                    );                }            }        }         anyhow::bail!("No GPU adapter found that can configure the display surface")    }     /// Try to use an adapter with a surface by creating a device and testing configuration.    /// Returns the device and queue if successful, allowing them to be reused.    #[cfg(not(target_family = "wasm"))]    async fn try_adapter_with_surface(        adapter: &wgpu::Adapter,        surface: &wgpu::Surface<'_>,    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {        let caps = surface.get_capabilities(adapter);        if caps.formats.is_empty() {            anyhow::bail!("no compatible surface formats");        }        if caps.alpha_modes.is_empty() {            anyhow::bail!("no compatible alpha modes");        }         let (device, queue, dual_source_blending, color_atlas_texture_format) =            Self::create_device(adapter).await?;        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);         let test_config = wgpu::SurfaceConfiguration {            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,            format: caps.formats[0],            width: 64,            height: 64,            present_mode: wgpu::PresentMode::Fifo,            desired_maximum_frame_latency: 2,            alpha_mode: caps.alpha_modes[0],            view_formats: vec![],        };         surface.configure(&device, &test_config);         let error = error_scope.pop().await;        if let Some(e) = error {            anyhow::bail!("surface configuration failed: {e}");        }         Ok((            device,            queue,            dual_source_blending,            color_atlas_texture_format,        ))    }     fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);        if bgra_features.allowed_usages.contains(required_usages) {            return Ok(wgpu::TextureFormat::Bgra8Unorm);        }         let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);        if rgba_features.allowed_usages.contains(required_usages) {            let info = adapter.get_info();            log::warn!(                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \                 falling back to Rgba8Unorm atlas textures.",                info.name,                info.backend,                required_usages,            );            return Ok(wgpu::TextureFormat::Rgba8Unorm);        }         let info = adapter.get_info();        Err(anyhow::anyhow!(            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \             Rgba8Unorm allowed usages: {:?}.",            info.name,            info.backend,            info.device,            required_usages,            bgra_features.allowed_usages,            rgba_features.allowed_usages,        ))    }    pub fn supports_dual_source_blending(&self) -> bool {        self.dual_source_blending    }     pub fn color_texture_format(&self) -> wgpu::TextureFormat {        self.color_texture_format    }     /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).    /// When this returns true, the context should be recreated.    pub fn device_lost(&self) -> bool {        self.device_lost.load(Ordering::Relaxed)    }     /// Returns a clone of the device_lost flag for sharing with renderers.    pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {        Arc::clone(&self.device_lost)    }} #[cfg(not(target_family = "wasm"))]fn parse_pci_id(id: &str) -> anyhow::Result<u32> {    let mut id = id.trim();     if id.starts_with("0x") || id.starts_with("0X") {        id = &id[2..];    }    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());    let is_4_chars = id.len() == 4;    anyhow::ensure!(        is_4_chars && is_hex_string,        "Expected a 4 digit PCI ID in hexadecimal format"    );     u32::from_str_radix(id, 16).context("parsing PCI ID as hex")} #[cfg(test)]mod tests {    use super::parse_pci_id;     #[test]    fn test_parse_device_id() {        assert!(parse_pci_id("0xABCD").is_ok());        assert!(parse_pci_id("ABCD").is_ok());        assert!(parse_pci_id("abcd").is_ok());        assert!(parse_pci_id("1234").is_ok());        assert!(parse_pci_id("123").is_err());        assert_eq!(            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),        );         assert_eq!(            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),        );    }}