add threading
This commit is contained in:
parent
fcca1a2db4
commit
3f21afadb2
@ -13,6 +13,7 @@ tracing = { version = "0.1", features = [
|
|||||||
"attributes",
|
"attributes",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
||||||
|
crossbeam = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# For examples and testing
|
# For examples and testing
|
||||||
|
|||||||
223
turtle-lib/src/commands_channel.rs
Normal file
223
turtle-lib/src/commands_channel.rs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
//! Per-turtle command channels for multi-threaded game logic
|
||||||
|
//!
|
||||||
|
//! Enables sending turtle commands from game logic threads to the render thread
|
||||||
|
//! without blocking the render loop.
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use turtle_lib::*;
|
||||||
|
//! use std::thread;
|
||||||
|
//!
|
||||||
|
//! # #[macroquad::main("Threading")]
|
||||||
|
//! # async fn main() {
|
||||||
|
//! let mut app = TurtleApp::new();
|
||||||
|
//!
|
||||||
|
//! // Create a turtle and get its command sender
|
||||||
|
//! let turtle_tx = app.create_turtle_channel(100);
|
||||||
|
//!
|
||||||
|
//! // Spawn a game logic thread
|
||||||
|
//! thread::spawn({
|
||||||
|
//! let tx = turtle_tx.clone();
|
||||||
|
//! move || {
|
||||||
|
//! let mut plan = create_turtle_plan();
|
||||||
|
//! plan.forward(100.0).right(90.0);
|
||||||
|
//! tx.send(plan.build()).ok();
|
||||||
|
//! }
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! // Main render loop
|
||||||
|
//! loop {
|
||||||
|
//! clear_background(WHITE);
|
||||||
|
//! app.process_commands();
|
||||||
|
//! app.update();
|
||||||
|
//! app.render();
|
||||||
|
//! next_frame().await;
|
||||||
|
//! }
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::commands::CommandQueue;
|
||||||
|
use crossbeam::channel::{bounded, Receiver, Sender};
|
||||||
|
|
||||||
|
/// Sender for turtle commands from a game logic thread
|
||||||
|
///
|
||||||
|
/// This is tied to a specific turtle created via `TurtleApp::create_turtle_channel()`.
|
||||||
|
/// The turtle is guaranteed to exist on the render thread.
|
||||||
|
///
|
||||||
|
/// # Thread Safety
|
||||||
|
/// Can be cloned and shared across threads. Multiple game threads can send
|
||||||
|
/// commands to the same turtle safely.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # fn example() -> Result<(), String> {
|
||||||
|
/// # let mut app = TurtleApp::new();
|
||||||
|
/// let tx = app.create_turtle_channel(100);
|
||||||
|
///
|
||||||
|
/// // Send commands from game thread
|
||||||
|
/// let mut plan = create_turtle_plan();
|
||||||
|
/// plan.forward(50.0);
|
||||||
|
/// tx.send(plan.build())?;
|
||||||
|
///
|
||||||
|
/// // Or non-blocking variant
|
||||||
|
/// tx.try_send(plan.build()).ok();
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TurtleCommandSender {
|
||||||
|
turtle_id: usize,
|
||||||
|
tx: Sender<CommandQueue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receiver for turtle commands on the render thread
|
||||||
|
///
|
||||||
|
/// Paired with `TurtleCommandSender` via `turtle_command_channel()`.
|
||||||
|
/// Automatically managed by `TurtleApp::process_commands()`.
|
||||||
|
pub struct TurtleCommandReceiver {
|
||||||
|
turtle_id: usize,
|
||||||
|
rx: Receiver<CommandQueue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurtleCommandSender {
|
||||||
|
/// Get the turtle ID this sender is bound to
|
||||||
|
#[must_use]
|
||||||
|
pub fn turtle_id(&self) -> usize {
|
||||||
|
self.turtle_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send commands (blocking)
|
||||||
|
///
|
||||||
|
/// Blocks if the channel buffer is full. This is appropriate for game logic
|
||||||
|
/// threads where blocking is acceptable. The buffer size is specified when
|
||||||
|
/// creating the channel.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns error if the receiver has been dropped (render thread exited).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # fn example() -> Result<(), String> {
|
||||||
|
/// # let mut app = TurtleApp::new();
|
||||||
|
/// # let tx = app.create_turtle_channel(100);
|
||||||
|
/// let mut plan = create_turtle_plan();
|
||||||
|
/// plan.forward(100.0);
|
||||||
|
/// tx.send(plan.build())?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn send(&self, queue: CommandQueue) -> Result<(), String> {
|
||||||
|
self.tx
|
||||||
|
.send(queue)
|
||||||
|
.map_err(|e| format!("Channel disconnected: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send commands (non-blocking)
|
||||||
|
///
|
||||||
|
/// Returns immediately. If the channel buffer is full, returns an error
|
||||||
|
/// without blocking.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns error if the buffer is full or the receiver has been dropped.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # fn example() {
|
||||||
|
/// # let mut app = TurtleApp::new();
|
||||||
|
/// # let tx = app.create_turtle_channel(100);
|
||||||
|
/// let mut plan = create_turtle_plan();
|
||||||
|
/// plan.forward(100.0);
|
||||||
|
/// tx.try_send(plan.build()).ok(); // Ignore if buffer full
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn try_send(&self, queue: CommandQueue) -> Result<(), String> {
|
||||||
|
self.tx
|
||||||
|
.try_send(queue)
|
||||||
|
.map_err(|e| format!("Failed to send: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurtleCommandReceiver {
|
||||||
|
/// Get the turtle ID this receiver is bound to
|
||||||
|
#[must_use]
|
||||||
|
pub fn turtle_id(&self) -> usize {
|
||||||
|
self.turtle_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain all pending commands for this turtle (non-blocking)
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # async fn example() {
|
||||||
|
/// # let mut app = TurtleApp::new();
|
||||||
|
/// # let _tx = app.create_turtle_channel(100);
|
||||||
|
/// // This is called automatically by app.process_commands()
|
||||||
|
/// // But you can also do it manually:
|
||||||
|
/// loop {
|
||||||
|
/// app.update();
|
||||||
|
/// app.render();
|
||||||
|
/// # break;
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn recv_all(&self) -> Vec<CommandQueue> {
|
||||||
|
self.rx.try_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to receive one command batch (non-blocking)
|
||||||
|
#[must_use]
|
||||||
|
pub fn try_recv(&self) -> Option<CommandQueue> {
|
||||||
|
self.rx.try_recv().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this receiver's queue is empty
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.rx.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of pending command batches
|
||||||
|
#[must_use]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.rx.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a command channel for a specific turtle
|
||||||
|
///
|
||||||
|
/// The tuple represents (sender, receiver) where:
|
||||||
|
/// - Sender goes to game logic threads (cloneable, can be distributed)
|
||||||
|
/// - Receiver stays in the render thread (part of TurtleApp internally)
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `turtle_id` - The ID of the turtle this channel is for (must be valid)
|
||||||
|
/// * `buffer_size` - Maximum number of pending command batches before sender blocks
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if buffer_size is 0.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # fn example() {
|
||||||
|
/// let (tx, _rx) = turtle_command_channel(0, 100);
|
||||||
|
/// // Sender goes to game threads
|
||||||
|
/// // Receiver stays in render thread (or TurtleApp)
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn turtle_command_channel(
|
||||||
|
turtle_id: usize,
|
||||||
|
buffer_size: usize,
|
||||||
|
) -> (TurtleCommandSender, TurtleCommandReceiver) {
|
||||||
|
assert!(buffer_size > 0, "buffer_size must be > 0");
|
||||||
|
let (tx, rx) = bounded(buffer_size);
|
||||||
|
(
|
||||||
|
TurtleCommandSender { turtle_id, tx },
|
||||||
|
TurtleCommandReceiver { turtle_id, rx },
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -49,6 +49,7 @@
|
|||||||
pub mod builders;
|
pub mod builders;
|
||||||
pub mod circle_geometry;
|
pub mod circle_geometry;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod commands_channel;
|
||||||
pub mod drawing;
|
pub mod drawing;
|
||||||
pub mod execution;
|
pub mod execution;
|
||||||
pub mod general;
|
pub mod general;
|
||||||
@ -60,6 +61,7 @@ pub mod tweening;
|
|||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
|
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
|
||||||
pub use commands::{CommandQueue, TurtleCommand};
|
pub use commands::{CommandQueue, TurtleCommand};
|
||||||
|
pub use commands_channel::{turtle_command_channel, TurtleCommandReceiver, TurtleCommandSender};
|
||||||
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
||||||
pub use shapes::{ShapeType, TurtleShape};
|
pub use shapes::{ShapeType, TurtleShape};
|
||||||
pub use state::{DrawCommand, Turtle, TurtleWorld};
|
pub use state::{DrawCommand, Turtle, TurtleWorld};
|
||||||
@ -74,10 +76,13 @@ pub use macroquad::prelude::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Main turtle application struct
|
/// Main turtle application struct
|
||||||
pub struct TurtleApp {
|
pub struct TurtleApp {
|
||||||
world: TurtleWorld,
|
world: TurtleWorld,
|
||||||
|
// Receivers for turtle command channels
|
||||||
|
receivers: HashMap<usize, TurtleCommandReceiver>,
|
||||||
// Mouse panning state
|
// Mouse panning state
|
||||||
is_dragging: bool,
|
is_dragging: bool,
|
||||||
last_mouse_pos: Option<Vec2>,
|
last_mouse_pos: Option<Vec2>,
|
||||||
@ -91,6 +96,7 @@ impl TurtleApp {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
world: TurtleWorld::new(),
|
world: TurtleWorld::new(),
|
||||||
|
receivers: HashMap::new(),
|
||||||
is_dragging: false,
|
is_dragging: false,
|
||||||
last_mouse_pos: None,
|
last_mouse_pos: None,
|
||||||
zoom_level: 1.0,
|
zoom_level: 1.0,
|
||||||
@ -102,6 +108,78 @@ impl TurtleApp {
|
|||||||
self.world.add_turtle()
|
self.world.add_turtle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a turtle and a command channel for it
|
||||||
|
///
|
||||||
|
/// This is the preferred way to set up turtles when using threading.
|
||||||
|
/// Call this ONCE per turtle during setup, before spawning game logic threads.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `buffer_size` - Maximum pending command batches before sender blocks (typically 50-200)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `TurtleCommandSender` that can be cloned and sent to game logic threads.
|
||||||
|
/// The turtle is automatically managed by TurtleApp.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # #[macroquad::main("Threading")]
|
||||||
|
/// # async fn main() {
|
||||||
|
/// let mut app = TurtleApp::new();
|
||||||
|
///
|
||||||
|
/// // Create turtle and get sender
|
||||||
|
/// let turtle_tx = app.create_turtle_channel(100);
|
||||||
|
///
|
||||||
|
/// // Send to game threads
|
||||||
|
/// let tx_clone = turtle_tx.clone();
|
||||||
|
/// std::thread::spawn(move || {
|
||||||
|
/// let mut plan = create_turtle_plan();
|
||||||
|
/// plan.forward(100.0);
|
||||||
|
/// tx_clone.send(plan.build()).ok();
|
||||||
|
/// });
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn create_turtle_channel(&mut self, buffer_size: usize) -> TurtleCommandSender {
|
||||||
|
let turtle_id = self.world.add_turtle();
|
||||||
|
let (tx, rx) = commands_channel::turtle_command_channel(turtle_id, buffer_size);
|
||||||
|
self.receivers.insert(turtle_id, rx);
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process all pending commands from all turtle channels
|
||||||
|
///
|
||||||
|
/// Call this once per frame in your render loop, before `update()`.
|
||||||
|
/// Drains all receivers and applies commands to their respective turtles.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```no_run
|
||||||
|
/// # use turtle_lib::*;
|
||||||
|
/// # #[macroquad::main("Threading")]
|
||||||
|
/// # async fn main() {
|
||||||
|
/// # let mut app = TurtleApp::new();
|
||||||
|
/// # let _tx = app.create_turtle_channel(100);
|
||||||
|
/// loop {
|
||||||
|
/// clear_background(WHITE);
|
||||||
|
/// app.process_commands(); // ← Process channel commands
|
||||||
|
/// app.update();
|
||||||
|
/// app.render();
|
||||||
|
/// next_frame().await;
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn process_commands(&mut self) {
|
||||||
|
// Collect all turtle IDs to avoid borrow issues
|
||||||
|
let turtle_ids: Vec<usize> = self.receivers.keys().copied().collect();
|
||||||
|
|
||||||
|
for turtle_id in turtle_ids {
|
||||||
|
if let Some(receiver) = self.receivers.get(&turtle_id) {
|
||||||
|
for queue in receiver.recv_all() {
|
||||||
|
self.append_commands(turtle_id, queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Add commands from a turtle plan to the application for the default turtle (ID 0)
|
/// Add commands from a turtle plan to the application for the default turtle (ID 0)
|
||||||
///
|
///
|
||||||
/// Speed is controlled by `SetSpeed` commands in the queue.
|
/// Speed is controlled by `SetSpeed` commands in the queue.
|
||||||
@ -157,6 +235,21 @@ impl TurtleApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append commands from a CommandQueue to a turtle's animation queue
|
||||||
|
///
|
||||||
|
/// Used internally by `process_commands()` and can be used directly
|
||||||
|
/// when you have a `CommandQueue` instead of a `TurtlePlan`.
|
||||||
|
pub fn append_commands(&mut self, turtle_id: usize, queue: CommandQueue) {
|
||||||
|
// Ensure turtle exists
|
||||||
|
while self.world.turtles.len() <= turtle_id {
|
||||||
|
self.world.add_turtle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
|
||||||
|
turtle.tween_controller.append_commands(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Update animation state (call every frame)
|
/// Update animation state (call every frame)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
// Handle mouse panning and zoom
|
// Handle mouse panning and zoom
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user