From ea9a8839c4aec7c4041038cc2578f80f24e0f9a0 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Fri, 18 Sep 2026 21:34:07 +0200 Subject: [PATCH] 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>`. [`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`. --- turtle-lib/src/drawing.rs | 20 ++++++++++---------- turtle-lib/src/execution.rs | 29 +++++++---------------------- turtle-lib/src/state.rs | 22 +--------------------- turtle-lib/src/tessellation.rs | 30 +++++++++++++++--------------- 4 files changed, 33 insertions(+), 68 deletions(-) diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index c31c095..1d2971d 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -33,8 +33,8 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { for turtle in &world.turtles { for cmd in &turtle.commands { match cmd { - DrawCommand::Mesh { data } => { - draw_mesh(&data.to_mesh()); + DrawCommand::Mesh(mesh) => { + draw_mesh(mesh); } DrawCommand::Text { text, @@ -224,8 +224,8 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { &all_contours, fill_state.fill_color, ) { - Ok(mesh_data) => { - draw_mesh(&mesh_data.to_mesh()); + Ok(mesh) => { + draw_mesh(&mesh); } Err(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. - 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 @@ -316,7 +316,7 @@ fn draw_tween_arc( let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1 // 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, radius, geom.start_angle_from_center.to_degrees(), @@ -326,7 +326,7 @@ fn draw_tween_arc( ((steps as f32 * progress).ceil() as usize).max(1), direction, ) { - draw_mesh(&mesh_data.to_mesh()); + draw_mesh(&mesh); } } @@ -343,10 +343,10 @@ pub(crate) fn draw_turtle(turtle_params: &TurtleParams) { .collect(); // 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)) { - draw_mesh(&mesh_data.to_mesh()); + draw_mesh(&mesh); } else { // Fallback to simple triangle fan if Lyon fails let first = absolute_vertices[0]; diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 57c005f..7e0ffd3 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -123,7 +123,7 @@ pub(crate) fn execute_command_side_effects( } 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.fill_color, ) { @@ -132,7 +132,7 @@ pub(crate) fn execute_command_side_effects( contours = fill_state.contours.len(), "Successfully created fill mesh - persisting to commands" ); - commands.push(DrawCommand::Mesh { data: mesh_data }); + commands.push(DrawCommand::Mesh(mesh)); #[cfg(feature = "svg")] svg_log.push(crate::state::SvgRecord::Fill { contours: fill_state.contours, @@ -326,7 +326,7 @@ pub(crate) fn tessellate_command( match command { TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { - let mesh_data = tessellation::tessellate_stroke( + let mesh = tessellation::tessellate_stroke( &[start.position, end_position], start.color, start.pen_width, @@ -334,7 +334,7 @@ pub(crate) fn tessellate_command( ) .ok()?; - Some(DrawCommand::Mesh { data: mesh_data }) + Some(DrawCommand::Mesh(mesh)) } TurtleCommand::Circle { @@ -350,7 +350,7 @@ pub(crate) fn tessellate_command( *radius, *direction, ); - let mesh_data = tessellation::tessellate_arc( + let mesh = tessellation::tessellate_arc( geom.center, *radius, geom.start_angle_from_center.to_degrees(), @@ -362,7 +362,7 @@ pub(crate) fn tessellate_command( ) .ok()?; - Some(DrawCommand::Mesh { data: mesh_data }) + Some(DrawCommand::Mesh(mesh)) } // `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) use crate::state::TurtleParams; - let state = Turtle { + let mut state = Turtle { turtle_id: 0, params: TurtleParams { position: vec2(0.0, 0.0), @@ -503,21 +503,6 @@ mod tests { 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) assert_eq!(state.params.position.x, 0.0); assert_eq!(state.params.position.y, 0.0); diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index fb58ba6..e84126c 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -55,7 +55,6 @@ impl Default for TurtleParams { } /// State of a single turtle -#[derive(Clone, Debug)] pub(crate) struct Turtle { pub(crate) turtle_id: usize, 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, - pub(crate) indices: Vec, -} - -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. /// All drawing is done via Lyon-tessellated meshes for consistency and quality. -#[derive(Clone, Debug)] pub(crate) enum DrawCommand { /// Pre-tessellated mesh data (lines, arcs, circles, polygons — all use this). - Mesh { data: MeshData }, + Mesh(macroquad::prelude::Mesh), /// Text rendering command. Text { text: String, diff --git a/turtle-lib/src/tessellation.rs b/turtle-lib/src/tessellation.rs index 196acb4..c30961b 100644 --- a/turtle-lib/src/tessellation.rs +++ b/turtle-lib/src/tessellation.rs @@ -3,7 +3,6 @@ //! This module provides helper functions to tessellate paths using Lyon, //! which replaces the manual triangulation with GPU-optimized tessellation. -use crate::state::MeshData; use lyon::math::{point, Point}; use lyon::path::{LineCap, LineJoin, Path}; use lyon::tessellation::{ @@ -31,13 +30,13 @@ pub(crate) struct SimpleVertex { pub(crate) position: [f32; 2], } -/// Build mesh data from Lyon tessellation +/// Build mesh from Lyon tessellation #[must_use] -pub(crate) fn build_mesh_data( +pub(crate) fn build_mesh( vertices: &[SimpleVertex], indices: &[u16], color: Color, -) -> MeshData { +) -> Mesh { let verts: Vec = vertices .iter() .map(|v| Vertex { @@ -53,9 +52,10 @@ pub(crate) fn build_mesh_data( }) .collect(); - MeshData { + Mesh { vertices: verts, indices: indices.to_vec(), + texture: None, } } @@ -69,7 +69,7 @@ pub(crate) fn build_mesh_data( pub(crate) fn tessellate_polygon( vertices: &[Vec2], color: Color, -) -> Result> { +) -> Result> { if vertices.is_empty() { 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.indices, color, @@ -114,7 +114,7 @@ pub(crate) fn tessellate_polygon( pub(crate) fn tessellate_multi_contour( contours: &[Vec], color: Color, -) -> Result> { +) -> Result> { if contours.is_empty() { 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.indices, color, @@ -212,7 +212,7 @@ pub(crate) fn tessellate_stroke( color: Color, width: f32, closed: bool, -) -> Result> { +) -> Result> { if vertices.is_empty() { 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.indices, color, @@ -259,7 +259,7 @@ pub(crate) fn tessellate_circle( color: Color, filled: bool, stroke_width: f32, -) -> Result> { +) -> Result> { let mut builder = Path::builder(); builder.add_circle(to_lyon_point(center), radius, lyon::path::Winding::Positive); let path = builder.build(); @@ -286,7 +286,7 @@ pub(crate) fn tessellate_circle( )?; } - Ok(build_mesh_data( + Ok(build_mesh( &geometry.vertices, &geometry.indices, color, @@ -308,7 +308,7 @@ pub(crate) fn tessellate_arc( stroke_width: f32, segments: usize, direction: crate::circle_geometry::CircleDirection, -) -> Result> { +) -> Result> { use crate::circle_geometry::arc_points; 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.indices, color,