Skip to main content

trueno/registry/
wgpu_probe.rs

1//! wgpu factory: every adapter is an entry; a software rasteriser is listed as
2//! `NoBackend`, never silently used as a GPU; the Metal adapter on Apple silicon
3//! appears here with `transport = metal` (lane 2: a transport, not a peer kind).
4use super::{Api, BackendEntry, BackendFactory, BackendKind, MemKind, Reason, Source, Status};
5
6/// Discovers wgpu adapters.
7pub struct WgpuFactory;
8
9impl BackendFactory for WgpuFactory {
10    fn kind(&self) -> BackendKind {
11        BackendKind::Wgpu
12    }
13
14    fn discover(&self) -> Vec<BackendEntry> {
15        let adapters = crate::backends::gpu::runtime::block_on(async {
16            let instance = crate::backends::gpu::shared_instance();
17            instance
18                .enumerate_adapters(crate::backends::gpu::gpu_backends())
19                .iter()
20                .map(|a| (a.get_info(), a.limits()))
21                .collect::<Vec<_>>()
22        });
23        if adapters.is_empty() {
24            return vec![BackendEntry::unavailable(
25                BackendKind::Wgpu,
26                Api::Wgpu,
27                Source::CompiledIn,
28                Reason::NoDevice,
29            )];
30        }
31        adapters.iter().enumerate().map(|(i, (info, limits))| entry(i, info, limits)).collect()
32    }
33}
34
35/// PCI vendor id -> vendor name. `"unknown"` for anything not in the table.
36fn vendor_name(vendor_id: u32) -> &'static str {
37    match vendor_id {
38        0x10de => "NVIDIA",
39        0x1002 => "AMD",
40        0x8086 => "Intel",
41        0x106b => "Apple",
42        _ => "unknown",
43    }
44}
45
46/// wgpu backend -> the transport string `apr devices` prints (lane 2).
47fn transport_name(backend: wgpu::Backend) -> &'static str {
48    match backend {
49        wgpu::Backend::Vulkan => "vulkan",
50        wgpu::Backend::Metal => "metal",
51        wgpu::Backend::Dx12 => "dx12",
52        wgpu::Backend::Gl => "gl",
53        wgpu::Backend::BrowserWebGpu => "webgpu",
54        wgpu::Backend::Noop => "noop",
55    }
56}
57
58/// Device type -> (`device_type` string, status). A software rasteriser is
59/// `Unavailable(NoBackend)`: listed, never used as a GPU.
60fn classify(device_type: wgpu::DeviceType, name: &str) -> (&'static str, Status) {
61    match device_type {
62        wgpu::DeviceType::DiscreteGpu => ("discrete-gpu", Status::Ready),
63        wgpu::DeviceType::IntegratedGpu => ("integrated-gpu", Status::Ready),
64        wgpu::DeviceType::VirtualGpu => ("virtual-gpu", Status::Ready),
65        wgpu::DeviceType::Cpu => (
66            "software",
67            Status::Unavailable(Reason::NoBackend {
68                vendor: format!("software rasteriser ({name}) is not a GPU"),
69            }),
70        ),
71        wgpu::DeviceType::Other => ("other", Status::Ready),
72    }
73}
74
75/// Unified memory: Metal, or any integrated part. wgpu exposes no VRAM figure;
76/// `max_buffer_size` is an allocation cap, so it is the working-set limit here
77/// and `mem_total` stays unknown.
78fn mem_kind(
79    backend: wgpu::Backend,
80    device_type: wgpu::DeviceType,
81    limits: &wgpu::Limits,
82) -> MemKind {
83    let unified = matches!(backend, wgpu::Backend::Metal)
84        || matches!(device_type, wgpu::DeviceType::IntegratedGpu);
85    if unified {
86        MemKind::Unified { working_set_limit: Some(limits.max_buffer_size) }
87    } else {
88        MemKind::Discrete
89    }
90}
91
92#[allow(clippy::cast_possible_truncation)]
93fn entry(i: usize, info: &wgpu::AdapterInfo, limits: &wgpu::Limits) -> BackendEntry {
94    let vendor = vendor_name(info.vendor);
95    let (device_type, status) = classify(info.device_type, &info.name);
96    BackendEntry {
97        kind: BackendKind::Wgpu,
98        api: Api::Wgpu,
99        device_index: Some(i as u32),
100        device_uid: Some(super::device_uid(vendor, &info.name)),
101        device_name: info.name.clone(),
102        vendor: vendor.to_string(),
103        vendor_id: if vendor == "unknown" { None } else { Some(info.vendor) },
104        device_type: device_type.to_string(),
105        mem_total: None,
106        mem_free: None,
107        mem_kind: mem_kind(info.backend, info.device_type, limits),
108        compute_class: None,
109        caps: vec![format!("max_buffer_size={}", limits.max_buffer_size)],
110        source: Source::CompiledIn,
111        status,
112        transport: Some(transport_name(info.backend).to_string()),
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn vendor_table_names_the_four_and_unknown_for_the_rest() {
122        assert_eq!(vendor_name(0x10de), "NVIDIA");
123        assert_eq!(vendor_name(0x1002), "AMD");
124        assert_eq!(vendor_name(0x8086), "Intel");
125        assert_eq!(vendor_name(0x106b), "Apple");
126        assert_eq!(vendor_name(0x0000), "unknown");
127        assert_eq!(vendor_name(0xffff), "unknown");
128    }
129
130    #[test]
131    fn transport_table_is_total_over_wgpu_backends() {
132        assert_eq!(transport_name(wgpu::Backend::Vulkan), "vulkan");
133        assert_eq!(transport_name(wgpu::Backend::Metal), "metal");
134        assert_eq!(transport_name(wgpu::Backend::Dx12), "dx12");
135        assert_eq!(transport_name(wgpu::Backend::Gl), "gl");
136        assert_eq!(transport_name(wgpu::Backend::BrowserWebGpu), "webgpu");
137        assert_eq!(transport_name(wgpu::Backend::Noop), "noop");
138    }
139
140    #[test]
141    fn software_rasteriser_is_listed_but_never_ready() {
142        let (ty, status) = classify(wgpu::DeviceType::Cpu, "llvmpipe");
143        assert_eq!(ty, "software");
144        match status {
145            Status::Unavailable(Reason::NoBackend { vendor }) => {
146                assert!(vendor.contains("llvmpipe"), "names the adapter: {vendor}");
147            }
148            other => panic!("software rasteriser must be Unavailable(NoBackend), got {other:?}"),
149        }
150        for (dt, want) in [
151            (wgpu::DeviceType::DiscreteGpu, "discrete-gpu"),
152            (wgpu::DeviceType::IntegratedGpu, "integrated-gpu"),
153            (wgpu::DeviceType::VirtualGpu, "virtual-gpu"),
154            (wgpu::DeviceType::Other, "other"),
155        ] {
156            let (ty, status) = classify(dt, "x");
157            assert_eq!(ty, want);
158            assert!(matches!(status, Status::Ready), "{want} is Ready");
159        }
160    }
161
162    #[test]
163    fn unified_memory_is_metal_or_integrated_and_carries_the_buffer_cap() {
164        let mut limits = wgpu::Limits::default();
165        limits.max_buffer_size = 4096;
166        for (b, dt) in [
167            (wgpu::Backend::Metal, wgpu::DeviceType::DiscreteGpu),
168            (wgpu::Backend::Vulkan, wgpu::DeviceType::IntegratedGpu),
169        ] {
170            match mem_kind(b, dt, &limits) {
171                MemKind::Unified { working_set_limit } => assert_eq!(working_set_limit, Some(4096)),
172                other => panic!("{b:?}/{dt:?} must be Unified, got {other:?}"),
173            }
174        }
175        assert!(matches!(
176            mem_kind(wgpu::Backend::Vulkan, wgpu::DeviceType::DiscreteGpu, &limits),
177            MemKind::Discrete
178        ));
179    }
180}