From 092f28feb33a4a9d4ebe1d7da445a007af6972c6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 08:53:26 -0800 Subject: [PATCH] Canvas: each document keeps its own placed-bitmap value in the frame key The canvas cache key held the process-wide placed-document bitmap generation, so a bitmap made for a file that only another document holds changed every document's key, and the next frame differed from the frame shown in more than the view. In the web build, which has no render worker, a heavy canvas then rendered in the middle of a pan instead of waiting until the view was still. On desktop the pan dropped its low-resolution fill, and the render worker did not copy pixels from its last frame. Each document now keeps its own value, checked once per generation change, as the plug-in key is. The value moves to the current generation when the document shows a placed document of a file whose bitmap was made. A file kept only for Undo does not count. When more bitmaps were made since the last check than are recorded, the value moves when the document shows a placed document of one of its images. After the value moves, the part of the frame to draw again comes from the bitmaps made since the last check before the move, so bitmaps made earlier for other documents no longer make the whole frame render. The canvas tests that retried or tolerated a new frame when another test made a bitmap now assert without those workarounds. --- crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/caches.rs | 21 +- crates/ui-egui/src/canvas.rs | 84 +++++++- crates/ui-egui/src/canvas/panes.rs | 7 +- crates/ui-egui/src/canvas/panes/tests.rs | 16 ++ crates/ui-egui/src/canvas/raster.rs | 7 +- crates/ui-egui/src/canvas/tests.rs | 244 ++++++++++++++++++----- crates/ui-egui/src/lib.rs | 2 +- 8 files changed, 322 insertions(+), 60 deletions(-) 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 202a0542a8..f1bf96bd6d 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)]