#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![recursion_limit = "256"]
henad_compute::include_shaders!();
mod icons;
mod init;
#[cfg(not(target_arch = "wasm32"))]
mod native;
mod options;
mod sim_runner;
mod state;
mod ui;
#[cfg(target_arch = "wasm32")]
mod web;
use eframe::egui_wgpu;
use egui_dock::{DockArea, DockState, Style};
use crate::init::{setup_custom_fonts, setup_custom_styles};
#[cfg(not(target_arch = "wasm32"))]
pub use crate::native::{results_folder, run_native};
#[cfg(not(target_arch = "wasm32"))]
pub use crate::options::AppError;
#[cfg(target_arch = "wasm32")]
pub use crate::options::WebStartError;
pub use crate::options::{AppOpening, AppOptions};
pub use crate::state::OpenAt;
#[cfg(target_arch = "wasm32")]
pub use crate::web::{init_web_logger, start_web};
use crate::sim_runner::SimRunner;
use crate::state::AppState;
use crate::ui::dock::{Tab, default_dock_state, focus_tab};
use henad_compute::fault::{FaultSink, install_panic_hook};
use henad_compute::runner::CAN_SPAWN_THREADS;
use henad_compute::runtime_info::{RuntimeInfo, supports_compute};
#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
pub use wasm_bindgen_rayon::init_thread_pool;
#[cfg(any(all(target_arch = "wasm32", target_feature = "atomics"), test))]
pub(crate) fn requested_threads(search: &str, available: usize) -> usize {
let available = available.max(1);
search
.trim_start_matches('?')
.split('&')
.find_map(|pair| pair.strip_prefix("threads="))
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(available)
.clamp(1, available)
}
use crate::state::FrameTimings;
const SWEEP_REPAINT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
struct HenadApp {
dock: DockState<Tab>,
state: AppState,
}
impl HenadApp {
fn new(cc: &eframe::CreationContext<'_>, options: AppOptions) -> Self {
install_panic_hook();
let render_state = &cc
.wgpu_render_state
.as_ref()
.expect("wgpu_render_state must exist for wgpu backend");
let adapter_info = render_state.adapter.get_info();
log::info!("{}", egui_wgpu::adapter_info_summary(&adapter_info));
let render_ctx = henad_compute::gpu::GpuContext::new(
render_state.device.clone(),
render_state.queue.clone(),
render_state.target_format,
FaultSink::new(),
);
let gpu_ctx = supports_compute(&adapter_info).then(|| render_ctx.clone());
setup_custom_fonts(&cc.egui_ctx);
setup_custom_styles(&cc.egui_ctx);
let AppOptions {
models,
product,
opening,
thread_pool_note,
} = options;
let mut state = AppState::new(
cc.egui_ctx.clone(),
models,
product,
render_ctx,
gpu_ctx,
RuntimeInfo::collect(&render_state.adapter, &render_state.device),
);
state.thread_pool_note = thread_pool_note;
if let Some(opening) = opening {
state.open(opening);
}
Self {
dock: default_dock_state(),
state,
}
}
}
impl eframe::App for HenadApp {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let dt = ctx.input(|i| f64::from(i.unstable_dt));
if let Some(thread) = &mut self.state.sim_thread {
thread.update(dt);
}
ui::sweep::update(&mut self.state, dt);
let fresh = self.state.sim_thread.as_mut().and_then(SimRunner::take_snapshot);
if let Some(snap) = fresh {
let passed_target = self
.state
.run_to_target
.filter(|&target| snap.tick > target && self.state.selection_is_loaded());
if self.state.run_to_target.is_some_and(|target| snap.tick >= target) {
self.state.run_to_target = None;
}
if let Some(history) = &mut self.state.stats_history {
history.push_entries(&snap.stats, snap.tick);
}
self.state.record(&snap);
if let Some(previous) = self.state.snapshot.replace(snap)
&& let Some(thread) = &mut self.state.sim_thread
{
thread.recycle(previous);
}
if let Some(target) = passed_target {
self.state.run_to(target);
}
}
if let Some(fault) = self.state.render_ctx.faults.take() {
self.state.report_fault(fault);
}
self.state.poll_saves();
self.state.poll_opens();
ui::results::poll(ctx, &mut self.state);
self.state.poll_capture();
let frame_drives_sim = !CAN_SPAWN_THREADS;
if self.state.sim_running || (frame_drives_sim && self.state.run_to_target.is_some()) {
ctx.request_repaint_after(std::time::Duration::ZERO);
}
if frame_drives_sim && self.state.sweep.is_stepping() {
ctx.request_repaint_after(std::time::Duration::ZERO);
} else if self.state.sweep.is_running() {
ctx.request_repaint_after(SWEEP_REPAINT_INTERVAL);
}
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let frame_start = web_time::Instant::now();
self.state.timings.frame_render_ms = 0.0;
ui::menu_bar::menu_bar_panel(ui, &mut self.dock, &mut self.state);
ui::fault::fault_modal(ui.ctx(), &mut self.state);
ui::about::about_modal(ui.ctx(), &mut self.state);
let mut dock_style = Style::from_egui(ui.style());
dock_style.tab_bar.corner_radius = egui::CornerRadius::ZERO;
dock_style.tab_bar.show_scroll_bar_on_overflow = false;
DockArea::new(&mut self.dock)
.style(dock_style)
.show_close_buttons(true)
.show_leaf_close_all_buttons(true)
.show_inside(ui, &mut self.state);
if let Some(tab) = self.state.focus_request.take() {
focus_tab(&mut self.dock, tab);
ui.ctx().request_repaint();
}
let total_ms = frame_start.elapsed().as_secs_f64() * 1000.0;
let render_ms = self.state.timings.frame_render_ms;
FrameTimings::update_ema(&mut self.state.timings.render_ms, render_ms);
FrameTimings::update_ema(&mut self.state.timings.ui_ms, (total_ms - render_ms).max(0.0));
}
}
#[cfg(test)]
mod tests {
use super::requested_threads;
#[test]
fn an_absent_query_asks_for_every_core() {
assert_eq!(requested_threads("", 14), 14);
assert_eq!(requested_threads("?debug=1", 14), 14);
}
#[test]
fn threads_one_is_how_a_single_threaded_run_is_asked_for() {
assert_eq!(requested_threads("?threads=1", 14), 1);
assert_eq!(requested_threads("?foo=a&threads=4", 14), 4);
}
#[test]
fn a_request_is_clamped_to_the_host() {
assert_eq!(requested_threads("?threads=99", 14), 14);
assert_eq!(requested_threads("?threads=0", 14), 1);
assert_eq!(requested_threads("?threads=nonsense", 14), 14);
}
}