wgpu_context.rs

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