mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 03:46:50 -06:00
Merge pull request #320 from tylerjenningsw/perf/zoom-preview-cold
perf(canvas): preview cold zoom and page transitions once they run long (M8)
This commit is contained in:
@@ -23,6 +23,8 @@ pub struct CanvasState {
|
||||
tex_of: Option<(u64, NodeId)>,
|
||||
/// The texture is a coarse preview (see [`crate::render_worker`]).
|
||||
pub tex_coarse: bool,
|
||||
/// The request the texture answers ([`crate::render_worker::Done::seq`]).
|
||||
tex_seq: u64,
|
||||
submitted: Option<JobKey>,
|
||||
/// The view of the last render asked for.
|
||||
want_view: Option<figcraft_render::View>,
|
||||
@@ -58,6 +60,7 @@ impl Default for CanvasState {
|
||||
tex_view: None,
|
||||
tex_of: None,
|
||||
tex_coarse: false,
|
||||
tex_seq: 0,
|
||||
submitted: None,
|
||||
want_view: None,
|
||||
render_work: None,
|
||||
@@ -246,6 +249,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
|
||||
app.canvas.tex_view = Some(done.view);
|
||||
app.canvas.tex_of = Some((done.uid, done.page));
|
||||
app.canvas.tex_coarse = done.coarse;
|
||||
app.canvas.tex_seq = done.seq;
|
||||
}
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect_filled(rect, CornerRadius::ZERO, bg);
|
||||
@@ -343,14 +347,18 @@ fn describe_box([x0, y0, x1, y1]: [f64; 4]) -> String {
|
||||
}
|
||||
|
||||
/// Whether a finished render goes on screen: an image of the file and page shown (`shown`), and
|
||||
/// not a coarse preview over a refined image that already shows the whole view asked for at its
|
||||
/// zoom (the same zoom elsewhere on the page is no such image).
|
||||
/// not a coarse preview over a refined image of the same or a newer request, or one that already
|
||||
/// shows the whole view asked for at its zoom (the same zoom elsewhere on the page is no such
|
||||
/// image).
|
||||
fn shows(c: &CanvasState, done: &Done, shown: Option<(u64, NodeId)>) -> bool {
|
||||
if shown != Some((done.uid, done.page)) {
|
||||
return false;
|
||||
}
|
||||
let refined_here = !c.tex_coarse
|
||||
&& c.tex_of == shown
|
||||
let refined = !c.tex_coarse && c.tex_of == shown;
|
||||
if done.coarse && refined && done.seq <= c.tex_seq {
|
||||
return false;
|
||||
}
|
||||
let refined_here = refined
|
||||
&& c.tex_view
|
||||
.as_ref()
|
||||
.zip(c.want_view.as_ref())
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
//! whole view is first rendered coarsely ([`PREVIEW_FACTOR`] times fewer pixels each way, kept
|
||||
//! apart from the full image so pans and edits still build on it), then refined unless a newer
|
||||
//! request is already waiting.
|
||||
//! Without a measure of the new render (the first one, another page or file, a zoom after quick
|
||||
//! renders) nothing is spent up front: on the desktop a watchdog thread renders the preview once
|
||||
//! the render from scratch has run past [`PREVIEW_AFTER_MS`], beside it (the full render keeps the
|
||||
//! other cores), and drops it if the full image is there first.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -19,7 +23,7 @@ use web_time::Instant;
|
||||
|
||||
/// A preview has this many times fewer pixels each way than the view it stands in for.
|
||||
pub const PREVIEW_FACTOR: f64 = 4.0;
|
||||
/// Full renders quicker than this (about a frame) show without a preview first.
|
||||
/// Full renders quicker than this (about a frame) show without a preview.
|
||||
pub const PREVIEW_AFTER_MS: f64 = 12.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -62,6 +66,8 @@ pub struct Done {
|
||||
pub coarse: bool,
|
||||
/// [`Job::at`].
|
||||
pub at: Instant,
|
||||
/// The request it answers, numbered by the worker in order (0 outside a worker).
|
||||
pub seq: u64,
|
||||
}
|
||||
|
||||
/// A render of a document in `profile` as a texture image for the screen (sRGB).
|
||||
@@ -107,7 +113,7 @@ fn run(job: &Job, view: &View, retained: &mut Retained, coarse: bool) -> Option<
|
||||
mark_outside_srgb(&mut image, pix, job.doc.color_profile);
|
||||
}
|
||||
let ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
Some(Done { image, view, ms, work, uid: job.uid, page: job.page, coarse, at: job.at })
|
||||
Some(Done { image, view, ms, work, uid: job.uid, page: job.page, coarse, at: job.at, seq: 0 })
|
||||
}
|
||||
|
||||
/// `view` with [`PREVIEW_FACTOR`] times fewer pixels each way, showing at least the same area.
|
||||
@@ -141,13 +147,21 @@ impl Renders {
|
||||
Self { full: Retained::default(), preview: Retained::default(), last: None, full_ms: 0.0, preview_after_ms: ms }
|
||||
}
|
||||
|
||||
/// Whether `job` gets a preview first: the kept image is of another zoom, page or file (so the
|
||||
/// full render starts from scratch) and those have been slow. The first render has no
|
||||
/// measure yet and comes without one.
|
||||
/// Whether `job` is a render from scratch: the kept image is of another zoom, page or file, or
|
||||
/// there is none.
|
||||
fn starts_over(&self, job: &Job) -> bool {
|
||||
self.last.is_none_or(|(uid, page, scale)| uid != job.uid || page != job.page || scale.to_bits() != job.view.scale.to_bits())
|
||||
}
|
||||
|
||||
/// Whether `job` gets a preview first: it renders from scratch and those have been slow on a
|
||||
/// page already drawn (the first render has no measure).
|
||||
fn wants_preview(&self, job: &Job) -> bool {
|
||||
job.zoom_preview
|
||||
&& self.full_ms >= self.preview_after_ms
|
||||
&& self.last.is_some_and(|(uid, page, scale)| uid != job.uid || page != job.page || scale.to_bits() != job.view.scale.to_bits())
|
||||
job.zoom_preview && self.last.is_some() && self.full_ms >= self.preview_after_ms && self.starts_over(job)
|
||||
}
|
||||
|
||||
/// Whether `job` should get a preview should its render run long: from scratch, with none first.
|
||||
pub fn wants_late_preview(&self, job: &Job) -> bool {
|
||||
job.zoom_preview && self.starts_over(job) && !self.wants_preview(job)
|
||||
}
|
||||
|
||||
/// The coarse preview of the whole of `job`'s view, when one should come first.
|
||||
@@ -155,8 +169,7 @@ impl Renders {
|
||||
if !self.wants_preview(job) {
|
||||
return None;
|
||||
}
|
||||
let view = coarse_view(&job.view);
|
||||
guarded(&mut self.preview, |r| run(job, &view, r, true))
|
||||
preview_with(job, &mut self.preview)
|
||||
}
|
||||
|
||||
/// The image of `job`'s view, built on the kept one where it can be.
|
||||
@@ -172,6 +185,26 @@ impl Renders {
|
||||
}
|
||||
}
|
||||
|
||||
/// The coarse preview of the whole of `job`'s view, kept in `retained`.
|
||||
pub fn preview_with(job: &Job, retained: &mut Retained) -> Option<Done> {
|
||||
let view = coarse_view(&job.view);
|
||||
guarded(retained, |r| run(job, &view, r, true))
|
||||
}
|
||||
|
||||
/// `d` numbered as the answer to request `seq`.
|
||||
fn answering(d: Done, seq: u64) -> Done {
|
||||
Done { seq, ..d }
|
||||
}
|
||||
|
||||
/// Of two finished renders, the one to show: the newer request's, and of one request's the refined
|
||||
/// image (the watchdog's preview can arrive after it).
|
||||
pub fn better(a: Option<Done>, b: Done) -> Done {
|
||||
match a {
|
||||
Some(a) if a.seq > b.seq || (a.seq == b.seq && !a.coarse && b.coarse) => a,
|
||||
_ => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `f` on `retained`; after a panic, forget the image it may have left half drawn.
|
||||
fn guarded(retained: &mut Retained, f: impl FnOnce(&mut Retained) -> Option<Done>) -> Option<Done> {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(retained))) {
|
||||
@@ -203,6 +236,40 @@ impl Worker {
|
||||
let done_with = finished.clone();
|
||||
// The window that renders with it (each window has its own worker).
|
||||
let viewport = ctx.viewport_id();
|
||||
// The request being rendered from scratch without a preview, and when it started, for the
|
||||
// watchdog; the newest request refined so far.
|
||||
let (watch, watched) = std::sync::mpsc::channel::<(u64, Job, std::time::Instant)>();
|
||||
let refined = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
let refined_seen = refined.clone();
|
||||
let (preview_tx, preview_ctx) = (done_tx.clone(), ctx.clone());
|
||||
let watchdog = std::thread::Builder::new().name("figcraft-render-preview".into()).spawn(move || {
|
||||
let mut retained = Retained::default();
|
||||
let after = std::time::Duration::from_secs_f64(PREVIEW_AFTER_MS / 1000.0);
|
||||
while let Ok((mut seq, mut job, mut started)) = watched.recv() {
|
||||
// Wait out the threshold; a newer arming means that render is over.
|
||||
loop {
|
||||
match watched.recv_timeout(after.saturating_sub(started.elapsed())) {
|
||||
Ok(newer) => (seq, job, started) = newer,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => break,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
|
||||
}
|
||||
}
|
||||
let still_rendering = || refined_seen.load(std::sync::atomic::Ordering::Acquire) < seq;
|
||||
if !still_rendering() {
|
||||
continue;
|
||||
}
|
||||
let Some(d) = preview_with(&job, &mut retained) else { continue };
|
||||
if still_rendering() {
|
||||
if preview_tx.send(answering(d, seq)).is_err() {
|
||||
break;
|
||||
}
|
||||
preview_ctx.request_repaint_of(viewport);
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Err(e) = watchdog {
|
||||
log::error!("render preview thread: {e}");
|
||||
}
|
||||
let spawned = std::thread::Builder::new().name("figcraft-render".into()).spawn(move || {
|
||||
let mut renders = Renders::default();
|
||||
let mut next = None;
|
||||
@@ -213,7 +280,7 @@ impl Worker {
|
||||
(seq, job) = newer;
|
||||
}
|
||||
if let Some(d) = renders.preview(&job) {
|
||||
if done_tx.send(d).is_err() {
|
||||
if done_tx.send(answering(d, seq)).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint_of(viewport);
|
||||
@@ -223,8 +290,14 @@ impl Worker {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(d) = renders.refine(&job)
|
||||
&& done_tx.send(d).is_err()
|
||||
if renders.wants_late_preview(&job) {
|
||||
// Without the watchdog (it failed to start) there is just no preview.
|
||||
let _ = watch.send((seq, job.clone(), std::time::Instant::now()));
|
||||
}
|
||||
let done = renders.refine(&job);
|
||||
refined.store(seq, std::sync::atomic::Ordering::Release);
|
||||
if let Some(d) = done
|
||||
&& done_tx.send(answering(d, seq)).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -250,12 +323,13 @@ impl Worker {
|
||||
self.finished.load(std::sync::atomic::Ordering::Acquire) < self.submitted
|
||||
}
|
||||
|
||||
/// The best of the renders finished since the last poll ([`better`]).
|
||||
pub fn poll(&mut self) -> Option<Done> {
|
||||
let mut last = None;
|
||||
let mut best = None;
|
||||
while let Ok(d) = self.rx.try_recv() {
|
||||
last = Some(d);
|
||||
best = Some(better(best, d));
|
||||
}
|
||||
last
|
||||
best
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +339,8 @@ impl Worker {
|
||||
pub struct Inline {
|
||||
ctx: egui::Context,
|
||||
pending: Option<Job>,
|
||||
/// Requests submitted so far.
|
||||
submitted: u64,
|
||||
previewed: bool,
|
||||
renders: Renders,
|
||||
}
|
||||
@@ -274,10 +350,11 @@ pub type Worker = Inline;
|
||||
|
||||
impl Inline {
|
||||
pub fn new(ctx: egui::Context) -> Self {
|
||||
Self { ctx, pending: None, previewed: false, renders: Renders::default() }
|
||||
Self { ctx, pending: None, submitted: 0, previewed: false, renders: Renders::default() }
|
||||
}
|
||||
pub fn submit(&mut self, job: Job) {
|
||||
self.pending = Some(job);
|
||||
self.submitted += 1;
|
||||
self.previewed = false;
|
||||
}
|
||||
pub fn in_flight(&self) -> bool {
|
||||
@@ -288,11 +365,11 @@ impl Inline {
|
||||
self.previewed = true;
|
||||
if let Some(d) = self.pending.as_ref().and_then(|job| self.renders.preview(job)) {
|
||||
self.ctx.request_repaint();
|
||||
return Some(d);
|
||||
return Some(answering(d, self.submitted));
|
||||
}
|
||||
}
|
||||
let job = self.pending.take()?;
|
||||
self.renders.refine(&job)
|
||||
self.renders.refine(&job).map(|d| answering(d, self.submitted))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
//! then the refined image, which matches a render from scratch; pans and edits still build on the
|
||||
//! kept image; previews never replace a refined image that already shows the whole view asked
|
||||
//! for, and no image of another file or page goes on screen. The browser's inline worker previews
|
||||
//! by the same measure as the render thread.
|
||||
//! by the same measure as the render thread. Renders from scratch with no measure to go by (the
|
||||
//! first, another page, a zoom after quick renders) get a preview on the desktop only if they run
|
||||
//! long, from the watchdog beside the render thread.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -13,7 +15,9 @@ use figcraft_render::{View, Work};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{CanvasState, shows};
|
||||
use crate::render_worker::{Done, Inline, Job, PREVIEW_AFTER_MS, PREVIEW_FACTOR, Renders, coarse_view, render_now};
|
||||
use figcraft_render::Retained;
|
||||
|
||||
use crate::render_worker::{Done, Inline, Job, PREVIEW_AFTER_MS, PREVIEW_FACTOR, Renders, Worker, better, coarse_view, preview_with, render_now};
|
||||
|
||||
/// A page of ellipses with a rectangle on top; the session and the rectangle.
|
||||
fn document() -> (Session, NodeId) {
|
||||
@@ -226,9 +230,9 @@ fn the_canvas_shows_images_of_its_own_page_and_never_a_coarser_one_of_its_view()
|
||||
let here = Some((ds.uid, ds.page));
|
||||
let mut r = Renders::with_threshold(0.0);
|
||||
let near = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 2.0, 200, 150));
|
||||
let refined = r.refine(&near).unwrap();
|
||||
let refined = Done { seq: 1, ..r.refine(&near).unwrap() };
|
||||
let far = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 0.5, 200, 150));
|
||||
let preview = r.preview(&far).unwrap();
|
||||
let preview = Done { seq: 2, ..r.preview(&far).unwrap() };
|
||||
|
||||
let mut c = CanvasState { want_view: Some(near.view.clone()), ..Default::default() };
|
||||
assert!(shows(&c, &refined, here));
|
||||
@@ -236,7 +240,7 @@ fn the_canvas_shows_images_of_its_own_page_and_never_a_coarser_one_of_its_view()
|
||||
assert!(!shows(&c, &refined, Some((ds.uid, NodeId(ds.page.0 + 1)))), "another page");
|
||||
assert!(!shows(&c, &refined, None), "no file open");
|
||||
// Showing the refined image of the view asked for: a preview arriving late stays off.
|
||||
(c.tex_view, c.tex_of, c.tex_coarse) = (Some(refined.view.clone()), here, false);
|
||||
(c.tex_view, c.tex_of, c.tex_coarse, c.tex_seq) = (Some(refined.view.clone()), here, false, refined.seq);
|
||||
assert!(!shows(&c, &preview, here));
|
||||
// Once the view asked for moves on, the preview is the better match.
|
||||
c.want_view = Some(far.view.clone());
|
||||
@@ -247,6 +251,103 @@ fn the_canvas_shows_images_of_its_own_page_and_never_a_coarser_one_of_its_view()
|
||||
assert!(shows(&c, &preview, here));
|
||||
(c.tex_coarse, c.tex_of) = (false, Some((ds.uid, NodeId(ds.page.0 + 1))));
|
||||
assert!(shows(&c, &preview, here));
|
||||
// Never over the refined image of its own request or a newer one (the watchdog's preview can
|
||||
// come after the refined image), even once the view asked for moved on.
|
||||
(c.tex_of, c.want_view) = (here, Some(far.view.clone()));
|
||||
for seq in [preview.seq, preview.seq + 1] {
|
||||
c.tex_seq = seq;
|
||||
assert!(!shows(&c, &preview, here), "refined image of request {seq}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_poll_takes_the_newest_request_and_of_one_request_the_refined_image() {
|
||||
let (s, _) = document();
|
||||
let ds = s.ds().unwrap();
|
||||
let v = job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 0.5, 64, 48));
|
||||
let preview = |seq| Done { seq, ..preview_with(&v, &mut Retained::default()).unwrap() };
|
||||
let refined = |seq| Done { seq, ..render_now(&v).unwrap() };
|
||||
let pick = |order: Vec<Done>| order.into_iter().fold(None, |best, d| Some(better(best, d))).map(|d| (d.seq, d.coarse));
|
||||
assert_eq!(pick(vec![refined(3), preview(4)]), Some((4, true)), "a newer request's preview over an older refined image");
|
||||
assert_eq!(pick(vec![preview(3), refined(3)]), Some((3, false)));
|
||||
assert_eq!(pick(vec![refined(3), preview(3)]), Some((3, false)), "the watchdog's preview after its refined image");
|
||||
assert_eq!(pick(vec![refined(4), preview(4), preview(3)]), Some((4, false)));
|
||||
assert_eq!(pick(vec![preview(4)]), Some((4, true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_from_scratch_with_nothing_to_go_by_get_a_late_preview() {
|
||||
let (mut s, _) = document();
|
||||
let (doc, page, uid) = {
|
||||
let ds = s.ds().unwrap();
|
||||
(ds.doc.clone(), ds.page, ds.uid)
|
||||
};
|
||||
let at = |doc: &Arc<Document>, page, uid, scale| job(doc, page, uid, view_at((300.0, 200.0), scale, 200, 150));
|
||||
// Renders that have been quick: nothing up front, a preview only should one run long.
|
||||
let mut r = Renders::with_threshold(f64::INFINITY);
|
||||
let first = at(&doc, page, uid, 1.0);
|
||||
assert!(r.wants_late_preview(&first) && r.preview(&first).is_none(), "the first render");
|
||||
r.refine(&first).unwrap();
|
||||
assert!(r.wants_late_preview(&at(&doc, page, uid, 4.0)), "a zoom after quick renders");
|
||||
assert!(r.wants_late_preview(&at(&doc, page, uid + 1, 1.0)), "another file");
|
||||
let mut panned = first.clone();
|
||||
panned.view.origin.0 += 5.0;
|
||||
assert!(!r.wants_late_preview(&panned), "a pan builds on the kept image");
|
||||
let mut pixels = at(&doc, page, uid, 4.0);
|
||||
pixels.zoom_preview = false;
|
||||
assert!(!r.wants_late_preview(&pixels), "pixel preview");
|
||||
s.execute("page.add", &json!({})).unwrap();
|
||||
let ds = s.ds().unwrap();
|
||||
assert!(r.wants_late_preview(&at(&ds.doc, ds.page, uid, 1.0)), "another page");
|
||||
// Renders that have been slow: the preview comes first instead.
|
||||
let mut slow = Renders::with_threshold(0.0);
|
||||
slow.refine(&first).unwrap();
|
||||
let zoom = at(&doc, page, uid, 4.0);
|
||||
assert!(!slow.wants_late_preview(&zoom) && slow.preview(&zoom).is_some());
|
||||
}
|
||||
|
||||
/// Every image `w` hands out until it has nothing left in flight: (request, coarse).
|
||||
fn drain(w: &mut Worker) -> Vec<(u64, bool)> {
|
||||
let start = std::time::Instant::now();
|
||||
let mut got = vec![];
|
||||
while w.in_flight() || got.is_empty() {
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(60), "the render never finished");
|
||||
if let Some(d) = w.poll() {
|
||||
got.push((d.seq, d.coarse));
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
got.extend(w.poll().map(|d| (d.seq, d.coarse)));
|
||||
got
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slow_first_render_gets_a_preview_beside_it_and_a_quick_one_none() {
|
||||
// Slow: a render from scratch that runs past the threshold, then the preview, with as much
|
||||
// again to spare, so the preview comes first however fast this machine and the renderer are.
|
||||
let view = view_at((300.0, 200.0), 2.0, 1200, 1200);
|
||||
let mut want = 4.0 * PREVIEW_AFTER_MS;
|
||||
let slow = loop {
|
||||
let s = slow_scene(std::slice::from_ref(&view), want);
|
||||
let ds = s.ds().unwrap();
|
||||
let slow = job(&ds.doc, ds.page, ds.uid, view.clone());
|
||||
let (full, preview) = (render_now(&slow).unwrap().ms, preview_with(&slow, &mut Retained::default()).unwrap().ms);
|
||||
if full >= 2.0 * (PREVIEW_AFTER_MS + preview) {
|
||||
break slow;
|
||||
}
|
||||
assert!(want < 64.0 * PREVIEW_AFTER_MS, "previews keep up with full renders: {preview} ms of {full} ms");
|
||||
want *= 2.0;
|
||||
};
|
||||
let mut w = Worker::new(egui::Context::default());
|
||||
w.submit(slow);
|
||||
assert_eq!(drain(&mut w), [(1, true), (1, false)], "the preview while it renders, then the refined image");
|
||||
// Quick: a small view of a few shapes on a fresh worker (the slow render above would make the
|
||||
// next zoom get its preview first).
|
||||
let (s, _) = document();
|
||||
let ds = s.ds().unwrap();
|
||||
let mut w = Worker::new(egui::Context::default());
|
||||
w.submit(job(&ds.doc, ds.page, ds.uid, view_at((300.0, 200.0), 0.5, 120, 90)));
|
||||
assert_eq!(drain(&mut w), [(1, false)], "done before the threshold: no preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -261,24 +362,25 @@ fn a_preview_replaces_a_refined_image_of_the_same_zoom_elsewhere_on_the_page() {
|
||||
let b = job(&ds.doc, ds.page, ds.uid, view((0.0, 0.0), 8.0));
|
||||
let a2 = job(&ds.doc, ds.page, ds.uid, view((-700.0, -525.0), 1.0));
|
||||
let mut r = Renders::with_threshold(0.0);
|
||||
let refined_a = r.refine(&a).unwrap();
|
||||
let refined_a = Done { seq: 1, ..r.refine(&a).unwrap() };
|
||||
// The canvas shows A; B is asked for, then A' while B renders. The thread finishes B, then
|
||||
// previews A' before the canvas polls: the queue holds [refined B, preview A'], and a poll
|
||||
// takes the last.
|
||||
let queue = [r.refine(&b).unwrap(), r.preview(&a2).expect("A' is a new zoom for the thread")];
|
||||
let polled = queue.last().unwrap();
|
||||
// takes the newer request's.
|
||||
let queue = [Done { seq: 2, ..r.refine(&b).unwrap() }, Done { seq: 3, ..r.preview(&a2).expect("A' is a new zoom for the thread") }];
|
||||
let polled = &queue.into_iter().fold(None, |best, d| Some(better(best, d))).unwrap();
|
||||
assert!(polled.coarse && covers(&polled.view, &a2.view));
|
||||
let c = CanvasState {
|
||||
tex_view: Some(refined_a.view.clone()),
|
||||
tex_of: here,
|
||||
tex_coarse: false,
|
||||
tex_seq: refined_a.seq,
|
||||
want_view: Some(a2.view.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(shows(&c, polled, here), "A at the same zoom shows 1/64 of A': the preview of all of it goes on");
|
||||
// Once A' itself is refined on screen, a preview of it would be a step down.
|
||||
let refined_a2 = r.refine(&a2).unwrap();
|
||||
let c = CanvasState { tex_view: Some(refined_a2.view.clone()), ..c };
|
||||
let c = CanvasState { tex_view: Some(refined_a2.view.clone()), tex_seq: 3, ..c };
|
||||
assert!(!shows(&c, polled, here));
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -244,7 +244,9 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
- **Missing:** GPU rendering of the canvas. FigCraft renders with tiny-skia on the CPU (banded,
|
||||
parallel, retained) and shows the image through wgpu. A new zoom can't reuse the retained
|
||||
image; when renders from scratch take a frame or more, the whole view is previewed with a
|
||||
sixteenth of the pixels first, then refined.
|
||||
sixteenth of the pixels first, then refined. With no measure to go by (the first render, another
|
||||
page, a zoom after quick renders), a watchdog thread previews any such render still running
|
||||
after 12 ms (desktop only; the browser renders inline).
|
||||
- **Landed (2026-10-15):** a bounded render margin is reused during panning and refilled before
|
||||
the visible view reaches its edge. The extra rendered area is capped at 1.6× visible pixels;
|
||||
this reduces blank strips without changing the CPU renderer.
|
||||
|
||||
Reference in New Issue
Block a user