Run ticks in a background thread in game_of_life

This commit is contained in:
Héctor Ramón Jiménez 2020-05-02 07:01:27 +02:00
parent 8fa9e4c94e
commit 916a1bfc70
2 changed files with 211 additions and 52 deletions

View File

@ -7,5 +7,6 @@ publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] } iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
tokio = { version = "0.2", features = ["blocking"] }
itertools = "0.9" itertools = "0.9"
rustc-hash = "1.1" rustc-hash = "1.1"

View File

@ -19,7 +19,7 @@ pub fn main() {
#[derive(Default)] #[derive(Default)]
struct GameOfLife { struct GameOfLife {
grid: Grid, grid: Grid,
is_playing: bool, state: State,
speed: u64, speed: u64,
next_speed: Option<u64>, next_speed: Option<u64>,
toggle_button: button::State, toggle_button: button::State,
@ -28,6 +28,17 @@ struct GameOfLife {
speed_slider: slider::State, speed_slider: slider::State,
} }
enum State {
Paused,
Playing { last_tick: Instant },
}
impl Default for State {
fn default() -> State {
State::Paused
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Grid(grid::Message), Grid(grid::Message),
@ -62,37 +73,61 @@ impl Application for GameOfLife {
Message::Grid(message) => { Message::Grid(message) => {
self.grid.update(message); self.grid.update(message);
} }
Message::Tick(_) | Message::Next => { Message::Tick(_) | Message::Next => match &mut self.state {
self.grid.tick(); State::Paused => {
if let Some(task) = self.grid.tick(1) {
if let Some(speed) = self.next_speed.take() { return Command::perform(task, Message::Grid);
self.speed = speed; }
} }
} State::Playing { last_tick } => {
let seconds_elapsed =
last_tick.elapsed().as_millis() as f32 / 1000.0;
let needed_ticks =
(self.speed as f32 * seconds_elapsed).ceil() as usize;
if let Some(task) = self.grid.tick(needed_ticks) {
*last_tick = Instant::now();
if let Some(speed) = self.next_speed.take() {
self.speed = speed;
}
return Command::perform(task, Message::Grid);
}
}
},
Message::Toggle => { Message::Toggle => {
self.is_playing = !self.is_playing; self.state = match self.state {
State::Paused => State::Playing {
last_tick: Instant::now(),
},
State::Playing { .. } => State::Paused,
};
} }
Message::Clear => { Message::Clear => {
self.grid = Grid::default(); self.grid.clear();
} }
Message::SpeedChanged(speed) => { Message::SpeedChanged(speed) => match self.state {
if self.is_playing { State::Paused => {
self.next_speed = Some(speed.round() as u64);
} else {
self.speed = speed.round() as u64; self.speed = speed.round() as u64;
} }
} State::Playing { .. } => {
self.next_speed = Some(speed.round() as u64);
}
},
} }
Command::none() Command::none()
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
if self.is_playing { match self.state {
time::every(Duration::from_millis(1000 / self.speed)) State::Paused => Subscription::none(),
.map(Message::Tick) State::Playing { .. } => {
} else { time::every(Duration::from_millis(1000 / self.speed))
Subscription::none() .map(Message::Tick)
}
} }
} }
@ -102,7 +137,11 @@ impl Application for GameOfLife {
.push( .push(
Button::new( Button::new(
&mut self.toggle_button, &mut self.toggle_button,
Text::new(if self.is_playing { "Pause" } else { "Play" }), Text::new(if let State::Paused = self.state {
"Play"
} else {
"Pause"
}),
) )
.on_press(Message::Toggle) .on_press(Message::Toggle)
.style(style::Button), .style(style::Button),
@ -111,11 +150,6 @@ impl Application for GameOfLife {
Button::new(&mut self.next_button, Text::new("Next")) Button::new(&mut self.next_button, Text::new("Next"))
.on_press(Message::Next) .on_press(Message::Next)
.style(style::Button), .style(style::Button),
)
.push(
Button::new(&mut self.clear_button, Text::new("Clear"))
.on_press(Message::Clear)
.style(style::Button),
); );
let selected_speed = self.next_speed.unwrap_or(self.speed); let selected_speed = self.next_speed.unwrap_or(self.speed);
@ -138,10 +172,14 @@ impl Application for GameOfLife {
.padding(10) .padding(10)
.spacing(20) .spacing(20)
.push(playback_controls) .push(playback_controls)
.push(speed_controls); .push(speed_controls)
.push(
Button::new(&mut self.clear_button, Text::new("Clear"))
.on_press(Message::Clear)
.style(style::Button),
);
let content = Column::new() let content = Column::new()
.spacing(10)
.align_items(Align::Center) .align_items(Align::Center)
.push(self.grid.view().map(Message::Grid)) .push(self.grid.view().map(Message::Grid))
.push(controls); .push(controls);
@ -160,28 +198,40 @@ mod grid {
mouse, Color, Element, Length, Point, Rectangle, Size, Vector, mouse, Color, Element, Length, Point, Rectangle, Size, Vector,
}; };
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use std::future::Future;
pub struct Grid { pub struct Grid {
life: Life, state: State,
interaction: Interaction, interaction: Interaction,
cache: Cache, cache: Cache,
translation: Vector, translation: Vector,
scaling: f32, scaling: f32,
version: usize,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
Populate(Cell), Populate(Cell),
Ticked {
result: Result<Life, TickError>,
version: usize,
},
}
#[derive(Debug, Clone)]
pub enum TickError {
JoinFailed,
} }
impl Default for Grid { impl Default for Grid {
fn default() -> Self { fn default() -> Self {
Self { Self {
life: Life::default(), state: State::default(),
interaction: Interaction::None, interaction: Interaction::None,
cache: Cache::default(), cache: Cache::default(),
translation: Vector::default(), translation: Vector::default(),
scaling: 1.0, scaling: 1.0,
version: 0,
} }
} }
} }
@ -190,17 +240,44 @@ mod grid {
const MIN_SCALING: f32 = 0.1; const MIN_SCALING: f32 = 0.1;
const MAX_SCALING: f32 = 2.0; const MAX_SCALING: f32 = 2.0;
pub fn tick(&mut self) { pub fn tick(
self.life.tick(); &mut self,
self.cache.clear() amount: usize,
) -> Option<impl Future<Output = Message>> {
use iced::futures::FutureExt;
let version = self.version;
let tick = self.state.tick(amount)?;
Some(tick.map(move |result| Message::Ticked { result, version }))
}
pub fn clear(&mut self) {
self.state = State::default();
self.version += 1;
self.cache.clear();
} }
pub fn update(&mut self, message: Message) { pub fn update(&mut self, message: Message) {
match message { match message {
Message::Populate(cell) => { Message::Populate(cell) => {
self.life.populate(cell); self.state.populate(cell);
self.cache.clear() self.cache.clear()
} }
Message::Ticked {
result: Ok(life),
version,
} if version == self.version => {
self.state.update(life);
self.cache.clear()
}
Message::Ticked {
result: Err(error), ..
} => {
dbg!(error);
}
Message::Ticked { .. } => {}
} }
} }
@ -211,11 +288,11 @@ mod grid {
.into() .into()
} }
pub fn visible_region(&self, size: Size) -> Rectangle { pub fn visible_region(&self, size: Size) -> Region {
let width = size.width / self.scaling; let width = size.width / self.scaling;
let height = size.height / self.scaling; let height = size.height / self.scaling;
Rectangle { Region {
x: -self.translation.x - width / 2.0, x: -self.translation.x - width / 2.0,
y: -self.translation.y - height / 2.0, y: -self.translation.y - height / 2.0,
width, width,
@ -247,7 +324,7 @@ mod grid {
let cursor_position = cursor.position_in(&bounds)?; let cursor_position = cursor.position_in(&bounds)?;
let cell = Cell::at(self.project(cursor_position, bounds.size())); let cell = Cell::at(self.project(cursor_position, bounds.size()));
let populate = if self.life.contains(&cell) { let populate = if self.state.contains(&cell) {
None None
} else { } else {
Some(Message::Populate(cell)) Some(Message::Populate(cell))
@ -339,7 +416,7 @@ mod grid {
let region = self.visible_region(frame.size()); let region = self.visible_region(frame.size());
for cell in self.life.within(region) { for cell in region.view(self.state.cells()) {
frame.fill_rectangle( frame.fill_rectangle(
Point::new(cell.j as f32, cell.i as f32), Point::new(cell.j as f32, cell.i as f32),
Size::UNIT, Size::UNIT,
@ -394,6 +471,63 @@ mod grid {
} }
#[derive(Default)] #[derive(Default)]
struct State {
life: Life,
births: FxHashSet<Cell>,
is_ticking: bool,
}
impl State {
fn contains(&self, cell: &Cell) -> bool {
self.life.contains(cell) || self.births.contains(cell)
}
fn cells(&self) -> impl Iterator<Item = &Cell> {
self.life.iter().chain(self.births.iter())
}
fn populate(&mut self, cell: Cell) {
if self.is_ticking {
self.births.insert(cell);
} else {
self.life.populate(cell);
}
}
fn update(&mut self, mut life: Life) {
self.births.drain().for_each(|cell| life.populate(cell));
self.life = life;
self.is_ticking = false;
}
fn tick(
&mut self,
amount: usize,
) -> Option<impl Future<Output = Result<Life, TickError>>> {
if self.is_ticking {
return None;
}
self.is_ticking = true;
let mut life = self.life.clone();
Some(async move {
tokio::task::spawn_blocking(move || {
for _ in 0..amount {
life.tick();
}
life
})
.await
.map_err(|_| TickError::JoinFailed)
})
}
}
#[derive(Clone, Default)]
pub struct Life { pub struct Life {
cells: FxHashSet<Cell>, cells: FxHashSet<Cell>,
} }
@ -433,21 +567,16 @@ mod grid {
self.cells.insert(cell); self.cells.insert(cell);
} }
fn within(&self, region: Rectangle) -> impl Iterator<Item = &Cell> { pub fn iter(&self) -> impl Iterator<Item = &Cell> {
let first_row = (region.y / Cell::SIZE as f32).floor() as isize; self.cells.iter()
let first_column = (region.x / Cell::SIZE as f32).floor() as isize; }
}
let visible_rows = impl std::fmt::Debug for Life {
(region.height / Cell::SIZE as f32).ceil() as isize; fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let visible_columns = f.debug_struct("Life")
(region.width / Cell::SIZE as f32).ceil() as isize; .field("cells", &self.cells.len())
.finish()
let rows = first_row..=first_row + visible_rows;
let columns = first_column..=first_column + visible_columns;
self.cells.iter().filter(move |cell| {
rows.contains(&cell.i) && columns.contains(&cell.j)
})
} }
} }
@ -484,6 +613,35 @@ mod grid {
} }
} }
pub struct Region {
x: f32,
y: f32,
width: f32,
height: f32,
}
impl Region {
fn view<'a>(
&self,
cells: impl Iterator<Item = &'a Cell>,
) -> impl Iterator<Item = &'a Cell> {
let first_row = (self.y / Cell::SIZE as f32).floor() as isize;
let first_column = (self.x / Cell::SIZE as f32).floor() as isize;
let visible_rows =
(self.height / Cell::SIZE as f32).ceil() as isize;
let visible_columns =
(self.width / Cell::SIZE as f32).ceil() as isize;
let rows = first_row..=first_row + visible_rows;
let columns = first_column..=first_column + visible_columns;
cells.filter(move |cell| {
rows.contains(&cell.i) && columns.contains(&cell.j)
})
}
}
enum Interaction { enum Interaction {
None, None,
Drawing, Drawing,