1use anyhow::Context as _;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use util::ResultExt;
5
6pub struct WgpuContext {
7 pub instance: wgpu::Instance,
8 pub adapter: wgpu::Adapter,
9 pub device: Arc<wgpu::Device>,
10 pub queue: Arc<wgpu::Queue>,
11 dual_source_blending: bool,
12 device_lost: Arc<AtomicBool>,
13}
14
15#[derive(Clone, Copy)]
16pub struct CompositorGpuHint {
17 pub vendor_id: u32,
18 pub device_id: u32,
19}
20
21impl WgpuContext {
22 pub fn new(
23 instance: wgpu::Instance,
24 surface: &wgpu::Surface<'_>,
25 compositor_gpu: Option<CompositorGpuHint>,
26 ) -> anyhow::Result<Self> {
27 let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
28 Ok(val) => parse_pci_id(&val)
29 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
30 .log_err(),
31 Err(std::env::VarError::NotPresent) => None,
32 err => {
33 err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
34 .log_err();
35 None
36 }
37 };
38
39 // Select an adapter by actually testing surface configuration with the real device.
40 // This is the only reliable way to determine compatibility on hybrid GPU systems.
41 let (adapter, device, queue, dual_source_blending) =
42 smol::block_on(Self::select_adapter_and_device(
43 &instance,
44 device_id_filter,
45 surface,
46 compositor_gpu.as_ref(),
47 ))?;
48
49 let device_lost = Arc::new(AtomicBool::new(false));
50 device.set_device_lost_callback({
51 let device_lost = Arc::clone(&device_lost);
52 move |reason, message| {
53 log::error!("wgpu device lost: reason={reason:?}, message={message}");
54 if reason != wgpu::DeviceLostReason::Destroyed {
55 device_lost.store(true, Ordering::Relaxed);
56 }
57 }
58 });
59
60 log::info!(
61 "Selected GPU adapter: {:?} ({:?})",
62 adapter.get_info().name,
63 adapter.get_info().backend
64 );
65
66 Ok(Self {
67 instance,
68 adapter,
69 device: Arc::new(device),
70 queue: Arc::new(queue),
71 dual_source_blending,
72 device_lost,
73 })
74 }
75
76 pub fn instance() -> wgpu::Instance {
77 wgpu::Instance::new(&wgpu::InstanceDescriptor {
78 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
79 flags: wgpu::InstanceFlags::default(),
80 backend_options: wgpu::BackendOptions::default(),
81 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
82 })
83 }
84
85 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
86 let caps = surface.get_capabilities(&self.adapter);
87 if caps.formats.is_empty() {
88 let info = self.adapter.get_info();
89 anyhow::bail!(
90 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
91 display surface for this window.",
92 info.name,
93 info.backend,
94 info.device,
95 );
96 }
97 Ok(())
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 /// Select an adapter and create a device, testing that the surface can actually be configured.
135 /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
136 /// adapters may report surface compatibility via get_capabilities() but fail when actually
137 /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
138 /// Wayland compositor runs on the Intel GPU).
139 async fn select_adapter_and_device(
140 instance: &wgpu::Instance,
141 device_id_filter: Option<u32>,
142 surface: &wgpu::Surface<'_>,
143 compositor_gpu: Option<&CompositorGpuHint>,
144 ) -> anyhow::Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, bool)> {
145 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
146
147 if adapters.is_empty() {
148 anyhow::bail!("No GPU adapters found");
149 }
150
151 if let Some(device_id) = device_id_filter {
152 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
153 }
154
155 // Sort adapters into a single priority order. Tiers (from highest to lowest):
156 //
157 // 1. ZED_DEVICE_ID match — explicit user override
158 // 2. Compositor GPU match — the GPU the display server is rendering on
159 // 3. Device type — WGPU HighPerformance order (Discrete > Integrated >
160 // Other > Virtual > Cpu). "Other" ranks above "Virtual" because
161 // backends like OpenGL may report real hardware as "Other".
162 // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
163 adapters.sort_by_key(|adapter| {
164 let info = adapter.get_info();
165
166 // Backends like OpenGL report device=0 for all adapters, so
167 // device-based matching is only meaningful when non-zero.
168 let device_known = info.device != 0;
169
170 let user_override: u8 = match device_id_filter {
171 Some(id) if device_known && info.device == id => 0,
172 _ => 1,
173 };
174
175 let compositor_match: u8 = match compositor_gpu {
176 Some(hint)
177 if device_known
178 && info.vendor == hint.vendor_id
179 && info.device == hint.device_id =>
180 {
181 0
182 }
183 _ => 1,
184 };
185
186 let type_priority: u8 = match info.device_type {
187 wgpu::DeviceType::DiscreteGpu => 0,
188 wgpu::DeviceType::IntegratedGpu => 1,
189 wgpu::DeviceType::Other => 2,
190 wgpu::DeviceType::VirtualGpu => 3,
191 wgpu::DeviceType::Cpu => 4,
192 };
193
194 let backend_priority: u8 = match info.backend {
195 wgpu::Backend::Vulkan => 0,
196 wgpu::Backend::Metal => 0,
197 wgpu::Backend::Dx12 => 0,
198 _ => 1,
199 };
200
201 (
202 user_override,
203 compositor_match,
204 type_priority,
205 backend_priority,
206 )
207 });
208
209 // Log all available adapters (in sorted order)
210 log::info!("Found {} GPU adapter(s):", adapters.len());
211 for adapter in &adapters {
212 let info = adapter.get_info();
213 log::info!(
214 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
215 info.name,
216 info.vendor,
217 info.device,
218 info.backend,
219 info.device_type,
220 );
221 }
222
223 // Test each adapter by creating a device and configuring the surface
224 for adapter in adapters {
225 let info = adapter.get_info();
226 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
227
228 match Self::try_adapter_with_surface(&adapter, surface).await {
229 Ok((device, queue, dual_source_blending)) => {
230 log::info!(
231 "Selected GPU (passed configuration test): {} ({:?})",
232 info.name,
233 info.backend
234 );
235 return Ok((adapter, device, queue, dual_source_blending));
236 }
237 Err(e) => {
238 log::info!(
239 " Adapter {} ({:?}) failed: {}, trying next...",
240 info.name,
241 info.backend,
242 e
243 );
244 }
245 }
246 }
247
248 anyhow::bail!("No GPU adapter found that can configure the display surface")
249 }
250
251 /// Try to use an adapter with a surface by creating a device and testing configuration.
252 /// Returns the device and queue if successful, allowing them to be reused.
253 async fn try_adapter_with_surface(
254 adapter: &wgpu::Adapter,
255 surface: &wgpu::Surface<'_>,
256 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool)> {
257 let caps = surface.get_capabilities(adapter);
258 if caps.formats.is_empty() {
259 anyhow::bail!("no compatible surface formats");
260 }
261 if caps.alpha_modes.is_empty() {
262 anyhow::bail!("no compatible alpha modes");
263 }
264
265 // Create the real device with full features
266 let (device, queue, dual_source_blending) = Self::create_device(adapter).await?;
267
268 // Use an error scope to capture any validation errors during configure
269 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
270
271 let test_config = wgpu::SurfaceConfiguration {
272 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
273 format: caps.formats[0],
274 width: 64,
275 height: 64,
276 present_mode: wgpu::PresentMode::Fifo,
277 desired_maximum_frame_latency: 2,
278 alpha_mode: caps.alpha_modes[0],
279 view_formats: vec![],
280 };
281
282 surface.configure(&device, &test_config);
283
284 // Check if there was a validation error
285 let error = error_scope.pop().await;
286 if let Some(e) = error {
287 anyhow::bail!("surface configuration failed: {e}");
288 }
289
290 Ok((device, queue, dual_source_blending))
291 }
292
293 pub fn supports_dual_source_blending(&self) -> bool {
294 self.dual_source_blending
295 }
296
297 /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
298 /// When this returns true, the context should be recreated.
299 pub fn device_lost(&self) -> bool {
300 self.device_lost.load(Ordering::Relaxed)
301 }
302
303 /// Returns a clone of the device_lost flag for sharing with renderers.
304 pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {
305 Arc::clone(&self.device_lost)
306 }
307}
308
309fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
310 let mut id = id.trim();
311
312 if id.starts_with("0x") || id.starts_with("0X") {
313 id = &id[2..];
314 }
315 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
316 let is_4_chars = id.len() == 4;
317 anyhow::ensure!(
318 is_4_chars && is_hex_string,
319 "Expected a 4 digit PCI ID in hexadecimal format"
320 );
321
322 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
323}
324
325#[cfg(test)]
326mod tests {
327 use super::parse_pci_id;
328
329 #[test]
330 fn test_parse_device_id() {
331 assert!(parse_pci_id("0xABCD").is_ok());
332 assert!(parse_pci_id("ABCD").is_ok());
333 assert!(parse_pci_id("abcd").is_ok());
334 assert!(parse_pci_id("1234").is_ok());
335 assert!(parse_pci_id("123").is_err());
336 assert_eq!(
337 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
338 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
339 );
340
341 assert_eq!(
342 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
343 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
344 );
345 }
346}