Skip to main content

henad_compute/cpu/
grid_engine.rs

1//! The engine that runs a [`GridModel`] as a [`SimState`], and the parameters it prepends to the model's own
2//! parameters.
3
4use henad_core::action::action_seed;
5use henad_core::authoring::model::field::{Extent, FieldLayer as _};
6use henad_core::authoring::model::grid_model::GridModel;
7use henad_core::helpers::{extract_u32, u32_param};
8use henad_core::model::SimState;
9use henad_core::params::{ParamDescriptor, ParamStore, ParamValue};
10use henad_core::view::{GridView, StatEntry, stat_entries};
11
12use crate::cpu::field::CaField;
13
14pub use crate::cpu::field::{GRID_INIT_SEED, grid_init_rng};
15
16/// Engine wrapper that implements `SimState` for any `GridModel`.
17pub struct GridModelState<M: GridModel> {
18    field: CaField<M>,
19    params: ParamStore,
20    /// RNG stream for actions, separate from the stream the ticks draw from.
21    action_seed: u64,
22    tick: u64,
23}
24
25impl<M: GridModel> std::fmt::Debug for GridModelState<M> {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("GridModelState")
28            .field("model", &M::ID)
29            .field("tick", &self.tick)
30            .field("params", &self.params)
31            .finish_non_exhaustive()
32    }
33}
34
35impl<M: GridModel> GridModelState<M> {
36    /// Builds a state from the full parameter list, with its RNG starting from [`GRID_INIT_SEED`].
37    pub fn from_params(params: &[ParamValue]) -> Self {
38        Self::from_params_seeded(params, None)
39    }
40
41    /// Builds a state whose RNG starts from `seed`, or [`GRID_INIT_SEED`] when it is `None`.
42    pub fn from_params_seeded(params: &[ParamValue], seed: Option<u64>) -> Self {
43        let extent = Extent {
44            w: extract_u32(params, GRID_WIDTH, 1024) as f32,
45            h: extract_u32(params, GRID_HEIGHT, 1024) as f32,
46        };
47        Self {
48            field: CaField::with_seed(extent, own_params(params), seed),
49            params: ParamStore::new(&grid_model_param_descriptors::<M>(), params),
50            action_seed: action_seed(seed),
51            tick: 0,
52        }
53    }
54
55    /// Builds a state whose grid holds `cells`, with the default seeds.
56    ///
57    /// Returns `None` unless `cells` is exactly the length `params` implies.
58    pub fn from_cells(params: &[ParamValue], cells: &[u8]) -> Option<Self> {
59        let extent = Extent {
60            w: extract_u32(params, GRID_WIDTH, 1024) as f32,
61            h: extract_u32(params, GRID_HEIGHT, 1024) as f32,
62        };
63        Some(Self {
64            field: CaField::from_cells(extent, cells)?,
65            params: ParamStore::new(&grid_model_param_descriptors::<M>(), params),
66            action_seed: action_seed(None),
67            tick: 0,
68        })
69    }
70}
71
72// Indices of the params the engine prepends before a model's own params.
73/// Index of the grid's width in cells.
74pub const GRID_WIDTH: usize = 0;
75/// Index of the grid's height in cells.
76pub const GRID_HEIGHT: usize = 1;
77
78/// Number of params the engine prepends, and the index of a model's first own param.
79pub const GRID_PARAM_BASE: usize = 2;
80
81/// A grid model's own slice of a composed list.
82fn own_params(params: &[ParamValue]) -> &[ParamValue] {
83    &params[GRID_PARAM_BASE.min(params.len())..]
84}
85
86/// Returns the full descriptor list: `grid_width` and `grid_height`, then the model's own params.
87pub fn grid_model_param_descriptors<M: GridModel>() -> Vec<ParamDescriptor> {
88    let mut descs = vec![
89        u32_param("grid_width", "Grid Width", 1024, 1, 10_000).on_reload(),
90        u32_param("grid_height", "Grid Height", 1024, 1, 10_000).on_reload(),
91    ];
92    descs.extend(M::param_descriptors());
93    descs
94}
95
96impl<M: GridModel> SimState for GridModelState<M> {
97    fn step(&mut self) {
98        let hot = M::from_params(own_params(self.params.values()));
99        self.field.update(&(), &hot, self.tick);
100        self.tick += 1;
101    }
102
103    fn tick(&self) -> u64 {
104        self.tick
105    }
106
107    fn grid_view(&self) -> Option<GridView<'_>> {
108        self.field.grid_view()
109    }
110
111    fn stats(&self) -> Vec<StatEntry> {
112        stat_entries(M::STATS, M::stats(self.field.grid()))
113    }
114
115    fn set_param(&mut self, index: usize, value: &ParamValue) -> bool {
116        self.params.set(index, value)
117    }
118
119    fn act(&mut self, index: usize) -> bool {
120        if index >= M::ACTIONS.len() {
121            return false;
122        }
123        // The state is destructured, so the params borrow and the grid borrow cover different fields.
124        let Self {
125            field,
126            params,
127            action_seed,
128            ..
129        } = self;
130        M::act(index, field.grid_mut(), own_params(params.values()), action_seed);
131        true
132    }
133
134    fn population(&self) -> u64 {
135        self.field.cell_count() as u64
136    }
137
138    fn heap_bytes(&self) -> usize {
139        self.field.heap_bytes()
140    }
141
142    fn parallel_jobs(&self) -> Option<usize> {
143        Some(self.field.parallel_jobs())
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::GridModelState;
150    use henad_core::authoring::model::grid_model::GridModel;
151    use henad_core::authoring::primitives::rng::xorshift64;
152    use henad_core::grid::Grid2D;
153    use henad_core::model::SimState as _;
154    use henad_core::params::{ParamDescriptor, ParamValue};
155    use henad_core::topology::NeighborhoodKind;
156    use henad_core::view::{StatDescriptor, StatValue};
157
158    fn live_neighbors(neighbors: &[u8]) -> u8 {
159        neighbors.iter().filter(|&&n| n != 0).count() as u8
160    }
161
162    /// A model that reports how many live neighbours each cell saw, so one step pins the exact set
163    /// the engine gathered instead of whatever a real rule would collapse it to.
164    macro_rules! counting_model {
165        ($ty:ident, $id:literal, $kind:expr) => {
166            struct $ty;
167
168            impl GridModel for $ty {
169                const NAME: &'static str = $id;
170                const ID: &'static str = $id;
171                const DESCRIPTION: &'static str = $id;
172                const PALETTE: &'static [[u8; 4]] = &[[0, 0, 0, 255]; 9];
173                const NEIGHBORHOOD: NeighborhoodKind = $kind;
174                const STATS: &'static [StatDescriptor] = &[];
175                type Params = ();
176
177                fn param_descriptors() -> Vec<ParamDescriptor> {
178                    Vec::new()
179                }
180
181                fn from_params(_params: &[ParamValue]) -> Self::Params {}
182
183                fn init(grid: &mut Grid2D<u8>, _params: &[ParamValue], rng: &mut u64) {
184                    for cell in grid.current_mut() {
185                        *rng = xorshift64(*rng);
186                        *cell = (*rng & 1) as u8;
187                    }
188                }
189
190                fn step_cell(_cell: u8, neighbors: &[u8], _params: &Self::Params, _rng: &mut u64) -> u8 {
191                    live_neighbors(neighbors)
192                }
193
194                fn stats(_grid: &Grid2D<u8>) -> Vec<StatValue> {
195                    Vec::new()
196                }
197            }
198        };
199    }
200
201    counting_model!(MooreCount, "moore_count", NeighborhoodKind::Moore);
202    counting_model!(VnCount, "vn_count", NeighborhoodKind::VonNeumann);
203
204    /// A model whose every cell draws from the step's random stream. A change to how a row is seeded changes the
205    /// grid.
206    struct Noisy;
207
208    impl GridModel for Noisy {
209        const NAME: &'static str = "noisy";
210        const ID: &'static str = "noisy";
211        const DESCRIPTION: &'static str = "noisy";
212        const PALETTE: &'static [[u8; 4]] = &[[0, 0, 0, 255]; 2];
213        const NEIGHBORHOOD: NeighborhoodKind = NeighborhoodKind::Moore;
214        const STATS: &'static [StatDescriptor] = &[];
215        type Params = ();
216
217        fn param_descriptors() -> Vec<ParamDescriptor> {
218            Vec::new()
219        }
220
221        fn from_params(_params: &[ParamValue]) -> Self::Params {}
222
223        fn init(grid: &mut Grid2D<u8>, params: &[ParamValue], rng: &mut u64) {
224            MooreCount::init(grid, params, rng);
225        }
226
227        fn step_cell(_cell: u8, neighbors: &[u8], _params: &Self::Params, rng: &mut u64) -> u8 {
228            *rng = xorshift64(*rng);
229            (live_neighbors(neighbors) + (*rng >> 63) as u8) & 1
230        }
231
232        fn stats(_grid: &Grid2D<u8>) -> Vec<StatValue> {
233            Vec::new()
234        }
235    }
236
237    /// The plain modulo gather that the row loops avoid by peeling their edge columns.
238    fn reference(cells: &[u8], w: usize, h: usize, moore: bool) -> Vec<u8> {
239        let mut out = vec![0u8; cells.len()];
240        for y in 0..h {
241            let (ym, yp) = ((y + h - 1) % h, (y + 1) % h);
242            for x in 0..w {
243                let (xm, xp) = ((x + w - 1) % w, (x + 1) % w);
244                let neighbors = if moore {
245                    vec![
246                        cells[ym * w + xm],
247                        cells[ym * w + x],
248                        cells[ym * w + xp],
249                        cells[y * w + xm],
250                        cells[y * w + xp],
251                        cells[yp * w + xm],
252                        cells[yp * w + x],
253                        cells[yp * w + xp],
254                    ]
255                } else {
256                    vec![
257                        cells[ym * w + x],
258                        cells[y * w + xm],
259                        cells[y * w + xp],
260                        cells[yp * w + x],
261                    ]
262                };
263                out[y * w + x] = live_neighbors(&neighbors);
264            }
265        }
266        out
267    }
268
269    /// Widths 1 and 2 are the interesting ones: both peeled columns land on the same cells.
270    const SIZES: [(u32, u32); 7] = [(1, 1), (1, 6), (2, 2), (3, 4), (6, 1), (7, 9), (65, 3)];
271
272    fn cells<M: GridModel>(state: &GridModelState<M>) -> Vec<u8> {
273        state
274            .grid_view()
275            .expect("a grid model always has a grid view")
276            .cells
277            .to_vec()
278    }
279
280    #[test]
281    fn moore_gather_wraps_like_the_reference() {
282        for (w, h) in SIZES {
283            let params = vec![ParamValue::U32(w), ParamValue::U32(h)];
284            let mut state = GridModelState::<MooreCount>::from_params(&params);
285            let before = cells(&state);
286            state.step();
287            assert_eq!(
288                cells(&state),
289                reference(&before, w as usize, h as usize, true),
290                "{w}x{h}"
291            );
292        }
293    }
294
295    #[test]
296    fn von_neumann_gather_wraps_like_the_reference() {
297        for (w, h) in SIZES {
298            let params = vec![ParamValue::U32(w), ParamValue::U32(h)];
299            let mut state = GridModelState::<VnCount>::from_params(&params);
300            let before = cells(&state);
301            state.step();
302            assert_eq!(
303                cells(&state),
304                reference(&before, w as usize, h as usize, false),
305                "{w}x{h}"
306            );
307        }
308    }
309
310    /// Two seeds must give different runs, and `None` must reproduce the fixed default exactly.
311    #[test]
312    fn seeds_produce_independent_replicates() {
313        let params = vec![ParamValue::U32(64), ParamValue::U32(64)];
314        let run = |seed: Option<u64>| -> Vec<u8> {
315            let mut state = GridModelState::<MooreCount>::from_params_seeded(&params, seed);
316            state.step();
317            cells(&state)
318        };
319
320        assert_eq!(run(None), run(None), "the default must be reproducible");
321        assert_ne!(run(Some(1)), run(Some(2)), "different seeds must give different runs");
322        assert_eq!(run(Some(7)), run(Some(7)), "a seed must still be reproducible");
323        assert_ne!(run(None), run(Some(1)), "a user seed must not land on the default");
324
325        // `xorshift64(0) == 0` is absorbing, so if the engine's RNG state is stuck it will produce a uniform grid.
326        let zero = run(Some(0));
327        assert!(
328            zero.iter().any(|&c| c != zero[0]),
329            "seed 0 produced a uniform grid, so its RNG state was stuck"
330        );
331    }
332
333    /// The row seed comes from the row index, so a grid stepped in one thread and the same grid
334    /// stepped across many must agree bit for bit. Every cell of [`Noisy`] draws. The row seeds
335    /// then reach the result.
336    #[test]
337    fn results_do_not_depend_on_the_thread_count() {
338        // A 128-column grid holds 64 rows per job, and 1024 rows split into 16 jobs.
339        let params = vec![ParamValue::U32(128), ParamValue::U32(1024)];
340        let run = |threads: usize| -> Vec<u8> {
341            let pool = rayon::ThreadPoolBuilder::new()
342                .num_threads(threads)
343                .build()
344                .expect("rayon pool");
345            pool.install(|| {
346                let mut state = GridModelState::<Noisy>::from_params(&params);
347                for _ in 0..20 {
348                    state.step();
349                }
350                cells(&state)
351            })
352        };
353        assert_eq!(run(1), run(7), "grid contents depend on the thread count");
354    }
355}