From 660746f16be18c744c5b88e35ec35b38482db8e0 Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Sat, 12 Apr 2025 16:36:27 +0200 Subject: [PATCH 01/23] split step into functions and set up stuff for flamegraph to be usable --- .gitignore | 2 + Cargo.toml | 4 ++ Makefile | 6 ++ src/marble_engine.rs | 157 ++++++++++++++++++++++++++++--------------- 4 files changed, 115 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index ab75dc1..9374d18 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /user* *.zip version.txt +perf.data* +flamegraph.svg diff --git a/Cargo.toml b/Cargo.toml index d7d32d7..91b6fc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,10 @@ raylib = "5.5" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" +[features] +# affects simulation sub-steps, used for profiling +inline_less = [] + [[bin]] name = "bench" path = "src/benchmark.rs" diff --git a/Makefile b/Makefile index 69bf9cf..e5a2446 100644 --- a/Makefile +++ b/Makefile @@ -22,3 +22,9 @@ windows: rm -rf ${RELEASE_DIRNAME}_win all: windows linux + +flamegraph: + cargo flamegraph --release --bin bench --features "inline_less" + +bench: + cargo run --release --bin bench diff --git a/src/marble_engine.rs b/src/marble_engine.rs index e18c13f..aee8eee 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -12,13 +12,19 @@ use tile::*; pub struct Machine { grid: Grid, marbles: Vec, - powered: Vec, input: Vec, input_index: usize, output: Vec, steps: usize, pub subtick_index: usize, pub debug_subticks: Vec, + // used across steps + powered: Vec, + // used within steps + new_marbles: Vec<(Pos, MarbleValue, Direction)>, + influenced_direction: Vec<(bool, DirInfluence)>, + claim_positions: Vec, + removed_marbles: Vec, } #[derive(Debug)] @@ -27,18 +33,29 @@ pub struct DebugSubTick { pub pos: Option, } +#[derive(Clone, Copy, Debug)] +enum DirInfluence { + None, + One(Direction), + Multiple, +} + impl Machine { pub fn new_empty() -> Self { Self { grid: Grid::new_empty(5, 5), marbles: Vec::new(), - powered: Vec::new(), input: Vec::new(), input_index: 0, output: Vec::new(), steps: 0, subtick_index: 0, debug_subticks: Vec::new(), + powered: Vec::new(), + new_marbles: Vec::new(), + influenced_direction: Vec::new(), + claim_positions: Vec::new(), + removed_marbles: Vec::new(), } } @@ -119,9 +136,51 @@ impl Machine { }); } + self.step_power_complete(); + + if self.marbles.is_empty() { + return; + } + + self.step_find_bounces(); + self.step_apply_direct_bounces(); + + self.step_prepare_creating_marbles(); let old_marbles = self.marbles.len(); - let mut new_marbles = Vec::new(); + self.step_create_marbles(); + + // #### movement #### + // mark claims to figure out what spaces can be moved to + self.step_mark_claims(old_marbles); + self.step_move_marbles(old_marbles); + + for pos in self.claim_positions.drain(..) { + if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) { + *claim_state = Claim::Free; + } + } + + // remove marbles + for &i in self.removed_marbles.iter().rev() { + self.grid.set(self.marbles[i], Tile::BLANK); + self.marbles.swap_remove(i); + } + + self.step_propagate_power(); + + #[cfg(debug_assertions)] + { + self.debug_subticks.push(DebugSubTick { + grid: self.grid.clone(), + pos: None, + }); + self.subtick_index = self.debug_subticks.len() - 1; + } + } + + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_power_complete(&mut self) { // activate all powered machines for &pos in &self.powered { match self.grid.get_mut(pos) { @@ -157,7 +216,7 @@ impl Machine { MathOp::Div => val_a.checked_div(val_b).unwrap_or_default(), MathOp::Rem => val_a.checked_rem(val_b).unwrap_or_default(), }; - new_marbles.push((front_pos, value, dir)); + self.new_marbles.push((front_pos, value, dir)); } } PTile::IO => { @@ -165,12 +224,12 @@ impl Machine { { let value = self.input[self.input_index] as MarbleValue; self.input_index += 1; - new_marbles.push((front_pos, value, dir)); + self.new_marbles.push((front_pos, value, dir)); } } PTile::Silo => { if front_tile == &Tile::BLANK { - new_marbles.push((front_pos, 0, dir)); + self.new_marbles.push((front_pos, 0, dir)); } } PTile::Flipper => { @@ -198,21 +257,13 @@ impl Machine { }; } self.powered.clear(); + } - if self.marbles.is_empty() { - return; - } - - #[derive(Clone, Copy, Debug)] - enum DirInfluence { - None, - One(Direction), - Multiple, - } + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_find_bounces(&mut self) { // #### find all direct bounces #### - let mut will_reverse_direction = vec![false; self.marbles.len()]; // todo store in tile to remove search through self.marbles - let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()]; + self.influenced_direction = vec![(false, DirInfluence::None); self.marbles.len()]; for (i, &pos) in self.marbles.iter().enumerate() { let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else { @@ -229,11 +280,11 @@ impl Machine { } => { if other_dir != dir { // this marble is facing another marble, and will therefore definitely bounce - will_reverse_direction[i] = true; + self.influenced_direction[i].0 = true; // the other marble will bounce too, either let other_index = self.marbles.iter().position(|m| *m == front_pos).unwrap(); - let influence = &mut influenced_direction[other_index]; + let influence = &mut self.influenced_direction[other_index].1; *influence = match *influence { DirInfluence::None => DirInfluence::One(dir), DirInfluence::One(_) => DirInfluence::Multiple, @@ -244,38 +295,49 @@ impl Machine { Tile::Arrow(arrow_dir) => { if arrow_dir == dir.opposite() { // bounce on a reverse facing arrow - will_reverse_direction[i] = true; + self.influenced_direction[i].0 = true; } } _ => (), } } + } + + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_apply_direct_bounces(&mut self) { // #### apply all direct bounces #### for (i, &pos) in self.marbles.iter().enumerate() { let Some(Tile::Marble { value: _, dir }) = self.grid.get_mut(pos) else { unreachable!() }; - if will_reverse_direction[i] { + if self.influenced_direction[i].0 { *dir = dir.opposite(); - } else if let DirInfluence::One(new_dir) = influenced_direction[i] { + } else if let DirInfluence::One(new_dir) = self.influenced_direction[i].1 { *dir = new_dir; } } + } + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_prepare_creating_marbles(&mut self) { // #### new marbles #### - let mut claim_positions = Vec::new(); + // self.claim_positions.clear(); // already drained // prepare creating the new marbles - for &(pos, _val, _dir) in &new_marbles { + for &(pos, _val, _dir) in &self.new_marbles { let Some(Tile::Open(OpenTile::Blank, claim)) = self.grid.get_mut(pos) else { unreachable!() }; if claim.claim_indirect() { - claim_positions.push(pos); + self.claim_positions.push(pos); } } + } + + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_create_marbles(&mut self) { // create new marbles // new marbles are past old_marbles index, so will not move this step - for (pos, value, dir) in new_marbles { + for (pos, value, dir) in self.new_marbles.drain(..) { let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.grid.get_mut(pos) else { continue; @@ -283,9 +345,10 @@ impl Machine { self.grid.set(pos, Tile::Marble { value, dir }); self.marbles.push(pos); } + } - // #### movement #### - // mark claims to figure out what spaces can be moved to + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_mark_claims(&mut self, old_marbles: usize) { for &pos in &self.marbles[..old_marbles] { let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else { unreachable!() @@ -296,7 +359,7 @@ impl Machine { }; if let Tile::Open(_type, claim) = front_tile { if claim.claim() { - claim_positions.push(front_pos); + self.claim_positions.push(front_pos); } } else { let target_pos = match front_tile { @@ -310,13 +373,16 @@ impl Machine { }; if let Tile::Open(_type, claim) = target_tile { if claim.claim_indirect() { - claim_positions.push(front_pos); + self.claim_positions.push(front_pos); } } } } + } - let mut removed_marbles = Vec::new(); + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_move_marbles(&mut self, old_marbles: usize) { + self.removed_marbles.clear(); // move marbles for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() { let Some(Tile::Marble { value, dir }) = self.grid.get(*pos) else { @@ -369,11 +435,11 @@ impl Machine { target_pos = dir.step(front_pos); } Tile::Powerable(PTile::Silo, _) => { - removed_marbles.push(i); + self.removed_marbles.push(i); continue; } Tile::Powerable(PTile::IO, _) => { - removed_marbles.push(i); + self.removed_marbles.push(i); self.output.push(value as u8); continue; } @@ -390,19 +456,10 @@ impl Machine { } } } + } - for pos in claim_positions { - if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) { - *claim_state = Claim::Free; - } - } - - // remove marbles - for &i in removed_marbles.iter().rev() { - self.grid.set(self.marbles[i], Tile::BLANK); - self.marbles.swap_remove(i); - } - + #[cfg_attr(feature = "inline_less", inline(never), no_mangle)] + fn step_propagate_power(&mut self) { // propagate power let mut i = 0; while i < self.powered.len() { @@ -527,13 +584,5 @@ impl Machine { } i += 1; } - #[cfg(debug_assertions)] - { - self.debug_subticks.push(DebugSubTick { - grid: self.grid.clone(), - pos: None, - }); - self.subtick_index = self.debug_subticks.len() - 1; - } } } From a7556785673f5ff2264b4afd8d6a621f11fed42e Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Sat, 12 Apr 2025 20:51:55 +0200 Subject: [PATCH 02/23] prevent bindings from triggering when a modifier is held down that matches a different binding --- src/input.rs | 62 ++++++++++--- src/layout.rs | 245 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +- src/ui2.rs | 142 +++++++++++++++++++++++++++++ 4 files changed, 440 insertions(+), 12 deletions(-) create mode 100644 src/layout.rs create mode 100644 src/ui2.rs diff --git a/src/input.rs b/src/input.rs index 4d695b0..bc4241b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -54,6 +54,7 @@ impl Default for Input { let mut bindings = [(); ActionId::SIZE].map(|_| Vec::new()); let mut bind_key = |action, mods, trigger| { bindings[action as usize].push(Binding { + blocking_modifiers: Vec::new(), modifiers: mods, trigger, }); @@ -141,21 +142,26 @@ impl Input { if text_button(d, &globals.mouse, buttons_x + 85, y, 45, "edit") { self.editing_binding = Some((action, binding_index, BindingEdit::Init)); } + // let trigger = format!("{:?}", binding.trigger); - d.draw_text(&trigger, binding_text_x, y + 5, 20, Color::LIMEGREEN); - let x = binding_text_x + 10 + d.measure_text(&trigger, 20); + let mut x = binding_text_x; + d.draw_text(&trigger, x, y + 5, 20, Color::LIMEGREEN); + x += 10 + d.measure_text(&trigger, 20); + // let modifiers = format!("{:?}", binding.modifiers); d.draw_text(&modifiers, x, y + 5, 20, Color::LIGHTBLUE); + x += 10 + d.measure_text(&modifiers, 20); + // let conflicts = conflicts(&self.bindings, binding, action); if !conflicts.is_empty() { - let x = x + 10 + d.measure_text(&modifiers, 20); - d.draw_text( - &format!("also used by: {conflicts:?}"), - x, - y + 5, - 20, - Color::ORANGERED, - ); + let conflict_text = format!("also used by: {conflicts:?}"); + d.draw_text(&conflict_text, x, y + 5, 20, Color::ORANGERED); + x += 10 + d.measure_text(&conflict_text, 20); + } + // + if !binding.blocking_modifiers.is_empty() { + let blocking_text = format!("not while: {:?}", binding.blocking_modifiers); + d.draw_text(&blocking_text, x, y + 5, 20, Color::GRAY); } y += 32; } @@ -193,6 +199,7 @@ impl Input { BindingEdit::Init => { if key.just_pressed(d) { *edit_state = BindingEdit::Adding(Binding { + blocking_modifiers: Vec::new(), modifiers: Vec::new(), trigger: key, }); @@ -218,6 +225,7 @@ impl Input { globals.mouse.is_over(ok_btn_rect) && key == Button::MouseLeft; if key.just_pressed(d) && !clicking_ok { *edit_state = BindingEdit::Adding(Binding { + blocking_modifiers: Vec::new(), modifiers: Vec::new(), trigger: key, }); @@ -255,6 +263,7 @@ impl Input { binding_list.push(binding.clone()); } self.editing_binding = None; + self.update_modifier_blocks(); } } if text_button(d, &globals.mouse, x + 100, y + 40, 80, "cancel") { @@ -268,7 +277,9 @@ impl Input { let bindings = &self.bindings[i]; let mut is_active = false; for binding in bindings { - if binding.modifiers.iter().all(|&m| m.is_down(rl)) { + if binding.modifiers.iter().all(|&m| m.is_down(rl)) + && !binding.blocking_modifiers.iter().any(|&m| m.is_down(rl)) + { is_active |= binding.trigger.is_down(rl); } } @@ -299,6 +310,33 @@ impl Input { pub fn is_released(&self, action: ActionId) -> bool { self.states[action as usize] == BindingState::Released } + + /// Must be called after any binding has changed. + /// Ensures a binding "S" is not triggered by "Ctrl+S", if "Ctrl+S" is bound to something else. + pub fn update_modifier_blocks(&mut self) { + for i in 0..ActionId::SIZE { + let bindings = &self.bindings[i]; + for binding_index in 0..bindings.len() { + let binding = &self.bindings[i][binding_index]; + let mut blocking_mods = Vec::new(); + for i in 0..ActionId::SIZE { + let other_bindings = &self.bindings[i]; + for other_binding in other_bindings { + if other_binding.trigger == binding.trigger { + for modifier in &other_binding.modifiers { + if !blocking_mods.contains(modifier) + && !binding.modifiers.contains(modifier) + { + blocking_mods.push(*modifier); + } + } + } + } + } + self.bindings[i][binding_index].blocking_modifiers = blocking_mods; + } + } + } } fn conflicts( @@ -338,6 +376,8 @@ impl ActionId { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Binding { + #[serde(skip)] + blocking_modifiers: Vec