From 37e73248b7a43c0c35724405a0ea727895e9fe09 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Tue, 20 May 2025 20:32:33 +0200 Subject: [PATCH 01/15] prints for `get_stroke_mut` failure and print initial `key_tree` insert time --- crates/rnote-engine/src/pens/brush.rs | 7 +++++++ crates/rnote-engine/src/store/mod.rs | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/pens/brush.rs b/crates/rnote-engine/src/pens/brush.rs index 5979de0dd2..e6f13d9bd4 100644 --- a/crates/rnote-engine/src/pens/brush.rs +++ b/crates/rnote-engine/src/pens/brush.rs @@ -202,6 +202,13 @@ impl PenBehaviour for Brush { { brushstroke.extend_w_segments(segments); widget_flags.store_modified = true; + } else { + // maybe the get_stroke_mut fails ? + // kinda weird as the only reason to fail here is that the stroke key is not found + println!( + "unexpected failure to push the last {:?} elements to the current stroke", + n_segments + ); } engine_view.store.append_rendering_last_segments( diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index e73fc2f7ef..b25b68871b 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; use slotmap::{HopSlotMap, SecondaryMap}; use std::collections::VecDeque; use std::sync::Arc; -use std::time::Instant; +use std::time::{Instant, SystemTime}; use tracing::debug; slotmap::new_key_type! { @@ -323,7 +323,12 @@ impl StrokeStore { let layer = layer.unwrap_or_else(|| stroke.extract_default_layer()); let key = Arc::make_mut(&mut self.stroke_components).insert(Arc::new(stroke)); + + // for now very basic timing to (at the very least) identify whether this is our hotspot on the start + let now = SystemTime::now(); self.key_tree.insert_with_key(key, bounds); + let elapsed = now.elapsed(); + println!("insertion of the stroke took {:?} seconds", elapsed); self.chrono_counter += 1; Arc::make_mut(&mut self.trash_components).insert(key, Arc::new(TrashComponent::default())); From 4582fbde9b11afd043d10a66129021ab1b5e1a2f Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Tue, 20 May 2025 20:49:31 +0200 Subject: [PATCH 02/15] window ci fixes --- .github/workflows/release-windows.yml | 13 +++++++++++-- justfile | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index 3eac20da37..38bfdf7e5c 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -1,5 +1,4 @@ --- - name: Release Windows "on": @@ -9,7 +8,7 @@ name: Release Windows jobs: build: - runs-on: windows-2022 + runs-on: windows-2025 permissions: # needed for uploading release artifact contents: write @@ -17,6 +16,16 @@ jobs: run: shell: msys2 {0} steps: + - name: Install Inno 6 + id: install_inno + shell: pwsh + run: | + winget install -e --id JRSoftware.InnoSetup -v 6.4.3 --accept-package-agreements --accept-source-agreements --disable-interactivity --scope machine + if (Test-Path "C:\Program Files (x86)\Inno Setup 6\") { + echo "inno installed successfully" + } else { + throw "could not find inno's installation folder" + } - name: Set installer name id: set_installer_name diff --git a/justfile b/justfile index 2de38e1d10..d36ea80937 100644 --- a/justfile +++ b/justfile @@ -123,7 +123,7 @@ setup-win-installer installer_name="rnote-win-installer": meson setup \ --prefix={{ mingw64_prefix_path }} \ -Dprofile=default \ - -Dcli=false \ + -Dcli=true \ -Dwin-installer-name={{ installer_name }} \ -Dci={{ ci }} \ {{ build_folder }} From 72b33e94621939c0eb4d4564efde9a26269ffcc1 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 24 May 2025 12:54:58 +0200 Subject: [PATCH 03/15] rustflag for profiling --- build-aux/cargo_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/cargo_build.py b/build-aux/cargo_build.py index 5fca7ef271..7d13f65401 100644 --- a/build-aux/cargo_build.py +++ b/build-aux/cargo_build.py @@ -22,7 +22,7 @@ output_file: {output_file} """, file=sys.stderr) -cargo_call = f"env {cargo_env} {cargo_cmd} build {cargo_options}" +cargo_call = f"RUSTFLAGS=\"-C force-frame-pointers=yes\" env {cargo_env} {cargo_cmd} build {cargo_options} " cp_call = f"cp {bin_output} {output_file}" print(cargo_call, file=sys.stderr) From 63a03a04034c01aa309b30fe712492897a95f99b Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 24 May 2025 12:57:38 +0200 Subject: [PATCH 04/15] debug prints for history filtering, ,insert stroke time and elements counts --- crates/rnote-engine/src/pens/brush.rs | 81 +++++++++++++++++++++++++++ crates/rnote-engine/src/store/mod.rs | 17 ++++-- crates/rnote-ui/src/canvas/input.rs | 40 +++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/src/pens/brush.rs b/crates/rnote-engine/src/pens/brush.rs index e6f13d9bd4..aa4cf14b1e 100644 --- a/crates/rnote-engine/src/pens/brush.rs +++ b/crates/rnote-engine/src/pens/brush.rs @@ -31,12 +31,18 @@ enum BrushState { #[derive(Debug)] pub struct Brush { state: BrushState, + /// counts the number of elements send back by the builder + n_elements_out: usize, + /// counts the number of pen events sent to the builder + n_elements_in: usize, } impl Default for Brush { fn default() -> Self { Self { state: BrushState::Idle, + n_elements_out: 0, + n_elements_in: 0, } } } @@ -119,6 +125,14 @@ impl PenBehaviour for Brush { ), current_stroke_key, }; + // count the start element + self.n_elements_out = 1; + self.n_elements_in = 1; + + println!( + "BRUSH_STATS: start of brush stroke with builder {:?}", + engine_view.config.pens_config.brush_config.builder_type + ); EventResult { handled: true, @@ -158,7 +172,12 @@ impl PenBehaviour for Brush { .document .resize_autoexpand(engine_view.store, engine_view.camera); + // DEBUG PRINT + self.debug_print_end_stroke(engine_view); + self.state = BrushState::Idle; + self.n_elements_out = 0; + self.n_elements_in = 0; widget_flags |= engine_view.store.record(Instant::now()); widget_flags.store_modified = true; @@ -178,6 +197,8 @@ impl PenBehaviour for Brush { ) => { let builder_result = path_builder.handle_event(pen_event, now, Constraints::default()); + self.n_elements_in += 1; + let handled = builder_result.handled; let propagate = builder_result.propagate; @@ -218,6 +239,21 @@ impl PenBehaviour for Brush { engine_view.camera.viewport(), engine_view.camera.image_scale(), ); + // update state to count the n segments + self.n_elements_out += n_segments; + + // verify equality + if let Some(Stroke::BrushStroke(brushstroke)) = + engine_view.store.get_stroke_mut(*current_stroke_key) + { + let path_len = brushstroke.path.segments.len() + 1; + if path_len != self.n_elements_out { + println!( + "BRUSH_STATS: expected {:?} elements to be in the current engine brushstroke got {:?}", + self.n_elements_out, path_len + ); + } + } } PenProgress::InProgress @@ -240,6 +276,20 @@ impl PenBehaviour for Brush { engine_view.camera.viewport(), engine_view.camera.image_scale(), ); + self.n_elements_out += n_segments; + + // verify equality + if let Some(Stroke::BrushStroke(brushstroke)) = + engine_view.store.get_stroke_mut(*current_stroke_key) + { + let path_len = brushstroke.path.segments.len() + 1; + if path_len != self.n_elements_out { + println!( + "BRUSH_STATS: expected {:?} elements to be in the current engine brushstroke got {:?}", + self.n_elements_out, path_len + ); + } + } } // Finish up the last stroke @@ -256,8 +306,14 @@ impl PenBehaviour for Brush { .document .resize_autoexpand(engine_view.store, engine_view.camera); + // DEBUG PRINT + self.debug_print_end_stroke(engine_view); + self.state = BrushState::Idle; + self.n_elements_in = 0; + self.n_elements_out = 0; + widget_flags |= engine_view.store.record(Instant::now()); widget_flags.store_modified = true; @@ -326,6 +382,31 @@ impl DrawableOnDoc for Brush { impl Brush { const INPUT_OVERSHOOT: f64 = 30.0; + + /// debug print to count in/out events + fn debug_print_end_stroke(&self, engine_view: &mut EngineViewMut) { + let key = match &self.state { + BrushState::Drawing { + path_builder: _, + current_stroke_key, + } => Some(current_stroke_key), + BrushState::Idle => None, + }; + println!( + "BRUSH_STATS: End of stroke with key {:?}, sent {:?} to the builder, got {:?} back.", + key, self.n_elements_in, self.n_elements_out + ); + + if let Some(Stroke::BrushStroke(brushstroke)) = + engine_view.store.get_stroke_mut(*key.unwrap()) + { + println!( + "BRUSH_STATS: In the store, the key {:?} has {:?} elements", + key, + brushstroke.path.segments.len() + 1 // +1 for the start element + ); + } + } } fn play_marker_sound(engine_view: &mut EngineViewMut) { diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index b25b68871b..844d6a13b3 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -319,18 +319,20 @@ impl StrokeStore { stroke: Stroke, layer: Option, ) -> StrokeKey { + let now = SystemTime::now(); let bounds = stroke.bounds(); + + let stroke_bounds_time = now.elapsed(); + let layer = layer.unwrap_or_else(|| stroke.extract_default_layer()); let key = Arc::make_mut(&mut self.stroke_components).insert(Arc::new(stroke)); - // for now very basic timing to (at the very least) identify whether this is our hotspot on the start - let now = SystemTime::now(); self.key_tree.insert_with_key(key, bounds); - let elapsed = now.elapsed(); - println!("insertion of the stroke took {:?} seconds", elapsed); + self.chrono_counter += 1; + let insertion_starts = SystemTime::now(); Arc::make_mut(&mut self.trash_components).insert(key, Arc::new(TrashComponent::default())); Arc::make_mut(&mut self.selection_components) .insert(key, Arc::new(SelectionComponent::default())); @@ -340,6 +342,13 @@ impl StrokeStore { ); self.render_components .insert(key, RenderComponent::default()); + let time_insetion = insertion_starts.elapsed(); + + let elapsed = now.elapsed(); + println!( + "insertion of the stroke took {:?} seconds with {:?} seconds for stroke bounds, {:?} for insertions", + elapsed, stroke_bounds_time, time_insetion + ); key } diff --git a/crates/rnote-ui/src/canvas/input.rs b/crates/rnote-ui/src/canvas/input.rs index 221ece6282..85ed22fce0 100644 --- a/crates/rnote-ui/src/canvas/input.rs +++ b/crates/rnote-ui/src/canvas/input.rs @@ -9,6 +9,7 @@ use rnote_engine::ext::GraphenePointExt; use rnote_engine::pens::PenMode; use rnote_engine::pens::penholder::BacklogPolicy; use std::collections::HashSet; +use std::ops::Add; use std::time::{Duration, Instant}; use tracing::trace; @@ -164,6 +165,7 @@ pub(crate) fn handle_pointer_controller_event( let pen_mode = retrieve_pen_mode(event); for (element, event_time) in elements { + // this is something I'm interested in: activate the trace trace!(?element, ?pen_state, ?modifier_keys, ?pen_mode, event_time_delta=?now.duration_since(event_time), msg="handle pen event element"); // Workaround for https://github.com/flxzt/rnote/issues/785 @@ -355,6 +357,8 @@ fn retrieve_pointer_elements( .unwrap() }; + // Idea: check whether or not we lose events because of lag making the last event type ? + // Kinda unlikely but worth checking for if event.event_type() == gdk::EventType::MotionNotify && backlog_policy != BacklogPolicy::Disable { @@ -366,11 +370,13 @@ fn retrieve_pointer_elements( if !(available_axes.contains(gdk::AxisFlags::X) && available_axes.contains(gdk::AxisFlags::Y)) { + println!("HISTORY: X/Y informmation missing, ignoring"); continue; } let entry_delta = Duration::from_millis(event_time.saturating_sub(entry.time()) as u64); let Some(entry_time) = now.checked_sub(entry_delta) else { + println!("HISTORY: incoherent timing for event, ignoring"); continue; }; @@ -399,6 +405,40 @@ fn retrieve_pointer_elements( } elements.extend(entries.into_iter().rev()); + + println!( + "HISTORY: size of the history before filtering {:?} and after {:?}", + event.history().len().add(1), // +1 because of the end element + elements.len() + 1 // not yet pushed the origin event + ); + } else { + println!( + "HISTORY: event type {:?}. History is not read in that instance. History size {:?}", + event.event_type(), + event.history().len(), + ); + + // iterate over events + for entry in event.history().into_iter().rev() { + let available_axes = entry.flags(); + if !(available_axes.contains(gdk::AxisFlags::X) + && available_axes.contains(gdk::AxisFlags::Y)) + { + println!("HISTORY: X/Y informmation missing, ignoring"); + continue; + } + + let axes = entry.axes(); + let event_time = entry.time(); + let pos = transform_pos(na::vector![ + axes[crate::utils::axis_use_idx(gdk::AxisUse::X)], + axes[crate::utils::axis_use_idx(gdk::AxisUse::Y)] + ]); + println!( + "HISTORY: ignored event in the history at time {:?}, position {:?} ", + event_time, pos + ); + } } let pos = event From 35b7162e80497320ff43a7813fe1ad0c37bbeba8 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 24 May 2025 13:06:54 +0200 Subject: [PATCH 05/15] Revert "rustflag for profiling" This reverts commit 72b33e94621939c0eb4d4564efde9a26269ffcc1. --- build-aux/cargo_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/cargo_build.py b/build-aux/cargo_build.py index 7d13f65401..5fca7ef271 100644 --- a/build-aux/cargo_build.py +++ b/build-aux/cargo_build.py @@ -22,7 +22,7 @@ output_file: {output_file} """, file=sys.stderr) -cargo_call = f"RUSTFLAGS=\"-C force-frame-pointers=yes\" env {cargo_env} {cargo_cmd} build {cargo_options} " +cargo_call = f"env {cargo_env} {cargo_cmd} build {cargo_options}" cp_call = f"cp {bin_output} {output_file}" print(cargo_call, file=sys.stderr) From 0881915e974c9b6846c4a337932e688328ebf27e Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 31 May 2025 14:39:18 +0200 Subject: [PATCH 06/15] time rendering code and more debugs notes --- crates/rnote-engine/src/engine/rendering.rs | 51 +++++++++++++++++-- .../rnote-engine/src/engine/visual_debug.rs | 20 ++++++-- crates/rnote-engine/src/pens/brush.rs | 11 ++-- crates/rnote-engine/src/pens/penholder.rs | 5 ++ crates/rnote-engine/src/store/chrono_comp.rs | 1 + crates/rnote-engine/src/store/render_comp.rs | 13 +++++ crates/rnote-engine/src/store/stroke_comp.rs | 17 ++++++- crates/rnote-ui/src/appwindow/mod.rs | 4 +- crates/rnote-ui/src/canvas/input.rs | 32 +++++++++--- 9 files changed, 131 insertions(+), 23 deletions(-) diff --git a/crates/rnote-engine/src/engine/rendering.rs b/crates/rnote-engine/src/engine/rendering.rs index 55066bbec1..d3a72c2c5f 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -163,23 +163,45 @@ impl Engine { snapshot: >k4::Snapshot, surface_bounds: p2d::bounding_volume::Aabb, ) -> anyhow::Result<()> { + use std::time::SystemTime; + use crate::drawable::DrawableOnDoc; use crate::engine::visual_debug; use crate::engine_view; use gtk4::prelude::*; + let start_rendering = SystemTime::now(); + let doc_bounds = self.document.bounds(); let viewport = self.camera.viewport(); let camera_transform = self.camera.transform_for_gtk_snapshot(); snapshot.save(); snapshot.transform(Some(&camera_transform)); - self.draw_document_shadow_to_gtk_snapshot(snapshot); - self.draw_background_to_gtk_snapshot(snapshot)?; - self.draw_format_borders_to_gtk_snapshot(snapshot)?; - self.draw_origin_indicator_to_gtk_snapshot(snapshot)?; + + let elapsed_snapshot_setup = start_rendering.elapsed(); + + // expensive but constant time : can we not translate ? + // seems like the gtk snapshot does NOT maintain layer so we'd need + // to hold stacked widgets for this + // OR hold onto render nodes and translates then instead + let draw_doc_shadow_start = SystemTime::now(); + self.draw_document_shadow_to_gtk_snapshot(snapshot); // constant time (or should be) + let draw_doc_shadow_elapsed = draw_doc_shadow_start.elapsed(); + let draw_bgrd_start = SystemTime::now(); + self.draw_background_to_gtk_snapshot(snapshot)?; // constant time (or should be) + let draw_bgrd_elapsed = draw_bgrd_start.elapsed(); + let draw_fmt_start = SystemTime::now(); + self.draw_format_borders_to_gtk_snapshot(snapshot)?; // constant time (or should be) + let draw_fmt_elapsed = draw_fmt_start.elapsed(); + let draw_orig_start = SystemTime::now(); + self.draw_origin_indicator_to_gtk_snapshot(snapshot)?; // constant time (or should be) + let draw_orig_elapsed = draw_orig_start.elapsed(); + let draw_stroke_start = SystemTime::now(); self.store .draw_strokes_to_gtk_snapshot(snapshot, doc_bounds, viewport); + let draw_stroke_elapsed = draw_stroke_start.elapsed(); + snapshot.restore(); /* let cairo_cx = snapshot.append_cairo(&graphene::Rect::from_p2d_aabb(surface_bounds)); @@ -192,8 +214,10 @@ impl Engine { self.camera.image_scale(), ); */ + let penholder_draw_start = SystemTime::now(); self.penholder .draw_on_doc_to_gtk_snapshot(snapshot, &engine_view!(self))?; + let penholder_elapsed = penholder_draw_start.elapsed(); if self.config.read().visual_debug { snapshot.save(); @@ -204,6 +228,25 @@ impl Engine { visual_debug::draw_statistics_to_gtk_snapshot(snapshot, self, surface_bounds)?; } + //stats + println!( + "Time for `draw_to_gtk_snapshot` : {:?} + snapshot setup {:?} + draw shadow {:?} + draw background {:?} + draw fmt {:?} + draw orig {:?} + draw stroke {:?} + penholder {:?}", + start_rendering.elapsed(), + elapsed_snapshot_setup, + draw_doc_shadow_elapsed, + draw_bgrd_elapsed, + draw_fmt_elapsed, + draw_orig_elapsed, + draw_stroke_elapsed, + penholder_elapsed + ); Ok(()) } diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index f50342d38c..a7de45e49f 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -143,10 +143,10 @@ pub(crate) fn draw_statistics_to_gtk_snapshot( let text_bounds = Aabb::new( na::point![ surface_bounds.maxs[0] - 320.0, - surface_bounds.mins[1] + 20.0 + surface_bounds.mins[1] + 10.0 ], na::point![ - surface_bounds.maxs[0] - 20.0, + surface_bounds.maxs[0] - 10.0, surface_bounds.mins[1] + 120.0 ], ); @@ -157,7 +157,12 @@ pub(crate) fn draw_statistics_to_gtk_snapshot( let strokes_total = engine.store.keys_unordered(); let strokes_in_viewport = engine .store - .keys_unordered_intersecting_bounds(engine.camera.viewport()); + .keys_unordered_intersecting_bounds(engine.camera.viewport()); // same as the call we do for rendering ? + + let stroke_in_viewport_for_rendering = engine + .store + .stroke_keys_as_rendered_intersecting_bounds(engine.camera.viewport()); + let selected_strokes = engine.store.selection_keys_unordered(); let trashed_strokes = engine.store.trashed_keys_unordered(); let strokes_hold_image = strokes_total @@ -165,13 +170,20 @@ pub(crate) fn draw_statistics_to_gtk_snapshot( .filter(|&&key| engine.store.holds_images(key)) .count(); + let strokes_hold_rendernode = strokes_total + .iter() + .filter(|&&key| engine.store.hold_rendernode(key)) + .count(); + let statistics_text_string = format!( - "strokes in store: {}\nstrokes in current viewport: {}\nstrokes selected: {}\nstroke trashed: {}\nstrokes holding images: {}", + "strokes in store: {}\nstrokes in current viewport: {}\nstrokes selected: {}\nstroke trashed: {}\nstrokes holding images: {}\n strokes in the viewport for the rendering {} strokes holding rendernodes {}", strokes_total.len(), strokes_in_viewport.len(), selected_strokes.len(), trashed_strokes.len(), strokes_hold_image, + stroke_in_viewport_for_rendering.len(), //verify we don't have much more things here + strokes_hold_rendernode, ); let text_layout = piet_cx .text() diff --git a/crates/rnote-engine/src/pens/brush.rs b/crates/rnote-engine/src/pens/brush.rs index aa4cf14b1e..d8d7aeb42b 100644 --- a/crates/rnote-engine/src/pens/brush.rs +++ b/crates/rnote-engine/src/pens/brush.rs @@ -90,7 +90,7 @@ impl PenBehaviour for Brush { .config .pens_config .brush_config - .new_style_seeds(); + .new_style_seeds(); // constant time let brushstroke = Stroke::BrushStroke(BrushStroke::new( element, @@ -99,7 +99,7 @@ impl PenBehaviour for Brush { .pens_config .brush_config .style_for_current_options(), - )); + )); // constant time let current_stroke_key = engine_view.store.insert_stroke( brushstroke, Some( @@ -109,13 +109,13 @@ impl PenBehaviour for Brush { .brush_config .layer_for_current_options(), ), - ); + ); //not constant time but very much not to explain for this engine_view.store.regenerate_rendering_for_stroke( current_stroke_key, engine_view.camera.viewport(), engine_view.camera.image_scale(), - ); + ); //only applies to the stroke so constant time self.state = BrushState::Drawing { path_builder: new_builder( @@ -220,6 +220,7 @@ impl PenBehaviour for Brush { if n_segments != 0 { if let Some(Stroke::BrushStroke(brushstroke)) = engine_view.store.get_stroke_mut(*current_stroke_key) + // should be constant as well { brushstroke.extend_w_segments(segments); widget_flags.store_modified = true; @@ -266,7 +267,7 @@ impl PenBehaviour for Brush { engine_view.store.get_stroke_mut(*current_stroke_key) { brushstroke.extend_w_segments(segments); - widget_flags.store_modified = true; + widget_flags.store_modified = true; //ofc the stroke is modified here } engine_view.store.append_rendering_last_segments( diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 1b80b2e1ce..2313e1a3bc 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -209,6 +209,11 @@ impl PenHolder { // // This is also needed because pens might have claimed/requested an animation frame. widget_flags.redraw = true; + // this can be expensive + // so probably the actual pen handling path is quick enough + // but the async redraw makes it hang + // --> make it false for now + // issue : the partial rendering phase (already done) scales with the nof strokes ? (event_result.propagate, widget_flags) } diff --git a/crates/rnote-engine/src/store/chrono_comp.rs b/crates/rnote-engine/src/store/chrono_comp.rs index a06c984825..99c93ff9d0 100644 --- a/crates/rnote-engine/src/store/chrono_comp.rs +++ b/crates/rnote-engine/src/store/chrono_comp.rs @@ -124,6 +124,7 @@ impl StrokeStore { let mut keys = self.key_tree.keys_intersecting_bounds(bounds); + // scales with the elements but shouldn't matter that much keys.par_sort_unstable_by(|&first, &second| { if let (Some(first_chrono), Some(second_chrono)) = (chrono_components.get(first), chrono_components.get(second)) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 013c0d825a..f51dcb6039 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -96,6 +96,14 @@ impl StrokeStore { .unwrap_or(false) } + #[cfg(feature = "ui")] + pub(crate) fn hold_rendernode(&self, key: StrokeKey) -> bool { + self.render_components + .get(key) + .map(|s| !s.rendernodes.is_empty()) + .unwrap_or(false) + } + pub(crate) fn regenerate_rendering_for_stroke( &mut self, key: StrokeKey, @@ -498,6 +506,11 @@ impl StrokeStore { snapshot.push_clip(&graphene::Rect::from_p2d_aabb(doc_bounds)); + // does the rtree slow down that much with larger stroke content + // query issue ? + // could we cache the output ? for a viewport that doesn't move + // for stroke content that hasn't changed except the current stroke in progress + // could be a wortwhile optim for key in self.stroke_keys_as_rendered_intersecting_bounds(viewport) { if let (Some(stroke), Some(render_comp)) = ( self.stroke_components.get(key), diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7acfa08093..c96809c97c 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -12,6 +12,7 @@ use rnote_compose::penpath::Element; use rnote_compose::shapes::Shapeable; use rnote_compose::transform::Transformable; use std::sync::Arc; +use std::time::SystemTime; #[cfg(feature = "ui")] use tracing::error; @@ -80,10 +81,22 @@ impl StrokeStore { &self, bounds: Aabb, ) -> Vec { - self.keys_sorted_chrono_intersecting_bounds(bounds) + let now = SystemTime::now(); + let out = self + .keys_sorted_chrono_intersecting_bounds(bounds) .into_iter() .filter(|&key| !(self.trashed(key).unwrap_or(false))) - .collect::>() + .collect::>(); + println!( + "stroke_keys_as_rendered_intersecting_bounds took {:?}", + now.elapsed() + ); // is the same thing but sorted by time + // actually this shouldn't take that much time + // maybe this has to do with gtk management ? + // on the issue even if the store is very large + // we see that we have the correct nof element holding images + + out } /// Stroke keys contained in the given bounds, in the order that they should be rendered. diff --git a/crates/rnote-ui/src/appwindow/mod.rs b/crates/rnote-ui/src/appwindow/mod.rs index d8b7e23bb3..6988e65f40 100644 --- a/crates/rnote-ui/src/appwindow/mod.rs +++ b/crates/rnote-ui/src/appwindow/mod.rs @@ -276,9 +276,11 @@ impl RnAppWindow { // Returns true if the flags indicate that any loop that handles the flags should be quit. (usually an async event loop) pub(crate) fn handle_widget_flags(&self, widget_flags: WidgetFlags, canvas: &RnCanvas) { - //debug!("handling widget flags: '{widget_flags:?}'"); + debug!("handling widget flags: '{widget_flags:?}'"); if widget_flags.redraw { + // queue draw ? + // This means widget‘s Gtk.WidgetClass.snapshot implementation will be called. canvas.queue_draw(); } if widget_flags.resize { diff --git a/crates/rnote-ui/src/canvas/input.rs b/crates/rnote-ui/src/canvas/input.rs index 85ed22fce0..9eca9279b7 100644 --- a/crates/rnote-ui/src/canvas/input.rs +++ b/crates/rnote-ui/src/canvas/input.rs @@ -324,6 +324,9 @@ pub(crate) fn reject_pointer_input(event: &gdk::Event, touch_drawing: bool) -> b fn event_is_stylus(event: &gdk::Event) -> bool { // As in gtk4 'gtkgesturestylus.c:106' we detect if the pointer is a stylus when it has a device tool event.device_tool().is_some() + // could be that we don't have a device tool for the pen ? + // if so there won't be pressure + // but not doing so will sigsev } fn retrieve_pointer_elements( @@ -385,6 +388,10 @@ fn retrieve_pointer_elements( // // If the backlog input rate is higher than the limit, filter it out if entry_delta.saturating_sub(prev_delta) < delta_limit { + println!( + "event removed before of backlog policy {:?}", + backlog_policy + ); continue; } } @@ -395,7 +402,10 @@ fn retrieve_pointer_elements( axes[crate::utils::axis_use_idx(gdk::AxisUse::X)], axes[crate::utils::axis_use_idx(gdk::AxisUse::Y)] ]); - let pressure = if is_stylus { + let pressure = if true { + //is_stylus { + // so it seems to work well now with no issue + // but a mouse will return 0 .. axes[crate::utils::axis_use_idx(gdk::AxisUse::Pressure)] } else { Element::PRESSURE_DEFAULT @@ -445,7 +455,8 @@ fn retrieve_pointer_elements( .position() .map(|(x, y)| transform_pos(na::vector![x, y]))?; - let pressure = if is_stylus { + let pressure = if true { + //if is_stylus { event.axis(gdk::AxisUse::Pressure).unwrap() } else { Element::PRESSURE_DEFAULT @@ -486,11 +497,18 @@ pub(crate) fn retrieve_modifier_keys(modifier: gdk::ModifierType) -> HashSet Option { - let device_tool = event.device_tool()?; - match device_tool.tool_type() { - gdk::DeviceToolType::Pen => Some(PenMode::Pen), - gdk::DeviceToolType::Eraser => Some(PenMode::Eraser), - _ => None, + // for the eraser or device to work, we need to know the device_tool + let device_tool = event.device_tool(); // no mode if the device_tool can't be retrieved + match device_tool { + Some(device) => match device.tool_type() { + gdk::DeviceToolType::Pen => Some(PenMode::Pen), + gdk::DeviceToolType::Eraser => Some(PenMode::Eraser), + _ => None, + }, + None => { + trace!("no device tool found here"); + None + } } } From 20a5f504ea909634cde21102b31fd13645bb5d06 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 31 May 2025 14:39:40 +0200 Subject: [PATCH 07/15] comment on redraw --- crates/rnote-engine/src/pens/penholder.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 2313e1a3bc..821b33020a 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -213,6 +213,9 @@ impl PenHolder { // so probably the actual pen handling path is quick enough // but the async redraw makes it hang // --> make it false for now + // actually needed for penholder draws + // A compromise could be to separate in the redraw the actual part we want to redraw and the + // rest ? // issue : the partial rendering phase (already done) scales with the nof strokes ? (event_result.propagate, widget_flags) From 6ff52792ecb816794cb80171b18e7c47d7782c5f Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 18:52:32 +0200 Subject: [PATCH 08/15] revert test on input Turns out querying pressure on non stylus really does sometimes segfault --- crates/rnote-ui/src/canvas/input.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rnote-ui/src/canvas/input.rs b/crates/rnote-ui/src/canvas/input.rs index 9eca9279b7..18fe88fd28 100644 --- a/crates/rnote-ui/src/canvas/input.rs +++ b/crates/rnote-ui/src/canvas/input.rs @@ -402,10 +402,10 @@ fn retrieve_pointer_elements( axes[crate::utils::axis_use_idx(gdk::AxisUse::X)], axes[crate::utils::axis_use_idx(gdk::AxisUse::Y)] ]); - let pressure = if true { - //is_stylus { + let pressure = if is_stylus { // so it seems to work well now with no issue // but a mouse will return 0 .. + // !! can also fail on the other option !! axes[crate::utils::axis_use_idx(gdk::AxisUse::Pressure)] } else { Element::PRESSURE_DEFAULT @@ -455,8 +455,8 @@ fn retrieve_pointer_elements( .position() .map(|(x, y)| transform_pos(na::vector![x, y]))?; - let pressure = if true { - //if is_stylus { + // on recent gtk this DOES fail + let pressure = if is_stylus { event.axis(gdk::AxisUse::Pressure).unwrap() } else { Element::PRESSURE_DEFAULT From 8faf383a04e3f729fd44af539a0617ebf8ff4597 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 18:56:06 +0200 Subject: [PATCH 09/15] perf: use `push_repeat` for background nodes This diminishes the cost for this - one element of somewhat fixed size (no vec pushing) - one element given to gtk that can tile this as it pleases with less buffer pushes --- crates/rnote-compose/src/ext.rs | 21 ++++++++++ crates/rnote-engine/src/engine/mod.rs | 6 ++- crates/rnote-engine/src/engine/rendering.rs | 43 ++++++++++++--------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/crates/rnote-compose/src/ext.rs b/crates/rnote-compose/src/ext.rs index d41ba18f68..2244d2fc50 100644 --- a/crates/rnote-compose/src/ext.rs +++ b/crates/rnote-compose/src/ext.rs @@ -161,6 +161,8 @@ where /// splits a aabb into multiple which have a maximum of the given size. Their union is the given aabb. /// the split bounds are exactly fitted to not overlap, or extend the given bounds fn split(self, split_size: na::Vector2) -> Vec; + /// get the top patch for a given split size + fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self; /// splits a aabb into multiple of the given size. Their union contains the given aabb. /// The boxes on the edges most likely extend beyond the given aabb. fn split_extended(self, split_size: na::Vector2) -> Vec; @@ -394,6 +396,25 @@ impl AabbExt for Aabb { split_aabbs } + fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self { + let (outer_idx, inner_idx) = match split_order { + SplitOrder::RowMajor => (1, 0), + SplitOrder::ColumnMajor => (0, 1), + }; + + let offset_outer = + (self.mins[outer_idx] / split_size[outer_idx]).floor() * split_size[outer_idx]; + + let offset_inner = + (self.mins[inner_idx] / split_size[inner_idx]).floor() * split_size[inner_idx]; + + let mins = match split_order { + SplitOrder::RowMajor => na::point![offset_inner, offset_outer], + SplitOrder::ColumnMajor => na::point![offset_outer, offset_inner], + }; + Aabb::new(mins, mins + split_size) + } + fn split_extended_origin_aligned( self, split_size: na::Vector2, diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 974ad0dd91..ce46087164 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -199,7 +199,8 @@ pub struct Engine { background_tile_image: Option, #[cfg(feature = "ui")] #[serde(skip)] - background_rendernodes: Vec, + background_rendernode: Option, + origin_background_rendernode: Option, // Origin indicator rendering #[serde(skip)] origin_indicator_image: Option, @@ -225,7 +226,8 @@ impl Default for Engine { tasks_rx: Some(EngineTaskReceiver(tasks_rx)), background_tile_image: None, #[cfg(feature = "ui")] - background_rendernodes: Vec::default(), + background_rendernode: None, + origin_background_rendernode: None, origin_indicator_image: None, #[cfg(feature = "ui")] origin_indicator_rendernode: None, diff --git a/crates/rnote-engine/src/engine/rendering.rs b/crates/rnote-engine/src/engine/rendering.rs index d3a72c2c5f..bec2fa217e 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -21,7 +21,6 @@ impl Engine { use rnote_compose::ext::AabbExt; let viewport = self.camera.viewport(); - let mut rendernodes: Vec = vec![]; if let Some(image) = &self.background_tile_image { // Only create the texture once, it is expensive @@ -35,21 +34,20 @@ impl Engine { } }; - for split_bounds in viewport.split_extended_origin_aligned( + let origin_aabb = viewport.get_origin( self.document.config.background.tile_size(), SplitOrder::default(), - ) { - rendernodes.push( - gsk::TextureNode::new( - &new_texture, - &graphene::Rect::from_p2d_aabb(split_bounds), - ) - .upcast(), - ); - } - } + ); - self.background_rendernodes = rendernodes; + self.background_rendernode = Some( + gsk::TextureNode::new( + &new_texture, + &graphene::Rect::from_p2d_aabb(origin_aabb), + ) + .upcast(), + ); + self.origin_background_rendernode = Some(origin_aabb); + } } #[cfg(feature = "ui")] @@ -117,7 +115,8 @@ impl Engine { self.origin_indicator_image.take(); #[cfg(feature = "ui")] { - self.background_rendernodes.clear(); + self.background_rendernode = None; + self.origin_background_rendernode = None; self.origin_indicator_rendernode.take(); } widget_flags.redraw = true; @@ -294,17 +293,23 @@ impl Engine { snapshot.append_node( gsk::ColorNode::new( &gdk::RGBA::from_compose_color(self.document.config.background.color), - //&gdk::RGBA::RED, &graphene::Rect::from_p2d_aabb(doc_bounds), ) .upcast(), ); + snapshot.pop(); - for r in self.background_rendernodes.iter() { - snapshot.append_node(r); + if let (Some(bounds), Some(render_node)) = ( + self.origin_background_rendernode, + self.background_rendernode.clone(), + ) { + snapshot.push_repeat( + &graphene::Rect::from_p2d_aabb(doc_bounds), + Some(&graphene::Rect::from_p2d_aabb(bounds)), + ); + snapshot.append_node(render_node); + snapshot.pop(); } - - snapshot.pop(); Ok(()) } From 65331d63ddc41df3a144adee6d09b322ec1901e8 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:45:01 +0200 Subject: [PATCH 10/15] visual debug : show rtree bounds --- crates/rnote-engine/src/store/keytree.rs | 5 +++++ crates/rnote-engine/src/store/render_comp.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 72428c5e4c..a41427f64e 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -69,6 +69,11 @@ impl KeyTree { pub(crate) fn clear(&mut self) { *self = Self::default() } + + #[cfg(feature = "ui")] + pub(crate) fn get_tree(&self) -> &rstar::RTree { + &self.0 + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index f51dcb6039..310838c259 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -669,6 +669,18 @@ impl StrokeStore { } } + // draw the rtree root + let tree_bounds = self.key_tree.get_tree().root().envelope(); + visual_debug::draw_bounds_to_gtk_snapshot( + Aabb::new( + na::point![tree_bounds.lower()[0], tree_bounds.lower()[1]], + na::point![tree_bounds.upper()[0], tree_bounds.upper()[1]], + ), + rnote_compose::Color::new(1.0, 0.5, 0., 1.0), + snapshot, + border_widths, + ); + Ok(()) } } From 6534d3079327b2e24e80afa1630ad9f688988124 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:31:26 +0200 Subject: [PATCH 11/15] use rtree to speed up `regenerate_rendering_for_strokes_threaded` We use the rtree to find the element in the viewport, mark these ones using a hashmap, call the rendering function, then remove the rest by iterating on the keys a second time, filtering on keys not in the hashmap --- crates/rnote-engine/src/store/keytree.rs | 1 - crates/rnote-engine/src/store/render_comp.rs | 66 ++++++++++++++------ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index a41427f64e..9a09adec7f 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -70,7 +70,6 @@ impl KeyTree { *self = Self::default() } - #[cfg(feature = "ui")] pub(crate) fn get_tree(&self) -> &rstar::RTree { &self.0 } diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 310838c259..e2c71ca0ef 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -6,9 +6,12 @@ use crate::strokes::content::GeneratedContentImages; use crate::{Drawable, render}; use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::ext::AabbExt; -use rnote_compose::shapes::Shapeable; +use std::collections::HashMap; use tracing::error; +#[cfg(feature = "ui")] +use rnote_compose::shapes::Shapeable; + /// The tolerance where check between scale-factors are considered "equal". pub(crate) const RENDER_IMAGE_SCALE_TOLERANCE: f64 = 0.01; @@ -250,28 +253,39 @@ impl StrokeStore { viewport: Aabb, image_scale: f64, ) { - let keys = self.render_components.keys().collect::>(); - - for key in keys { + // use the rtree to reduce the number of keys to search through + // for now we are using directly the tree because we want to iter without actually + // collecting elements + let viewport_extended = + viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); + + // we want to iterate on the keys that are in the viewport using the + // rtree but also get from this the keys that are not in here + // for that also create a slotmap of keys that are in the viewport + // so that we can iterate a second time on keys and filter on elements not in the slotmap + let mut hashmap_in_viewport: HashMap = HashMap::new(); + + let keys_in_viewport = self + .key_tree + .get_tree() + .locate_in_envelope_intersecting(&rstar::AABB::from_corners( + [viewport_extended.mins[0], viewport_extended.mins[1]], + [viewport_extended.maxs[0], viewport_extended.maxs[1]], + )) + .map(|object| { + let key = object.data; + hashmap_in_viewport.insert(key, ()); + key + }) + .into_iter() + .collect::>(); + + for key in keys_in_viewport { if let (Some(stroke), Some(render_comp)) = ( self.stroke_components.get(key), self.render_components.get_mut(key), ) { let tasks_tx = tasks_tx.clone(); - let stroke_bounds = stroke.bounds(); - let viewport_extended = - viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); - - // skip and clear image buffer if stroke is not in viewport - if !viewport_extended.intersects(&stroke_bounds) { - #[cfg(feature = "ui")] - { - render_comp.rendernodes = vec![]; - } - render_comp.images = vec![]; - render_comp.state = RenderCompState::Dirty; - continue; - } // only check if rerendering is not forced if !force_regenerate { @@ -322,6 +336,22 @@ impl StrokeStore { ); } } + + // iterate a second time on stroke keys that we know are not in + // the viewport + // This way we can skip calculting their bounds + for (_key, render_comp) in self + .render_components + .iter_mut() + .filter(|x| !hashmap_in_viewport.contains_key(&x.0)) + { + #[cfg(feature = "ui")] + { + render_comp.rendernodes = vec![]; + } + render_comp.images = vec![]; + render_comp.state = RenderCompState::Dirty; + } } /// Clear all rendering for all strokes. From 742d1e12328064095c5b3fda28b0f8fb2aabbc26 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:42:38 +0200 Subject: [PATCH 12/15] make the rtree only keep track of non-trashed strokes --- crates/rnote-engine/src/store/trash_comp.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index 9e9b24c58b..cf7e1bb4ba 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -49,6 +49,18 @@ impl StrokeStore { .map(Arc::make_mut) { trash_comp.trashed = trash; + // remove the key from the rtree (so that the rtree holds information + // only for non trashed strokes) + if trash { + self.key_tree.remove_with_key(key); + } else { + if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) + .get_mut(key) + .map(Arc::make_mut) + { + self.key_tree.update_with_key(key, stroke.bounds()); + } + } self.update_chrono_to_last(key); } } From 3b4522dfc9184535ce358b0b89c26aadadb918fe Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:44:07 +0200 Subject: [PATCH 13/15] fix typo --- crates/rnote-engine/src/store/stroke_comp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index c96809c97c..045146f1ad 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -68,7 +68,7 @@ impl StrokeStore { .collect() } - /// Storke keys in the order that they should be rendered. + /// Stroke keys in the order that they should be rendered. pub(crate) fn stroke_keys_as_rendered(&self) -> Vec { self.keys_sorted_chrono() .into_iter() From a413246632f4d4b6b3082f299060401a7b6822a6 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:04:10 +0200 Subject: [PATCH 14/15] use rtree for `update_content_rendering_current_viewport` calculations --- crates/rnote-engine/src/document/mod.rs | 24 ++++++++++++++------ crates/rnote-engine/src/store/keytree.rs | 9 ++++++-- crates/rnote-engine/src/store/mod.rs | 2 +- crates/rnote-engine/src/store/stroke_comp.rs | 13 ++--------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 84b360e1ed..929b38c5e5 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -9,6 +9,7 @@ pub use background::Background; pub use config::DocumentConfig; pub use format::Format; pub use layout::Layout; +use rstar::Envelope; // Imports use crate::engine::EngineConfig; @@ -256,10 +257,14 @@ impl Document { ); if include_content { - let keys = store.stroke_keys_as_rendered(); - let content_bounds = if let Some(content_bounds) = store.bounds_for_strokes(&keys) { - content_bounds - .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) + let rendered_bounds = store.key_tree.get_bounds(); + + let content_bounds = if rendered_bounds.area() > 0.0 { + Aabb::new( + na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], + na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], + ) + .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) } else { // If doc is empty, resize to one page with the format size Aabb::new(na::point![0.0, 0.0], self.config.format.size().into()) @@ -301,9 +306,14 @@ impl Document { .merged(&viewport.extend_by(na::vector![padding_horizontal, padding_vertical])); if include_content { - let keys = store.stroke_keys_as_rendered(); - let content_bounds = if let Some(content_bounds) = store.bounds_for_strokes(&keys) { - content_bounds.extend_by(na::vector![padding_horizontal, padding_vertical]) + let rendered_bounds = store.key_tree.get_bounds(); + + let content_bounds = if rendered_bounds.area() > 0.0 { + Aabb::new( + na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], + na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], + ) + .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) } else { // If doc is empty, resize to one page with the format size Aabb::new(na::point![0.0, 0.0], self.config.format.size().into()) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 9a09adec7f..9df2f60bfd 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -1,16 +1,17 @@ // Imports use super::StrokeKey; use p2d::bounding_volume::Aabb; +use rstar::AABB; use rstar::primitives::GeomWithData; /// The rtree object that holds the bounds and [StrokeKey]. -type KeyTreeObject = GeomWithData, StrokeKey>; +pub(crate) type KeyTreeObject = GeomWithData, StrokeKey>; #[derive(Debug, Default)] /// A Rtree with [StrokeKey]'s as associated data. /// /// Used for faster spatial queries. -pub(super) struct KeyTree(rstar::RTree); +pub(crate) struct KeyTree(rstar::RTree); impl KeyTree { /// Insert a new tree object with the given [StrokeKey] and bounds. @@ -73,6 +74,10 @@ impl KeyTree { pub(crate) fn get_tree(&self) -> &rstar::RTree { &self.0 } + + pub fn get_bounds(&self) -> AABB<[f64; 2]> { + self.0.root().envelope() + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index 844d6a13b3..c68e97ca9a 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -95,7 +95,7 @@ pub struct StrokeStore { /// /// Needs to be updated with `update_with_key()` when strokes changed their geometry or position! #[serde(skip)] - key_tree: KeyTree, + pub(crate) key_tree: KeyTree, } impl Default for StrokeStore { diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 045146f1ad..e638c54ca6 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -140,17 +140,8 @@ impl StrokeStore { /// Calculate the height needed to fit all strokes. pub(crate) fn calc_height(&self) -> f64 { - let strokes_iter = self - .stroke_keys_unordered() - .into_iter() - .filter_map(|key| self.stroke_components.get(key)); - - let strokes_min_y = strokes_iter - .clone() - .fold(0.0, |acc, stroke| stroke.bounds().mins[1].min(acc)); - let strokes_max_y = strokes_iter.fold(0.0, |acc, stroke| stroke.bounds().maxs[1].max(acc)); - - strokes_max_y - strokes_min_y + let bounds = self.key_tree.get_tree().root().envelope(); + bounds.upper()[1] - bounds.lower()[1] } /// Calculate the width needed to fit all strokes. From 6293f63e61b6ec77c048b5e6e83be6bbf1307fbe Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:19:04 +0200 Subject: [PATCH 15/15] rebuilt rtree for non trashed components only --- crates/rnote-engine/src/store/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index c68e97ca9a..73ca637ad7 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -147,6 +147,7 @@ impl StrokeStore { let tree_objects = self .stroke_components .iter() + .filter(|(key, _stroke)| self.trashed(*key).is_some_and(|x| !x)) .map(|(key, stroke)| (key, stroke.bounds())) .collect(); self.key_tree.rebuild_from_vec(tree_objects);