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