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