1#![cfg_attr(docsrs, feature(doc_cfg))]
22#![warn(missing_docs)]
23#![recursion_limit = "256"]
26
27henad_compute::include_shaders!();
28
29mod icons;
30mod init;
31#[cfg(not(target_arch = "wasm32"))]
32mod native;
33mod options;
34mod sim_runner;
35mod state;
36mod ui;
37#[cfg(target_arch = "wasm32")]
38mod web;
39
40use eframe::egui_wgpu;
41use egui_dock::{DockArea, DockState, Style};
42
43use crate::init::{setup_custom_fonts, setup_custom_styles};
44
45#[cfg(not(target_arch = "wasm32"))]
46pub use crate::native::{results_folder, run_native};
47#[cfg(not(target_arch = "wasm32"))]
48pub use crate::options::AppError;
49#[cfg(target_arch = "wasm32")]
50pub use crate::options::WebStartError;
51pub use crate::options::{AppOpening, AppOptions};
52pub use crate::state::OpenAt;
53#[cfg(target_arch = "wasm32")]
54pub use crate::web::{init_web_logger, start_web};
55
56use crate::sim_runner::SimRunner;
57use crate::state::AppState;
58use crate::ui::dock::{Tab, default_dock_state, focus_tab};
59use henad_compute::fault::{FaultSink, install_panic_hook};
60use henad_compute::runner::CAN_SPAWN_THREADS;
61use henad_compute::runtime_info::{RuntimeInfo, supports_compute};
62#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
64pub use wasm_bindgen_rayon::init_thread_pool;
65
66#[cfg(any(all(target_arch = "wasm32", target_feature = "atomics"), test))]
71pub(crate) fn requested_threads(search: &str, available: usize) -> usize {
72 let available = available.max(1);
73 search
74 .trim_start_matches('?')
75 .split('&')
76 .find_map(|pair| pair.strip_prefix("threads="))
77 .and_then(|value| value.parse::<usize>().ok())
78 .unwrap_or(available)
79 .clamp(1, available)
80}
81
82use crate::state::FrameTimings;
83
84const SWEEP_REPAINT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
86
87struct HenadApp {
88 dock: DockState<Tab>,
89 state: AppState,
90}
91
92impl HenadApp {
93 fn new(cc: &eframe::CreationContext<'_>, options: AppOptions) -> Self {
99 install_panic_hook();
100
101 let render_state = &cc
102 .wgpu_render_state
103 .as_ref()
104 .expect("wgpu_render_state must exist for wgpu backend");
105
106 let adapter_info = render_state.adapter.get_info();
107 log::info!("{}", egui_wgpu::adapter_info_summary(&adapter_info));
108
109 let render_ctx = henad_compute::gpu::GpuContext::new(
112 render_state.device.clone(),
113 render_state.queue.clone(),
114 render_state.target_format,
115 FaultSink::new(),
116 );
117
118 let gpu_ctx = supports_compute(&adapter_info).then(|| render_ctx.clone());
119
120 setup_custom_fonts(&cc.egui_ctx);
121 setup_custom_styles(&cc.egui_ctx);
122
123 let AppOptions {
124 models,
125 product,
126 opening,
127 thread_pool_note,
128 } = options;
129 let mut state = AppState::new(
130 cc.egui_ctx.clone(),
131 models,
132 product,
133 render_ctx,
134 gpu_ctx,
135 RuntimeInfo::collect(&render_state.adapter, &render_state.device),
136 );
137 state.thread_pool_note = thread_pool_note;
138 if let Some(opening) = opening {
139 state.open(opening);
140 }
141 Self {
142 dock: default_dock_state(),
143 state,
144 }
145 }
146}
147
148impl eframe::App for HenadApp {
149 fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
150 let dt = ctx.input(|i| f64::from(i.unstable_dt));
152 if let Some(thread) = &mut self.state.sim_thread {
153 thread.update(dt);
154 }
155 ui::sweep::update(&mut self.state, dt);
156
157 let fresh = self.state.sim_thread.as_mut().and_then(SimRunner::take_snapshot);
159 if let Some(snap) = fresh {
160 let passed_target = self
163 .state
164 .run_to_target
165 .filter(|&target| snap.tick > target && self.state.selection_is_loaded());
166 if self.state.run_to_target.is_some_and(|target| snap.tick >= target) {
167 self.state.run_to_target = None;
168 }
169 if let Some(history) = &mut self.state.stats_history {
172 history.push_entries(&snap.stats, snap.tick);
173 }
174 self.state.record(&snap);
175 if let Some(previous) = self.state.snapshot.replace(snap)
177 && let Some(thread) = &mut self.state.sim_thread
178 {
179 thread.recycle(previous);
180 }
181 if let Some(target) = passed_target {
182 self.state.run_to(target);
183 }
184 }
185
186 if let Some(fault) = self.state.render_ctx.faults.take() {
187 self.state.report_fault(fault);
188 }
189
190 self.state.poll_saves();
191 self.state.poll_opens();
192 ui::results::poll(ctx, &mut self.state);
193 self.state.poll_capture();
194
195 let frame_drives_sim = !CAN_SPAWN_THREADS;
198 if self.state.sim_running || (frame_drives_sim && self.state.run_to_target.is_some()) {
199 ctx.request_repaint_after(std::time::Duration::ZERO);
200 }
201 if frame_drives_sim && self.state.sweep.is_stepping() {
203 ctx.request_repaint_after(std::time::Duration::ZERO);
204 } else if self.state.sweep.is_running() {
205 ctx.request_repaint_after(SWEEP_REPAINT_INTERVAL);
206 }
207 }
208
209 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
210 let frame_start = web_time::Instant::now();
211 self.state.timings.frame_render_ms = 0.0;
212
213 ui::menu_bar::menu_bar_panel(ui, &mut self.dock, &mut self.state);
214 ui::fault::fault_modal(ui.ctx(), &mut self.state);
215 ui::about::about_modal(ui.ctx(), &mut self.state);
216
217 let mut dock_style = Style::from_egui(ui.style());
218 dock_style.tab_bar.corner_radius = egui::CornerRadius::ZERO;
220 dock_style.tab_bar.show_scroll_bar_on_overflow = false;
222 DockArea::new(&mut self.dock)
223 .style(dock_style)
224 .show_close_buttons(true)
225 .show_leaf_close_all_buttons(true)
226 .show_inside(ui, &mut self.state);
227
228 if let Some(tab) = self.state.focus_request.take() {
230 focus_tab(&mut self.dock, tab);
231 ui.ctx().request_repaint();
232 }
233
234 let total_ms = frame_start.elapsed().as_secs_f64() * 1000.0;
236 let render_ms = self.state.timings.frame_render_ms;
237 FrameTimings::update_ema(&mut self.state.timings.render_ms, render_ms);
238 FrameTimings::update_ema(&mut self.state.timings.ui_ms, (total_ms - render_ms).max(0.0));
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::requested_threads;
245
246 #[test]
247 fn an_absent_query_asks_for_every_core() {
248 assert_eq!(requested_threads("", 14), 14);
249 assert_eq!(requested_threads("?debug=1", 14), 14);
250 }
251
252 #[test]
253 fn threads_one_is_how_a_single_threaded_run_is_asked_for() {
254 assert_eq!(requested_threads("?threads=1", 14), 1);
255 assert_eq!(requested_threads("?foo=a&threads=4", 14), 4);
256 }
257
258 #[test]
260 fn a_request_is_clamped_to_the_host() {
261 assert_eq!(requested_threads("?threads=99", 14), 14);
262 assert_eq!(requested_threads("?threads=0", 14), 1);
263 assert_eq!(requested_threads("?threads=nonsense", 14), 14);
264 }
265}