Skip to content

Commit

Permalink
Clearing the entire drawing also stops all animations
Browse files Browse the repository at this point in the history
  • Loading branch information
sunjay committed Aug 29, 2020
1 parent bfd74a8 commit 4a06b0f
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 37 deletions.
32 changes: 13 additions & 19 deletions examples/concurrencytest.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
//! This example uses multiple turtles to demonstrate the sequential consistency guarantees of this
//! crate.
//! This example uses multiple turtles to demonstrate what happens when you clear the drawing while
//! other turtles are in the middle of drawing.
//!
//! Turtle 1: Draws a single long line across the top of the window
//! Thread 1: Draws a single long line across the top of the window
//!
//! Turtle 2: Draws many short lines across the top of the window
//! Thread 2: Draws many short lines across the top of the window
//!
//! Turtle 3: Draws a short line, then clears the image (including the lines from Turtle 1 and 2),
//! then continues drawing
//! Thread 3 (main thread): Draws a short line, then clears the entire drawing, then continues
//! drawing
//!
//! If Turtle 1 has already started drawing before Turtle 3 clears the image, Turtle 3 will have to
//! wait for Turtle 1 to finish drawing its line before it can clear the image and keep drawing.
//! This is because Turtle 1 has exclusive access to its turtle while it is drawing. The clear
//! command cannot execute until that line has finished drawing and it is given access.
//!
//! Turtle 2 also stops drawing when the drawing is cleared, but for different reasons than
//! Turtle 3. Turtle 2 can technically still continue because it isn't blocked on a clear command.
//! The issue is that when clear is run, it reserves all turtles, including Turtle 2. Turtle 2 has
//! to wait for the clear command to run before it can draw its next line. This is how we ensure
//! that commands run in the order they are executed (sequential consistency). No command gets
//! precedence just because the resources it needs are available.
//! When Thread 3 clears the entire drawing, the line from Thread 1 is deleted and the turtle is
//! stopped immediately at its current position. Thread 2 is also stopped, but continues drawing its
//! remaining lines from wherever it was stopped. Note that the lines in Thread 2 will not finish at
//! their intended position. This is because the code is not able to account for the fact that
//! Thread 2 may be interrupted while it is drawing a line.
// To run this example, use the command: cargo run --features unstable --example concurrencytest
#[cfg(all(not(feature = "unstable")))]
Expand Down Expand Up @@ -76,10 +70,10 @@ fn main() {
turtle3.set_pen_color("blue");
turtle3.set_pen_size(5.0);

turtle3.forward(100.0);
turtle3.forward(200.0);
drawing.clear();
turtle3.set_pen_color("red");
turtle3.forward(100.0);
turtle3.forward(200.0);

//TODO: Currently, if the main thread ends before the other threads, the window just closes
thread::park();
Expand Down
2 changes: 1 addition & 1 deletion src/renderer_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn dispatch_request(
},

ClearAll => {
handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop)
handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop, anim_runner)
},
ClearTurtle(id) => {
handlers::clear_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id)
Expand Down
62 changes: 45 additions & 17 deletions src/renderer_server/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,20 @@ fn rotate(angle: Radians, rotation: Radians, direction: RotationDirection) -> Ra
angle - radians::TWO_PI * (angle / radians::TWO_PI).floor()
}

#[derive(Debug)]
enum Message {
/// Run the given animation
Play(Animation),
/// Stop all animations that are currently playing
///
/// Animations stop at wherever they were last updated.
StopAll,
}

/// Spawns a task to manage running animations and drive them to completion
#[derive(Debug)]
pub struct AnimationRunner {
sender: mpsc::UnboundedSender<Animation>,
sender: mpsc::UnboundedSender<Message>,
}

impl AnimationRunner {
Expand All @@ -412,8 +422,16 @@ impl AnimationRunner {
}

pub fn play(&self, turtle_id: TurtleId, kind: impl Into<AnimationKind>, client_id: ClientId) {
self.sender.send(Animation::new(turtle_id, kind, client_id))
.expect("bug: animation runner task should run as long as server task")
self.send(Message::Play(Animation::new(turtle_id, kind, client_id)));
}

pub fn stop_all(&self) {
self.send(Message::StopAll);
}

fn send(&self, mess: Message) {
self.sender.send(mess)
.expect("bug: animation runner task should run as long as server task");
}
}

Expand All @@ -423,7 +441,7 @@ async fn animation_loop(
app: SharedApp,
display_list: SharedDisplayList,
event_loop: EventLoopNotifier,
mut incoming_animations: mpsc::UnboundedReceiver<Animation>,
mut receiver: mpsc::UnboundedReceiver<Message>,
) {
// Map of turtle ID to the current animation playing for it (if any)
let mut animations: HashMap<TurtleId, Animation> = HashMap::new();
Expand All @@ -438,20 +456,30 @@ async fn animation_loop(

loop {
tokio::select! {
anim = incoming_animations.recv() => {
let anim = match anim {
Some(anim) => anim,
// Sender has been dropped, so renderer server has ended
None => break,
};

// Insert the new animation so we can account for it when selecting the next update
// time. Keeping the previous next frame value since we don't want to bump to
// another future frame just because we got another animation.
debug_assert!(!animations.contains_key(&anim.turtle_id),
"bug: cannot animate turtle while another animation is playing");
animations.insert(anim.turtle_id, anim);
mess = receiver.recv() => match mess {
Some(Message::Play(anim)) => {
// Insert the new animation so we can account for it when selecting the next
// update time. Keeping the previous next frame value since we don't want to
// bump to another future frame just because we got another animation.
debug_assert!(!animations.contains_key(&anim.turtle_id),
"bug: cannot animate turtle while another animation is playing");
animations.insert(anim.turtle_id, anim);
},

Some(Message::StopAll) => {
// Complete all pending animations at their last update
for anim in animations.values() {
handle_handler_result(conn.send(
anim.client_id,
ServerResponse::AnimationComplete(anim.turtle_id),
).map_err(HandlerError::IpcChannelError));
}

animations.clear();
},

// Sender has been dropped, so renderer server has stopped running
None => break,
},

// Trigger an update once the next update time has elapsed
Expand Down
5 changes: 5 additions & 0 deletions src/renderer_server/handlers/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ use super::HandlerError;
use super::super::{
event_loop_notifier::EventLoopNotifier,
app::{App, TurtleId, TurtleDrawings},
animation::AnimationRunner,
renderer::display_list::DisplayList,
};

pub(crate) fn clear_all(
app: &mut App,
display_list: &mut DisplayList,
event_loop: &EventLoopNotifier,
anim_runner: &AnimationRunner,
) -> Result<(), HandlerError> {
display_list.clear();

Expand All @@ -19,6 +21,9 @@ pub(crate) fn clear_all(
*current_fill_polygon = None;
}

// Stop all animations that may have been running
anim_runner.stop_all();

// Signal the main thread that the image has changed
event_loop.request_redraw()?;

Expand Down

0 comments on commit 4a06b0f

Please sign in to comment.