diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 7865375b69..8f7b74a6c4 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -89,6 +89,7 @@ impl VectorcraftApp { selection_bounds: None, content_bounds: None, plugin_keys: Default::default(), + placed_keys: Default::default(), cursors: Default::default(), view_moved: None, hover_cursor: None, diff --git a/crates/ui-egui/src/caches.rs b/crates/ui-egui/src/caches.rs index 38b790ecc4..7f0d73511d 100644 --- a/crates/ui-egui/src/caches.rs +++ b/crates/ui-egui/src/caches.rs @@ -61,6 +61,8 @@ pub struct CanvasCache { /// [`CacheKey::plugins`] by document uid, with the plug-in revision /// ([`vectorcraft_render::effects::plugin_revision`]) it was last checked at. pub plugin_keys: std::collections::HashMap, + /// [`CacheKey::placed`] by document uid ([`canvas::placed_key`]). + pub placed_keys: std::collections::HashMap, /// The tools' cursors as OS cursor bitmaps. pub cursors: cursors::Images, /// The view the canvas last drew and when it last changed (egui's input time): while it moves, @@ -82,6 +84,20 @@ pub struct CanvasCache { pub windows: crate::doc_windows::Windows, } +/// [`CanvasCache::placed_keys`]: a document's [`CacheKey::placed`] and the placed-document bitmap +/// generations ([`vectorcraft_render::placed_document::generation`]) it was checked at. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PlacedKey { + /// The last generation the document was checked at. + pub checked: u64, + /// The document's [`CacheKey::placed`]: the generation of its first check, or of the last check + /// that found a newly made bitmap the document may show. + pub key: u64, + /// The value before it last changed, and the last generation that value was checked at: no + /// bitmap of a placed document the document shows was made between the two. + pub before: (u64, u64), +} + /// [`CanvasCache::slices`]: the layout of the slices of (document uid, revision). pub type SliceCache = ((u64, u64), std::sync::Arc>); @@ -120,7 +136,10 @@ pub struct CacheKey { pub highlight_alternate_glyphs: bool, /// Type › Highlight Substituted Fonts. pub highlight_substituted_fonts: bool, - /// [`vectorcraft_render::placed_document::generation`]: placed documents' bitmaps made since. + /// A [`vectorcraft_render::placed_document::generation`] that changes when the frame may show a + /// newly made bitmap of a placed document ([`canvas::placed_key`]). Bitmaps of files that only + /// other documents hold leave it as it is, unless more bitmaps were made since the last check + /// than are recorded and the document shows a placed document. pub placed: u64, /// The plug-in revision ([`vectorcraft_render::effects::plugin_revision`]) of the last plug-in /// install or removal, or of the last time deferred plug-in effect stages gave results, after diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 781ca2c2f6..f68fbee17f 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -327,9 +327,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // their pixels as they rasterize, hard-edged. let smooth_images = pixel.is_none() || app.session.prefs.anti_aliased_bitmaps; let plugins = plugin_key(&mut app.canvas.plugin_keys, &app.session, st); + let placed = placed_key(&mut app.canvas.placed_keys, &app.session, st); // The frame renders the view at its zoom in the interface's points, as `xf` maps it. let drawn = v.on_screen(app.canvas_ui_zoom); - let key = cache_key(app, ui, st, &t, &drawn, (w, h, ppp), (pixel, smooth_images, plugins)); + let key = cache_key(app, ui, st, &t, &drawn, (w, h, ppp), (pixel, smooth_images, plugins, placed)); // Placed documents' bitmaps are being made: draw again when they are ready. if vectorcraft_render::placed_document::busy() { ui.ctx().request_repaint_after(std::time::Duration::from_millis(30)); @@ -579,7 +580,7 @@ fn cache_key( t: &Tokens, v: &View, (w, h, ppp): (u32, u32, f32), - (pixel, smooth_images, plugins): (Option<[i64; 4]>, bool, u64), + (pixel, smooth_images, plugins, placed): (Option<[i64; 4]>, bool, u64, u64), ) -> CacheKey { // Paths made into guides are drawn as the ruler guides are (Preferences › Guides & Grid). let guide_look = LineLook::guides(&app.session.prefs, t); @@ -603,7 +604,7 @@ fn cache_key( anti_alias: app.session.prefs.anti_aliased_artwork, highlight_alternate_glyphs: app.session.prefs.highlight_alternate_glyphs, highlight_substituted_fonts: app.session.prefs.highlight_substituted_fonts, - placed: vectorcraft_render::placed_document::generation(), + placed, plugins, pixel, smooth_images, @@ -637,6 +638,83 @@ fn plugin_key( } } +/// [`CacheKey::placed`] of document `st`, one of the documents open in `session`. `keys` holds each +/// document's value and the placed-document bitmap generations it was checked at +/// ([`vectorcraft_render::placed_document::generation`]). +fn placed_key( + keys: &mut std::collections::HashMap, + session: &vectorcraft_engine::Session, + st: &vectorcraft_engine::DocState, +) -> u64 { + placed_key_at(keys, session, st, vectorcraft_render::placed_document::generation(), vectorcraft_render::placed_document::made_between) +} + +/// [`placed_key`] at generation `now`, with `made` giving the files whose bitmaps were made between +/// two generations ([`vectorcraft_render::placed_document::made_between`]). The value becomes `now` +/// when `st` shows a placed document of a file whose bitmap was made since the last check, or when +/// `made` no longer knows which files they were and `st` shows a placed document of a file in its +/// images, as [`vectorcraft_render::placed_area`] decides (every file shows while a pattern is +/// edited). Otherwise it stays: a bitmap made for another document, or for a file `st` keeps only +/// for Undo, leaves this document's frame as it is. A document checked for the first time takes +/// `now`. +fn placed_key_at( + keys: &mut std::collections::HashMap, + session: &vectorcraft_engine::Session, + st: &vectorcraft_engine::DocState, + now: u64, + made: impl FnOnce(u64, u64) -> Option>, +) -> u64 { + let entry = match keys.get(&st.uid).copied() { + Some(p) if p.checked == now => return p.key, + None => crate::PlacedKey { checked: now, key: now, before: (now, now) }, + Some(p) => { + let images = &st.doc.images; + // The files made that `st` holds: a placed document of one of them may show. + let files: std::collections::HashSet = if images.is_empty() { + Default::default() + } else { + match made(p.checked, now) { + Some(files) => files.into_iter().filter(|f| images.contains_key(f)).collect(), + None => images.keys().cloned().collect(), + } + }; + let stays = matches!(vectorcraft_render::placed_area(&st.doc, &files), vectorcraft_render::Changed::Nothing); + if stays { crate::PlacedKey { checked: now, ..p } } else { crate::PlacedKey { checked: now, key: now, before: (p.key, p.checked) } } + } + }; + keys.retain(|uid, _| session.documents().iter().any(|d| d.uid == *uid)); + keys.insert(st.uid, entry); + entry.key +} + +/// The generation after which the bitmaps made decide what a frame shown at [`CacheKey::placed`] +/// value `shown` draws again at value `key`, with `entry` the document's [`crate::PlacedKey`]. When +/// the value changed once since `shown`, it is the last generation `shown` was checked at: no +/// bitmap of the document's placed documents was made between `shown` and that generation. +/// Otherwise it is `shown`. +fn placed_since(entry: Option<&crate::PlacedKey>, shown: u64, key: u64) -> u64 { + entry.filter(|p| p.key == key && p.before.0 == shown).map_or(shown, |p| p.before.1) +} + +/// What placed documents' bitmaps change in `doc` between a frame shown at [`CacheKey::placed`] +/// value `shown` and the frame at value `key`: where `doc` shows the files whose bitmaps were made +/// after [`placed_since`], with `entry` the document's [`crate::PlacedKey`] and `made` giving the +/// files whose bitmaps were made between two generations +/// ([`vectorcraft_render::placed_document::made_between`]). None when those files are no longer +/// known. +fn placed_changes( + entry: Option<&crate::PlacedKey>, + shown: u64, + key: u64, + doc: &vectorcraft_doc::Document, + made: impl FnOnce(u64, u64) -> Option>, +) -> Option { + if shown == key { + return Some(vectorcraft_render::Changed::Nothing); + } + Some(vectorcraft_render::placed_area(doc, &made(placed_since(entry, shown, key), key)?)) +} + /// Once the first view of a document has been drawn, the status bar says how many plug-in effect /// stages it left deferred, if any. fn report_deferred(app: &mut VectorcraftApp, effects: &vectorcraft_plugins::deferred::Deferrals) { diff --git a/crates/ui-egui/src/canvas/panes.rs b/crates/ui-egui/src/canvas/panes.rs index fb305d29dd..fc440be673 100644 --- a/crates/ui-egui/src/canvas/panes.rs +++ b/crates/ui-egui/src/canvas/panes.rs @@ -12,8 +12,8 @@ use super::outline::{c32, node_outline, stroke_path, too_many_anchors}; use super::view::fit_zoom; use super::{ Affine, CacheKey, Point, Rect, RulerScale, Sense, Stroke, StrokeKind, Tokens, Ui, VectorcraftApp, View, Xf, artboard_edges, cache_key, - dim_on_canvas, frame_options, frame_view, grid, guides_of, json, paint_paper, pixel_region, plugin_key, pos2, rulers, shows_artboards, - textured_quad, vec2, view_rects, + dim_on_canvas, frame_options, frame_view, grid, guides_of, json, paint_paper, pixel_region, placed_key, plugin_key, pos2, rulers, + shows_artboards, textured_quad, vec2, view_rects, }; use crate::doc_windows::Win; use crate::thumb_worker::ThumbWorker; @@ -305,8 +305,9 @@ pub(crate) fn pane(app: &mut VectorcraftApp, ui: &Ui, w: Win, cell: egui::Rect) let pixel = (app.ui.view.pixel_preview && xf.zoom * ppp as f64 > 1.0).then(|| pixel_region(&xf)).flatten(); let smooth_images = pixel.is_none() || app.session.prefs.anti_aliased_bitmaps; let plugins = plugin_key(&mut app.canvas.plugin_keys, &app.session, st); + let placed = placed_key(&mut app.canvas.placed_keys, &app.session, st); let drawn = v.on_screen(ui_zoom); - let mut key = cache_key(app, ui, st, &t, &drawn, (pw, ph, ppp), (pixel, smooth_images, plugins)); + let mut key = cache_key(app, ui, st, &t, &drawn, (pw, ph, ppp), (pixel, smooth_images, plugins, placed)); // The current document's drag and Image Trace hold are not this document's, and this view has // its own Outline or Preview. (key.dragging, key.trace_source, key.outline) = (false, None, v.outline); diff --git a/crates/ui-egui/src/canvas/panes/tests.rs b/crates/ui-egui/src/canvas/panes/tests.rs index 9dc008a896..cb1394f612 100644 --- a/crates/ui-egui/src/canvas/panes/tests.rs +++ b/crates/ui-egui/src/canvas/panes/tests.rs @@ -323,6 +323,22 @@ fn other_panes_show_pixel_preview() { assert!(x1 > x0 && y1 > y0); } +/// A pane's render key takes its document's [`CacheKey::placed`] value ([`placed_key`]), which a +/// bitmap made for a test running beside this one does not move: the document holds no images. +#[test] +fn a_panes_key_takes_its_documents_placed_value() { + let mut app = app_with(2); + let ctx = egui::Context::default(); + app.run("window.arrange.tile", json!({})).unwrap(); + let uid = app.session.documents()[0].uid; + let generation = vectorcraft_render::placed_document::generation(); + let marked = generation.wrapping_add(1000); + app.canvas.placed_keys.insert(uid, crate::PlacedKey { checked: generation, key: marked, before: (marked, generation) }); + frame(&mut app, &ctx, vec![]); + let key = app.canvas.panes.shown.get(&(uid, 1)).map(|(k, _)| k.placed); + assert_eq!(key, Some(marked)); +} + #[test] fn a_resized_window_fits_the_tiled_documents_again_unless_one_was_zoomed() { let mut app = app_with(3); diff --git a/crates/ui-egui/src/canvas/raster.rs b/crates/ui-egui/src/canvas/raster.rs index 64fc3599f5..2722bc79fe 100644 --- a/crates/ui-egui/src/canvas/raster.rs +++ b/crates/ui-egui/src/canvas/raster.rs @@ -57,11 +57,8 @@ pub(super) fn changed_pixels( return None; } // The placed documents' files whose bitmaps were made since the frame shown. - let placed = if shown_key.placed == key.placed { - vectorcraft_render::Changed::Nothing - } else { - vectorcraft_render::placed_area(doc, &vectorcraft_render::placed_document::made_between(shown_key.placed, key.placed)?) - }; + let entry = app.session.documents().iter().find(|d| d.uid as usize == key.doc).and_then(|d| app.canvas.placed_keys.get(&d.uid)); + let placed = super::placed_changes(entry, shown_key.placed, key.placed, doc, vectorcraft_render::placed_document::made_between)?; match app.canvas.renderer.changed(shown, doc, opts).and(placed) { vectorcraft_render::Changed::Nothing => Some(None), vectorcraft_render::Changed::Area(area) => match vectorcraft_render::view_pixels(area, view, w, h) { diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 66f3f1423c..04a9f2e4c8 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -2856,13 +2856,10 @@ fn cached_frames_keep_their_scale_during_resize_rotation_and_dpi_changes() { "{old_rot}° -> {new_rot}°, DPI {old_ppp} -> {new_ppp}, pixel={pixel}: {vertex:?}" ); } - // Deliver the actual replacement and verify that it replaces the cached key. The frame - // reports a slow render: when a parallel test makes a placed document's bitmap, the key's - // process-wide `placed` generation moves, and the next frame goes to the controlled worker - // instead of rendering over the delivered frame on this thread. + // Deliver the actual replacement and verify that it replaces the cached key. let img = app.canvas.renderer.render(&job.doc, job.w, job.h, job.view, &job.opts); let expected = job.key.clone(); - completed.send(crate::render_worker::Done { key: job.key, doc: job.doc, opts: job.opts, img, ms: 50.0, moved: 0, reused: false }).unwrap(); + completed.send(crate::render_worker::Done { key: job.key, doc: job.doc, opts: job.opts, img, ms: 1.0, moved: 0, reused: false }).unwrap(); frame(&mut app, size, new_ppp); assert_eq!(app.canvas.key.as_ref(), Some(&expected), "the worker's frame arrived"); } @@ -2942,21 +2939,6 @@ fn a_turned_view_draws_the_last_frame_turned_until_its_own_arrives() { /// fill is dropped. #[test] fn a_pan_fills_the_area_the_last_frame_leaves_until_the_worker_frame_arrives() { - // The canvas key holds the global placed-document bitmap clock, which tests on other threads - // move: a frame whose key changed by more than the view drops the fill. An attempt that fails - // while the clock moved runs again; one that fails while it stood still is a real failure. - for _ in 0..20 { - let clock = vectorcraft_render::placed_document::generation(); - match std::panic::catch_unwind(pan_fills_the_area_the_last_frame_leaves) { - Ok(()) => return, - Err(_) if vectorcraft_render::placed_document::generation() != clock => {} - Err(e) => std::panic::resume_unwind(e), - } - } - pan_fills_the_area_the_last_frame_leaves(); -} - -fn pan_fills_the_area_the_last_frame_leaves() { let ctx = egui::Context::default(); let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); @@ -3185,6 +3167,200 @@ fn the_canvas_waits_only_while_the_view_of_a_slow_frame_changes() { assert_eq!(still_wait(&mut still, Some(&shown), &shown, true, 15.0), None, "the frame shown"); } +/// A bitmap made in the background for a file that only another document holds leaves this +/// document's canvas key as it is. Without a render worker (the web build's setting), a pan of a +/// heavy canvas keeps drawing the frame shown until the view is still. A light canvas renders the +/// pan at once, and its key differs from the frame shown only in the view: the pan fill and the +/// worker's pixel copy ask for that. +#[test] +fn a_bitmap_made_for_another_documents_file_leaves_a_pan_waiting() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 100, "y": 50, "width": 200, "height": 200})).unwrap(); + app.canvas.worker_started = true; // no worker, as in the web build + app.canvas.waits_for_still = true; + // Another window's document places a file, which its canvas asks a bitmap of. + let mut other = VectorcraftApp::new(Session::new(), Default::default()); + other.canvas.worker_started = true; // render on this thread + let mut src = Session::new(); + src.execute("file.new", &json!({"width": 70, "height": 40})).unwrap(); + src.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 70, "height": 40})).unwrap(); + let dir = std::env::temp_dir().join(format!("vectorcraft-placed-pan-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("badge.vectorcraft"); + std::fs::write(&path, vectorcraft_format::save_file(&src.active().unwrap().doc)).unwrap(); + other.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + other.session.execute("file.place", &json!({"path": path.to_string_lossy(), "at": [100, 100]})).unwrap(); + let ctx = egui::Context::default(); + let frame = |app: &mut VectorcraftApp| { + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| show(app, ui)); + out.textures_delta.clear(); + }; + frame(&mut app); // the first frame fits the view to the artboard + frame(&mut app); + app.canvas.last_ms = 1.0; + app.view_mut().unwrap().center.x += 10.0; + frame(&mut app); + let shown = app.canvas.key.clone().unwrap(); + // The other canvas asks for the bitmap, and it is made: the generation changes before the + // bitmap stops being pending. + let generation = vectorcraft_render::placed_document::generation(); + shapes(&mut other, &egui::Context::default()); + while vectorcraft_render::placed_document::busy() { + std::thread::yield_now(); + } + assert_ne!(vectorcraft_render::placed_document::generation(), generation, "a bitmap was made"); + app.canvas.last_ms = 50.0; + app.view_mut().unwrap().center.x += 10.0; + frame(&mut app); + assert_eq!(app.canvas.key.as_ref(), Some(&shown), "a heavy canvas is not rendered while the view moves"); + app.canvas.last_ms = 1.0; + app.view_mut().unwrap().center.x += 10.0; + frame(&mut app); + let next = app.canvas.key.clone().unwrap(); + assert_eq!(next.cx, app.view().unwrap().center.x, "a light canvas renders at once"); + assert!(super::cursor::only_view_differs(&shown, &next), "only the view differs: {shown:?} {next:?}"); + std::fs::remove_dir_all(&dir).ok(); +} + +/// A document's [`CacheKey::placed`] moves to the current placed-document bitmap generation only +/// when a bitmap of a file it shows in a placed document was made, or when the files made are no +/// longer known and it shows a placed document. A bitmap of another document's file, or of a file +/// it keeps only for Undo, leaves it, so a pan of a heavy +/// document keeps waiting for the view to stop. Each change keeps the value before it and the +/// last generation that value was checked at. A closed document's value is dropped. +#[test] +fn a_documents_placed_key_moves_only_for_bitmaps_of_its_own_files() { + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + let plain = session.active().unwrap().uid; + let mut src = Session::new(); + src.execute("file.new", &json!({"width": 60, "height": 30})).unwrap(); + src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 60, "height": 30})).unwrap(); + let dir = std::env::temp_dir().join(format!("vectorcraft-placed-key-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("card.vectorcraft"); + std::fs::write(&path, vectorcraft_format::save_file(&src.active().unwrap().doc)).unwrap(); + session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let r = session.execute("file.place", &json!({"path": path.to_string_lossy(), "at": [60, 50]})).unwrap(); + let st = session.active().unwrap(); + let placing = st.uid; + let file = match &st.doc.node(vectorcraft_doc::NodeId(r["ids"][0].as_u64().unwrap())).unwrap().kind { + vectorcraft_doc::NodeKind::PlacedDocument(p) => p.key.clone(), + other => panic!("a placed document: {other:?}"), + }; + type Made<'a> = &'a dyn Fn(u64, u64) -> Option>; + fn key(keys: &mut std::collections::HashMap, session: &Session, uid: u64, now: u64, made: Made<'_>) -> u64 { + let st = session.documents().iter().find(|d| d.uid == uid).unwrap(); + placed_key_at(keys, session, st, now, made) + } + let files = |names: &[&str]| Some(names.iter().map(|n| n.to_string()).collect::>()); + let unasked = |_: u64, _: u64| -> Option> { panic!("no bitmaps to look up") }; + let mut keys = std::collections::HashMap::new(); + assert_eq!(key(&mut keys, &session, placing, 5, &unasked), 5, "checked for the first time"); + assert_eq!(key(&mut keys, &session, placing, 5, &unasked), 5, "no bitmap made since"); + let other = |from: u64, to: u64| { + assert_eq!((from, to), (5, 6), "the bitmaps made since the last check"); + files(&["another document's file"]) + }; + assert_eq!(key(&mut keys, &session, placing, 6, &other), 5, "another document's file"); + assert_eq!(key(&mut keys, &session, placing, 7, &|_, _| files(&["another document's file", file.as_str()])), 7, "its own file"); + assert_eq!(keys[&placing], crate::PlacedKey { checked: 7, key: 7, before: (5, 6) }); + assert_eq!(key(&mut keys, &session, placing, 8, &|_, _| None), 8, "files no longer known"); + assert_eq!(keys[&placing].before, (7, 7)); + assert_eq!(key(&mut keys, &session, plain, 8, &unasked), 8, "the other document, checked for the first time"); + assert_eq!(key(&mut keys, &session, plain, 9, &|_, _| None), 8, "no images: files no longer known"); + assert_eq!(key(&mut keys, &session, plain, 10, &|_, _| files(&[file.as_str()])), 8, "no images: a file another document holds"); + // The same document holding an image and showing no placed document. + let mut raster = session.documents().iter().find(|d| d.uid == plain).unwrap().clone(); + let blob = vectorcraft_doc::ImageBlob { mime: "image/png".into(), bytes: Default::default(), proxy: None }; + std::sync::Arc::make_mut(&mut raster.doc).images.insert("an image".into(), blob); + assert_eq!(placed_key_at(&mut keys, &session, &raster, 11, |_, _| None), 8, "an image and no placed document: files no longer known"); + assert_eq!(keys.len(), 2); + let index = session.documents().iter().position(|d| d.uid == plain).unwrap(); + session.execute("file.close", &json!({"index": index})).unwrap(); + assert_eq!(key(&mut keys, &session, placing, 12, &|_, _| files(&[])), 8, "no bitmaps of its files"); + assert_eq!(keys.keys().copied().collect::>(), [placing], "the closed document's value is dropped"); + // The placed document deleted: its file stays in the images for Undo, and no longer shows. + assert_eq!(session.active().unwrap().uid, placing); + session.execute("edit.clear", &json!({"ids": [r["ids"][0]]})).unwrap(); + assert!(session.active().unwrap().doc.images.contains_key(&file), "kept for Undo"); + assert_eq!(key(&mut keys, &session, placing, 13, &|_, _| files(&[file.as_str()])), 8, "a file kept only for Undo"); + std::fs::remove_dir_all(&dir).ok(); +} + +/// The canvas key takes the document's value from [`placed_key`], which a bitmap made for a test +/// running beside this one does not move: this document holds no images. +#[test] +fn the_canvas_key_takes_the_documents_placed_value() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 100, "y": 50, "width": 200, "height": 200})).unwrap(); + app.canvas.worker_started = true; // render on this thread + let uid = app.session.active().unwrap().uid; + let generation = vectorcraft_render::placed_document::generation(); + let marked = generation.wrapping_add(1000); + app.canvas.placed_keys.insert(uid, crate::PlacedKey { checked: generation, key: marked, before: (marked, generation) }); + shapes(&mut app, &egui::Context::default()); + assert_eq!(app.canvas.key.as_ref().map(|k| k.placed), Some(marked)); +} + +/// A frame shown at one [`CacheKey::placed`] value and drawn again at the next looks at the +/// bitmaps made since the last check before the change: those made earlier were of other +/// documents' files, and past 256 bitmaps they are no longer known ([`placed_since`]). +#[test] +fn a_frame_drawn_again_after_one_change_of_its_placed_value_looks_at_the_bitmaps_since_the_last_check() { + let entry = crate::PlacedKey { checked: 400, key: 400, before: (5, 399) }; + assert_eq!(placed_since(Some(&entry), 5, 400), 399, "the value changed once since the frame shown"); + assert_eq!(placed_since(Some(&entry), 3, 400), 3, "it changed more than once"); + assert_eq!(placed_since(Some(&entry), 5, 401), 5, "a value the document no longer has"); + assert_eq!(placed_since(None, 5, 400), 5, "a document not checked"); +} + +/// A frame drawn again at a new [`CacheKey::placed`] value draws again where the document shows +/// the files whose bitmaps were made after [`placed_since`]. After one change of the value, the +/// bitmaps made before the last check of the old value are not looked up: past 256 bitmaps they +/// are no longer known, and the whole frame would render. At the same value nothing is looked up. +#[test] +fn a_frame_drawn_again_at_a_new_placed_value_draws_where_the_bitmaps_since_the_last_check_show() { + let mut session = Session::new(); + let mut src = Session::new(); + src.execute("file.new", &json!({"width": 60, "height": 30})).unwrap(); + src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 60, "height": 30})).unwrap(); + let dir = std::env::temp_dir().join(format!("vectorcraft-placed-changes-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("card.vectorcraft"); + std::fs::write(&path, vectorcraft_format::save_file(&src.active().unwrap().doc)).unwrap(); + session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let r = session.execute("file.place", &json!({"path": path.to_string_lossy(), "at": [60, 50]})).unwrap(); + session.execute("shape.rectangle", &json!({"x": 250, "y": 200, "width": 40, "height": 30})).unwrap(); + let doc = &session.active().unwrap().doc; + let placed = doc.node(vectorcraft_doc::NodeId(r["ids"][0].as_u64().unwrap())).unwrap(); + let file = match &placed.kind { + vectorcraft_doc::NodeKind::PlacedDocument(p) => p.key.clone(), + other => panic!("a placed document: {other:?}"), + }; + let bounds = placed.geometric_bounds().unwrap(); + let entry = crate::PlacedKey { checked: 400, key: 400, before: (5, 399) }; + // Only the bitmaps made after generation 399 are known: one of the placed file. + let made = |from: u64, to: u64| (from >= 399 && to == 400).then(|| std::collections::HashSet::from([file.clone()])); + let unasked = |_: u64, _: u64| -> Option> { panic!("no bitmaps to look up") }; + assert_eq!(placed_changes(Some(&entry), 400, 400, doc, unasked), Some(vectorcraft_render::Changed::Nothing), "the same value"); + match placed_changes(Some(&entry), 5, 400, doc, made) { + Some(vectorcraft_render::Changed::Area(a)) => { + assert!(a.x0 <= bounds.x0 && a.y0 <= bounds.y0 && a.x1 >= bounds.x1 && a.y1 >= bounds.y1, "{a:?} holds {bounds:?}"); + assert!(a.x1 < 250.0, "{a:?}: not the rectangle"); + } + other => panic!("the placed document's part: {other:?}"), + } + assert_eq!(placed_changes(Some(&entry), 3, 400, doc, made), None, "the value changed more than once: no longer known"); + let none = |_: u64, _: u64| Some(std::collections::HashSet::new()); + assert_eq!(placed_changes(Some(&entry), 5, 400, doc, none), Some(vectorcraft_render::Changed::Nothing), "no bitmaps since"); + std::fs::remove_dir_all(&dir).ok(); +} + /// File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (#394): off, Pixel /// Preview draws images with each pixel taking its nearest image pixel; on, smoothly, as the /// canvas always does out of Pixel Preview. @@ -11592,31 +11768,11 @@ fn a_pan_over_art_the_frame_size_decides_renders_whole() { } } -/// Runs `test`, whose canvas key holds the global placed-document bitmap clock, which tests on -/// other threads move: a frame whose key changed by more than the view renders whole. An attempt -/// that fails while the clock moved runs again; one that fails while it stood still is a real -/// failure. -fn while_the_placed_clock_stands_still(test: fn()) { - for _ in 0..20 { - let clock = vectorcraft_render::placed_document::generation(); - match std::panic::catch_unwind(test) { - Ok(()) => return, - Err(_) if vectorcraft_render::placed_document::generation() != clock => {} - Err(e) => std::panic::resume_unwind(e), - } - } - test(); -} - /// The canvas asks the worker for a frame that may copy pixels when the view moves, keeps the whole /// frame's time, and renders the frame whole once the view has been still for STILL_SECS, not /// before and only once. #[test] fn a_frame_with_copied_pixels_renders_whole_once_the_view_stops() { - while_the_placed_clock_stands_still(frame_with_copied_pixels_renders_whole_once_the_view_stops); -} - -fn frame_with_copied_pixels_renders_whole_once_the_view_stops() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); app.canvas.worker_started = true; @@ -11663,10 +11819,6 @@ fn frame_with_copied_pixels_renders_whole_once_the_view_stops() { /// first step of the next pan copies from it. The jobs are the ones the canvas asks for. #[test] fn a_pan_after_the_settling_frame_copies() { - while_the_placed_clock_stands_still(pan_after_the_settling_frame_copies); -} - -fn pan_after_the_settling_frame_copies() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); app.canvas.worker_started = true; @@ -11778,9 +11930,7 @@ fn a_documents_first_frame_renders_on_the_worker() { let job = jobs.try_recv().expect("the first frame is queued on the worker"); assert!(app.canvas.texture.is_none(), "not rendered on the UI thread"); frame(&mut app); - // A placed document of a test running beside this one moves the process-wide generation in - // the key: that makes a new frame, not this one again. - assert!(jobs.try_iter().all(|j| j.key.placed != job.key.placed), "not queued again while it renders"); + assert!(jobs.try_recv().is_err(), "not queued again while it renders"); assert!(app.canvas.texture.is_none()); done_tx.send(finished(&job)).unwrap(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b5964fe274..c4c6ded189 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -116,7 +116,7 @@ mod clock; mod services; pub use app::MAX_RECENT_FONTS; -pub use caches::{CacheKey, CanvasCache, LivePaintGapsCache, Perf, PrintTilingCache, SliceCache}; +pub use caches::{CacheKey, CanvasCache, LivePaintGapsCache, Perf, PlacedKey, PrintTilingCache, SliceCache}; pub use clock::{now_ms, now_secs}; pub use craft_ui::docking::Monitor as DockMonitor; #[cfg(test)]