mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Canvas: render again for finished image decodes in batches
The canvas renders with RenderOptions::defer_images and keeps the image generation each texture was rendered at. Finished background decodes are taken up at most every 250 ms and not while a render is in flight, so a page of photos decoding one after another re-renders a few times instead of once per photo, and a newer request doesn't drop the render in flight before it is shown. The eyedropper still decodes the image it samples. Based on #2051 by texjer.
This commit is contained in:
1 parent
9563e93bd2
commit
28fdd9a50e
4 files changed
+103
-23
No files matched your search
@@ -24,7 +24,10 @@ pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[
|
||||
let placed: Vec<designcraft_render::Placed> = layout.slots.iter().map(|s| designcraft_render::Placed { spread: s.spread, xf: s.xf }).collect();
|
||||
let ppp = ctx.pixels_per_point() as f64;
|
||||
let view = Affine::scale(ppp) * Affine::scale(xf.zoom) * Affine::translate((-at.x, -at.y));
|
||||
let opts = screen_options(app, &layout, app.ui.screen_mode != ScreenMode::Normal, ppp);
|
||||
let mut opts = screen_options(app, &layout, app.ui.screen_mode != ScreenMode::Normal, ppp);
|
||||
// The colour picked is applied: an image still decoding is decoded for it here, rather than
|
||||
// sampling the placeholder or a coarser stand-in the canvas shows meanwhile.
|
||||
opts.defer_images = false;
|
||||
let mut img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, (1, 1), view, &opts);
|
||||
// The colour as the canvas shows it (proofed).
|
||||
screen_post(app, &mut img);
|
||||
@@ -53,6 +56,7 @@ pub(super) fn screen_options(app: &DesignApp, layout: &CanvasLayout, preview: bo
|
||||
overprint_preview: app.ui.overprint_preview,
|
||||
blend_space_view: !app.ui.proof_colors,
|
||||
document_grid: if app.ui.document_grid && !preview { grid_areas(layout) } else { vec![] },
|
||||
defer_images: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -120,6 +124,26 @@ pub(super) fn invalidate_on_display_change(app: &mut DesignApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// How often the canvas renders again for images decoded in the background: a page of photos
|
||||
/// decoding one after another renders a few times, not once per photo.
|
||||
pub(super) const IMAGE_BATCH_MS: f64 = 250.0;
|
||||
|
||||
/// The image generation the canvas renders with, given the `live` one: `seen` (generation, when
|
||||
/// taken) moves to `live` at most every [`IMAGE_BATCH_MS`], and not while a render is in
|
||||
/// flight (it would be dropped for the newer one and never shown; finishing it repaints). Also
|
||||
/// returns how long to wait (ms) before asking again when `live` is newer.
|
||||
pub(super) fn image_generation(seen: &mut (u64, f64), live: u64, now: f64, rendering: bool) -> (u64, Option<f64>) {
|
||||
if live == seen.0 || rendering {
|
||||
return (seen.0, None);
|
||||
}
|
||||
let wait = seen.1 + IMAGE_BATCH_MS - now;
|
||||
if wait <= 0.0 {
|
||||
*seen = (live, now);
|
||||
return (live, None);
|
||||
}
|
||||
(seen.0, Some(wait))
|
||||
}
|
||||
|
||||
/// Keep the canvas texture current. Panning inside the overscanned region just shifts the
|
||||
/// texture; zooming shows the old texture scaled until the background render arrives; document
|
||||
/// edits render synchronously so editing never lags.
|
||||
@@ -129,26 +153,6 @@ pub(super) fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rec
|
||||
let ppp = ctx.pixels_per_point() as f64;
|
||||
let doc_key = (st.uid, std::sync::Arc::as_ptr(&st.doc) as u64, st.revision, preview);
|
||||
let editing_parents = st.editing_parents;
|
||||
// Overscan: 25% of the viewport on every side, within the GPU's maximum texture size.
|
||||
let max_side = ctx.input(|i| i.max_texture_side) as f32 / ppp as f32;
|
||||
let (ow, oh) =
|
||||
((rect.width() * 0.25).min((max_side - rect.width()) / 2.0).max(0.0), (rect.height() * 0.25).min((max_side - rect.height()) / 2.0).max(0.0));
|
||||
let want = |origin: Point| crate::Shown {
|
||||
doc: doc_key,
|
||||
editing_parents,
|
||||
origin: Point::new(origin.x - ow as f64 / xf.zoom, origin.y - oh as f64 / xf.zoom),
|
||||
zoom: xf.zoom,
|
||||
size: (rect.width() + 2.0 * ow, rect.height() + 2.0 * oh),
|
||||
};
|
||||
let target = want(xf.origin);
|
||||
// Does the current texture cover the viewport at this zoom and document state?
|
||||
let covers = |s: &crate::Shown| {
|
||||
s.doc == doc_key && s.editing_parents == editing_parents && (s.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
let x0 = (xf.origin.x - s.origin.x) * s.zoom;
|
||||
let y0 = (xf.origin.y - s.origin.y) * s.zoom;
|
||||
x0 >= -0.5 && y0 >= -0.5 && x0 + rect.width() as f64 <= s.size.0 as f64 + 0.5 && y0 + rect.height() as f64 <= s.size.1 as f64 + 0.5
|
||||
}
|
||||
};
|
||||
// Collect finished background renders.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let (Some(w), Some((tok, shown))) = (app.canvas.worker.as_ref(), app.canvas.pending)
|
||||
@@ -161,6 +165,35 @@ pub(super) fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rec
|
||||
app.canvas.pending = None;
|
||||
app.perf.render_ms = done.ms;
|
||||
}
|
||||
let images = {
|
||||
let live = designcraft_render::images::generation();
|
||||
let (images, wait) = image_generation(&mut app.canvas.images, live, crate::now_ms(), app.canvas.pending.is_some());
|
||||
if let Some(after) = wait.and_then(|ms| std::time::Duration::try_from_secs_f64(ms / 1000.0).ok()) {
|
||||
ctx.request_repaint_after(after);
|
||||
}
|
||||
images
|
||||
};
|
||||
// Overscan: 25% of the viewport on every side, within the GPU's maximum texture size.
|
||||
let max_side = ctx.input(|i| i.max_texture_side) as f32 / ppp as f32;
|
||||
let (ow, oh) =
|
||||
((rect.width() * 0.25).min((max_side - rect.width()) / 2.0).max(0.0), (rect.height() * 0.25).min((max_side - rect.height()) / 2.0).max(0.0));
|
||||
let want = |origin: Point| crate::Shown {
|
||||
doc: doc_key,
|
||||
editing_parents,
|
||||
origin: Point::new(origin.x - ow as f64 / xf.zoom, origin.y - oh as f64 / xf.zoom),
|
||||
zoom: xf.zoom,
|
||||
size: (rect.width() + 2.0 * ow, rect.height() + 2.0 * oh),
|
||||
images,
|
||||
};
|
||||
let target = want(xf.origin);
|
||||
// Does the current texture cover the viewport at this zoom and document state?
|
||||
let covers = |s: &crate::Shown| {
|
||||
s.doc == doc_key && s.editing_parents == editing_parents && s.images == images && (s.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
let x0 = (xf.origin.x - s.origin.x) * s.zoom;
|
||||
let y0 = (xf.origin.y - s.origin.y) * s.zoom;
|
||||
x0 >= -0.5 && y0 >= -0.5 && x0 + rect.width() as f64 <= s.size.0 as f64 + 0.5 && y0 + rect.height() as f64 <= s.size.1 as f64 + 0.5
|
||||
}
|
||||
};
|
||||
if app.canvas.shown.is_some_and(|s| covers(&s)) {
|
||||
return;
|
||||
}
|
||||
@@ -209,7 +242,7 @@ pub(super) fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rec
|
||||
{
|
||||
// Already rendering this view?
|
||||
if app.canvas.pending.is_some_and(|(_, p)| {
|
||||
p.doc == doc_key && p.editing_parents == editing_parents && (p.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
p.doc == doc_key && p.editing_parents == editing_parents && p.images == images && (p.zoom - xf.zoom).abs() < 1e-9 && {
|
||||
let x0 = (xf.origin.x - p.origin.x) * p.zoom;
|
||||
let y0 = (xf.origin.y - p.origin.y) * p.zoom;
|
||||
x0 >= 0.0 && y0 >= 0.0 && x0 + rect.width() as f64 <= p.size.0 as f64 && y0 + rect.height() as f64 <= p.size.1 as f64
|
||||
@@ -293,6 +326,9 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn render_worker_spawn(ctx: &egui::Context) -> Option<crate::render_worker::Worker> {
|
||||
// Images decoded in the background are drawn by the next render.
|
||||
let wake = ctx.clone();
|
||||
designcraft_render::images::set_waker(move || wake.request_repaint());
|
||||
if std::env::var_os("DESIGNCRAFT_SYNC_RENDER").is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -410,10 +410,24 @@ fn harness_with_step(app: DesignApp, step_dt: f32) -> Harness<'static, DesignApp
|
||||
h
|
||||
}
|
||||
|
||||
/// Background decodes finishing one after another are taken up in batches, and not while a
|
||||
/// render is in flight (a newer request would drop it before it is shown).
|
||||
#[test]
|
||||
fn finished_image_decodes_rerender_the_canvas_in_batches() {
|
||||
use render::{IMAGE_BATCH_MS, image_generation};
|
||||
let mut seen = (0, f64::NEG_INFINITY);
|
||||
assert_eq!(image_generation(&mut seen, 1, 1000.0, false), (1, None), "the first is taken at once");
|
||||
assert_eq!(image_generation(&mut seen, 2, 1010.0, false), (1, Some(IMAGE_BATCH_MS - 10.0)));
|
||||
assert_eq!(image_generation(&mut seen, 9, 1100.0, false).0, 1, "more finish meanwhile");
|
||||
assert_eq!(image_generation(&mut seen, 9, 1000.0 + IMAGE_BATCH_MS, true), (1, None), "a render is in flight");
|
||||
assert_eq!(image_generation(&mut seen, 9, 1000.0 + IMAGE_BATCH_MS, false), (9, None), "all taken by one render");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_option_change_drops_the_render_in_flight() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
let shown = crate::Shown { doc: (1, 2, 3, false), origin: Point::new(0.0, 0.0), zoom: 1.0, size: (10.0, 10.0), editing_parents: false };
|
||||
let shown =
|
||||
crate::Shown { doc: (1, 2, 3, false), origin: Point::new(0.0, 0.0), zoom: 1.0, size: (10.0, 10.0), editing_parents: false, images: 0 };
|
||||
invalidate_on_display_change(&mut app);
|
||||
app.canvas.shown = Some(shown);
|
||||
app.canvas.pending = Some((7, shown));
|
||||
|
||||
@@ -540,6 +540,9 @@ pub struct Shown {
|
||||
pub origin: Point,
|
||||
pub zoom: f64,
|
||||
pub size: (f32, f32),
|
||||
/// [`designcraft_render::images::generation`] when rendered: images decoded since are
|
||||
/// drawn by rendering again.
|
||||
pub images: u64,
|
||||
}
|
||||
|
||||
impl CanvasCache {
|
||||
@@ -562,6 +565,7 @@ impl CanvasCache {
|
||||
patcher: designcraft_render::Renderer::new(),
|
||||
patches: 0,
|
||||
separations: separations::Preview::default(),
|
||||
images: (0, f64::NEG_INFINITY),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,6 +599,9 @@ pub struct CanvasCache {
|
||||
pub patches: u64,
|
||||
/// View › Separations Preview: the visible spreads' plates.
|
||||
pub separations: separations::Preview,
|
||||
/// The [`designcraft_render::images::generation`] the canvas renders with, and when it took
|
||||
/// it (ms): finished background decodes are taken up in batches, one render for many.
|
||||
pub images: (u64, f64),
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Serialize)]
|
||||
|
||||
@@ -2295,3 +2295,26 @@ fn design_generation_native_drop_batch_without_a_change_still_places_every_file(
|
||||
let _ = std::fs::remove_file(first);
|
||||
let _ = std::fs::remove_file(second);
|
||||
}
|
||||
|
||||
/// The eyedropper samples an image that hasn't been decoded yet as it is, not the placeholder
|
||||
/// the canvas shows while it decodes in the background.
|
||||
#[test]
|
||||
fn the_eyedropper_samples_an_image_still_decoding() {
|
||||
let mut app = DesignApp::new(Session::new(), Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let red = designcraft_render::Rendered { width: 40, height: 40, pixels: [255u8, 0, 0, 255].repeat(1600) }.to_png();
|
||||
let b64 = designcraft_engine::cmd::base64_encode(&red);
|
||||
app.run("file.place", json!({"base64": b64, "name": "red.png", "x": 100, "y": 100, "width": 200})).unwrap();
|
||||
app.canvas_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0)));
|
||||
let _ = app.view_mut();
|
||||
let at = {
|
||||
let (view, rect) = (*app.view().unwrap(), app.canvas_rect.unwrap());
|
||||
let st = app.session.active().unwrap();
|
||||
let layout = designcraft_tools::CanvasLayout::new(&st.doc, false);
|
||||
canvas::Xf::new(rect, &view).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(200.0, 200.0))
|
||||
};
|
||||
// Nothing decoded: the canvas would draw the grey placeholder here.
|
||||
designcraft_render::images::clear();
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(canvas::color_at(&mut app, &ctx, at), Some([255, 0, 0]));
|
||||
}
|
||||
Reference in new issue
Block a user