Large document: full CPU canvas redraws run in the background, visible part first (#4024) (#5001)

When the wgpu compositor can't draw a refresh (layers over its memory
budget, or a feature it doesn't cover), the CPU compositor redrew the
whole document inside the draw call: minutes of Not Responding on a
multi-gigabyte PSB. With background jobs, a refresh of 4 MP or more is
now composited by a worker in 1024 px blocks, the visible ones first;
each frame uploads the finished blocks and the canvas keeps its previous
contents until they land. An edit while it runs draws its own damage and
restarts the worker on the edited document with the blocks still to
draw. The status bar shows Redrawing N%. Web and tests keep the
synchronous path.
This commit is contained in:
Ian Swope 2026-10-13 11:49:58 -05:00 committed by GitHub
parent 9a4d925b34
commit 74cb293570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 526 additions and 20 deletions

View File

@ -0,0 +1,252 @@
//! Full CPU redraws of the GPU canvas off the UI thread (#4024). When the wgpu compositor can't
//! draw a full refresh (the layers exceed its memory budget, or the document uses something it
//! doesn't cover), the CPU compositor redraws the whole document: minutes on a multi-gigabyte
//! PSB. With background jobs (the desktop app) a worker composites it in blocks instead, the
//! visible ones first, and each frame uploads the blocks that are done, so the window keeps
//! drawing and answering while the rest of the canvas fills in. Until a block lands the texture
//! keeps what it showed before.
//!
//! An edit while it runs restarts the worker on the new document with the blocks still to draw;
//! the edit's own damage is drawn by the canvas as usual. A full change (a new preview, a new
//! display transform) starts over. Without background jobs (web, tests) the canvas keeps the
//! synchronous [`GpuCanvas::upload_composite`].
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use photocraft_doc::{DocId, Document};
use photocraft_engine::display_color::CanvasDisplay;
use photocraft_geom::Rect;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use crate::PhotocraftApp;
use crate::gpu_canvas::GpuCanvas;
/// Side of a block in document pixels: whole render tiles, and a power of two so each block's
/// mip levels don't depend on its neighbours.
const BLOCK: i32 = 1024;
/// Blocks composited at once (in parallel, on top of each block's own tiles), and how many
/// finished blocks may wait for the UI thread: bounds the memory a redraw holds.
const BATCH: usize = 4;
/// One finished block, as texels of the canvas texture.
struct Piece {
block: usize,
texels: Vec<u8>,
}
/// A redraw in progress for one document's GPU texture.
pub(crate) struct Redraw {
blocks: Vec<Rect>,
pending: Vec<bool>,
left: usize,
/// Pixels uploaded so far.
drawn: u64,
rx: Receiver<Piece>,
stop: Arc<AtomicBool>,
started_ms: f64,
fallback: Option<String>,
}
impl Redraw {
/// Share of the document drawn, 0..=1.
pub fn fraction(&self) -> f32 {
if self.blocks.is_empty() { 1.0 } else { 1.0 - self.left as f32 / self.blocks.len() as f32 }
}
}
impl Drop for Redraw {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
}
}
/// Whether full CPU refreshes run in the background here.
pub(crate) fn enabled(app: &PhotocraftApp) -> bool {
cfg!(not(target_arch = "wasm32")) && app.background_jobs
}
/// `bounds` cut into [`BLOCK`]-sized blocks, row by row.
fn blocks(bounds: Rect) -> Vec<Rect> {
let mut out = Vec::new();
let mut y = bounds.y0;
while y < bounds.y1 {
let mut x = bounds.x0;
while x < bounds.x1 {
out.push(Rect::new(x, y, (x + BLOCK).min(bounds.x1), (y + BLOCK).min(bounds.y1)));
x += BLOCK;
}
y += BLOCK;
}
out
}
/// The order to draw `wanted` blocks in: those `visible` shows first, then the rest, each group
/// nearest the view's centre first.
fn order(blocks: &[Rect], wanted: impl Iterator<Item = usize>, visible: Rect) -> Vec<usize> {
let (cx, cy) = ((visible.x0 as i64 + visible.x1 as i64) / 2, (visible.y0 as i64 + visible.y1 as i64) / 2);
let mut out: Vec<usize> = wanted.collect();
out.sort_by_key(|&i| {
let b = blocks[i];
let (bx, by) = ((b.x0 as i64 + b.x1 as i64) / 2, (b.y0 as i64 + b.y1 as i64) / 2);
(b.intersect(&visible).is_empty(), (bx - cx).pow(2) + (by - cy).pow(2))
});
out
}
/// Redraw `region` of document `doc` (`doc.id`'s texture, prepared by
/// [`GpuCanvas::refresh_with`]) in the background. A redraw already running for it continues on
/// `doc` with `region`'s blocks added.
#[allow(clippy::too_many_arguments)]
pub(crate) fn start(
app: &mut PhotocraftApp,
ctx: &egui::Context,
gpu: &GpuCanvas,
doc: Arc<Document>,
display: Option<Arc<CanvasDisplay>>,
visible: Rect,
region: Rect,
fallback: Option<String>,
) {
let all = blocks(doc.bounds());
let (mut pending, since, fallback) = match app.redraws.remove(&doc.id) {
Some(old) if old.blocks == all => (old.pending.clone(), (old.started_ms, old.drawn), fallback.or_else(|| old.fallback.clone())),
_ => (vec![false; all.len()], (crate::gpu_canvas::now_ms(), 0), fallback),
};
for (p, b) in pending.iter_mut().zip(&all) {
*p |= !b.intersect(&region).is_empty();
}
if pending.contains(&true) {
spawn(app, ctx, gpu, doc, display, visible, all, pending, since, fallback);
}
}
/// After an edit drew its own damage: continue document `doc.id`'s redraw, if one is running, on
/// the edited document, so blocks still to come don't bring back what was there before it.
pub(crate) fn restart(app: &mut PhotocraftApp, ctx: &egui::Context, gpu: &GpuCanvas, doc: Arc<Document>, display: Option<Arc<CanvasDisplay>>, visible: Rect) {
if app.redraws.contains_key(&doc.id) {
start(app, ctx, gpu, doc, display, visible, Rect::EMPTY, None);
}
}
/// Upload the blocks document `doc`'s redraw has finished since the last frame. When the last one
/// lands, the redraw is recorded like the synchronous refresh it replaces (and explained once if
/// it was slow).
pub(crate) fn poll(app: &mut PhotocraftApp, gpu: &GpuCanvas, doc: DocId) {
let Some(r) = app.redraws.get_mut(&doc) else { return };
loop {
match r.rx.try_recv() {
Ok(p) => {
let b = r.blocks[p.block];
if r.pending[p.block] && gpu.upload_rect(doc.0, [b.x0 as u32, b.y0 as u32], [b.width(), b.height()], &p.texels) {
r.pending[p.block] = false;
r.left -= 1;
r.drawn += b.width() as u64 * b.height() as u64;
}
}
Err(TryRecvError::Empty) => return,
Err(TryRecvError::Disconnected) => break,
}
}
let Some(r) = app.redraws.remove(&doc) else { return };
if r.left > 0 {
log::warn!("canvas redraw stopped with {} of {} blocks left", r.left, r.blocks.len());
return;
}
let ms = crate::gpu_canvas::now_ms() - r.started_ms;
app.perf.record("background", r.drawn, ms, 0.0);
crate::notices::slow_cpu_refresh(app, doc.0, ms, r.fallback.as_deref());
}
/// The active document's redraw in the status bar: "Redrawing" and how much is done.
pub(crate) fn status(app: &PhotocraftApp, ui: &mut egui::Ui) {
let Some(r) = app.session.active().and_then(|st| app.redraws.get(&st.doc.id)) else { return };
let t = crate::theme::Tokens::get(ui.ctx());
ui.label(egui::RichText::new(format!("{:.0}%", r.fraction() * 100.0)).color(t.text_dim).size(11.5).monospace());
ui.label(egui::RichText::new(tl!("Redrawing")).color(t.text_dim).size(12.0));
}
/// Stop document `doc`'s redraw (its canvas was redrawn some other way, or is gone).
pub(crate) fn cancel(app: &mut PhotocraftApp, doc: DocId) {
app.redraws.remove(&doc);
}
/// Stop redraws of documents that are no longer open.
pub(crate) fn retain(app: &mut PhotocraftApp) {
let docs = app.session.documents();
app.redraws.retain(|id, _| docs.iter().any(|st| st.doc.id == *id));
}
#[allow(clippy::too_many_arguments)]
fn spawn(
app: &mut PhotocraftApp,
ctx: &egui::Context,
gpu: &GpuCanvas,
doc: Arc<Document>,
display: Option<Arc<CanvasDisplay>>,
visible: Rect,
blocks: Vec<Rect>,
pending: Vec<bool>,
(started_ms, drawn): (f64, u64),
fallback: Option<String>,
) {
let id = doc.id;
let Some(format) = gpu.prepare_texture(id.0, &doc) else { return };
let todo = order(&blocks, (0..blocks.len()).filter(|&i| pending[i]), visible);
let left = todo.len();
let (tx, rx) = std::sync::mpsc::sync_channel(BATCH);
let stop = Arc::new(AtomicBool::new(false));
let work = (blocks.clone(), stop.clone(), ctx.clone());
let spawned = std::thread::Builder::new().name("canvas-redraw".into()).spawn(move || {
let (blocks, stop, ctx) = work;
// A panic stops the redraw: the receiver sees the worker gone and drops it.
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| draw(&doc, display.as_deref(), format, &blocks, &todo, &stop, &tx, &ctx)));
ctx.request_repaint();
});
if let Err(e) = spawned {
log::warn!("couldn't start the canvas redraw: {e}");
return;
}
app.redraws.insert(id, Redraw { blocks, pending, left, drawn, rx, stop, started_ms, fallback });
}
/// The worker: composite `todo`'s blocks of `doc` in batches and hand each over, until done or
/// stopped.
#[allow(clippy::too_many_arguments)]
fn draw(
doc: &Document,
display: Option<&CanvasDisplay>,
format: eframe::wgpu::TextureFormat,
blocks: &[Rect],
todo: &[usize],
stop: &AtomicBool,
tx: &SyncSender<Piece>,
ctx: &egui::Context,
) {
for batch in todo.chunks(BATCH) {
if stop.load(Ordering::Relaxed) {
return;
}
// Never reached in the browser ([`enabled`]); it only has to build there, without rayon.
#[cfg(not(target_arch = "wasm32"))]
let batch = batch.par_iter();
#[cfg(target_arch = "wasm32")]
let batch = batch.iter();
let pieces: Vec<Piece> = batch
.map(|&block| Piece { block, texels: crate::gpu_canvas::display_texels(format, display, &photocraft_compose::render(doc, blocks[block])) })
.collect();
for p in pieces {
if stop.load(Ordering::Relaxed) || tx.send(p).is_err() {
return;
}
ctx.request_repaint();
}
}
}
#[cfg(test)]
#[path = "background_redraw_tests.rs"]
mod tests;

View File

@ -0,0 +1,165 @@
//! Background full CPU redraws (#4024). The GPU tests skip when there is no adapter.
use super::*;
use serde_json::json;
#[test]
fn blocks_cover_the_document_once() {
let bounds = Rect::new(0, 0, 2500, 1100);
let b = blocks(bounds);
assert_eq!(b.len(), 3 * 2);
assert_eq!(b.iter().map(|r| r.width() as u64 * r.height() as u64).sum::<u64>(), 2500 * 1100);
for (i, r) in b.iter().enumerate() {
assert!(r.x0 % BLOCK == 0 && r.y0 % BLOCK == 0, "block {i} starts on the grid: {r:?}");
assert!(b[i + 1..].iter().all(|o| o.intersect(r).is_empty()), "block {i} overlaps another");
}
}
#[test]
fn visible_blocks_come_first_nearest_the_view_centre() {
let b = blocks(Rect::new(0, 0, 4096, 4096));
// The view shows the right half of the second block row.
let visible = Rect::new(2100, 1100, 4000, 1900);
let o = order(&b, 0..b.len(), visible);
assert_eq!(o.len(), 16);
let shown: Vec<usize> = (0..b.len()).filter(|&i| !b[i].intersect(&visible).is_empty()).collect();
assert_eq!(shown.len(), 2);
assert!(o[..2].iter().all(|i| shown.contains(i)), "{o:?}");
// Then the block row's neighbours, the far corners last.
assert!(o[14..].contains(&12), "the bottom-left corner is among the last: {o:?}");
// Only the blocks asked for.
assert_eq!(order(&b, [3, 9].into_iter(), visible).len(), 2);
}
struct Gpu {
harness: egui_kittest::Harness<'static, PhotocraftApp>,
observer: GpuCanvas,
}
/// The app on a real device with background jobs, or `None` without a GPU adapter.
fn gpu_app() -> Option<Gpu> {
let observer = Arc::new(std::sync::Mutex::new(None));
let observer_out = observer.clone();
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).with_pixels_per_point(1.0).with_max_steps(64).wgpu().build_eframe(move |cc| {
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
if let Some(rs) = cc.wgpu_render_state.as_ref() {
*observer_out.lock().expect("observer") = Some(GpuCanvas::new(rs));
app.set_wgpu(rs.clone());
}
app.background_jobs = true;
app
})
}));
let Ok(harness) = built else {
eprintln!("skipping: no GPU adapter");
return None;
};
let observer = observer.lock().expect("observer").take()?;
Some(Gpu { harness, observer })
}
/// A document the wgpu compositor leaves to the CPU (knockout on its top layer, which also has
/// Blend If).
fn cpu_only_document(app: &mut PhotocraftApp, w: u32, h: u32) -> DocId {
app.run("file.new", json!({"width": w, "height": h, "background": "#336699"})).expect("new");
app.run("layer.new.layer", json!({})).expect("layer");
app.run("edit.fill", json!({"color": "#cc8844"})).expect("fill");
app.run("layer.layerStyle.blendingOptions", json!({"opacity": 60, "knockout": "shallow", "blendIf": {"channel": "gray", "underlying": [0, 10, 245, 255]}}))
.expect("blending options");
app.session.active().expect("document").doc.id
}
/// Frames until no redraw is running.
fn settle(g: &mut Gpu) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
loop {
g.harness.step();
if g.harness.state().redraws.is_empty() {
return;
}
assert!(std::time::Instant::now() < deadline, "the redraw didn't finish");
std::thread::sleep(std::time::Duration::from_millis(2));
}
}
/// The canvas texture of `doc` against the synchronous CPU refresh of the current document.
fn assert_matches_synchronous(g: &mut Gpu, doc: DocId) {
let app = g.harness.state_mut();
let d = app.session.active().expect("document").doc.clone();
let (display, _) = crate::canvas::canvas_display(app, &d, None);
let reference = doc.0 ^ (1u64 << 60);
g.observer.upload_composite(reference, &d, display.as_deref());
let (_, size, shown) = g.observer.read_texels(doc.0).expect("canvas texture");
let (_, _, want) = g.observer.read_texels(reference).expect("reference texture");
assert_eq!(size, [d.size.width, d.size.height]);
let differ = shown.iter().zip(&want).filter(|(a, b)| a != b).count();
assert_eq!(differ, 0, "{differ} texels differ from the synchronous refresh");
}
#[test]
fn a_full_cpu_refresh_draws_in_the_background_with_the_synchronous_pixels() {
let _gpu = photocraft_testkit::gpu_lock();
let Some(mut g) = gpu_app() else { return };
let doc = cpu_only_document(g.harness.state_mut(), 2600, 1700);
g.harness.step();
assert!(g.harness.state().redraws.contains_key(&doc), "the frame started a redraw instead of compositing inline");
settle(&mut g);
let app = g.harness.state();
assert_eq!(app.perf.last_refresh, "background");
assert!(app.perf.gpu_fallback.as_deref().is_some_and(|f| f.contains("Advanced Blending")), "{:?}", app.perf.gpu_fallback);
assert_matches_synchronous(&mut g, doc);
}
#[test]
fn an_edit_during_a_redraw_is_not_overwritten_by_blocks_drawn_before_it() {
let _gpu = photocraft_testkit::gpu_lock();
let Some(mut g) = gpu_app() else { return };
// 4×3 blocks: the worker can't hand them all over before the next frame.
let doc = cpu_only_document(g.harness.state_mut(), 4096, 3072);
g.harness.step();
assert!(g.harness.state().redraws.contains_key(&doc));
// A corner, drawn last (farthest from the view's centre), changes on a layer of its own while
// the redraw runs: a small edit, drawn at once.
g.harness.state_mut().run("layer.new.layer", json!({})).expect("layer");
g.harness.step();
g.harness.state_mut().run("select.rect", json!({"x": 3900, "y": 2900, "width": 150, "height": 150})).expect("select");
g.harness.step();
g.harness.state_mut().run("edit.fill", json!({"color": "#22ee22"})).expect("fill");
g.harness.step();
assert_eq!(g.harness.state().perf.last_refresh, "rect", "the fill's damage was drawn synchronously");
assert!(g.harness.state().redraws.contains_key(&doc), "the redraw goes on");
settle(&mut g);
assert_matches_synchronous(&mut g, doc);
}
#[test]
fn an_edit_damaging_most_of_a_large_document_is_drawn_in_the_background_too() {
let _gpu = photocraft_testkit::gpu_lock();
let Some(mut g) = gpu_app() else { return };
let doc = cpu_only_document(g.harness.state_mut(), 2600, 1700);
settle(&mut g);
// Filling part of the Blend If layer can change the composite anywhere: the whole canvas.
g.harness.state_mut().run("select.rect", json!({"x": 100, "y": 100, "width": 300, "height": 300})).expect("select");
g.harness.step();
g.harness.state_mut().run("edit.fill", json!({"color": "#22ee22"})).expect("fill");
assert_eq!(g.harness.state().session.active().expect("document").last_damage, Some(Rect::new(0, 0, 2600, 1700)));
g.harness.step();
assert!(g.harness.state().redraws.contains_key(&doc), "not composited inline: {}", g.harness.state().perf.last_refresh);
settle(&mut g);
assert_eq!(g.harness.state().perf.last_refresh, "background");
assert_matches_synchronous(&mut g, doc);
}
#[test]
fn closing_the_document_stops_its_redraw() {
let _gpu = photocraft_testkit::gpu_lock();
let Some(mut g) = gpu_app() else { return };
let doc = cpu_only_document(g.harness.state_mut(), 4096, 3072);
g.harness.step();
assert!(g.harness.state().redraws.contains_key(&doc));
g.harness.state_mut().run("file.close", json!({"save": false})).expect("close");
g.harness.step();
assert!(g.harness.state().redraws.is_empty());
}

View File

@ -1527,7 +1527,7 @@ pub(crate) fn effect_reach(layers: &[photocraft_doc::Layer]) -> i32 {
/// GPU path: make sure document `idx` is current in the GPU canvas. Edits draw their damage;
/// unchanged lower-stack composites can be reused by the compositor inside that region.
/// Returns false if there is no GPU canvas.
fn ensure_gpu(app: &mut PhotocraftApp, idx: usize, visible: DRect) -> bool {
fn ensure_gpu(app: &mut PhotocraftApp, ctx: &egui::Context, idx: usize, visible: DRect) -> bool {
let Some(gpu) = app.gpu.clone() else { return false };
let Some((revision, last_damage, id)) = app
.session
@ -1559,6 +1559,8 @@ fn ensure_gpu(app: &mut PhotocraftApp, idx: usize, visible: DRect) -> bool {
app.navigator_gpu.remove(&id);
}
let size = [doc.size.width, doc.size.height];
// A full CPU redraw running in the background: show the blocks it has finished.
crate::background_redraw::poll(app, &gpu, id);
let cache = app.canvases.entry((id, GPU_OUTPUT)).or_insert(CanvasCache {
revision: 0,
texture: None,
@ -1581,22 +1583,37 @@ fn ensure_gpu(app: &mut PhotocraftApp, idx: usize, visible: DRect) -> bool {
// Display textures for a canvas this large would not fit the GPU: the reduced CPU canvas
// draws it (checked before the memory budget, which walks the history, every frame).
if !gpu.fits_canvas(&doc) {
crate::background_redraw::cancel(app, id);
return false;
}
let partial = present && damage.is_some();
gpu_budget(app, &gpu, idx, partial, visible);
let r = gpu.refresh(id.0, &doc, if partial { damage } else { None }, display.as_deref());
let defer = crate::background_redraw::enabled(app);
let r = gpu.refresh_with(id.0, &doc, if partial { damage } else { None }, display.as_deref(), defer);
if r.kind == "lost" || r.kind == crate::gpu_canvas::REFRESH_OVER_BUDGET {
// The device is gone (`gpu_status` then drops the GPU canvas) or the canvas is over the
// texture budget: draw this frame on the CPU path.
crate::background_redraw::cancel(app, id);
return false;
}
if r.kind == crate::gpu_canvas::REFRESH_DEFERRED {
// Composited off the UI thread, the visible part first (#4024).
let region = if partial { damage.unwrap_or(doc.bounds()) } else { doc.bounds() };
crate::background_redraw::start(app, ctx, &gpu, doc.clone(), display.clone(), visible, region, r.fallback.clone());
} else if partial {
// The edit drew its damage; blocks of a running redraw still to come follow the edit.
crate::background_redraw::restart(app, ctx, &gpu, doc.clone(), display.clone(), visible);
} else {
crate::background_redraw::cancel(app, id);
}
if let Some(e) = &r.fallback
&& app.perf.gpu_fallback.as_deref() != Some(e.as_str())
{
log::info!("{e}; using the CPU compositor");
}
app.perf.record(r.kind, r.px, r.composite_ms, r.upload_ms);
if r.kind != crate::gpu_canvas::REFRESH_DEFERRED {
app.perf.record(r.kind, r.px, r.composite_ms, r.upload_ms);
}
if r.kind == "full" {
crate::notices::slow_cpu_refresh(app, id.0, r.composite_ms + r.upload_ms, r.fallback.as_deref());
}
@ -1924,6 +1941,7 @@ pub(crate) fn retain_gpu_documents(app: &mut PhotocraftApp) {
live.extend(crate::adjust_preview::gpu_keys(app));
gpu.retain(&live);
}
crate::background_redraw::retain(app);
// Upload markers must not outlive the resources they describe: native reopen can reuse
// both the document ID and revision before the next frame while a preview dialog stays open.
let documents = app.session.documents();
@ -2913,7 +2931,7 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
offset: [0.0, 0.0],
};
paint_gpu_view(&painter, rect, params, &pattern);
} else if gpu_ok && ensure_gpu(app, idx, visible_doc_rect(&xf)) {
} else if gpu_ok && ensure_gpu(app, &ctx, idx, visible_doc_rect(&xf)) {
on_gpu = true;
app.perf.gpu = true;
// Shadow, checkerboard, document and pixel grid in one custom shader (gpu_canvas.rs).

View File

@ -527,6 +527,20 @@ impl GpuCanvas {
doc: &photocraft_doc::Document,
damage: Option<photocraft_geom::Rect>,
display: Option<&photocraft_engine::display_color::CanvasDisplay>,
) -> Refresh {
self.refresh_with(key, doc, damage, display, false)
}
/// [`GpuCanvas::refresh`], except that with `defer`, a refresh of at least [`DEFER_PX`] the
/// CPU compositor would draw is left to the caller (`background_redraw`): the texture is made
/// ready (keeping what it shows when it already fits) and the kind is [`REFRESH_DEFERRED`].
pub fn refresh_with(
&self,
key: u64,
doc: &photocraft_doc::Document,
damage: Option<photocraft_geom::Rect>,
display: Option<&photocraft_engine::display_color::CanvasDisplay>,
defer: bool,
) -> Refresh {
if let Some(f) = self.fault() {
// Nothing reaches the GPU any more; the app switches to the CPU canvas.
@ -564,6 +578,12 @@ impl GpuCanvas {
}
Err(e) => out.fallback = Some(e.0),
}
let px = region.width() as u64 * region.height() as u64;
if defer && px >= DEFER_PX && self.prepare_texture(key, doc).is_some() {
out.kind = REFRESH_DEFERRED;
out.px = px;
return out;
}
if let Some(r) = damage {
if r.is_empty() {
out.kind = "rect";
@ -591,22 +611,8 @@ impl GpuCanvas {
/// never needs a full-size float composite or texture-format copy in memory.
pub fn upload_composite(&self, key: u64, doc: &photocraft_doc::Document, display: Option<&photocraft_engine::display_color::CanvasDisplay>) {
let size = [doc.size.width, doc.size.height];
if size[0] == 0 || size[1] == 0 || !self.health.is_ok() {
return;
}
let format = self.format_for(doc.depth, size);
let Some(format) = self.prepare_texture(key, doc) else { return };
let (device, queue) = (&self.rs.device, &self.rs.queue);
{
let mut renderer = self.rs.renderer.write();
let Some(res) = renderer.callback_resources.get_mut::<Resources>() else { return };
if res.docs.get(&key).is_none_or(|d| d.size != size || d.format != format) {
let Some(tex) = DocTextures::new(device, res, size, self.tile, format) else {
res.docs.remove(&key);
return;
};
res.docs.insert(key, tex);
}
}
let mut rgba = Vec::new();
let _ = photocraft_compose::render_bands(doc, doc.bounds(), 0, |band| -> Result<(), ()> {
if !self.health.is_ok() {
@ -633,6 +639,26 @@ impl GpuCanvas {
}
}
/// Make sure document `key` has a display texture of `doc`'s size and format, keeping the one
/// it has when that fits. Returns its format, or `None` when it can't be created.
pub fn prepare_texture(&self, key: u64, doc: &photocraft_doc::Document) -> Option<wgpu::TextureFormat> {
let size = [doc.size.width, doc.size.height];
if size[0] == 0 || size[1] == 0 || !self.health.is_ok() {
return None;
}
let format = self.format_for(doc.depth, size);
let mut renderer = self.rs.renderer.write();
let res = renderer.callback_resources.get_mut::<Resources>()?;
if res.docs.get(&key).is_none_or(|d| d.size != size || d.format != format) {
let Some(tex) = DocTextures::new(&self.rs.device, res, size, self.tile, format) else {
res.docs.remove(&key);
return None;
};
res.docs.insert(key, tex);
}
Some(format)
}
/// Free textures, display LUTs and their signatures for documents not in `live`.
pub fn retain(&self, live: &[u64]) {
let mut renderer = self.rs.renderer.write();
@ -764,6 +790,16 @@ fn texture_buffer<'a>(
}
}
/// A CPU composite `buf` as texels of a `format` canvas texture, in `display`'s encoding: what
/// [`GpuCanvas::upload_rect`] takes. Runs anywhere (a redraw's worker converts its own pieces).
pub fn display_texels(
format: wgpu::TextureFormat,
display: Option<&photocraft_engine::display_color::CanvasDisplay>,
buf: &photocraft_compose::Buffer,
) -> Vec<u8> {
texels(format, &texture_buffer(display, buf).px)
}
/// What a [`GpuCanvas::refresh`] did.
#[derive(Clone, Debug, Default)]
pub struct Refresh {
@ -1151,6 +1187,13 @@ pub const OVER_CANVAS_BUDGET: &str = "the canvas exceeds the GPU texture budget;
/// was created, and the caller draws the CPU canvas instead.
pub const REFRESH_OVER_BUDGET: &str = "over-budget";
/// [`Refresh::kind`] of a CPU refresh left to the caller ([`GpuCanvas::refresh_with`]).
pub const REFRESH_DEFERRED: &str = "deferred";
/// The smallest CPU refresh (pixels) [`GpuCanvas::refresh_with`] defers: below it, the
/// synchronous composite takes a fraction of a second even with many layers.
pub const DEFER_PX: u64 = 1 << 22;
/// GPU bytes one document's display texture set (every tile and mip level) may take: a quarter
/// of physical memory (`ram`; 16 GB assumed when unknown), at least [`MIN_GPU_BUDGET`]. The set
/// covers the whole canvas at full resolution, so its size follows the declared canvas, not the

View File

@ -2428,6 +2428,7 @@ cameraRaw Color Barvy
Violet Fialová
Reset Obnovit
Large document: redrawing on the CPU Velký dokument: překreslování na CPU
Redrawing Překreslování
This redraw took {seconds} s on the CPU. Toto překreslení na CPU trvalo {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Úpravy, které mění celý dokument, například režim prolnutí, krytí nebo viditelnost vrstvy, jej překreslí celý.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Vrstvy se nevejdou do rozpočtu paměti GPU. Zvýšením hodnoty Využití paměti v Předvolbách › Výkon se rozpočet zvětší, nejvýše na čtvrtinu paměti tohoto počítače.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2467,6 +2467,7 @@ layerLabel No Color Keine Farbe
Fuchsia Fuchsia
Violet Violett
Large document: redrawing on the CPU Großes Dokument: Neuzeichnen auf der CPU
Redrawing Neuzeichnen
This redraw took {seconds} s on the CPU. Dieses Neuzeichnen auf der CPU dauerte {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Änderungen, die das ganze Dokument betreffen, etwa Füllmethode, Deckkraft oder Sichtbarkeit einer Ebene, zeichnen es vollständig neu.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Die Ebenen passen nicht in das GPU-Speicherbudget. Eine höhere Speichernutzung unter Voreinstellungen › Leistung erhöht das Budget, bis zu einem Viertel des Arbeitsspeichers dieses Computers.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3432,6 +3432,7 @@ cameraRaw Yellow Κίτρινο
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Τα επίπεδά του δεν χωρούν στο όριο μνήμης της GPU. Αυξάνοντας τη Χρήση μνήμης στις Προτιμήσεις › Επιδόσεις αυξάνεται το όριο, έως το ένα τέταρτο της μνήμης αυτού του υπολογιστή.
Knockout Διάτρηση
Large document: redrawing on the CPU Μεγάλο έγγραφο: επανασχεδίαση στη CPU
Redrawing Επανασχεδίαση
Median… Διάμεσος…
Move Μετακίνηση
New Swatch Group… Νέα ομάδα δειγμάτων χρωμάτων…

Can't render this file because it has a wrong number of fields in line 7.

View File

@ -2497,6 +2497,7 @@ cameraRaw Amount Cantidad
Violet Violeta
Reset Restaurar
Large document: redrawing on the CPU Documento grande: redibujando en la CPU
Redrawing Redibujando
This redraw took {seconds} s on the CPU. Este redibujado en la CPU tardó {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Las ediciones que cambian todo el documento, como el modo de fusión, la opacidad o la visibilidad de una capa, lo redibujan por completo.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Las capas no caben en el presupuesto de memoria de la GPU. Aumentar Uso de memoria en Preferencias › Rendimiento amplía el presupuesto, hasta una cuarta parte de la memoria de este equipo.

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2848,6 +2848,7 @@ cameraRaw Blending Fusion
Fuchsia Fuchsia
Violet Violet
Large document: redrawing on the CPU Document volumineux : redessin sur le CPU
Redrawing Redessin
This redraw took {seconds} s on the CPU. Ce redessin sur le CPU a pris {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Les modifications qui touchent tout le document, comme le mode de fusion, l'opacité ou la visibilité d'un calque, le redessinent entièrement.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Les calques dépassent le budget de mémoire GPU. Augmenter Utilisation de la mémoire dans Préférences › Performances relève ce budget, jusqu'au quart de la mémoire de cet ordinateur.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -4050,3 +4050,4 @@ cameraRaw Yellow Sárga
Color Indicates: A szín jelzése:
Ink Characteristics Festék jellemzői
Solidity: Fedőképesség:
Redrawing Újrarajzolás

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2835,6 +2835,7 @@ cameraRaw Blending Pembauran
Fuchsia Fuksia
Violet Ungu
Large document: redrawing on the CPU Dokumen besar: menggambar ulang di CPU
Redrawing Menggambar ulang
This redraw took {seconds} s on the CPU. Penggambaran ulang di CPU ini memakan waktu {seconds} dtk.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Suntingan yang mengubah seluruh dokumen, seperti mode campuran, opasitas, atau visibilitas layer, menggambar ulang semuanya.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Layer tidak muat dalam anggaran memori GPU. Menaikkan Pemakaian memori di Preferensi › Performa menaikkan anggaran, hingga seperempat memori komputer ini.

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2497,6 +2497,7 @@ cameraRaw Amount Quantità
Fuchsia Fucsia
Violet Viola
Large document: redrawing on the CPU Documento grande: ridisegno sulla CPU
Redrawing Ridisegno
This redraw took {seconds} s on the CPU. Questo ridisegno sulla CPU ha richiesto {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Le modifiche che cambiano tutto il documento, come metodo di fusione, opacità o visibilità di un livello, lo ridisegnano per intero.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. I livelli non rientrano nel budget di memoria della GPU. Aumentare Uso memoria in Preferenze › Prestazioni alza il budget, fino a un quarto della memoria di questo computer.

Can't render this file because it has a wrong number of fields in line 16.

View File

@ -2398,6 +2398,7 @@ cameraRaw Color カラー
Violet 紫
Reset 初期化
Large document: redrawing on the CPU 大きなドキュメント: CPU で再描画しています
Redrawing 再描画中
This redraw took {seconds} s on the CPU. この再描画は CPU で {seconds} 秒かかりました。
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. レイヤーの描画モード、不透明度、表示/非表示など、ドキュメント全体に関わる編集では全体が再描画されます。
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. レイヤーが GPU メモリの予算に収まりません。環境設定 › パフォーマンスのメモリ使用量を増やすと、このコンピューターのメモリの4分の1まで予算が増えます。

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3008,6 +3008,7 @@ cameraRaw Color 색상
Indigo 남색
Fuchsia 자홍색
Large document: redrawing on the CPU 큰 문서: CPU에서 다시 그리는 중
Redrawing 다시 그리는 중
This redraw took {seconds} s on the CPU. 이번 다시 그리기는 CPU에서 {seconds}초 걸렸습니다.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. 레이어의 혼합 모드, 불투명도, 표시 여부처럼 문서 전체를 바꾸는 편집은 문서 전체를 다시 그립니다.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. 레이어가 GPU 메모리 예산에 맞지 않습니다. 환경 설정 › 성능에서 메모리 사용량을 늘리면 이 컴퓨터 메모리의 4분의 1까지 예산이 늘어납니다.

Can't render this file because it has a wrong number of fields in line 11.

View File

@ -2427,6 +2427,7 @@ layerLabel No Color Geen kleur
Fuchsia Fuchsia
Violet Violet
Large document: redrawing on the CPU Groot document: opnieuw tekenen op de CPU
Redrawing Opnieuw tekenen
This redraw took {seconds} s on the CPU. Opnieuw tekenen duurde {seconds} s op de CPU.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Bewerkingen die het hele document veranderen, zoals de overvloeimodus, dekking of zichtbaarheid van een laag, tekenen alles opnieuw.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. De lagen passen niet binnen het GPU-geheugenbudget. Door Geheugengebruik te verhogen in Voorkeuren › Prestaties wordt het budget groter, tot een kwart van het geheugen van deze computer.

Can't render this file because it contains an unexpected character in line 8 and column 34.

View File

@ -3458,6 +3458,7 @@ cameraRaw Roughness Chropowatość
Indigo Indygo
Fuchsia Fuksja
Large document: redrawing on the CPU Duży dokument: odświeżanie na CPU
Redrawing Odświeżanie
This redraw took {seconds} s on the CPU. To odświeżenie na CPU trwało {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Zmiany obejmujące cały dokument, takie jak tryb mieszania, krycie lub widoczność warstwy, odświeżają go w całości.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Warstwy nie mieszczą się w budżecie pamięci GPU. Zwiększenie wartości Użycie pamięci w Preferencje › Wydajność zwiększa budżet, maksymalnie do jednej czwartej pamięci tego komputera.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2852,6 +2852,7 @@ cameraRaw Whites Brancos
Fuchsia Fúcsia
Violet Violeta
Large document: redrawing on the CPU Documento grande: redesenhando na CPU
Redrawing Redesenhando
This redraw took {seconds} s on the CPU. Este redesenho na CPU levou {seconds} s.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Edições que mudam o documento inteiro, como modo de mesclagem, opacidade ou visibilidade de uma camada, redesenham tudo.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. As camadas não cabem no orçamento de memória da GPU. Aumentar Uso de memória em Preferências › Desempenho eleva o orçamento, até um quarto da memória deste computador.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2697,6 +2697,7 @@ cameraRaw Roughness Неровность
Fuchsia Фуксия
Violet Фиолетовый
Large document: redrawing on the CPU Большой документ: перерисовка на CPU
Redrawing Перерисовка
This redraw took {seconds} s on the CPU. Эта перерисовка на CPU заняла {seconds} с.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Правки, затрагивающие весь документ, например режим наложения, непрозрачность или видимость слоя, перерисовывают его целиком.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Слои не помещаются в бюджет памяти GPU. Увеличение параметра «Память» в разделе Настройки › Производительность повышает бюджет, но не больше четверти памяти этого компьютера.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -4204,3 +4204,4 @@ shape Appearance Izgled
Color Indicates: Boja označava:
Ink Characteristics Karakteristike mastila
Solidity: Punoća:
Redrawing Ponovno iscrtavanje

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3861,3 +3861,4 @@ cameraData Lens Lens
Color Indicates: Rengin Gösterdiği:
Ink Characteristics Mürekkep Özellikleri
Solidity: Doluluk:
Redrawing Yeniden çiziliyor

Can't render this file because it contains an unexpected character in line 8 and column 34.

View File

@ -2667,6 +2667,7 @@ cameraRaw Roughness Шорсткість
Fuchsia Фуксія
Violet Фіалковий
Large document: redrawing on the CPU Великий документ: перемальовування на CPU
Redrawing Перемальовування
This redraw took {seconds} s on the CPU. Це перемальовування на CPU тривало {seconds} с.
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Зміни, що впливають на весь документ, як-от режим накладання, непрозорість чи видимість шару, перемальовують його повністю.
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Його шари не вміщуються в бюджет пам'яті GPU. Збільшення використання пам'яті в Налаштування › Продуктивність підвищує бюджет до чверті пам'яті цього комп'ютера.

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -2152,6 +2152,7 @@ layers Fill: Độ tô:
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Các lớp không vừa ngân sách bộ nhớ GPU. Tăng mức sử dụng bộ nhớ trong Tùy chọn › Hiệu năng sẽ nâng ngân sách, tối đa một phần tư bộ nhớ máy này.
Labels Nhãn
Large document: redrawing on the CPU Tài liệu lớn: đang vẽ lại trên CPU
Redrawing Đang vẽ lại
Layer deleted, but confirmation preference could not be saved: {error} Đã xóa lớp, nhưng không lưu được tùy chọn xác nhận: {error}
Layer From Background… Lớp từ nền…
Load a keyboard shortcut set saved as .kys Tải bộ phím tắt đã lưu dạng .kys

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2439,6 +2439,7 @@ cameraRaw Blending 混合
Violet 紫罗兰色
Reset 复位
Large document: redrawing on the CPU 大型文档:正在用 CPU 重绘
Redrawing 正在重绘
This redraw took {seconds} s on the CPU. 此次 CPU 重绘耗时 {seconds} 秒。
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. 更改整个文档的编辑(如图层的混合模式、不透明度或可见性)会重绘整个文档。
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. 图层超出 GPU 内存预算。在 首选项 › 性能 中提高内存使用量可增加预算,最多为本机内存的四分之一。

Can't render this file because it has a wrong number of fields in line 15.

View File

@ -2409,6 +2409,7 @@ cameraRaw Blending 混合
Violet 紫羅蘭色
Reset 重設
Large document: redrawing on the CPU 大型文件:正在以 CPU 重繪
Redrawing 正在重繪
This redraw took {seconds} s on the CPU. 此次 CPU 重繪耗時 {seconds} 秒。
Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. 變更整份文件的編輯(例如圖層的混合模式、不透明度或可見度)會重繪整份文件。
Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. 圖層超出 GPU 記憶體預算。在 偏好設定 › 效能 中提高記憶體使用量可增加預算,最多為本機記憶體的四分之一。

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -24,6 +24,7 @@ pub mod analysis_ui;
pub mod anchor_tools;
pub mod artboard_tool;
pub mod artboard_ui;
pub(crate) mod background_redraw;
mod blend_keys;
pub(crate) mod blend_preview;
mod brand;
@ -633,6 +634,8 @@ pub struct PhotocraftApp {
pub(crate) filter_preview: Option<filter_dialog::FilterPreview>,
#[cfg(not(target_arch = "wasm32"))]
filter_preview_worker: filter_preview_worker::Worker,
/// Full CPU canvas redraws running in the background, by document (#4024).
pub(crate) redraws: std::collections::HashMap<photocraft_doc::DocId, background_redraw::Redraw>,
/// Select › Color Range dialog preview (proxy document + mask / image textures).
pub(crate) color_range: Option<color_range_ui::Preview>,
/// Image › Adjustments dialog preview through a temporary clipped adjustment layer.
@ -823,6 +826,7 @@ impl PhotocraftApp {
filter_preview: None,
#[cfg(not(target_arch = "wasm32"))]
filter_preview_worker: Default::default(),
redraws: Default::default(),
color_range: None,
adjust_preview: None,
synthetic: Vec::new(),

View File

@ -1611,7 +1611,10 @@ pub fn status_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
if t.pro {
crate::chrome_ui::status_bar_pro(app, ui);
// Background job progress with Cancel, at the far end (#210).
ui.with_layout(crate::direction::trailing(), |ui| crate::jobs_ui::status_progress(app, ui));
ui.with_layout(crate::direction::trailing(), |ui| {
crate::jobs_ui::status_progress(app, ui);
crate::background_redraw::status(app, ui);
});
return;
}
if let (Some(st), Some(i)) = (app.session.active(), app.session.active_index()) {
@ -1650,6 +1653,7 @@ pub fn status_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
ui.add_space(6.0);
crate::jobs_ui::status_progress(app, ui);
}
crate::background_redraw::status(app, ui);
});
});
});