Skip to main content

brep_app/
diagnostics.rs

1//! What this build is running on — the ONE record of it.
2//!
3//! Two places need to say which renderer is in use: the Info window (so a user
4//! can read it) and the problem report (so a triager can read it about a user
5//! who is not there to ask). They must never be able to disagree, so there is
6//! one collector — this — and both read the SAME [`Diagnostics`] instance. The
7//! window renders [`Diagnostics::rows`]; the report embeds
8//! [`Diagnostics::report_text`], which is those very rows joined. A row added
9//! here appears in both, and neither can carry a value the other does not.
10//!
11//! # Why it is captured, never probed
12//!
13//! The renderer is CHOSEN once, at startup, by eframe: `wgpu`'s
14//! `new_instance_with_webgpu_detection` drops `BROWSER_WEBGPU` from the backend
15//! set when the browser has no WebGPU adapter (or the page is not a secure
16//! context), so the instance falls through to WebGL2. That choice is a fact of
17//! the running session, and the adapter that made it is the one drawing every
18//! frame — so we read it where eframe hands it to us
19//! ([`Diagnostics::from_render_state`], called from `BrepApp::new_with`) and
20//! keep it.
21//!
22//! Asking the question a SECOND time later — a fresh `request_adapter`, say —
23//! would be a different question with its own answer: an adapter enumeration at
24//! report time can succeed where the startup one failed (or pick differently),
25//! and a diagnostic that disagrees with what is on screen is worse than none at
26//! all, precisely in the case someone is filing a bug about the renderer.
27//!
28//! # What it may contain
29//!
30//! Hardware and build facts only: the backend, the adapter, the one adapter
31//! limit that bounds what the viewport can allocate, the app version and the
32//! target. The report already sends a screenshot, the model and an optional
33//! email; this adds nothing that identifies a PERSON, and it must stay that way
34//! — a report's description is served publicly by the reports endpoint.
35
36use eframe::egui_wgpu::RenderState;
37
38/// The startup facts about this session's renderer and build.
39///
40/// Constructed ONCE (`BrepApp::new_with`) and then read; there is no refresh,
41/// because nothing it records can change without restarting the app.
42#[derive(Debug, Clone, PartialEq)]
43pub struct Diagnostics {
44    /// The backend in the words a user would use: `WebGPU`, `WebGL2`, `Vulkan`,
45    /// `Metal`, `Direct3D 12`, `OpenGL`. This is THE answer to "is it WebGPU or
46    /// the WebGL fallback".
47    renderer: String,
48    /// wgpu's own name for the same backend (`BrowserWebGpu`, `Gl`, …), kept
49    /// beside the friendly one so a report can be matched against wgpu's docs
50    /// and issues without guessing at the translation.
51    backend: String,
52    /// The adapter's self-reported name (`NVIDIA GeForce RTX 3060`, `llvmpipe
53    /// (LLVM 15.0.7, 256 bits)`, `ANGLE (Intel, …)`).
54    adapter: String,
55    /// `DiscreteGpu` / `IntegratedGpu` / `Cpu` / `Other` — a software rasteriser
56    /// answers a whole class of "why is it slow" reports on its own.
57    device_type: String,
58    /// Driver name + version, when the backend reports one. The browser
59    /// backends usually do not, so this row is omitted when empty rather than
60    /// shown blank.
61    driver: String,
62    /// The largest 2D texture edge the ADAPTER supports. The viewport allocates
63    /// its offscreen colour/depth targets at the viewport's pixel size, so this
64    /// is the hardware ceiling on how large a window the 3D view can be drawn
65    /// into.
66    ///
67    /// The adapter's, deliberately, not the device's: eframe requests a FIXED
68    /// `max_texture_dimension_2d: 8192` for every device it creates (see
69    /// `egui_wgpu::WgpuSetupCreateNew`), so `device.limits()` would read 8192 in
70    /// every report ever filed and say nothing about the machine. The rest of
71    /// that requested set is `Limits::default()` — or `downlevel_webgl2_defaults`
72    /// on the GL backend — and is a constant of the eframe version for the same
73    /// reason, which is why no other limit is reported. (`max_buffer_size` is
74    /// worse than constant: wgpu-hal's GLES adapter reports `i32::MAX` for it,
75    /// a sentinel, on exactly the WebGL2 path this diagnostic exists for.)
76    max_texture_dimension_2d: u32,
77    /// The application version (`CARGO_PKG_VERSION`), so a report names the
78    /// build it came from.
79    version: &'static str,
80    /// `wasm32 (browser)` or `native <os>/<arch>` — which of the two shells this
81    /// is, since they take different code paths into the same renderer.
82    platform: String,
83}
84
85impl Diagnostics {
86    /// Capture the facts from the render state eframe built — the adapter and
87    /// device that are ACTUALLY drawing this session.
88    pub fn from_render_state(render_state: &RenderState) -> Self {
89        let info = render_state.adapter.get_info();
90        let limits = render_state.adapter.limits();
91        Self {
92            renderer: renderer_name(info.backend).to_string(),
93            backend: format!("{:?}", info.backend),
94            adapter: info.name.clone(),
95            device_type: format!("{:?}", info.device_type),
96            driver: match (info.driver.trim(), info.driver_info.trim()) {
97                ("", "") => String::new(),
98                ("", detail) => detail.to_string(),
99                (name, "") => name.to_string(),
100                (name, detail) => format!("{name} {detail}"),
101            },
102            max_texture_dimension_2d: limits.max_texture_dimension_2d,
103            version: env!("CARGO_PKG_VERSION"),
104            platform: platform(),
105        }
106    }
107
108    /// The rows, in display order: `(label, value)`. The Info window draws
109    /// these, [`Self::report_text`] joins these, and
110    /// [`Self::json`] keys off these — so no reader can show a field another
111    /// one lacks.
112    ///
113    /// A row whose value is empty is dropped (a browser adapter reports no
114    /// driver string, and a blank row reads as a missing value rather than an
115    /// absent one).
116    pub fn rows(&self) -> Vec<(&'static str, String)> {
117        let rows = [
118            ("Renderer", self.renderer.clone()),
119            ("wgpu backend", self.backend.clone()),
120            ("Adapter", self.adapter.clone()),
121            ("Device type", self.device_type.clone()),
122            ("Driver", self.driver.clone()),
123            ("Max texture size", format!("{} px", self.max_texture_dimension_2d)),
124            ("App version", self.version.to_string()),
125            ("Platform", self.platform.clone()),
126        ];
127        rows.into_iter().filter(|(_, value)| !value.is_empty()).collect()
128    }
129
130    /// The rows as one plain-text block, `label: value` per line, under a
131    /// heading. This is what a problem report carries; the window shows the
132    /// same rows in a grid.
133    pub fn report_text(&self) -> String {
134        let mut text = String::from("--- diagnostics ---");
135        for (label, value) in self.rows() {
136            text.push_str(&format!("\n{label}: {value}"));
137        }
138        text
139    }
140
141    /// The rows as a JSON object, for the automation surface (`diagnostics`)
142    /// and the `__brepDiagnostics` state blob.
143    pub fn json(&self) -> serde_json::Value {
144        serde_json::Value::Object(
145            self.rows()
146                .into_iter()
147                .map(|(label, value)| (label.to_string(), serde_json::Value::String(value)))
148                .collect(),
149        )
150    }
151
152    /// The renderer in a user's words (`WebGPU` / `WebGL2` / `Vulkan` / …).
153    pub fn renderer(&self) -> &str {
154        &self.renderer
155    }
156
157    /// One line naming the adapter and how it is reached — what the MCP banner
158    /// prints for the host that started the app. It reads this rather than
159    /// formatting `AdapterInfo` a second time, so the server's banner and the
160    /// user's Info window cannot name different hardware.
161    pub fn adapter_line(&self) -> String {
162        format!("{} ({}, {})", self.adapter, self.backend, self.device_type)
163    }
164}
165
166/// What a user calls the backend wgpu picked. `Gl` is the interesting one: in
167/// the browser it IS WebGL2 (the fallback path), natively it is desktop GL.
168fn renderer_name(backend: wgpu::Backend) -> &'static str {
169    match backend {
170        wgpu::Backend::BrowserWebGpu => "WebGPU",
171        wgpu::Backend::Gl if cfg!(target_arch = "wasm32") => "WebGL2",
172        wgpu::Backend::Gl => "OpenGL",
173        wgpu::Backend::Vulkan => "Vulkan",
174        wgpu::Backend::Metal => "Metal",
175        wgpu::Backend::Dx12 => "Direct3D 12",
176        wgpu::Backend::Noop => "none (no-op device)",
177    }
178}
179
180/// Which shell this is. The browser build has no `std::env::consts` worth
181/// printing (`unknown` OS), so it names itself instead.
182fn platform() -> String {
183    #[cfg(target_arch = "wasm32")]
184    {
185        "wasm32 (browser)".to_string()
186    }
187    #[cfg(not(target_arch = "wasm32"))]
188    {
189        format!("native {}/{}", std::env::consts::OS, std::env::consts::ARCH)
190    }
191}
192
193// BREP private tests: 6f1b6a4c2d0e77a3