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
15#[cfg(not(target_family = "wasm"))]
16pub struct CompositorGpuHint {
17 pub vendor_id: u32,
18 pub device_id: u32,
19}
20
21impl WgpuContext {
22 #[cfg(not(target_family = "wasm"))]
23 pub fn new(
24 instance: wgpu::Instance,
25 surface: &wgpu::Surface<'_>,
26 compositor_gpu: Option<CompositorGpuHint>,
27 ) -> anyhow::Result<Self> {
28 let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
29 Ok(val) => parse_pci_id(&val)
30 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
31 .log_err(),
32 Err(std::env::VarError::NotPresent) => None,
33 err => {
34 err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
35 .log_err();
36 None
37 }
38 };
39
40 // Select an adapter by actually testing surface configuration with the real device.
41 // This is the only reliable way to determine compatibility on hybrid GPU systems.
42 let (adapter, device, queue, dual_source_blending) =
43 pollster::block_on(Self::select_adapter_and_device(
44 &instance,
45 device_id_filter,
46 surface,
47 compositor_gpu.as_ref(),
48 ))?;
49
50 log::info!(
51 "Selected GPU adapter: {:?} ({:?})",
52 adapter.get_info().name,
53 adapter.get_info().backend
54 );
55
56 Ok(Self {
57 instance,
58 adapter,
59 device: Arc::new(device),
60 queue: Arc::new(queue),
61 dual_source_blending,
62 })
63 }
64
65 #[cfg(target_family = "wasm")]
66 pub async fn new_web() -> anyhow::Result<Self> {
67 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
68 backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
69 flags: wgpu::InstanceFlags::default(),
70 backend_options: wgpu::BackendOptions::default(),
71 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
72 });
73
74 let adapter = instance
75 .request_adapter(&wgpu::RequestAdapterOptions {
76 power_preference: wgpu::PowerPreference::HighPerformance,
77 compatible_surface: None,
78 force_fallback_adapter: false,
79 })
80 .await
81 .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?;
82
83 log::info!(
84 "Selected GPU adapter: {:?} ({:?})",
85 adapter.get_info().name,
86 adapter.get_info().backend
87 );
88
89 let (device, queue, dual_source_blending) = Self::create_device(&adapter).await?;
90
91 Ok(Self {
92 instance,
93 adapter,
94 device: Arc::new(device),
95 queue: Arc::new(queue),
96 dual_source_blending,
97 })
98 }
99
100 async fn create_device(
101 adapter: &wgpu::Adapter,
102 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
103 let dual_source_blending = adapter
104 .features()
105 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
106
107 let mut required_features = wgpu::Features::empty();
108 if dual_source_blending {
109 required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
110 } else {
111 log::warn!(
112 "Dual-source blending not available on this GPU. \
113 Subpixel text antialiasing will be disabled."
114 );
115 }
116
117 let (device, queue) = adapter
118 .request_device(&wgpu::DeviceDescriptor {
119 label: Some("gpui_device"),
120 required_features,
121 required_limits: wgpu::Limits::downlevel_defaults()
122 .using_resolution(adapter.limits())
123 .using_alignment(adapter.limits()),
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 /// Select an adapter and create a device, testing that the surface can actually be configured.
160 /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
161 /// adapters may report surface compatibility via get_capabilities() but fail when actually
162 /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
163 /// Wayland compositor runs on the Intel GPU).
164 #[cfg(not(target_family = "wasm"))]
165 async fn select_adapter_and_device(
166 instance: &wgpu::Instance,
167 device_id_filter: Option<u32>,
168 surface: &wgpu::Surface<'_>,
169 compositor_gpu: Option<&CompositorGpuHint>,
170 ) -> anyhow::Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, bool)> {
171 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
172
173 if adapters.is_empty() {
174 anyhow::bail!("No GPU adapters found");
175 }
176
177 if let Some(device_id) = device_id_filter {
178 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
179 }
180
181 // Sort adapters into a single priority order. Tiers (from highest to lowest):
182 //
183 // 1. ZED_DEVICE_ID match — explicit user override
184 // 2. Compositor GPU match — the GPU the display server is rendering on
185 // 3. Device type — WGPU HighPerformance order (Discrete > Integrated >
186 // Other > Virtual > Cpu). "Other" ranks above "Virtual" because
187 // backends like OpenGL may report real hardware as "Other".
188 // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
189 adapters.sort_by_key(|adapter| {
190 let info = adapter.get_info();
191
192 // Backends like OpenGL report device=0 for all adapters, so
193 // device-based matching is only meaningful when non-zero.
194 let device_known = info.device != 0;
195
196 let user_override: u8 = match device_id_filter {
197 Some(id) if device_known && info.device == id => 0,
198 _ => 1,
199 };
200
201 let compositor_match: u8 = match compositor_gpu {
202 Some(hint)
203 if device_known
204 && info.vendor == hint.vendor_id
205 && info.device == hint.device_id =>
206 {
207 0
208 }
209 _ => 1,
210 };
211
212 let type_priority: u8 = match info.device_type {
213 wgpu::DeviceType::DiscreteGpu => 0,
214 wgpu::DeviceType::IntegratedGpu => 1,
215 wgpu::DeviceType::Other => 2,
216 wgpu::DeviceType::VirtualGpu => 3,
217 wgpu::DeviceType::Cpu => 4,
218 };
219
220 let backend_priority: u8 = match info.backend {
221 wgpu::Backend::Vulkan => 0,
222 wgpu::Backend::Metal => 0,
223 wgpu::Backend::Dx12 => 0,
224 _ => 1,
225 };
226
227 (
228 user_override,
229 compositor_match,
230 type_priority,
231 backend_priority,
232 )
233 });
234
235 // Log all available adapters (in sorted order)
236 log::info!("Found {} GPU adapter(s):", adapters.len());
237 for adapter in &adapters {
238 let info = adapter.get_info();
239 log::info!(
240 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
241 info.name,
242 info.vendor,
243 info.device,
244 info.backend,
245 info.device_type,
246 );
247 }
248
249 // Test each adapter by creating a device and configuring the surface
250 for adapter in adapters {
251 let info = adapter.get_info();
252 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
253
254 match Self::try_adapter_with_surface(&adapter, surface).await {
255 Ok((device, queue, dual_source_blending)) => {
256 log::info!(
257 "Selected GPU (passed configuration test): {} ({:?})",
258 info.name,
259 info.backend
260 );
261 return Ok((adapter, device, queue, dual_source_blending));
262 }
263 Err(e) => {
264 log::info!(
265 " Adapter {} ({:?}) failed: {}, trying next...",
266 info.name,
267 info.backend,
268 e
269 );
270 }
271 }
272 }
273
274 anyhow::bail!("No GPU adapter found that can configure the display surface")
275 }
276
277 /// Try to use an adapter with a surface by creating a device and testing configuration.
278 /// Returns the device and queue if successful, allowing them to be reused.
279 #[cfg(not(target_family = "wasm"))]
280 async fn try_adapter_with_surface(
281 adapter: &wgpu::Adapter,
282 surface: &wgpu::Surface<'_>,
283 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
284 let caps = surface.get_capabilities(adapter);
285 if caps.formats.is_empty() {
286 anyhow::bail!("no compatible surface formats");
287 }
288 if caps.alpha_modes.is_empty() {
289 anyhow::bail!("no compatible alpha modes");
290 }
291
292 // Create the real device with full features
293 let (device, queue, dual_source_blending) = Self::create_device(adapter).await?;
294
295 // Use an error scope to capture any validation errors during configure
296 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
297
298 let test_config = wgpu::SurfaceConfiguration {
299 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
300 format: caps.formats[0],
301 width: 64,
302 height: 64,
303 present_mode: wgpu::PresentMode::Fifo,
304 desired_maximum_frame_latency: 2,
305 alpha_mode: caps.alpha_modes[0],
306 view_formats: vec![],
307 };
308
309 surface.configure(&device, &test_config);
310
311 // Check if there was a validation error
312 let error = error_scope.pop().await;
313 if let Some(e) = error {
314 anyhow::bail!("surface configuration failed: {e}");
315 }
316
317 Ok((device, queue, dual_source_blending))
318 }
319
320 pub fn supports_dual_source_blending(&self) -> bool {
321 self.dual_source_blending
322 }
323}
324
325#[cfg(not(target_family = "wasm"))]
326fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
327 let mut id = id.trim();
328
329 if id.starts_with("0x") || id.starts_with("0X") {
330 id = &id[2..];
331 }
332 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
333 let is_4_chars = id.len() == 4;
334 anyhow::ensure!(
335 is_4_chars && is_hex_string,
336 "Expected a 4 digit PCI ID in hexadecimal format"
337 );
338
339 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
340}
341
342#[cfg(test)]
343mod tests {
344 use super::parse_pci_id;
345
346 #[test]
347 fn test_parse_device_id() {
348 assert!(parse_pci_id("0xABCD").is_ok());
349 assert!(parse_pci_id("ABCD").is_ok());
350 assert!(parse_pci_id("abcd").is_ok());
351 assert!(parse_pci_id("1234").is_ok());
352 assert!(parse_pci_id("123").is_err());
353 assert_eq!(
354 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
355 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
356 );
357
358 assert_eq!(
359 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
360 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
361 );
362 }
363}