wgpu_context.rs

  1use anyhow::Context as _;
  2use std::sync::Arc;
  3use util::ResultExt;
  4
  5pub struct WgpuContext {
  6    pub instance: wgpu::Instance,
  7    pub adapter: wgpu::Adapter,
  8    pub device: Arc<wgpu::Device>,
  9    pub queue: Arc<wgpu::Queue>,
 10    dual_source_blending: bool,
 11}
 12
 13impl WgpuContext {
 14    pub fn new(instance: wgpu::Instance, surface: &wgpu::Surface<'_>) -> anyhow::Result<Self> {
 15        let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
 16            Ok(val) => parse_pci_id(&val)
 17                .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
 18                .log_err(),
 19            Err(std::env::VarError::NotPresent) => None,
 20            err => {
 21                err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
 22                    .log_err();
 23                None
 24            }
 25        };
 26
 27        let adapter = smol::block_on(Self::select_adapter(
 28            &instance,
 29            device_id_filter,
 30            Some(surface),
 31        ))?;
 32
 33        let caps = surface.get_capabilities(&adapter);
 34        if caps.formats.is_empty() {
 35            let info = adapter.get_info();
 36            anyhow::bail!(
 37                "No adapter compatible with the display surface could be found. \
 38                 Best candidate {:?} (backend={:?}, device={:#06x}) reports no \
 39                 supported surface formats.",
 40                info.name,
 41                info.backend,
 42                info.device,
 43            );
 44        }
 45
 46        log::info!(
 47            "Selected GPU adapter: {:?} ({:?})",
 48            adapter.get_info().name,
 49            adapter.get_info().backend
 50        );
 51
 52        let (device, queue, dual_source_blending) = Self::create_device(&adapter)?;
 53
 54        Ok(Self {
 55            instance,
 56            adapter,
 57            device: Arc::new(device),
 58            queue: Arc::new(queue),
 59            dual_source_blending,
 60        })
 61    }
 62
 63    pub fn instance() -> wgpu::Instance {
 64        wgpu::Instance::new(&wgpu::InstanceDescriptor {
 65            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
 66            flags: wgpu::InstanceFlags::default(),
 67            backend_options: wgpu::BackendOptions::default(),
 68            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
 69        })
 70    }
 71
 72    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
 73        let caps = surface.get_capabilities(&self.adapter);
 74        if caps.formats.is_empty() {
 75            let info = self.adapter.get_info();
 76            anyhow::bail!(
 77                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
 78                 display surface for this window.",
 79                info.name,
 80                info.backend,
 81                info.device,
 82            );
 83        }
 84        Ok(())
 85    }
 86
 87    fn create_device(adapter: &wgpu::Adapter) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
 88        let dual_source_blending_available = adapter
 89            .features()
 90            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
 91
 92        let mut required_features = wgpu::Features::empty();
 93        if dual_source_blending_available {
 94            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
 95        } else {
 96            log::warn!(
 97                "Dual-source blending not available on this GPU. \
 98                Subpixel text antialiasing will be disabled."
 99            );
100        }
101
102        let (device, queue) = smol::block_on(
103            adapter.request_device(&wgpu::DeviceDescriptor {
104                label: Some("gpui_device"),
105                required_features,
106                required_limits: wgpu::Limits::downlevel_defaults()
107                    .using_resolution(adapter.limits())
108                    .using_alignment(adapter.limits()),
109                memory_hints: wgpu::MemoryHints::MemoryUsage,
110                trace: wgpu::Trace::Off,
111                experimental_features: wgpu::ExperimentalFeatures::disabled(),
112            }),
113        )
114        .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
115
116        Ok((device, queue, dual_source_blending_available))
117    }
118
119    async fn select_adapter(
120        instance: &wgpu::Instance,
121        device_id_filter: Option<u32>,
122        compatible_surface: Option<&wgpu::Surface<'_>>,
123    ) -> anyhow::Result<wgpu::Adapter> {
124        if let Some(device_id) = device_id_filter {
125            let adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
126
127            if adapters.is_empty() {
128                anyhow::bail!("No GPU adapters found");
129            }
130
131            let mut non_matching_adapter_infos: Vec<wgpu::AdapterInfo> = Vec::new();
132
133            for adapter in adapters.into_iter() {
134                let info = adapter.get_info();
135                if info.device == device_id {
136                    if let Some(surface) = compatible_surface {
137                        let caps = surface.get_capabilities(&adapter);
138                        if caps.formats.is_empty() {
139                            log::warn!(
140                                "GPU matching ZED_DEVICE_ID={:#06x} ({}) is not compatible \
141                                 with the display surface. Falling back to auto-selection.",
142                                device_id,
143                                info.name,
144                            );
145                            break;
146                        }
147                    }
148                    log::info!(
149                        "Found GPU matching ZED_DEVICE_ID={:#06x}: {}",
150                        device_id,
151                        info.name
152                    );
153                    return Ok(adapter);
154                } else {
155                    non_matching_adapter_infos.push(info);
156                }
157            }
158
159            log::warn!(
160                "No compatible GPU found matching ZED_DEVICE_ID={:#06x}. Available devices:",
161                device_id
162            );
163
164            for info in &non_matching_adapter_infos {
165                log::warn!(
166                    "  - {} (device_id={:#06x}, backend={})",
167                    info.name,
168                    info.device,
169                    info.backend
170                );
171            }
172        }
173
174        instance
175            .request_adapter(&wgpu::RequestAdapterOptions {
176                power_preference: wgpu::PowerPreference::HighPerformance,
177                compatible_surface,
178                force_fallback_adapter: false,
179            })
180            .await
181            .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))
182    }
183
184    pub fn supports_dual_source_blending(&self) -> bool {
185        self.dual_source_blending
186    }
187}
188
189fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
190    let mut id = id.trim();
191
192    if id.starts_with("0x") || id.starts_with("0X") {
193        id = &id[2..];
194    }
195    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
196    let is_4_chars = id.len() == 4;
197    anyhow::ensure!(
198        is_4_chars && is_hex_string,
199        "Expected a 4 digit PCI ID in hexadecimal format"
200    );
201
202    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
203}
204
205#[cfg(test)]
206mod tests {
207    use super::parse_pci_id;
208
209    #[test]
210    fn test_parse_device_id() {
211        assert!(parse_pci_id("0xABCD").is_ok());
212        assert!(parse_pci_id("ABCD").is_ok());
213        assert!(parse_pci_id("abcd").is_ok());
214        assert!(parse_pci_id("1234").is_ok());
215        assert!(parse_pci_id("123").is_err());
216        assert_eq!(
217            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
218            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
219        );
220
221        assert_eq!(
222            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
223            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
224        );
225    }
226}