Skip to main content

rgpui_wgpu/
wgpu_context.rs

1#[cfg(not(target_family = "wasm"))]
2use anyhow::Context as _;
3#[cfg(not(target_family = "wasm"))]
4use rgpui::ResultExt;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use wgpu::TextureFormat;
8
9/// wgpu GPU 上下文,包含设备、队列和适配器等信息
10pub struct WgpuContext {
11    pub instance: wgpu::Instance,
12    pub adapter: wgpu::Adapter,
13    pub device: Arc<wgpu::Device>,
14    pub queue: Arc<wgpu::Queue>,
15    dual_source_blending: bool,
16    color_texture_format: wgpu::TextureFormat,
17    device_lost: Arc<AtomicBool>,
18}
19
20/// 合成器 GPU 提示,用于适配器选择
21#[derive(Clone, Copy)]
22pub struct CompositorGpuHint {
23    pub vendor_id: u32,
24    pub device_id: u32,
25}
26
27impl WgpuContext {
28    #[cfg(not(target_family = "wasm"))]
29    pub fn new(
30        instance: wgpu::Instance,
31        surface: &wgpu::Surface<'_>,
32        compositor_gpu: Option<CompositorGpuHint>,
33    ) -> anyhow::Result<Self> {
34        Self::new_with_options(instance, surface, compositor_gpu, false)
35    }
36
37    #[cfg(not(target_family = "wasm"))]
38    pub fn new_rejecting_software(
39        instance: wgpu::Instance,
40        surface: &wgpu::Surface<'_>,
41        compositor_gpu: Option<CompositorGpuHint>,
42    ) -> anyhow::Result<Self> {
43        Self::new_with_options(instance, surface, compositor_gpu, true)
44    }
45
46    #[cfg(not(target_family = "wasm"))]
47    fn new_with_options(
48        instance: wgpu::Instance,
49        surface: &wgpu::Surface<'_>,
50        compositor_gpu: Option<CompositorGpuHint>,
51        reject_software: bool,
52    ) -> anyhow::Result<Self> {
53        let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
54            Ok(val) => parse_pci_id(&val)
55                .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
56                .log_err(),
57            Err(std::env::VarError::NotPresent) => None,
58            err => {
59                err.context("读取 `ZED_DEVICE_ID` 环境变量失败").log_err();
60                None
61            }
62        };
63
64        // 通过实际测试表面配置来选择适配器。
65        // 这是在混合 GPU 系统上确定兼容性的唯一可靠方法。
66        let (adapter, device, queue, dual_source_blending, color_texture_format) =
67            rgpui::block_on(Self::select_adapter_and_device(
68                &instance,
69                device_id_filter,
70                surface,
71                compositor_gpu.as_ref(),
72                reject_software,
73            ))?;
74
75        let device_lost = Arc::new(AtomicBool::new(false));
76        device.set_device_lost_callback({
77            let device_lost = Arc::clone(&device_lost);
78            move |reason, message| {
79                log::error!("wgpu device lost: reason={reason:?}, message={message}");
80                if reason != wgpu::DeviceLostReason::Destroyed {
81                    device_lost.store(true, Ordering::Relaxed);
82                }
83            }
84        });
85
86        log::info!(
87            "Selected GPU adapter: {:?} ({:?})",
88            adapter.get_info().name,
89            adapter.get_info().backend
90        );
91        // 同步注册到核心 GPU 信息表(Inspector“运行”卡片读取;重复调用保持首次值)。
92        {
93            let info = adapter.get_info();
94            rgpui::set_gpu_info(info.name, format!("{:?}", info.backend));
95        }
96
97        let device = Arc::new(device);
98        let queue = Arc::new(queue);
99
100        // 注册到共享上下文,供 rgpui-3d 等第三方渲染器复用
101        #[cfg(not(target_family = "wasm"))]
102        crate::shared_context::register(instance.clone(), device.clone(), queue.clone());
103
104        Ok(Self {
105            instance,
106            adapter,
107            device,
108            queue,
109            dual_source_blending,
110            color_texture_format,
111            device_lost,
112        })
113    }
114
115    #[cfg(target_family = "wasm")]
116    /// 为 Web/WASM 平台创建 wgpu 上下文
117    pub async fn new_web() -> anyhow::Result<Self> {
118        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
119            backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
120            flags: wgpu::InstanceFlags::default(),
121            backend_options: wgpu::BackendOptions::default(),
122            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
123            display: None,
124        });
125
126        let adapter = match instance
127            .request_adapter(&wgpu::RequestAdapterOptions {
128                power_preference: wgpu::PowerPreference::HighPerformance,
129                compatible_surface: None,
130                force_fallback_adapter: false,
131                apply_limit_buckets: false,
132            })
133            .await
134        {
135            Ok(adapter) => adapter,
136            Err(_) => {
137                log::warn!("未找到高性能 GPU 适配器,尝试使用回退适配器(软件渲染)");
138                instance
139                    .request_adapter(&wgpu::RequestAdapterOptions {
140                        power_preference: wgpu::PowerPreference::LowPower,
141                        compatible_surface: None,
142                        force_fallback_adapter: true,
143                        apply_limit_buckets: false,
144                    })
145                    .await
146                    .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?
147            }
148        };
149
150        log::info!(
151            "Selected GPU adapter: {:?} ({:?})",
152            adapter.get_info().name,
153            adapter.get_info().backend
154        );
155        // 同上:注册到核心 GPU 信息表(WASM 平台同样上报)。
156        {
157            let info = adapter.get_info();
158            rgpui::set_gpu_info(info.name, format!("{:?}", info.backend));
159        }
160
161        let device_lost = Arc::new(AtomicBool::new(false));
162        let (device, queue, dual_source_blending, color_texture_format) =
163            Self::create_device(&adapter).await?;
164
165        let device = Arc::new(device);
166        let queue = Arc::new(queue);
167
168        // WASM 平台不注册共享上下文(wgpu WebGPU 后端不满足 Send+Sync)
169
170        Ok(Self {
171            instance,
172            adapter,
173            device,
174            queue,
175            dual_source_blending,
176            color_texture_format,
177            device_lost,
178        })
179    }
180
181    /// 创建 wgpu 设备和队列
182    async fn create_device(
183        adapter: &wgpu::Adapter,
184    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
185        let dual_source_blending = adapter
186            .features()
187            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
188
189        let mut required_features = wgpu::Features::empty();
190        if dual_source_blending {
191            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
192        } else {
193            log::warn!(
194                "Dual-source blending not available on this GPU. \
195                Subpixel text antialiasing will be disabled."
196            );
197        }
198
199        let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
200
201        let (device, queue) = adapter
202            .request_device(&wgpu::DeviceDescriptor {
203                label: Some("gpui_device"),
204                required_features,
205                required_limits: wgpu::Limits::downlevel_defaults()
206                    .using_resolution(adapter.limits())
207                    .using_alignment(adapter.limits()),
208                memory_hints: wgpu::MemoryHints::MemoryUsage,
209                trace: wgpu::Trace::Off,
210                experimental_features: wgpu::ExperimentalFeatures::disabled(),
211            })
212            .await
213            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
214
215        Ok((
216            device,
217            queue,
218            dual_source_blending,
219            color_atlas_texture_format,
220        ))
221    }
222
223    #[cfg(not(target_family = "wasm"))]
224    pub fn instance(display: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) -> wgpu::Instance {
225        wgpu::Instance::new(wgpu::InstanceDescriptor {
226            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
227            flags: wgpu::InstanceFlags::default(),
228            backend_options: wgpu::BackendOptions::default(),
229            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
230            display: Some(display),
231        })
232    }
233
234    /// 检查适配器是否与表面兼容
235    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
236        let caps = surface.get_capabilities(&self.adapter);
237        if caps.formats.is_empty() {
238            let info = self.adapter.get_info();
239            anyhow::bail!(
240                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
241                 display surface for this window.",
242                info.name,
243                info.backend,
244                info.device,
245            );
246        }
247        Ok(())
248    }
249
250    /// 选择适配器并创建设备,测试表面是否可以实际配置。
251    /// 这是在混合 GPU 系统上确定兼容性的唯一可靠方法,
252    /// 适配器可能通过 get_capabilities() 报告表面兼容性,
253    /// 但在实际配置时失败(例如 NVIDIA 报告支持 Vulkan Wayland,
254    /// 但因为 Wayland 合成器运行在 Intel GPU 上而失败)。
255    #[cfg(not(target_family = "wasm"))]
256    async fn select_adapter_and_device(
257        instance: &wgpu::Instance,
258        device_id_filter: Option<u32>,
259        surface: &wgpu::Surface<'_>,
260        compositor_gpu: Option<&CompositorGpuHint>,
261        reject_software: bool,
262    ) -> anyhow::Result<(
263        wgpu::Adapter,
264        wgpu::Device,
265        wgpu::Queue,
266        bool,
267        TextureFormat,
268    )> {
269        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
270
271        if adapters.is_empty() {
272            anyhow::bail!("No GPU adapters found");
273        }
274
275        if let Some(device_id) = device_id_filter {
276            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
277        }
278
279        // 将适配器按单一优先级排序。层级(从高到低):
280        //
281        // 1. ZED_DEVICE_ID 匹配 — 用户显式覆盖
282        // 2. 合成器 GPU 匹配 — 显示服务器正在渲染的 GPU
283        // 3. 设备类型(Discrete > Integrated > Other > Virtual > Cpu)。
284        //    "Other" 排在 "Virtual" 之上,因为 OpenGL 似乎被归类为 "Other"。
285        // 4. 后端 — 优先选择 Vulkan/Metal/Dx12 而非 GL 等。
286        adapters.sort_by_key(|adapter| {
287            let info = adapter.get_info();
288
289            // OpenGL 等后端对所有适配器报告 device=0,
290            // 因此基于设备的匹配仅在非零时有意义。
291            let device_known = info.device != 0;
292
293            let user_override: u8 = match device_id_filter {
294                Some(id) if device_known && info.device == id => 0,
295                _ => 1,
296            };
297
298            let compositor_match: u8 = match compositor_gpu {
299                Some(hint)
300                    if device_known
301                        && info.vendor == hint.vendor_id
302                        && info.device == hint.device_id =>
303                {
304                    0
305                }
306                _ => 1,
307            };
308
309            let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu {
310                4
311            } else {
312                match info.device_type {
313                    wgpu::DeviceType::DiscreteGpu => 0,
314                    wgpu::DeviceType::IntegratedGpu => 1,
315                    wgpu::DeviceType::Other => 2,
316                    wgpu::DeviceType::VirtualGpu => 3,
317                    wgpu::DeviceType::Cpu => 4,
318                }
319            };
320
321            let backend_priority: u8 = match info.backend {
322                wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
323                _ => 1,
324            };
325
326            (
327                user_override,
328                compositor_match,
329                type_priority,
330                backend_priority,
331            )
332        });
333
334        // 记录所有可用的适配器(按排序顺序)
335        log::info!("Found {} GPU adapter(s):", adapters.len());
336        for adapter in &adapters {
337            let info = adapter.get_info();
338            log::info!(
339                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
340                info.name,
341                info.vendor,
342                info.device,
343                info.backend,
344                info.device_type,
345            );
346        }
347
348        // 测试每个适配器,创建设备并配置表面
349        for adapter in adapters {
350            let info = adapter.get_info();
351
352            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
353                log::info!(
354                    "Skipping software renderer: {} ({:?})",
355                    info.name,
356                    info.backend
357                );
358                continue;
359            }
360
361            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
362
363            match Self::try_adapter_with_surface(&adapter, surface).await {
364                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
365                    log::info!(
366                        "Selected GPU (passed configuration test): {} ({:?})",
367                        info.name,
368                        info.backend
369                    );
370                    return Ok((
371                        adapter,
372                        device,
373                        queue,
374                        dual_source_blending,
375                        color_atlas_texture_format,
376                    ));
377                }
378                Err(e) => {
379                    log::info!(
380                        "  Adapter {} ({:?}) failed: {}, trying next...",
381                        info.name,
382                        info.backend,
383                        e
384                    );
385                }
386            }
387        }
388
389        anyhow::bail!("No GPU adapter found that can configure the display surface")
390    }
391
392    /// 尝试使用适配器与表面,创建设备并测试配置。
393    /// 成功时返回设备和队列,以便复用。
394    #[cfg(not(target_family = "wasm"))]
395    async fn try_adapter_with_surface(
396        adapter: &wgpu::Adapter,
397        surface: &wgpu::Surface<'_>,
398    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
399        let caps = surface.get_capabilities(adapter);
400        if caps.formats.is_empty() {
401            anyhow::bail!("no compatible surface formats");
402        }
403        if caps.alpha_modes.is_empty() {
404            anyhow::bail!("no compatible alpha modes");
405        }
406
407        let (device, queue, dual_source_blending, color_atlas_texture_format) =
408            Self::create_device(adapter).await?;
409        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
410
411        let test_config = wgpu::SurfaceConfiguration {
412            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
413            format: caps.formats[0],
414            width: 64,
415            height: 64,
416            present_mode: wgpu::PresentMode::Fifo,
417            desired_maximum_frame_latency: 2,
418            alpha_mode: caps.alpha_modes[0],
419            view_formats: vec![],
420            color_space: wgpu::SurfaceColorSpace::Auto,
421        };
422
423        surface.configure(&device, &test_config);
424
425        let error = error_scope.pop().await;
426        if let Some(e) = error {
427            anyhow::bail!("surface configuration failed: {e}");
428        }
429
430        Ok((
431            device,
432            queue,
433            dual_source_blending,
434            color_atlas_texture_format,
435        ))
436    }
437
438    /// 选择适合的彩色纹理格式
439    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
440        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
441        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
442        if bgra_features.allowed_usages.contains(required_usages) {
443            return Ok(wgpu::TextureFormat::Bgra8Unorm);
444        }
445
446        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
447        if rgba_features.allowed_usages.contains(required_usages) {
448            let info = adapter.get_info();
449            log::warn!(
450                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
451                 falling back to Rgba8Unorm atlas textures.",
452                info.name,
453                info.backend,
454                required_usages,
455            );
456            return Ok(wgpu::TextureFormat::Rgba8Unorm);
457        }
458
459        let info = adapter.get_info();
460        Err(anyhow::anyhow!(
461            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
462             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
463             Rgba8Unorm allowed usages: {:?}.",
464            info.name,
465            info.backend,
466            info.device,
467            required_usages,
468            bgra_features.allowed_usages,
469            rgba_features.allowed_usages,
470        ))
471    }
472    /// 检查是否支持双源混合
473    pub fn supports_dual_source_blending(&self) -> bool {
474        self.dual_source_blending
475    }
476
477    /// 获取彩色纹理格式
478    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
479        self.color_texture_format
480    }
481
482    /// 返回 GPU 设备是否丢失(例如由于驱动崩溃、挂起/恢复)。
483    /// 当返回 true 时,需要重新创建上下文。
484    pub fn device_lost(&self) -> bool {
485        self.device_lost.load(Ordering::Relaxed)
486    }
487
488    /// 返回 device_lost 标志的克隆,用于与渲染器共享
489    pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {
490        Arc::clone(&self.device_lost)
491    }
492}
493
494#[cfg(not(target_family = "wasm"))]
495/// 解析 PCI 设备 ID 字符串为 u32
496fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
497    let mut id = id.trim();
498
499    if id.starts_with("0x") || id.starts_with("0X") {
500        id = &id[2..];
501    }
502    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
503    let is_4_chars = id.len() == 4;
504    anyhow::ensure!(
505        is_4_chars && is_hex_string,
506        "Expected a 4 digit PCI ID in hexadecimal format"
507    );
508
509    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
510}
511
512#[cfg(test)]
513mod tests {
514    use super::parse_pci_id;
515
516    #[test]
517    fn test_parse_device_id() {
518        assert!(parse_pci_id("0xABCD").is_ok());
519        assert!(parse_pci_id("ABCD").is_ok());
520        assert!(parse_pci_id("abcd").is_ok());
521        assert!(parse_pci_id("1234").is_ok());
522        assert!(parse_pci_id("123").is_err());
523        assert_eq!(
524            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
525            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
526        );
527
528        assert_eq!(
529            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
530            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
531        );
532    }
533}