implement rule match caching

This commit is contained in:
Crispy 2024-05-08 22:57:54 +02:00
parent d1d033565e
commit 0611be2837
2 changed files with 241 additions and 53 deletions

View file

@ -1,3 +1,5 @@
use std::ops::Not;
use rand::prelude::*; use rand::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,10 +11,21 @@ pub struct Cell(pub u16);
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Dish { pub struct Dish {
#[serde(skip)] #[serde(skip)]
pub chunk: Chunk, world: World,
pub rules: Vec<Rule>, pub rules: Vec<Rule>, // todo make read-only to ensure cache is updated
pub types: Vec<CellData>, pub types: Vec<CellData>,
pub groups: Vec<CellGroup>, pub groups: Vec<CellGroup>, // todo make read-only to ensure cache is updated
#[serde(skip)]
cache: Vec<RuleCache>,
#[serde(skip)]
match_cache: Vec<usize>,
}
#[derive(Debug)]
struct RuleCache {
rule: usize,
variant: usize,
matches: Vec<(isize, isize)>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@ -29,10 +42,15 @@ pub struct CellData {
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Chunk { struct Chunk {
pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>, pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>,
} }
#[derive(Debug, Default)]
struct World {
chunk: Chunk,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule { pub struct Rule {
#[serde(default)] #[serde(default)]
@ -289,7 +307,11 @@ impl Chunk {
} }
} }
fn fill_random(mut self) -> Self { fn fill(&mut self, cell: Cell) {
self.contents.fill([cell; CHUNK_SIZE]);
}
fn with_random_ones(mut self) -> Self {
for col in self.contents.iter_mut() { for col in self.contents.iter_mut() {
for cell in col.iter_mut() { for cell in col.iter_mut() {
if random::<u8>() % 4 == 0 { if random::<u8>() % 4 == 0 {
@ -300,7 +322,7 @@ impl Chunk {
self self
} }
pub fn get_cell(&self, x: usize, y: usize) -> Cell { fn get_cell(&self, x: usize, y: usize) -> Cell {
self.contents[x][y] self.contents[x][y]
} }
@ -348,7 +370,9 @@ impl Dish {
} }
Self { Self {
chunk: Chunk::new().fill_random(), world: World {
chunk: Chunk::new().with_random_ones(),
},
rules: default_rules, rules: default_rules,
types: vec![ types: vec![
CellData::new("air", 0, 0, 0), CellData::new("air", 0, 0, 0),
@ -359,13 +383,144 @@ impl Dish {
void: true, void: true,
cells: vec![Cell(0)], cells: vec![Cell(0)],
}], }],
cache: Vec::new(),
match_cache: Vec::new(),
} }
} }
pub fn update_rules(&mut self) { pub fn fill(&mut self, cell: Cell) {
self.world.fill(cell);
}
pub fn update_all_rules(&mut self) {
for rule in &mut self.rules { for rule in &mut self.rules {
rule.generate_variants(); rule.generate_variants();
} }
self.rebuild_cache();
}
/// run after any rule modification
pub fn update_cache_single_rule(&mut self, rule_index: usize) {
// remove old cache for this rule, since the variants may have changed
self.cache.retain(|c| c.rule != rule_index);
self.add_cache_single_rule(rule_index);
}
/// run after adding a rule
pub fn cache_last_added_rule(&mut self) {
if self.rules.is_empty() {
return;
}
let index = self.rules.len() - 1;
self.update_cache_single_rule(index);
}
fn add_cache_single_rule(&mut self, rule_index: usize) {
let rule = &self.rules[rule_index];
if !rule.enabled {
return;
}
for variant_index in 0..rule.variants.len() {
let mut matches = Vec::new();
let rule = &rule.variants[variant_index];
let border_x = rule.width as isize - 1;
let border_y = rule.height as isize - 1;
for px in -border_x..(CHUNK_SIZE as isize) {
for py in -border_y..(CHUNK_SIZE as isize) {
if self.world.subrule_matches(px, py, &rule, &self.groups) {
matches.push((px, py));
}
}
}
if !matches.is_empty() {
self.match_cache.push(self.cache.len());
}
self.cache.push(RuleCache {
rule: rule_index,
variant: variant_index,
matches,
});
}
}
pub fn rebuild_cache(&mut self) {
self.cache.clear();
for rule_index in 0..self.rules.len() {
self.add_cache_single_rule(rule_index);
}
self.update_match_cache();
}
pub fn update_cache(&mut self, x: isize, y: isize, width: usize, height: usize) {
fn overlap(
(x1, y1, w1, h1): (isize, isize, usize, usize),
(x2, y2, w2, h2): (isize, isize, usize, usize),
) -> bool {
x2 < x1.saturating_add_unsigned(w1)
&& x1 < x2.saturating_add_unsigned(w2)
&& y2 < y1.saturating_add_unsigned(h1)
&& y1 < y2.saturating_add_unsigned(h2)
}
let edited_rect = (x, y, width, height);
for cache in &mut self.cache {
let rule = &self.rules[cache.rule].variants[cache.variant];
let rule_width = rule.width;
let rule_height = rule.height;
// discard all overlapping matches
let mut i = 0;
while i < cache.matches.len() {
let match_pos = cache.matches[i];
let match_rect = (match_pos.0, match_pos.1, rule_width, rule_height);
if overlap(edited_rect, match_rect) {
cache.matches.swap_remove(i);
} else {
i += 1;
}
}
// check entire changed area and add matches
let border_x = rule_width - 1;
let border_y = rule_height - 1;
for px in (x.wrapping_sub_unsigned(border_x))..(x.wrapping_add_unsigned(width)) {
for py in (y.wrapping_sub_unsigned(border_y))..(y.wrapping_add_unsigned(height)) {
if self.world.subrule_matches(px, py, &rule, &self.groups) {
cache.matches.push((px, py));
}
}
}
}
self.update_match_cache();
}
fn update_match_cache(&mut self) {
self.match_cache = self
.cache
.iter()
.enumerate()
.filter_map(|(i, c)| c.matches.is_empty().not().then_some(i))
.collect();
}
pub fn fire_once(&mut self) {
if self.match_cache.is_empty() {
return;
}
let i = random::<usize>() % self.match_cache.len();
let i = self.match_cache[i];
let rule_cache = &self.cache[i];
let match_pos_index = random::<usize>() % rule_cache.matches.len();
let (x, y) = rule_cache.matches[match_pos_index];
let rule = &self.rules[rule_cache.rule].variants[rule_cache.variant];
let width = rule.width;
let height = rule.height;
self.apply_rule(x, y, rule_cache.rule, rule_cache.variant);
self.update_cache(x, y, width, height);
} }
pub fn fire_blindly(&mut self) { pub fn fire_blindly(&mut self) {
@ -381,12 +536,8 @@ impl Dish {
if enabled_rules.is_empty() { if enabled_rules.is_empty() {
return; return;
} }
let rule = random::<usize>() % enabled_rules.len(); let enabled_rule_index = random::<usize>() % enabled_rules.len();
let rule = enabled_rules[rule]; let rule_index = enabled_rules[enabled_rule_index];
self.fire_rule(rule);
}
fn fire_rule(&mut self, rule_index: usize) {
let rule = &self.rules[rule_index]; let rule = &self.rules[rule_index];
let variant_index = random::<usize>() % rule.variants.len(); let variant_index = random::<usize>() % rule.variants.len();
let variant = &rule.variants[variant_index].clone(); let variant = &rule.variants[variant_index].clone();
@ -397,17 +548,22 @@ impl Dish {
let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize) let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize)
.wrapping_sub_unsigned(border_y); .wrapping_sub_unsigned(border_y);
if !self.subrule_matches(x, y, variant) { if self.world.subrule_matches(x, y, variant, &self.groups) {
return; self.apply_rule(x, y, rule_index, variant_index);
} }
}
let fail: u8 = random(); fn apply_rule(&mut self, x: isize, y: isize, rule_index: usize, variant_index: usize) {
if rule.failrate > fail { let rule = &self.rules[rule_index];
let variant = &rule.variants[variant_index].clone();
if rule.failrate > random() {
return; return;
} }
let width = variant.width; let width = variant.width;
let height = variant.height; let height = variant.height;
let mut old_state = Vec::new(); let mut old_state = Vec::new();
for dy in 0..height { for dy in 0..height {
for dx in 0..width { for dx in 0..width {
@ -436,6 +592,7 @@ impl Dish {
RuleCellTo::Copy(x, y) => { RuleCellTo::Copy(x, y) => {
let index = x + y * variant.width; let index = x + y * variant.width;
if index >= old_state.len() { if index >= old_state.len() {
// TODO sanitize the rules somewhere else and remove this bounds check
// the copy source is outside the rule bounds // the copy source is outside the rule bounds
continue; continue;
} }
@ -451,7 +608,35 @@ impl Dish {
} }
} }
fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule) -> bool { //todo isize
pub fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
self.world.get_cell(x, y)
}
//todo isize
pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) {
if x >= CHUNK_SIZE || y >= CHUNK_SIZE {
return;
}
self.world.chunk.set_cell(x, y, cell);
}
}
impl World {
fn fill(&mut self, cell: Cell) {
self.chunk.fill(cell);
}
//todo isize
fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
if x >= CHUNK_SIZE || y >= CHUNK_SIZE {
None
} else {
Some(self.chunk.get_cell(x, y))
}
}
fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule, groups: &[CellGroup]) -> bool {
for dx in 0..subrule.width { for dx in 0..subrule.width {
for dy in 0..subrule.height { for dy in 0..subrule.height {
let x = x.wrapping_add_unsigned(dx) as usize; let x = x.wrapping_add_unsigned(dx) as usize;
@ -464,7 +649,7 @@ impl Dish {
} }
} }
RuleCellFrom::Group(group_id) => { RuleCellFrom::Group(group_id) => {
let group = &self.groups[group_id]; let group = &groups[group_id];
if let Some(cell) = cell { if let Some(cell) = cell {
if !group.cells.contains(&cell) { if !group.cells.contains(&cell) {
return false; return false;
@ -479,23 +664,6 @@ impl Dish {
} }
true true
} }
//todo isize
pub fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
if x >= CHUNK_SIZE || y >= CHUNK_SIZE {
None
} else {
Some(self.chunk.get_cell(x, y))
}
}
//todo isize
pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) {
if x >= CHUNK_SIZE || y >= CHUNK_SIZE {
return;
}
self.chunk.set_cell(x, y, cell)
}
} }
impl Cell { impl Cell {

View file

@ -14,7 +14,7 @@ use egui::{collapsing_header::CollapsingState, DragValue, PointerButton};
use native_dialog::FileDialog; use native_dialog::FileDialog;
use rand::prelude::*; use rand::prelude::*;
use petri::{Cell, CellData, CellGroup, Chunk, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; use petri::{Cell, CellData, CellGroup, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE};
fn main() { fn main() {
eframe::run_native( eframe::run_native(
@ -37,7 +37,7 @@ impl UScope {
fn new(_cc: &eframe::CreationContext<'_>) -> Self { fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self { Self {
dish: Dish::new(), dish: Dish::new(),
speed: 500, speed: 1,
show_grid: false, show_grid: false,
brush: Cell(1), brush: Cell(1),
} }
@ -65,7 +65,7 @@ impl UScope {
// TODO: show errors to user // TODO: show errors to user
let s = fs::read_to_string(path).unwrap(); let s = fs::read_to_string(path).unwrap();
self.dish = serde_json::from_str(&s).unwrap(); self.dish = serde_json::from_str(&s).unwrap();
self.dish.update_rules(); self.dish.update_all_rules();
} }
} }
} }
@ -74,15 +74,18 @@ impl eframe::App for UScope {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint(); ctx.request_repaint();
for _ in 0..self.speed { for _ in 0..self.speed {
self.dish.fire_blindly(); self.dish.fire_once();
} }
SidePanel::left("left_panel") SidePanel::left("left_panel")
.min_width(100.) .min_width(100.)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.heading("Simulation"); ui.heading("Simulation");
ui.label("speed"); ui.label("speed");
ui.add(Slider::new(&mut self.speed, 0..=5000).clamp_to_range(false)); ui.add(Slider::new(&mut self.speed, 0..=15).clamp_to_range(false));
ui.checkbox(&mut self.show_grid, "show grid"); ui.checkbox(&mut self.show_grid, "show grid");
if ui.button("regenerate rules and cache").clicked() {
self.dish.update_all_rules();
}
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Save").clicked() { if ui.button("Save").clicked() {
self.save_universe(); self.save_universe();
@ -113,7 +116,7 @@ impl eframe::App for UScope {
self.dish.types.push(CellData { name, color }) self.dish.types.push(CellData { name, color })
} }
if ui.button("fill").clicked() { if ui.button("fill").clicked() {
self.dish.chunk.contents.fill([self.brush; CHUNK_SIZE]); self.dish.fill(self.brush);
} }
ui.separator(); ui.separator();
@ -147,8 +150,9 @@ impl eframe::App for UScope {
let mut to_remove = None; let mut to_remove = None;
let mut to_clone = None; let mut to_clone = None;
let mut to_update = None;
for (i, rule) in self.dish.rules.iter_mut().enumerate() { for (i, rule) in self.dish.rules.iter_mut().enumerate() {
rule_editor( let changed = rule_editor(
ui, ui,
rule, rule,
i, i,
@ -157,25 +161,35 @@ impl eframe::App for UScope {
&mut to_remove, &mut to_remove,
&mut to_clone, &mut to_clone,
); );
if changed {
rule.generate_variants();
to_update = Some(i);
}
}
if let Some(i) = to_update {
self.dish.update_cache_single_rule(i);
} }
if let Some(i) = to_remove { if let Some(i) = to_remove {
self.dish.rules.remove(i); self.dish.rules.remove(i);
self.dish.rebuild_cache();
} }
if let Some(i) = to_clone { if let Some(i) = to_clone {
let mut new_rule = self.dish.rules[i].clone(); let mut new_rule = self.dish.rules[i].clone();
new_rule.enabled = false; new_rule.enabled = false;
self.dish.rules.push(new_rule); self.dish.rules.push(new_rule);
self.dish.cache_last_added_rule();
} }
ui.separator(); ui.separator();
if ui.button("add rule").clicked() { if ui.button("add rule").clicked() {
self.dish.rules.push(Rule::new()); self.dish.rules.push(Rule::new());
self.dish.cache_last_added_rule()
} }
}); });
}); });
CentralPanel::default().show(ctx, |ui| { CentralPanel::default().show(ctx, |ui| {
let bounds = ui.available_rect_before_wrap(); let bounds = ui.available_rect_before_wrap();
let painter = ui.painter_at(bounds); let painter = ui.painter_at(bounds);
paint_chunk(painter, &self.dish.chunk, &self.dish.types, self.show_grid); paint_world(painter, &self.dish, self.show_grid);
let rect = ui.allocate_rect(bounds, Sense::click_and_drag()); let rect = ui.allocate_rect(bounds, Sense::click_and_drag());
if let Some(pos) = rect.interact_pointer_pos() { if let Some(pos) = rect.interact_pointer_pos() {
@ -189,6 +203,7 @@ impl eframe::App for UScope {
} }
} else { } else {
self.dish.set_cell(x, y, self.brush); self.dish.set_cell(x, y, self.brush);
self.dish.update_cache(x as isize, y as isize, 1, 1);
} }
} }
}); });
@ -196,11 +211,12 @@ impl eframe::App for UScope {
} }
const GRID_SIZE: f32 = 16.; const GRID_SIZE: f32 = 16.;
fn paint_chunk(painter: Painter, chunk: &Chunk, cells: &[CellData], grid: bool) { fn paint_world(painter: Painter, world: &Dish, grid: bool) {
let cells = &world.types;
let bounds = painter.clip_rect(); let bounds = painter.clip_rect();
for x in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE {
for y in 0..CHUNK_SIZE { for y in 0..CHUNK_SIZE {
let cell = &chunk.get_cell(x, y); let cell = &world.get_cell(x, y).unwrap();
let corner = bounds.min + (Vec2::from((x as f32, y as f32)) * GRID_SIZE); let corner = bounds.min + (Vec2::from((x as f32, y as f32)) * GRID_SIZE);
let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE)); let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE));
if cell.id() >= cells.len() { if cell.id() >= cells.len() {
@ -229,11 +245,14 @@ fn rule_editor(
groups: &[CellGroup], groups: &[CellGroup],
to_remove: &mut Option<usize>, to_remove: &mut Option<usize>,
to_clone: &mut Option<usize>, to_clone: &mut Option<usize>,
) { ) -> bool {
let mut changed = false;
let id = ui.make_persistent_id(format!("rule {index}")); let id = ui.make_persistent_id(format!("rule {index}"));
CollapsingState::load_with_default_open(ui.ctx(), id, true) CollapsingState::load_with_default_open(ui.ctx(), id, true)
.show_header(ui, |ui| { .show_header(ui, |ui| {
ui.checkbox(&mut rule.enabled, &rule.name); if ui.checkbox(&mut rule.enabled, &rule.name).changed() {
changed = true;
}
if ui.button("delete").clicked() { if ui.button("delete").clicked() {
*to_remove = Some(index); *to_remove = Some(index);
} }
@ -245,13 +264,13 @@ fn rule_editor(
ui.text_edit_singleline(&mut rule.name); ui.text_edit_singleline(&mut rule.name);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.checkbox(&mut rule.flip_x, "flip X").changed() { if ui.checkbox(&mut rule.flip_x, "flip X").changed() {
rule.generate_variants(); changed = true;
} }
if ui.checkbox(&mut rule.flip_y, "flip Y").changed() { if ui.checkbox(&mut rule.flip_y, "flip Y").changed() {
rule.generate_variants(); changed = true;
} }
if ui.checkbox(&mut rule.rotate, "rotate").changed() { if ui.checkbox(&mut rule.rotate, "rotate").changed() {
rule.generate_variants(); changed = true;
} }
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -297,7 +316,7 @@ fn rule_editor(
&mut overlay_lines, &mut overlay_lines,
); );
if changed_left || changed_right { if changed_left || changed_right {
rule.generate_variants(); changed = true;
} }
} }
} }
@ -382,6 +401,7 @@ fn rule_editor(
ui.painter().line_segment([a, b], stroke); ui.painter().line_segment([a, b], stroke);
} }
}); });
changed
} }
fn rule_cell_edit_from( fn rule_cell_edit_from(