mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 18:11:22 +00:00
M8: zoomed-out images draw from small copies that stay decoded
The canvas drew an image shown smaller than its pixels from a 4096 or 8192 px copy, and an image of 4096 px or less from itself, whatever its size on screen. A page of a few hundred photos zoomed out then asks the decoded-image cache (512 MB) for several GB: every render decoded them again, and a zoom out took seconds with the canvas empty around the old frame. The copies now also come at 256, 512, 1024 and 2048 px, the smallest that holds the size shown. Measured on a file with 237 images (median 5.8 MP), canvas options, 2352 x 2090 view, M4 Max: | zoom | before | after, first | after, again | |---|---|---|---| | 10% | 0.25 s | 0.12 s | 6 ms | | 4% | 0.88 s | 0.46 s | 7 ms | | 1% | 3.4 s | 0.9 s | 4 ms | Peak memory over the run went from 3.9 GB to 2.3 GB. Exports and views at 100% and closer still draw from the image itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a2e87433d8
commit
c82986b26c
@@ -33,7 +33,7 @@ const MAX_CACHED_IMAGES: usize = 256;
|
||||
/// The largest sides of the copies the canvas draws larger images from while they show smaller
|
||||
/// (device pixels): two 16 000 × 10 000 images take 640 MB each in full, past the cache's budget
|
||||
/// together, and 42 MB each at 4096 (168 MB at 8192).
|
||||
pub const DISPLAY_SIDES: [u32; 2] = [4096, 8192];
|
||||
pub const DISPLAY_SIDES: [u32; 6] = [256, 512, 1024, 2048, 4096, 8192];
|
||||
|
||||
/// Decoded pixmaps by image, filters and profile, holding at most a byte budget of pixels: adding
|
||||
/// one past the budget (or past [`MAX_CACHED_IMAGES`]) drops the least recently used first, so
|
||||
|
||||
@@ -8,7 +8,7 @@ use figcraft_color::{ImageScaleMode, Paint};
|
||||
use figcraft_doc::{Document, ImageData, NodeId, NodeKind};
|
||||
use tiny_skia::Pixmap;
|
||||
|
||||
use crate::images::{self, DISPLAY_SIDES, MAX_CACHED_BYTES};
|
||||
use crate::images::{self, MAX_CACHED_BYTES};
|
||||
use crate::{Options, View, render_view};
|
||||
|
||||
const W: u32 = 6000;
|
||||
@@ -63,12 +63,11 @@ fn sharp_stripes(p: &Pixmap, y: u32, black_first: bool, each: u32) -> bool {
|
||||
#[test]
|
||||
fn zoomed_out_the_canvas_draws_both_images_from_copies_that_stay_decoded() {
|
||||
let (d, page, keys) = doc("zoomed-out");
|
||||
// At 25% each shows 1500 px long: drawn from 4096 px copies, not decoded in full.
|
||||
render_view(&d, page, &view(0.25), &canvas()).unwrap();
|
||||
let first = keys.each_ref().map(|k| shown(&d, k, 1500.0));
|
||||
for (k, p) in keys.iter().zip(&first) {
|
||||
assert_eq!((p.width(), p.height()), (DISPLAY_SIDES[0], 5), "{k}");
|
||||
assert_eq!(images::cached_sides(k), vec![DISPLAY_SIDES[0]], "{k}: only its copy is decoded");
|
||||
assert_eq!((p.width(), p.height()), (2048, 3), "{k}");
|
||||
assert_eq!(images::cached_sides(k), vec![2048], "{k}: only its copy is decoded");
|
||||
}
|
||||
// Drawn again (a drag, a pan), both are still the copies decoded the first time.
|
||||
render_view(&d, page, &view(0.25), &canvas()).unwrap();
|
||||
@@ -93,13 +92,15 @@ fn exports_and_close_ups_draw_from_the_image_itself() {
|
||||
#[test]
|
||||
fn the_copy_is_the_smallest_that_holds_what_shows() {
|
||||
let side = |long, shown| images::display_side(long, shown);
|
||||
assert_eq!(side(6000, 1500.0), 4096);
|
||||
assert_eq!(side(6000, 1500.0), 2048);
|
||||
assert_eq!(side(16_000, 4096.0), 4096);
|
||||
assert_eq!(side(16_000, 4097.0), 8192);
|
||||
assert_eq!(side(4096, 100.0), 256);
|
||||
assert_eq!(side(3000, 20.0), 256);
|
||||
// Shown at more than the largest copy, or an image no larger than the copy: the image itself.
|
||||
assert_eq!(side(16_000, 9000.0), 16_384);
|
||||
assert_eq!(side(6000, 5000.0), 16_384);
|
||||
assert_eq!(side(4096, 100.0), 16_384);
|
||||
assert_eq!(side(200, 100.0), 16_384);
|
||||
// An unknown size or a meaningless scale: the image itself.
|
||||
assert_eq!(side(0, 100.0), 16_384);
|
||||
assert_eq!(side(16_000, f64::NAN), 16_384);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hardware and platform parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** patch (under Wayland a minimised window answers control and MCP requests in about half a second instead of timing out after 60 s (#1906); images copied in other apps paste in the browser (#492); text reads only the fallback fonts it draws with; `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); a batch keeps one undo snapshot, not one per step (#1099); one depth cap bounds a whole render, pattern sources included, and render threads get 8 MiB stacks (#1082); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); egui/eframe 0.37 dependency migration; hidden-window screenshot deadlines; zoom previews; hidden windows answer control requests; macOS file opening, the Windows start-up message, Linux packaging; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses; macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (zoomed-out images draw from small copies that stay decoded, so a page of photos zooms out without decoding them again; under Wayland a minimised window answers control and MCP requests in about half a second instead of timing out after 60 s (#1906); images copied in other apps paste in the browser (#492); text reads only the fallback fonts it draws with; `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); a batch keeps one undo snapshot, not one per step (#1099); one depth cap bounds a whole render, pattern sources included, and render threads get 8 MiB stacks (#1082); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); macOS: the Quit menu and ⌘Q ask about unsaved changes (#857); egui/eframe 0.37 dependency migration; hidden-window screenshot deadlines; zoom previews; hidden windows answer control requests; macOS file opening, the Windows start-up message, Linux packaging; copy and paste in the browser; Windows QA pass; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; pen pressure for the Pencil and brushes on Windows and from browser styluses; macOS: quitting ends the process once everything is kept, avoiding the Touch Bar teardown crash (#781); Linux and BSD clipboard: images under Wayland, reads time out; minimised windows under Wayland; window controls on Wayland without server decorations; canvas textures on device pixels) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
How FigCraft uses hardware and which platforms it runs on, against Figma. Figma's desktop app is
|
||||
Electron with a GPU helper process; its canvas renders on the GPU (WebGL, with WebGPU rolling out
|
||||
@@ -12,7 +12,7 @@ since 2025). FigCraft renders the canvas on the CPU and presents it through wgpu
|
||||
|
||||
| Feature | Figma | FigCraft (macOS / Windows / Linux / web) | Hours |
|
||||
|---|---|---|---|
|
||||
| Canvas rendering | GPU | CPU (tiny-skia), banded across cores, retained (re-renders only what an edit or pan touches), levels of detail below 50% zoom, large images drawn from a copy no larger than they show (#1483); when renders from scratch are slow, a new zoom shows the whole view coarsely first, then refines it (#312); shown through wgpu (WebGPU or WebGL2 in the browser) | 30–60 for a GPU path |
|
||||
| Canvas rendering | GPU | CPU (tiny-skia), banded across cores, retained (re-renders only what an edit or pan touches), levels of detail below 50% zoom, images drawn from a copy no larger than they show, down to 256 px (#1483); when renders from scratch are slow, a new zoom shows the whole view coarsely first, then refines it (#312); shown through wgpu (WebGPU or WebGL2 in the browser) | 30–60 for a GPU path |
|
||||
| Effects (blurs, glass, noise) | GPU | CPU | in the above |
|
||||
| HiDPI / Retina | yes | yes (all); a canvas image rendered at the screen's density is placed on whole device pixels, so fractional pans, zooms and display scales don't blur it (#359; geometry tests, the snapping not judged by eye) | — |
|
||||
| Wide-gamut display (Display P3) | yes, on capable displays | colours stored and exported in P3; the screen shows sRGB: eframe/egui-wgpu 0.37 configure an 8-bit sRGB surface with no colour space option, although wgpu 30 can present Display P3 or extended sRGB on Metal, DX12, Vulkan and WebGPU (see gaps #18). The colour picker warns about colours outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas | 2–5 |
|
||||
@@ -50,6 +50,7 @@ Nothing has been timed against Figma on the same document (needs `.fig` import).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-10 | minor | Zoomed-out images: the canvas's copies of an image shown smaller than its pixels now also come at 256, 512, 1024 and 2048 px, the smallest that holds what the screen shows, and images of 4096 px or less get them too (they were drawn from the image itself at any size). On a page of 237 photos, about 4.6 GB decoded against the 512 MB cache, every zoomed-out render decoded them again: 3.4 s a frame at 1% before, 0.9 s the first time and 4 ms after; peak memory 3.9 → 2.3 GB. Exports and views at 100% and closer still draw from the image itself. |
|
||||
| 2026-11-06 | patch | M8, #1906: under Wayland (seen on GNOME and KDE Plasma) a minimised FigCraft window still reports itself visible and the compositor stops asking it to draw, so every control or attached-MCP request waited for a frame and timed out after 60 s ("it was not applied"), though eframe kept running the app's logic. `control::background` now treats a window that drew no frame for `NOT_DRAWING_AFTER` (0.2 s) while a request waited for it as hidden: state-only requests are answered from the logic passes (0.54 s each, measured on GNOME Wayland; they took 13.7 s or timed out before), the wait starts again when the window draws, and screenshots and `ui.input` still wait for a frame. Test: `idle_tests::a_window_that_stopped_drawing_answers_from_its_logic_passes` |
|
||||
| 2026-10-31 | minor | M1, #492: Web row: the browser pastes images copied in other apps, read from the paste event under the same limits as the desktop. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M8: text reads only the fallback fonts it draws with. Each fallback family reads the face nearest a run's style, not all of them (Noto Sans CJK is seven files of 16–20 MB); families naming the faces of one file share its bytes; and the faces are worked out once per style until fonts change, not for every text laid out. First text laid out on a Linux machine with Noto Sans CJK, DejaVu Sans and Inter: 41 files (265 MiB, kept in memory) and 41 faces parsed → 3 files (20 MiB) and 4 faces; fallback lookups per text laid out ~45 µs → 0.2 µs. Tests: `fontdb_fallback_tests`. Numbers unchanged |
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user