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/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/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/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 55066bbec1..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; @@ -163,23 +162,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 +213,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 +227,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(()) } @@ -251,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(()) } 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 5979de0dd2..d8d7aeb42b 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, } } } @@ -84,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, @@ -93,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( @@ -103,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( @@ -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; @@ -199,9 +220,17 @@ 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; + } 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( @@ -211,6 +240,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 @@ -223,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( @@ -233,6 +277,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 @@ -249,8 +307,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; @@ -319,6 +383,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/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 1b80b2e1ce..821b33020a 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -209,6 +209,14 @@ 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 + // 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) } 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/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 72428c5e4c..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. @@ -69,6 +70,14 @@ impl KeyTree { pub(crate) fn clear(&mut self) { *self = Self::default() } + + 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 e73fc2f7ef..73ca637ad7 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! { @@ -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 { @@ -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); @@ -319,13 +320,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)); + self.key_tree.insert_with_key(key, bounds); + 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())); @@ -335,6 +343,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-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 013c0d825a..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; @@ -96,6 +99,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, @@ -242,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 { @@ -314,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. @@ -498,6 +536,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), @@ -656,6 +699,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(()) } } diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7acfa08093..e638c54ca6 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; @@ -67,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() @@ -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. @@ -127,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. 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); } } 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 221ece6282..18fe88fd28 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 @@ -322,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( @@ -355,6 +360,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 +373,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; }; @@ -379,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; } } @@ -390,6 +403,9 @@ fn retrieve_pointer_elements( axes[crate::utils::axis_use_idx(gdk::AxisUse::Y)] ]); 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 @@ -399,12 +415,47 @@ 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 .position() .map(|(x, y)| transform_pos(na::vector![x, y]))?; + // on recent gtk this DOES fail let pressure = if is_stylus { event.axis(gdk::AxisUse::Pressure).unwrap() } else { @@ -446,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 + } } } 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 }}