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