Skip to content

Grid models

See Writing a CPU grid model for a tutorial.

GridModel is the authoring trait for a cellular automaton, a grid of u8 cells where every cell advances by one rule per tick. It fits a world made of small integer states, each cell updating from what surrounds it. You implement init, step_cell, stats and the consts, and the engine supplies everything else, including the parallel row-wise step.

fn step_cell(cell: u8, neighbors: &[u8], _params: &(), _rng: &mut u64) -> u8 {
    let alive_count: u8 = neighbors.iter().sum();
    match (cell, alive_count) {
        (ALIVE, 2..=3) | (DEAD, 3) => ALIVE,
        _ => DEAD,
    }
}

Read game_of_life.rs and sir.rs alongside this page. Game of Life is the smaller of the two and draws no random numbers during a step, which makes it the easier one to copy from when you begin.

Items you supply

Everything in the table below comes out of your impl.

Item Role
NAME, ID, DESCRIPTION Identity, read by the app and by henad-cli --list
PALETTE One RGBA colour per cell value. See palettes and views
NEIGHBORHOOD Moore or VonNeumann, deciding the neighbours step_cell receives
STATS The series the history chart plots. See statistics
ACTIONS One-off changes to the state, one button each, empty by default. See actions
type Params Hot parameters, rebuilt once a tick
param_descriptors This model's own parameters. See parameters
from_params Extracts Params from a value slice
init Fills the grid, given the parameters and a seed
step_cell The rule
act Runs one entry of ACTIONS
stats The reduction, in STATS order

The rule

fn step_cell(cell: u8, neighbors: &[u8], params: &Self::Params, rng: &mut u64) -> u8;

step_cell receives its neighbours already gathered, in the order your declared NEIGHBORHOOD fixes. Keep the function pure apart from the rng it receives. The engine steps rows in parallel and makes no guarantee about which row runs on which core.

Neighbours arrive row-major, with dy on the outer axis and dx on the inner. This ordering is published API, and a test asserts it against the tables in authoring primitives.

0 1 2      (-1,-1) ( 0,-1) (+1,-1)
3 . 4      (-1, 0)         (+1, 0)
5 6 7      (-1,+1) ( 0,+1) (+1,+1)
. 0 .      ( 0,-1)
1 . 2      (-1, 0)         (+1, 0)
. 3 .      ( 0,+1)

dy runs south, matching the display's downward y axis. The grid is a torus on both axes, and the engine wraps every coordinate before your rule runs, so step_cell never sees an edge.

Hot parameters

Your rule reads a &Self::Params. Passing it the raw value slice would put a ParamValue match inside a loop that runs once per cell, millions of times a tick, and from_params avoids that by running once at the start of a tick. Every cell of that tick then shares the result.

fn from_params(params: &[ParamValue]) -> SirParams {
    SirParams {
        infection_rate: extract_f32(params, INFECTION_RATE, 0.3),
        recovery_rate: extract_f32(params, RECOVERY_RATE, 0.05),
    }
}

A model with nothing to extract, such as Game of Life, sets type Params = () and writes an empty from_params. Anything a rule would otherwise recompute per cell also belongs here, such as a squared radius or a reciprocal.

Width and height

The engine prepends grid width and height at indices 0 and 1, and no grid model declares them itself. Its own parameters start at index 2, and the indices params! generates are already relative to the model's own slice.

init and from_params both receive that slice. The engine can then add an engine parameter without shifting anything that a model reads.

The initial state

fn init(grid: &mut Grid2D<u8>, params: &[ParamValue], rng: &mut u64);

init runs once, sequentially, on the current side of a freshly allocated grid. The rng argument is a plain u64 xorshift state, advanced by the random primitives.

A GPU port of the model calls this same function to seed its buffers, which keeps tick 0 bit-identical between the two backends. See porting a model to the GPU.

Left to the engine

With the trait implemented, the engine covers the rest:

  • Allocates the grid and its second buffer, and swaps the two after every tick.
  • Splits the step by row across rayon, on native and on the web alike.
  • Wraps both axes, peeling the x wrap off the row loop so the interior runs without a modulo.
  • Seeds an RNG per row per tick from the row index, never from anything a worker mutates.
  • Stores the parameters, and rejects any edit to a reload-only parameter.
  • Builds the grid view, the display texture, the history chart and the snapshots.

Next