use henad_compute::entry::ModelEntry;
use henad_compute::gpu::capacity::Demand;
use henad_core::helpers::fmt_bytes;
use henad_core::metadata::{LaneSpec, Structure};
use henad_core::params::ParamDescriptor;
use henad_core::topology::{NeighborhoodKind, TopologyHint};
use crate::icons::material_design_icons::{MDI_CHECK, MDI_CLOSE};
use crate::state::{AppState, OpeningRefusal};
use crate::ui::{KvGridRows, kv_grid};
const NO_MODEL_RUNS: &str = "No model in this build runs on this device. GPU models need a GPU with compute support.";
const NO_MODELS: &str = "This build includes no models.";
fn opening_refusal(ui: &mut egui::Ui, refusal: &OpeningRefusal, next_step: &str) {
ui.colored_label(ui.visuals().warn_fg_color, refusal.lead);
ui.label(format!("{}.", refusal.reason));
ui.label(next_step);
}
pub fn model_ui(ui: &mut egui::Ui, app: &mut AppState) {
let offered: Vec<(String, String)> = app
.offered_models()
.map(|entry| (entry.id().to_owned(), entry.name().to_owned()))
.collect();
if offered.is_empty() {
match &app.opening_refusal {
Some(refusal) => opening_refusal(ui, refusal, "No model in this build runs on this device."),
None if app.models.is_empty() => {
ui.label(NO_MODELS);
}
None => {
ui.label(NO_MODEL_RUNS);
}
}
return;
}
let selected_name = app.selected_entry().map_or("None", ModelEntry::name).to_owned();
let mut picked = None;
egui::ComboBox::from_label("Select Model")
.selected_text(selected_name)
.height(ui.ctx().content_rect().height())
.show_ui(ui, |ui| {
for (id, name) in &offered {
let selected = app.selected_model.as_deref() == Some(id.as_str());
if ui.selectable_label(selected, name).clicked() && !selected {
picked = Some(id.clone());
}
}
});
let changed_model = picked.is_some();
if let Some(id) = picked {
app.select_model(&id);
}
let Some(entry) = app.selected_entry() else {
if let Some(refusal) = &app.opening_refusal {
ui.separator();
opening_refusal(ui, refusal, "Select a model to continue.");
}
return;
};
ui.separator();
ui.label(entry.description());
ui.separator();
let demand = entry.demand(&app.param_values, &app.runtime.granted);
let storage_limit = app.runtime.granted.max_storage_buffers_per_shader_stage;
let mut scroll = egui::ScrollArea::vertical();
if changed_model {
scroll = scroll.vertical_scroll_offset(0.0);
}
scroll.show(ui, |ui| {
section(ui, "Identity");
kv_grid(ui, "model_identity_grid").show(ui, |ui, rows| {
row(ui, rows, "Id", entry.id());
row(ui, rows, "Backend", entry.metadata().backend.label());
row(ui, rows, "Topology", topology_label(entry.topology_hint()));
});
ui.add_space(8.0);
section(ui, "Structure");
kv_grid(ui, "model_structure_grid").show(ui, |ui, rows| structure_rows(ui, rows, &entry.metadata().structure));
ui.add_space(8.0);
section(ui, "Interface");
kv_grid(ui, "model_interface_grid").show(ui, |ui, rows| interface_rows(ui, rows, entry));
if let Some(demand) = &demand {
ui.add_space(8.0);
section(ui, "Footprint");
kv_grid(ui, "model_footprint_grid").show(ui, |ui, rows| {
footprint_rows(ui, rows, entry.id(), demand, storage_limit);
});
}
});
}
fn section(ui: &mut egui::Ui, title: &str) {
ui.strong(title);
ui.add_space(2.0);
}
fn row(ui: &mut egui::Ui, rows: &mut KvGridRows, label: &str, value: impl Into<egui::WidgetText>) {
ui.label(label);
ui.label(value);
rows.end_row(ui);
}
fn row_listing(ui: &mut egui::Ui, rows: &mut KvGridRows, label: &str, value: String, names: &str) {
ui.label(label).on_hover_text(names);
ui.label(value).on_hover_text(names);
rows.end_row(ui);
}
fn topology_label(hint: TopologyHint) -> &'static str {
match (hint.grid, hint.agents, hint.edges) {
(false, true, true) => "Network",
(true, true, true) => "Network and Grid",
(true, true, false) => "Agents and Grid",
(true, false, false) => "Grid",
(false, true, false) => "Agents",
_ => "None",
}
}
fn neighborhood_label(kind: NeighborhoodKind) -> &'static str {
match kind {
NeighborhoodKind::Moore => "Moore",
NeighborhoodKind::VonNeumann => "Von Neumann",
}
}
fn yes_no(present: bool) -> &'static str {
if present { MDI_CHECK } else { MDI_CLOSE }
}
fn count_of(n: usize, flagged: usize, flag: &str) -> String {
if flagged == 0 {
n.to_string()
} else {
format!("{n} ({flagged} {flag})")
}
}
fn lane_rows(ui: &mut egui::Ui, rows: &mut KvGridRows, lanes: &[LaneSpec]) {
let doubled = lanes.iter().filter(|lane| lane.double_buffered).count();
let names: Vec<String> = lanes
.iter()
.map(|lane| {
let mark = if lane.double_buffered { " (dual)" } else { "" };
format!("{}: {}{mark}", lane.name, lane.ty)
})
.collect();
row_listing(
ui,
rows,
"Lanes",
count_of(lanes.len(), doubled, "double-buffered"),
&names.join("\n"),
);
}
fn structure_rows(ui: &mut egui::Ui, rows: &mut KvGridRows, structure: &Structure) {
match structure {
Structure::Grid { neighborhood } => {
row(ui, rows, "Neighbourhood", neighborhood_label(*neighborhood));
}
Structure::Agents {
chunk,
lanes,
index,
field,
} => {
lane_rows(ui, rows, lanes);
row(ui, rows, "Chunk size", format!("{chunk} agents"));
row(ui, rows, "Neighbour index", *index);
row(ui, rows, "Field layer", *field);
}
Structure::Network { chunk, lanes, .. } => {
lane_rows(ui, rows, lanes);
row(ui, rows, "Chunk size", format!("{chunk} nodes"));
}
Structure::GpuGrid { buffers, workgroup } => {
row_listing(ui, rows, "Buffers", buffers.len().to_string(), &buffers.join("\n"));
row(ui, rows, "Workgroup", format!("{workgroup} × {workgroup}"));
}
Structure::GpuAgents {
buffers,
passes,
index,
display,
counters,
} => {
let doubled = buffers.iter().filter(|spec| spec.double_buffered).count();
let buffer_names: Vec<&str> = buffers.iter().map(|spec| spec.label).collect();
let pass_names: Vec<&str> = passes.iter().map(|pass| pass.label).collect();
row_listing(
ui,
rows,
"Buffers",
count_of(buffers.len(), doubled, "double-buffered"),
&buffer_names.join("\n"),
);
row_listing(
ui,
rows,
"Step passes",
passes.len().to_string(),
&pass_names.join("\n"),
);
row(
ui,
rows,
"Neighbour index",
if *index { "Spatial hash" } else { "None" },
);
row(ui, rows, "Display pass", yes_no(*display));
if *counters > 0 {
row(ui, rows, "Counters", counters.to_string());
}
}
}
}
fn interface_rows(ui: &mut egui::Ui, rows: &mut KvGridRows, entry: &ModelEntry) {
let descs: &[ParamDescriptor] = entry.param_descriptors();
let reload = descs.iter().filter(|desc| !desc.is_live()).count();
row(ui, rows, "Parameters", count_of(descs.len(), reload, "reload"));
ui.label("Statistics");
ui.horizontal(|ui| {
ui.label(entry.stat_descriptors().len().to_string());
swatches(ui, entry.stat_descriptors().iter().map(|stat| stat.color));
});
rows.end_row(ui);
ui.label("Palette");
match entry.metadata().palette {
Some(palette) => {
ui.horizontal(|ui| {
ui.label(palette.len().to_string());
swatches(ui, palette.iter().copied());
});
}
None => {
ui.label("In shader").on_hover_text(
"Palette colours are declared directly in the shaders and are not retrievable directly.",
);
}
}
rows.end_row(ui);
if let Structure::Network { edge_palette, .. } = &entry.metadata().structure {
ui.label("Edge palette");
ui.horizontal(|ui| {
ui.label(edge_palette.len().to_string());
swatches(ui, edge_palette.iter().copied());
});
rows.end_row(ui);
}
}
fn footprint_rows(ui: &mut egui::Ui, rows: &mut KvGridRows, id: &str, demand: &Demand, storage_limit: u32) {
row(ui, rows, "Expected memory", fmt_bytes(demand.bytes()));
if let Some(largest) = demand.buffers.iter().max_by_key(|alloc| alloc.bytes) {
row_listing(
ui,
rows,
"Largest buffer",
format!("{}, {}", strip_id(&largest.label, id), fmt_bytes(largest.bytes)),
&largest.label,
);
}
if let Some((w, h)) = demand.texture {
row(ui, rows, "Display texture", format!("{w} × {h}"));
}
if let Some(widest) = demand.passes.iter().max_by_key(|pass| pass.storage) {
let hint = format!("Widest pass is '{}'", widest.label);
ui.label("Storage bindings").on_hover_text(&hint);
ui.horizontal(|ui| {
ui.style_mut().spacing.item_spacing.x = 0.0;
ui.label(widest.storage.to_string()).on_hover_text(&hint);
ui.weak(format!(" of {storage_limit}")).on_hover_text(&hint);
});
rows.end_row(ui);
}
}
fn strip_id<'a>(label: &'a str, id: &str) -> &'a str {
label
.strip_prefix(id)
.and_then(|rest| rest.strip_prefix('_'))
.unwrap_or(label)
}
fn swatches(ui: &mut egui::Ui, colors: impl Iterator<Item = [u8; 4]>) {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
let size = egui::vec2(9.0, 9.0);
for [r, g, b, a] in colors {
let (rect, _) = ui.allocate_exact_size(size, egui::Sense::hover());
ui.painter().rect_filled(
rect,
egui::CornerRadius::ZERO,
egui::Color32::from_rgba_unmultiplied(r, g, b, a),
);
}
});
}
#[cfg(test)]
mod tests {
use super::{count_of, strip_id, topology_label};
use henad_core::topology::TopologyHint;
#[test]
fn topology_names_every_layer_combination() {
assert_eq!(topology_label(TopologyHint::GRID), "Grid");
assert_eq!(topology_label(TopologyHint::AGENTS), "Agents");
assert_eq!(topology_label(TopologyHint::COMPOSITE), "Agents and Grid");
assert_eq!(topology_label(TopologyHint::NETWORK), "Network");
assert_eq!(topology_label(TopologyHint::NONE), "None");
}
#[test]
fn a_count_drops_its_qualifier_when_nothing_carries_it() {
assert_eq!(count_of(5, 0, "reload"), "5");
assert_eq!(count_of(5, 2, "reload"), "5 (2 reload)");
}
#[test]
fn a_buffer_label_loses_only_the_model_id() {
assert_eq!(strip_id("gpu_ants_accum_a", "gpu_ants"), "accum_a");
assert_eq!(strip_id("gpu_ants_hash_counts", "gpu_ants"), "hash_counts");
assert_eq!(strip_id("scratch", "gpu_ants"), "scratch");
assert_eq!(strip_id("gpu_antsy", "gpu_ants"), "gpu_antsy");
}
}