henad_compute/runtime_info.rs
1//! Host and adapter facts, gathered once at startup for a host to show and a sweep to record.
2
3/// Host facts, available with or without an adapter.
4#[derive(Debug, Clone)]
5pub struct HostInfo {
6 /// Operating system name as in `std::env::consts::OS`, and `browser` on wasm32, where that constant is empty.
7 pub os: &'static str,
8 /// CPU architecture as in `std::env::consts::ARCH`.
9 pub arch: &'static str,
10 /// Number of logical CPUs, `None` where the platform cannot report it.
11 pub logical_cpus: Option<usize>,
12 /// Size of rayon's pool, the number of workers a CPU model runs on.
13 pub worker_threads: Option<usize>,
14}
15
16impl HostInfo {
17 /// Collects the host's facts.
18 ///
19 /// Call it once the thread pool exists. Asking rayon first would build the pool with rayon's own
20 /// defaults, and in a browser those request threads that the pool cannot spawn.
21 pub fn collect() -> Self {
22 Self {
23 os: os_name(std::env::consts::OS),
24 arch: std::env::consts::ARCH,
25 logical_cpus: logical_cpus(),
26 worker_threads: Some(rayon::current_num_threads()),
27 }
28 }
29}
30
31/// Returns `os`, or `browser` for the empty name that wasm32-unknown-unknown reports. Only a browser runs that target
32/// here.
33fn os_name(os: &'static str) -> &'static str {
34 if os.is_empty() { "browser" } else { os }
35}
36
37#[cfg(not(target_arch = "wasm32"))]
38fn logical_cpus() -> Option<usize> {
39 std::thread::available_parallelism()
40 .ok()
41 .map(std::num::NonZeroUsize::get)
42}
43
44/// Returns the number of logical CPUs that the browser reports, since `available_parallelism` is unsupported on wasm.
45#[cfg(target_arch = "wasm32")]
46fn logical_cpus() -> Option<usize> {
47 let cores = web_sys::window()?.navigator().hardware_concurrency();
48 (cores >= 1.0).then_some(cores as usize)
49}
50
51/// Host and adapter facts for one device.
52#[derive(Debug, Clone)]
53pub struct RuntimeInfo {
54 /// Facts about the host.
55 pub host: HostInfo,
56 /// Adapter the device came from.
57 pub adapter: wgpu::AdapterInfo,
58 /// Limits the device was created with, after `gpu::limits::raise`.
59 pub granted: wgpu::Limits,
60 /// Limits the adapter would have allowed, so a gap is headroom left unclaimed.
61 pub available: wgpu::Limits,
62 /// Whether the device granted `TIMESTAMP_QUERY`.
63 pub timestamp_query: bool,
64 /// Whether a vertex shader can read a storage buffer. Drawing network edges requires this.
65 pub vertex_storage: bool,
66}
67
68impl RuntimeInfo {
69 /// Collects the facts of `adapter`, `device` and the host. Call it once the thread pool exists, as
70 /// [`HostInfo::collect`] says.
71 pub fn collect(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Self {
72 Self {
73 host: HostInfo::collect(),
74 adapter: adapter.get_info(),
75 granted: device.limits(),
76 available: adapter.limits(),
77 timestamp_query: device.features().contains(wgpu::Features::TIMESTAMP_QUERY),
78 vertex_storage: adapter
79 .get_downlevel_capabilities()
80 .flags
81 .contains(wgpu::DownlevelFlags::VERTEX_STORAGE),
82 }
83 }
84
85 /// Longest side of a display texture on this device, after Henad's own cap.
86 pub fn display_cap(&self) -> u32 {
87 crate::display_scale::MAX_DISPLAY_DIM.min(self.granted.max_texture_dimension_2d)
88 }
89}
90
91/// The adapter's fitness for Henad's workload.
92#[derive(Clone, Copy, PartialEq, Eq, Debug)]
93pub enum GpuVerdict {
94 /// A discrete GPU.
95 Capable,
96 /// Any other GPU, which might be anything from a low-end integrated GPU to something like an M4 Pro.
97 Uncertain,
98 /// A CPU adapter, with no GPU behind it.
99 Absent,
100}
101
102/// Returns whether the adapter can run compute shaders. Every backend but `Gl` can, since WebGL2 has no compute stage.
103pub fn supports_compute(info: &wgpu::AdapterInfo) -> bool {
104 info.backend != wgpu::Backend::Gl
105}
106
107/// Returns the adapter's [`GpuVerdict`].
108///
109/// `DeviceType` describes memory topology and says nothing of speed, so only `DiscreteGpu` counts as
110/// [`GpuVerdict::Capable`].
111pub fn classify_adapter(info: &wgpu::AdapterInfo) -> GpuVerdict {
112 if info.device_type == wgpu::DeviceType::Cpu {
113 GpuVerdict::Absent
114 } else if info.device_type == wgpu::DeviceType::DiscreteGpu {
115 GpuVerdict::Capable
116 } else {
117 GpuVerdict::Uncertain
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::{HostInfo, os_name};
124
125 /// wasm32-unknown-unknown reports an empty name. Left alone, the System tab reads " (wasm32)".
126 #[test]
127 fn an_unnamed_os_reads_as_browser() {
128 assert_eq!(os_name(""), "browser");
129 assert_eq!(os_name("macos"), "macos");
130 assert!(!HostInfo::collect().os.is_empty());
131 }
132}