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) =
 56            pollster::block_on(Self::create_device(&adapter))?;
 57
 58        Ok(Self {
 59            instance,
 60            adapter,
 61            device: Arc::new(device),
 62            queue: Arc::new(queue),
 63            dual_source_blending,
 64        })
 65    }
 66
 67    #[cfg(target_family = "wasm")]
 68    pub async fn new_web() -> anyhow::Result<Self> {
 69        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
 70            backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
 71            flags: wgpu::InstanceFlags::default(),
 72            backend_options: wgpu::BackendOptions::default(),
 73            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
 74        });
 75
 76        let adapter = instance
 77            .request_adapter(&wgpu::RequestAdapterOptions {
 78                power_preference: wgpu::PowerPreference::HighPerformance,
 79                compatible_surface: None,
 80                force_fallback_adapter: false,
 81            })
 82            .await
 83            .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?;
 84
 85        log::info!(
 86            "Selected GPU adapter: {:?} ({:?})",
 87            adapter.get_info().name,
 88            adapter.get_info().backend
 89        );
 90
 91        let (device, queue, dual_source_blending) = Self::create_device(&adapter).await?;
 92
 93        Ok(Self {
 94            instance,
 95            adapter,
 96            device: Arc::new(device),
 97            queue: Arc::new(queue),
 98            dual_source_blending,
 99        })
100    }
101
102    async fn create_device(
103        adapter: &wgpu::Adapter,
104    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
105        let dual_source_blending = adapter
106            .features()
107            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
108
109        let mut required_features = wgpu::Features::empty();
110        if dual_source_blending {
111            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
112        } else {
113            log::warn!(
114                "Dual-source blending not available on this GPU. \
115                Subpixel text antialiasing will be disabled."
116            );
117        }
118
119        let (device, queue) = adapter
120            .request_device(&wgpu::DeviceDescriptor {
121                label: Some("gpui_device"),
122                required_features,
123                required_limits: wgpu::Limits::downlevel_defaults(),
124                memory_hints: wgpu::MemoryHints::MemoryUsage,
125                trace: wgpu::Trace::Off,
126                experimental_features: wgpu::ExperimentalFeatures::disabled(),
127            })
128            .await
129            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
130
131        Ok((device, queue, dual_source_blending))
132    }
133
134    #[cfg(not(target_family = "wasm"))]
135    pub fn instance() -> wgpu::Instance {
136        wgpu::Instance::new(&wgpu::InstanceDescriptor {
137            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
138            flags: wgpu::InstanceFlags::default(),
139            backend_options: wgpu::BackendOptions::default(),
140            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
141        })
142    }
143
144    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
145        let caps = surface.get_capabilities(&self.adapter);
146        if caps.formats.is_empty() {
147            let info = self.adapter.get_info();
148            anyhow::bail!(
149                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
150                 display surface for this window.",
151                info.name,
152                info.backend,
153                info.device,
154            );
155        }
156        Ok(())
157    }
158
159    #[cfg(not(target_family = "wasm"))]
160    async fn select_adapter(
161        instance: &wgpu::Instance,
162        device_id_filter: Option<u32>,
163        compatible_surface: Option<&wgpu::Surface<'_>>,
164    ) -> anyhow::Result<wgpu::Adapter> {
165        if let Some(device_id) = device_id_filter {
166            let adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
167
168            if adapters.is_empty() {
169                anyhow::bail!("No GPU adapters found");
170            }
171
172            let mut non_matching_adapter_infos: Vec<wgpu::AdapterInfo> = Vec::new();
173
174            for adapter in adapters.into_iter() {
175                let info = adapter.get_info();
176                if info.device == device_id {
177                    if let Some(surface) = compatible_surface {
178                        let caps = surface.get_capabilities(&adapter);
179                        if caps.formats.is_empty() {
180                            log::warn!(
181                                "GPU matching ZED_DEVICE_ID={:#06x} ({}) is not compatible \
182                                 with the display surface. Falling back to auto-selection.",
183                                device_id,
184                                info.name,
185                            );
186                            break;
187                        }
188                    }
189                    log::info!(
190                        "Found GPU matching ZED_DEVICE_ID={:#06x}: {}",
191                        device_id,
192                        info.name
193                    );
194                    return Ok(adapter);
195                } else {
196                    non_matching_adapter_infos.push(info);
197                }
198            }
199
200            log::warn!(
201                "No compatible GPU found matching ZED_DEVICE_ID={:#06x}. Available devices:",
202                device_id
203            );
204
205            for info in &non_matching_adapter_infos {
206                log::warn!(
207                    "  - {} (device_id={:#06x}, backend={})",
208                    info.name,
209                    info.device,
210                    info.backend
211                );
212            }
213        }
214
215        instance
216            .request_adapter(&wgpu::RequestAdapterOptions {
217                power_preference: wgpu::PowerPreference::HighPerformance,
218                compatible_surface,
219                force_fallback_adapter: false,
220            })
221            .await
222            .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))
223    }
224
225    pub fn supports_dual_source_blending(&self) -> bool {
226        self.dual_source_blending
227    }
228}
229
230#[cfg(not(target_family = "wasm"))]
231fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
232    let mut id = id.trim();
233
234    if id.starts_with("0x") || id.starts_with("0X") {
235        id = &id[2..];
236    }
237    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
238    let is_4_chars = id.len() == 4;
239    anyhow::ensure!(
240        is_4_chars && is_hex_string,
241        "Expected a 4 digit PCI ID in hexadecimal format"
242    );
243
244    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
245}
246
247#[cfg(test)]
248mod tests {
249    use super::parse_pci_id;
250
251    #[test]
252    fn test_parse_device_id() {
253        assert!(parse_pci_id("0xABCD").is_ok());
254        assert!(parse_pci_id("ABCD").is_ok());
255        assert!(parse_pci_id("abcd").is_ok());
256        assert!(parse_pci_id("1234").is_ok());
257        assert!(parse_pci_id("123").is_err());
258        assert_eq!(
259            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
260            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
261        );
262
263        assert_eq!(
264            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
265            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
266        );
267    }
268}