Model sets¶
A model joins the app and the command line through a model set, the ModelSet that a host receives.
The template's models() in src/lib.rs builds a project's set, and the app, the command line and the test all read their models from it.
Henad 0.3
This page describes Henad 0.3.
//! Models of the my-model project.
// Proving a type that holds wgpu handles `Send` or `Sync` walks wgpu-core's registries, deeper than the default
// limit of 128.
#![recursion_limit = "256"]
mod gpu_vote;
mod vote;
henad::include_shaders!();
use henad::authoring::register_gpu_grid_model;
use henad::authoring::register_grid_model;
/// Returns every model this crate provides.
///
/// # Errors
///
/// Returns [`henad::ModelSetError`] when two models share an id or an id breaks the id grammar.
pub fn models() -> Result<henad::ModelSet, henad::ModelSetError> {
let mut models = henad::ModelSet::new(henad::build_info!());
models.insert(register_grid_model::<vote::Vote>())?;
models.insert(register_gpu_grid_model::<gpu_vote::GpuVote>())?;
Ok(models)
}
#[cfg(test)]
mod tests {
use henad::testing::{CheckSettings, TestDeviceRequest, assert_set_conforms, headless_test_device};
#[test]
fn every_model_conforms() {
let models = super::models().expect("model ids are unique");
let mut settings = CheckSettings::default();
if let Some(device) = headless_test_device(&TestDeviceRequest::baseline()) {
settings = settings.gpu(device);
}
assert_set_conforms(&models, &settings);
}
}
Registering a model¶
The register_* function for a model's trait type-erases the model into a ModelEntry, and ModelSet::insert adds the entry to the set.
| Trait | Function |
|---|---|
GridModel |
register_grid_model::<M>() |
AgentModel |
register_agent_model::<M>() |
NetworkModel |
register_network_model::<M>() |
GpuGridModel |
register_gpu_grid_model::<M>() |
GpuAgentModel |
register_gpu_agent_model::<M>() |
All five sit at henad::authoring.
Adding a model to a project is a mod line and an insert line in src/lib.rs, and an import of its register_* function when src/lib.rs does not import it yet.
You never write any part of an entry by hand.
The name, parameters, statistics, actions and topology all derive from the trait impl.
The engines and the kernels compile in the crate that calls register_*, at that crate's opt-level, and the template sets its build profiles for them.
To check that the entry was added:
Ids¶
A set holds each id once.
insert rejects an id that the set already holds, and an id outside the grammar: a lowercase ASCII letter, then lowercase letters, digits and underscores.
An id identifies the model on the command line, in a spec file and in every results folder, so keep it once a sweep has used it.
Sources¶
Every entry records its source: the type path of the model, and the build of the crate that registered it.
ModelSet::new takes that build, and henad::build_info!() returns the build of the crate it expands in.
A set records its build on every entry inserted without a build, and an entry that already carries a source keeps it.
A crate that holds models builds its own set this way, and calls henad_build::stamp_commit() from its build.rs.
The build then carries the commit, whether the sources differed from it, and a hash of the sources.
A results folder records each model's source, and a resume or a merge warns when it differs, as the app does before it opens a run.
Your own project covers the stamp.
A model library exports a models() built from its own build_info!(), and its entries keep the library's name and version in any set they join.
ModelSet::extend adds a whole set, and each entry keeps its source:
extend checks every id before it adds any entry, and leaves the set unchanged when two ids clash.
The example models¶
With the example-models feature on, henad::models::example_models() returns the ten example models, with henad-models as their source.
It registers the six CPU models, then the four GPU models, and inserts them all into one set:
/// Returns the ten example models, with henad-models' own build as their source.
pub fn example_models() -> ModelSet {
let entries = [
register_grid_model::<crate::sir::SirGridModel>(),
register_agent_model::<crate::boids::BoidsModel>(),
register_grid_model::<crate::game_of_life::GameOfLifeModel>(),
register_agent_model::<crate::ants::AntsModel>(),
register_network_model::<crate::virus_network::VirusNetwork>(),
register_network_model::<crate::team_assembly::TeamAssembly>(),
register_gpu_grid_model::<crate::gpu_game_of_life::GpuGameOfLife>(),
register_gpu_grid_model::<crate::gpu_sir::GpuSir>(),
register_gpu_agent_model::<crate::gpu_boids::GpuBoids>(),
register_gpu_agent_model::<crate::gpu_ants::GpuAnts>(),
];
let mut models = ModelSet::new(henad_core::build_info!());
for entry in entries {
// The ids are fixed above, and `every_example_model_joins_the_set` checks them.
models.insert(entry).expect("every example model has an id of its own");
}
models
}
A project that wants every example model beside its own models extends its set with them:
A project that wants only some example models inserts clones of their entries:
let examples = henad::models::example_models();
for id in ["sir", "boids"] {
models.insert(examples.get(id).ok_or("the example set lacks this model")?.clone())?;
}
Each clone keeps henad-models as its source.
Network entries¶
register_network_model assigns the entry the TopologyHint::NETWORK hint, for nodes drawn as agents with edges between them and no grid.
The Model tab shows that topology as Network.
The entry's metadata is a Structure::Network carrying the chunk size, the node lanes and the edge palette, and the Model tab lists all three.
GPU entries¶
A GPU entry holds no device, and builds on the device that the host passes to ModelEntry::build.
Built with no device, it returns a fault instead of a state.
On a machine without a device the app and the command line leave the GPU models out of the list entirely, instead of listing them and letting them fail on selection.
A GPU entry also carries a capacity check. The app asks it whether the current parameters fit the device, and if they do not it disables Build with a reason instead of letting wgpu take the process down. See porting a model to the GPU.
Device needs¶
A WebGPU device offers eight storage buffers per shader stage by default, and a pass can bind more only on a device requested with a higher limit.
Each GPU entry declares what its widest pass binds as its GpuNeeds, read from its declared passes, action passes included.
ModelSet::gpu_needs merges the needs of every GPU entry, and a host requests a device with those needs before any model builds.
The app, the command line and henad::gpu::acquire_headless all request a device this way.
A browser at the default limits rejects a model whose defaults exceed those limits, and such a model fails the kit's DefaultsFit check unless its test exempts it.
Exact replay¶
A GPU model whose passes leave the order of their writes to the GPU, as atomic additions to one cell do, can step two runs of one seed to different states.
Such a model declares REPLAYS_EXACTLY = false, as the example gpu_boids does.
The entry's metadata carries the flag as replays_exactly, and a results folder records it.
The testing kit skips the checks that compare two runs on one seed, and the app notes beside a run's Open button that its replay can drift.
Every other model replays a recorded run exactly.
Next¶
- Testing your model covers the checks the template's test runs over the set.
- The example models shows what a registered entry looks like from the outside.