Skip to main content

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}