From a92f0264c2d7fb1b3e2da0abddb16505cb6934e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ju=CC=88rg=20Lehni?= Date: Wed, 21 Oct 2026 19:18:58 +0200 Subject: [PATCH] Canvas: each update copies the rendered frame whole instead of pixel by pixel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderer's pixels and egui's are both premultiplied RGBA8, so color_image copies the bytes in one go. At 3456×2234 that takes 0.9 ms instead of 3.4 ms per update, at 5120×2880 1.6 ms instead of 6.8 ms. Every rendered image the UI shows (canvas, thumbnails, previews) goes through it. --- Cargo.lock | 1 + crates/ui-egui/Cargo.toml | 4 ++- crates/ui-egui/src/canvas.rs | 32 ++++++++++++++++++- crates/ui-egui/src/dialogs/graph_design.rs | 2 +- crates/ui-egui/src/panels/asset_export.rs | 2 +- .../ui-egui/src/panels/flattener_preview.rs | 2 +- crates/ui-egui/src/panels/graphic_styles.rs | 4 +-- crates/ui-egui/src/panels/layers.rs | 2 +- crates/ui-egui/src/panels/navigator.rs | 2 +- crates/ui-egui/src/panels/swatches.rs | 2 +- crates/ui-egui/src/panels/symbols.rs | 2 +- crates/ui-egui/src/panels/transparency.rs | 2 +- crates/ui-egui/src/widgets.rs | 4 +-- 13 files changed, 47 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3e7de4e4..4daf5fd2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5022,6 +5022,7 @@ dependencies = [ name = "vectorcraft-ui-egui" version = "0.14.0" dependencies = [ + "bytemuck", "craft-ui", "egui", "egui_extras", diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 6c8f6a4f9..dc7311745 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -33,7 +33,9 @@ vectorcraft-pdf = { workspace = true } vectorcraft-cad = { workspace = true } vectorcraft-eps = { workspace = true } vectorcraft-format = { workspace = true } -egui = { workspace = true } +egui = { workspace = true, features = ["bytemuck"] } +# With egui's `bytemuck`: copies rendered rasters into egui images whole (`canvas::color_image`). +bytemuck = "1" egui_extras = { workspace = true } image = { workspace = true } serde = { workspace = true } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e5fedbc1d..fd20c2afd 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3114,9 +3114,22 @@ fn kurbo_flatten(p: &BezPath, tol: f64, f: &mut impl FnMut(PathEl)) { vectorcraft_geom::kurbo::flatten(p.elements().iter().copied(), tol, f); } +/// The rendered raster `img` as an egui image. Both are premultiplied RGBA8, so its bytes are copied +/// over whole instead of pixel by pixel, which costs milliseconds for a canvas on a large screen. +/// A raster whose bytes don't match its size gives an empty image of that size. +pub(crate) fn color_image(img: &vectorcraft_render::Rendered) -> egui::ColorImage { + let size = [img.width as usize, img.height as usize]; + let mut pixels = vec![egui::Color32::TRANSPARENT; size[0] * size[1]]; + let bytes: &mut [u8] = bytemuck::cast_slice_mut(&mut pixels); + if bytes.len() == img.pixels.len() { + bytes.copy_from_slice(&img.pixels); + } + egui::ColorImage::new(size, pixels) +} + /// Upload the art's raster; Pixel Preview's document pixels keep hard edges when magnified. fn upload(app: &mut VectorcraftApp, ctx: &egui::Context, img: &vectorcraft_render::Rendered, pixel: bool) { - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = color_image(img); let options = if pixel { egui::TextureOptions::NEAREST } else { egui::TextureOptions::LINEAR }; match &mut app.canvas.texture { Some(tex) => tex.set(color, options), @@ -3433,6 +3446,23 @@ mod tests { use vectorcraft_engine::Session; use vectorcraft_geom::Shape as _; + /// A rendered raster becomes the image egui's own conversion makes of it, half-transparent + /// edges and all; one whose bytes don't match its size becomes an empty image of that size. + #[test] + fn rendered_rasters_become_the_same_egui_images() { + let mut s = Session::new(); + s.execute("file.new", &serde_json::json!({})).unwrap(); + s.execute("shape.ellipse", &serde_json::json!({"x": 10, "y": 10, "width": 60, "height": 40})).unwrap(); + s.execute("paint.setFill", &serde_json::json!({"color": "#3366cc"})).unwrap(); + let doc = s.active().unwrap().doc.clone(); + let img = vectorcraft_render::Renderer::new().render(&doc, 97, 61, Affine::IDENTITY, &vectorcraft_render::RenderOptions::default()); + assert!(img.pixels.as_chunks::<4>().0.iter().any(|p| p[3] != 0 && p[3] != 255), "partly covered pixels"); + let egui = egui::ColorImage::from_rgba_premultiplied([97, 61], &img.pixels); + assert_eq!(color_image(&img), egui); + let short = vectorcraft_render::Rendered { width: 3, height: 2, pixels: vec![255; 5] }; + assert_eq!(color_image(&short), egui::ColorImage::new([3, 2], vec![egui::Color32::TRANSPARENT; 6])); + } + /// With VECTORCRAFT_SYSTEM_CURSORS set the glyphs aren't drawn and the system's cursors stand in: those of /// the Pen and the other drawing tools are the system crosshair already, so Use Precise Cursors makes no /// difference to them there. diff --git a/crates/ui-egui/src/dialogs/graph_design.rs b/crates/ui-egui/src/dialogs/graph_design.rs index deafd4060..75659920e 100644 --- a/crates/ui-egui/src/dialogs/graph_design.rs +++ b/crates/ui-egui/src/dialogs/graph_design.rs @@ -119,7 +119,7 @@ fn preview(ui: &egui::Ui, app: &VectorcraftApp, name: &str, r: egui::Rect) { let px = (r.width() * ui.ctx().pixels_per_point()).round() as u32; let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).or_else(|| { let img = RENDERER.with(|rr| rr.borrow_mut().render_node_thumbnail(&st.doc, &art, px.max(16), Some([255, 255, 255, 255])))?; - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); let tex = ui.ctx().load_texture(format!("graph-design-{key}"), color, egui::TextureOptions::LINEAR); CACHE.with(|c| { let mut c = c.borrow_mut(); diff --git a/crates/ui-egui/src/panels/asset_export.rs b/crates/ui-egui/src/panels/asset_export.rs index 006673d32..3cdd9a6f6 100644 --- a/crates/ui-egui/src/panels/asset_export.rs +++ b/crates/ui-egui/src/panels/asset_export.rs @@ -77,7 +77,7 @@ pub(crate) fn paint_thumb(ui: &Ui, st: &DocState, a: &ExportAsset, r: Rect) { rr.borrow_mut().render_node_thumbnail_with(&st.doc, &group, px, None, &opts) } })?; - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); let tex = ui.ctx().load_texture(format!("asset-thumb-{}", a.id), color, egui::TextureOptions::LINEAR); CACHE.with(|c| { let mut c = c.borrow_mut(); diff --git a/crates/ui-egui/src/panels/flattener_preview.rs b/crates/ui-egui/src/panels/flattener_preview.rs index 5d2f3e84d..0abd84238 100644 --- a/crates/ui-egui/src/panels/flattener_preview.rs +++ b/crates/ui-egui/src/panels/flattener_preview.rs @@ -349,7 +349,7 @@ fn render( } } } - egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels) + crate::canvas::color_image(&img) } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { diff --git a/crates/ui-egui/src/panels/graphic_styles.rs b/crates/ui-egui/src/panels/graphic_styles.rs index 6c6ba2791..5929185fb 100644 --- a/crates/ui-egui/src/panels/graphic_styles.rs +++ b/crates/ui-egui/src/panels/graphic_styles.rs @@ -171,7 +171,7 @@ fn render(app: &VectorcraftApp, paints: Paints, g: &GraphicStyle, preview: Previ let layer = doc.layers[0].id; doc.insert(Some(layer), 0, app.session.styled(&shape, g)).ok()?; let img = RENDERER.with(|r| r.borrow_mut().render_region(&doc, vectorcraft_geom::Rect::new(0.0, 0.0, SIDE, SIDE), px as f64 / SIDE, true)); - Some(egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels)) + Some(crate::canvas::color_image(&img)) } /// The thumbnail of style `g` (`look`: its [`look_hash`]) at `size` points. @@ -239,7 +239,7 @@ fn object_preview(app: &VectorcraftApp, ctx: &egui::Context, g: &GraphicStyle, l } let styled = app.session.styled(n, g); let img = RENDERER.with(|r| r.borrow_mut().render_node_thumbnail(&st.doc, &styled, px, Some([255; 4])))?; - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); let tex = ctx.load_texture("gs-large-object", color, egui::TextureOptions::LINEAR); ctx.data_mut(|d| d.insert_temp(slot, (key, tex.clone()))); Some(tex) diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index c670d110c..85e5a1035 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -1408,7 +1408,7 @@ fn small_node(n: &Node, limit: usize) -> bool { impl Thumbs { /// Keep `img` (None: nothing drawn) as the thumbnail of row `key` showing `node`. fn upload(&mut self, ctx: &egui::Context, key: (u64, NodeId), node: Weak, px: u32, img: Option<&vectorcraft_render::Rendered>, ms: f64) { - let color = img.map(|img| egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels)); + let color = img.map(crate::canvas::color_image); let used = ctx.cumulative_pass_nr(); let name = format!("layer-thumb-{}", key.1.0); match self.entries.get_mut(&key) { diff --git a/crates/ui-egui/src/panels/navigator.rs b/crates/ui-egui/src/panels/navigator.rs index a0d068c80..2b53ee61a 100644 --- a/crates/ui-egui/src/panels/navigator.rs +++ b/crates/ui-egui/src/panels/navigator.rs @@ -54,7 +54,7 @@ thread_local! { impl Cache { /// Upload image `r` showing `key` over `region`. fn upload(&mut self, ctx: &egui::Context, key: ImageKey, region: DRect, r: &vectorcraft_render::Rendered, ms: f64) { - let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.pixels); + let img = crate::canvas::color_image(r); match &mut self.tex { Some(tx) => tx.set(img, egui::TextureOptions::LINEAR), None => self.tex = Some(ctx.load_texture("navigator", img, egui::TextureOptions::LINEAR)), diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index f46de88ac..17679ca8d 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -194,7 +194,7 @@ fn pattern_thumb(app: &VectorcraftApp, ui: &Ui, r: Rect, paint: &Paint) { ); let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).unwrap_or_else(|| { let tex = vectorcraft_render::render_pattern_swatch(&st.doc, pattern, px).map(|img| { - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); ui.ctx().load_texture(format!("pattern-swatch-{pattern}-{px}"), color, egui::TextureOptions::LINEAR) }); CACHE.with(|c| { diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 016a5e327..eea7c86d1 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -38,7 +38,7 @@ fn thumb(ui: &Ui, doc: &Document, name: &str, size: f32) -> Option Some(tex), _ => RENDERER.with(|rr| rr.borrow_mut().render_node_thumbnail_with(&st.doc, n, px, bg, &super::layers::thumb_options())).map(|img| { - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); let tex = ui.ctx().load_texture(slot, color, egui::TextureOptions::LINEAR); ui.ctx().data_mut(|d| d.insert_temp(slot_id, (key, st.revision, tex.clone()))); tex diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index bb11987c0..c2d878ba1 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -2242,7 +2242,7 @@ pub fn doc_preview(ui: &Ui, key: &str, size: Vec2, build: impl FnOnce(f64, f64) } let doc = build(w, h)?; let img = RENDERER.with(|r| r.borrow_mut().render_region(&doc, vectorcraft_geom::Rect::new(0.0, 0.0, w, h), ppp, false)); - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); let tex = ui.ctx().load_texture(format!("preview-{key}"), color, egui::TextureOptions::LINEAR); CACHE.with(|c| { let mut c = c.borrow_mut(); @@ -2469,7 +2469,7 @@ pub fn region_texture( ) -> egui::TextureHandle { let scale = px / region.width().max(region.height()).max(1.0); let img = renderer.render_region(doc, region, scale, true); - let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); + let color = crate::canvas::color_image(&img); ctx.load_texture(name, color, egui::TextureOptions::LINEAR) }