Eliminate Per-Frame Allocation in Rendering Loop

We resolved **Issue 4.3 (Severe Per-Frame Allocation in Rendering
Loop)** by removing the intermediate `MeshData` structure and storing
`macroquad::prelude::Mesh` directly in
[`DrawCommand::Mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs).

[`DrawCommand`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs)
- Dropped `Clone` and `Debug` derives from
  [`Turtle`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L58)
  and
  [`DrawCommand`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L351),
  accommodating Macroquad's `Mesh` which does not implement these
  traits.
- Changed
  [`DrawCommand::Mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L353)
  to the tuple variant `DrawCommand::Mesh(macroquad::prelude::Mesh)`.
- Deleted `struct MeshData` and its `to_mesh` method.

[`tessellation.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs)
- Renamed `build_mesh_data` to
  [`build_mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L35)
  returning `macroquad::prelude::Mesh { vertices, indices, texture: None
  }` directly.
- Updated
  [`tessellate_polygon`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L71),
  [`tessellate_multi_contour`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L116),
  [`tessellate_stroke`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L214),
  [`tessellate_circle`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L259),
  and
  [`tessellate_arc`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L308)
  to return `Result<Mesh, Box<dyn std::error::Error>>`.

[`drawing.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs)
- In the main drawing loop
  ([`drawing.rs:36-38`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L36-L38)):
  ```rust
  DrawCommand::Mesh(mesh) => {
  draw_mesh(mesh);
  }
  ```
  Now borrows `&Mesh` directly from the command vector with **0 heap
  allocations and 0 vector clones per frame**.
- Updated fill preview ([line
  228](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L228)),
  tween arc/center indicators ([lines 309,
  329](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L309)),
  and turtle shape drawing ([line
  349](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L349))
  to pass `&mesh` directly to `draw_mesh(&mesh)` without vector cloning.

[`execution.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs)
- Updated
  [`commands.push(DrawCommand::Mesh(mesh))`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L135)
  and
  [`tessellate_command`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L337).
- Updated
  [`test_forward_left_forward`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L487)
  to instantiate `mut state` directly without dummy cloning.

---

```bash
cargo test --lib
```
Output:
```
running 10 tests
test circle_geometry::tests::test_circle_left_geometry ... ok
test circle_geometry::tests::test_circle_right_geometry ... ok
test general::angle::tests::degrees_to_radians_roundtrip ... ok
test general::angle::tests::from_integer ... ok
test execution::tests::test_forward_left_forward ... ok
test general::angle::tests::negation ... ok
test tweening::tests::test_animated_mode_pops_to_current_tween ... ok
test tweening::tests::test_instant_mode_drains_queue ... ok
test
tweening::tests::test_instant_mode_respects_batch_limit_and_retains_pending
... ok
test tweening::tests::test_streaming_append_commands_does_not_accumulate
... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s
```

All targets verified with `cargo check --all-targets` and `cargo clippy
--lib`.
This commit is contained in:
2026-09-18 21:34:07 +02:00
parent 1d0e149d22
commit ea9a8839c4
4 changed files with 33 additions and 68 deletions
+10 -10
View File
@@ -33,8 +33,8 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
for turtle in &world.turtles { for turtle in &world.turtles {
for cmd in &turtle.commands { for cmd in &turtle.commands {
match cmd { match cmd {
DrawCommand::Mesh { data } => { DrawCommand::Mesh(mesh) => {
draw_mesh(&data.to_mesh()); draw_mesh(mesh);
} }
DrawCommand::Text { DrawCommand::Text {
text, text,
@@ -224,8 +224,8 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
&all_contours, &all_contours,
fill_state.fill_color, fill_state.fill_color,
) { ) {
Ok(mesh_data) => { Ok(mesh) => {
draw_mesh(&mesh_data.to_mesh()); draw_mesh(&mesh);
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to tessellate fill preview: {:?}", e); tracing::error!("Failed to tessellate fill preview: {:?}", e);
@@ -304,9 +304,9 @@ fn draw_tween_arc(
); );
// Draw center using Lyon tessellation this helps visualizing what is done. // Draw center using Lyon tessellation this helps visualizing what is done.
if let Ok(mesh_data) = crate::tessellation::tessellate_circle(geom.center, 5.0, GRAY, true, 1.0) if let Ok(mesh) = crate::tessellation::tessellate_circle(geom.center, 5.0, GRAY, true, 1.0)
{ {
draw_mesh(&mesh_data.to_mesh()); draw_mesh(&mesh);
} }
// Calculate how much of the arc we've traveled based on tween progress // Calculate how much of the arc we've traveled based on tween progress
@@ -316,7 +316,7 @@ fn draw_tween_arc(
let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1 let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
// Use Lyon to tessellate and draw the partial arc // Use Lyon to tessellate and draw the partial arc
if let Ok(mesh_data) = crate::tessellation::tessellate_arc( if let Ok(mesh) = crate::tessellation::tessellate_arc(
geom.center, geom.center,
radius, radius,
geom.start_angle_from_center.to_degrees(), geom.start_angle_from_center.to_degrees(),
@@ -326,7 +326,7 @@ fn draw_tween_arc(
((steps as f32 * progress).ceil() as usize).max(1), ((steps as f32 * progress).ceil() as usize).max(1),
direction, direction,
) { ) {
draw_mesh(&mesh_data.to_mesh()); draw_mesh(&mesh);
} }
} }
@@ -343,10 +343,10 @@ pub(crate) fn draw_turtle(turtle_params: &TurtleParams) {
.collect(); .collect();
// Use Lyon for turtle shape too // Use Lyon for turtle shape too
if let Ok(mesh_data) = if let Ok(mesh) =
tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0)) tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0))
{ {
draw_mesh(&mesh_data.to_mesh()); draw_mesh(&mesh);
} else { } else {
// Fallback to simple triangle fan if Lyon fails // Fallback to simple triangle fan if Lyon fails
let first = absolute_vertices[0]; let first = absolute_vertices[0];
+7 -22
View File
@@ -123,7 +123,7 @@ pub(crate) fn execute_command_side_effects(
} }
if !fill_state.contours.is_empty() { if !fill_state.contours.is_empty() {
if let Ok(mesh_data) = tessellation::tessellate_multi_contour( if let Ok(mesh) = tessellation::tessellate_multi_contour(
&fill_state.contours, &fill_state.contours,
fill_state.fill_color, fill_state.fill_color,
) { ) {
@@ -132,7 +132,7 @@ pub(crate) fn execute_command_side_effects(
contours = fill_state.contours.len(), contours = fill_state.contours.len(),
"Successfully created fill mesh - persisting to commands" "Successfully created fill mesh - persisting to commands"
); );
commands.push(DrawCommand::Mesh { data: mesh_data }); commands.push(DrawCommand::Mesh(mesh));
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
svg_log.push(crate::state::SvgRecord::Fill { svg_log.push(crate::state::SvgRecord::Fill {
contours: fill_state.contours, contours: fill_state.contours,
@@ -326,7 +326,7 @@ pub(crate) fn tessellate_command(
match command { match command {
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
let mesh_data = tessellation::tessellate_stroke( let mesh = tessellation::tessellate_stroke(
&[start.position, end_position], &[start.position, end_position],
start.color, start.color,
start.pen_width, start.pen_width,
@@ -334,7 +334,7 @@ pub(crate) fn tessellate_command(
) )
.ok()?; .ok()?;
Some(DrawCommand::Mesh { data: mesh_data }) Some(DrawCommand::Mesh(mesh))
} }
TurtleCommand::Circle { TurtleCommand::Circle {
@@ -350,7 +350,7 @@ pub(crate) fn tessellate_command(
*radius, *radius,
*direction, *direction,
); );
let mesh_data = tessellation::tessellate_arc( let mesh = tessellation::tessellate_arc(
geom.center, geom.center,
*radius, *radius,
geom.start_angle_from_center.to_degrees(), geom.start_angle_from_center.to_degrees(),
@@ -362,7 +362,7 @@ pub(crate) fn tessellate_command(
) )
.ok()?; .ok()?;
Some(DrawCommand::Mesh { data: mesh_data }) Some(DrawCommand::Mesh(mesh))
} }
// `produces_drawing()` guards entry — this arm is only reachable if // `produces_drawing()` guards entry — this arm is only reachable if
@@ -484,7 +484,7 @@ mod tests {
// the turtle ends up at (100, -50) from initial position (0, 0) // the turtle ends up at (100, -50) from initial position (0, 0)
use crate::state::TurtleParams; use crate::state::TurtleParams;
let state = Turtle { let mut state = Turtle {
turtle_id: 0, turtle_id: 0,
params: TurtleParams { params: TurtleParams {
position: vec2(0.0, 0.0), position: vec2(0.0, 0.0),
@@ -503,21 +503,6 @@ mod tests {
tween_controller: TweenController::default(), tween_controller: TweenController::default(),
}; };
// We'll use a dummy world but won't actually call drawing commands
let world = TurtleWorld {
turtles: vec![state.clone()],
camera: macroquad::camera::Camera2D {
zoom: vec2(1.0, 1.0),
target: vec2(0.0, 0.0),
offset: vec2(0.0, 0.0),
rotation: 0.0,
render_target: None,
viewport: None,
},
background_color: Color::new(1.0, 1.0, 1.0, 1.0),
};
let mut state = world.turtles[0].clone();
// Initial state: position (0, 0), heading 0 (east) // Initial state: position (0, 0), heading 0 (east)
assert_eq!(state.params.position.x, 0.0); assert_eq!(state.params.position.x, 0.0);
assert_eq!(state.params.position.y, 0.0); assert_eq!(state.params.position.y, 0.0);
+1 -21
View File
@@ -55,7 +55,6 @@ impl Default for TurtleParams {
} }
/// State of a single turtle /// State of a single turtle
#[derive(Clone, Debug)]
pub(crate) struct Turtle { pub(crate) struct Turtle {
pub(crate) turtle_id: usize, pub(crate) turtle_id: usize,
pub(crate) params: TurtleParams, pub(crate) params: TurtleParams,
@@ -349,30 +348,11 @@ pub(crate) enum SvgRecord {
}, },
} }
/// Cached mesh data that can be cloned and converted to Mesh when needed
#[derive(Clone, Debug)]
pub(crate) struct MeshData {
pub(crate) vertices: Vec<macroquad::prelude::Vertex>,
pub(crate) indices: Vec<u16>,
}
impl MeshData {
#[must_use]
pub fn to_mesh(&self) -> macroquad::prelude::Mesh {
macroquad::prelude::Mesh {
vertices: self.vertices.clone(),
indices: self.indices.clone(),
texture: None,
}
}
}
/// Drawable elements in the world. /// Drawable elements in the world.
/// All drawing is done via Lyon-tessellated meshes for consistency and quality. /// All drawing is done via Lyon-tessellated meshes for consistency and quality.
#[derive(Clone, Debug)]
pub(crate) enum DrawCommand { pub(crate) enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons — all use this). /// Pre-tessellated mesh data (lines, arcs, circles, polygons — all use this).
Mesh { data: MeshData }, Mesh(macroquad::prelude::Mesh),
/// Text rendering command. /// Text rendering command.
Text { Text {
text: String, text: String,
+15 -15
View File
@@ -3,7 +3,6 @@
//! This module provides helper functions to tessellate paths using Lyon, //! This module provides helper functions to tessellate paths using Lyon,
//! which replaces the manual triangulation with GPU-optimized tessellation. //! which replaces the manual triangulation with GPU-optimized tessellation.
use crate::state::MeshData;
use lyon::math::{point, Point}; use lyon::math::{point, Point};
use lyon::path::{LineCap, LineJoin, Path}; use lyon::path::{LineCap, LineJoin, Path};
use lyon::tessellation::{ use lyon::tessellation::{
@@ -31,13 +30,13 @@ pub(crate) struct SimpleVertex {
pub(crate) position: [f32; 2], pub(crate) position: [f32; 2],
} }
/// Build mesh data from Lyon tessellation /// Build mesh from Lyon tessellation
#[must_use] #[must_use]
pub(crate) fn build_mesh_data( pub(crate) fn build_mesh(
vertices: &[SimpleVertex], vertices: &[SimpleVertex],
indices: &[u16], indices: &[u16],
color: Color, color: Color,
) -> MeshData { ) -> Mesh {
let verts: Vec<Vertex> = vertices let verts: Vec<Vertex> = vertices
.iter() .iter()
.map(|v| Vertex { .map(|v| Vertex {
@@ -53,9 +52,10 @@ pub(crate) fn build_mesh_data(
}) })
.collect(); .collect();
MeshData { Mesh {
vertices: verts, vertices: verts,
indices: indices.to_vec(), indices: indices.to_vec(),
texture: None,
} }
} }
@@ -69,7 +69,7 @@ pub(crate) fn build_mesh_data(
pub(crate) fn tessellate_polygon( pub(crate) fn tessellate_polygon(
vertices: &[Vec2], vertices: &[Vec2],
color: Color, color: Color,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<Mesh, Box<dyn std::error::Error>> {
if vertices.is_empty() { if vertices.is_empty() {
return Err("No vertices provided".into()); return Err("No vertices provided".into());
} }
@@ -96,7 +96,7 @@ pub(crate) fn tessellate_polygon(
}), }),
)?; )?;
Ok(build_mesh_data( Ok(build_mesh(
&geometry.vertices, &geometry.vertices,
&geometry.indices, &geometry.indices,
color, color,
@@ -114,7 +114,7 @@ pub(crate) fn tessellate_polygon(
pub(crate) fn tessellate_multi_contour( pub(crate) fn tessellate_multi_contour(
contours: &[Vec<Vec2>], contours: &[Vec<Vec2>],
color: Color, color: Color,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<Mesh, Box<dyn std::error::Error>> {
if contours.is_empty() { if contours.is_empty() {
return Err("No contours provided".into()); return Err("No contours provided".into());
} }
@@ -195,7 +195,7 @@ pub(crate) fn tessellate_multi_contour(
} }
} }
Ok(build_mesh_data( Ok(build_mesh(
&geometry.vertices, &geometry.vertices,
&geometry.indices, &geometry.indices,
color, color,
@@ -212,7 +212,7 @@ pub(crate) fn tessellate_stroke(
color: Color, color: Color,
width: f32, width: f32,
closed: bool, closed: bool,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<Mesh, Box<dyn std::error::Error>> {
if vertices.is_empty() { if vertices.is_empty() {
return Err("No vertices provided".into()); return Err("No vertices provided".into());
} }
@@ -241,7 +241,7 @@ pub(crate) fn tessellate_stroke(
}), }),
)?; )?;
Ok(build_mesh_data( Ok(build_mesh(
&geometry.vertices, &geometry.vertices,
&geometry.indices, &geometry.indices,
color, color,
@@ -259,7 +259,7 @@ pub(crate) fn tessellate_circle(
color: Color, color: Color,
filled: bool, filled: bool,
stroke_width: f32, stroke_width: f32,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<Mesh, Box<dyn std::error::Error>> {
let mut builder = Path::builder(); let mut builder = Path::builder();
builder.add_circle(to_lyon_point(center), radius, lyon::path::Winding::Positive); builder.add_circle(to_lyon_point(center), radius, lyon::path::Winding::Positive);
let path = builder.build(); let path = builder.build();
@@ -286,7 +286,7 @@ pub(crate) fn tessellate_circle(
)?; )?;
} }
Ok(build_mesh_data( Ok(build_mesh(
&geometry.vertices, &geometry.vertices,
&geometry.indices, &geometry.indices,
color, color,
@@ -308,7 +308,7 @@ pub(crate) fn tessellate_arc(
stroke_width: f32, stroke_width: f32,
segments: usize, segments: usize,
direction: crate::circle_geometry::CircleDirection, direction: crate::circle_geometry::CircleDirection,
) -> Result<MeshData, Box<dyn std::error::Error>> { ) -> Result<Mesh, Box<dyn std::error::Error>> {
use crate::circle_geometry::arc_points; use crate::circle_geometry::arc_points;
let start_angle = start_angle_degrees.to_radians(); let start_angle = start_angle_degrees.to_radians();
@@ -352,7 +352,7 @@ pub(crate) fn tessellate_arc(
}), }),
)?; )?;
Ok(build_mesh_data( Ok(build_mesh(
&geometry.vertices, &geometry.vertices,
&geometry.indices, &geometry.indices,
color, color,