From f14f4ad55cf726e07bce4340ae9b5947cf87bca7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 25 Oct 2026 21:52:56 -0700 Subject: [PATCH] Render: the decoded-image cache can grow to a quarter of installed memory on every desktop OS The renderers share decoded images within a budget that was 1 GiB on every desktop OS: the installed memory was read only on Linux, and there the budget was an eighth of it, at most 1 GiB. Images whose decoded pixels add up to more than that were decoded again each time the view moved from one image to another: 0.9 to 1.1 s for a 12000 x 9000 JPEG and 2.0 to 2.2 s for a 16384 x 16384 one on a Ryzen 7 9800X3D. The default budget is now a quarter of the installed memory, with no fixed ceiling: GlobalMemoryStatusEx on Windows (through winsafe, which vectorcraft-cli already uses), sysctl hw.memsize on macOS, hw.physmem on FreeBSD and /proc/meminfo on Linux. It stays 1 GiB where the memory can't be read and 256 MiB in the web build, and a 32-bit build keeps at most a quarter of its address space. Docs: docs/target-app-parity.md. --- Cargo.lock | 1 + crates/render/Cargo.toml | 5 ++++ crates/render/src/images.rs | 59 +++++++++++++++++++++++++++++++++---- docs/target-app-parity.md | 1 + 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5abe6b35d..734ccc628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6230,6 +6230,7 @@ dependencies = [ "vello_cpu", "wat", "weezl 0.2.1", + "winsafe", ] [[package]] diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index ad8696e1c..d8d56da1d 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -29,6 +29,11 @@ log = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] vello_cpu = { workspace = true, features = ["multithreading"] } +# The shared image store's budget (src/images.rs) follows the installed memory, which it reads with GlobalMemoryStatusEx +# through winsafe's safe wrappers over Win32 (MIT), which vectorcraft-cli's src/perf.rs also uses. +[target.'cfg(windows)'.dependencies] +winsafe = { version = "0.0.29", default-features = false, features = ["kernel"] } + [dev-dependencies] vectorcraft-testkit = { workspace = true } # A variable font built in code (no font file is committed). diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 63577ab89..f71188e43 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -19,8 +19,9 @@ use vello_cpu::color::PremulRgba8; use crate::ink::Ink; -/// Largest default budget of the shared image store, in bytes; less on machines with under 8 GiB -/// of memory (an eighth of it). [`set_image_cache_budget`] changes it. +/// Default budget of the shared image store, in bytes, where the installed memory can't be read. +/// Elsewhere the default is a quarter of the installed memory, and on the web it is 256 MiB. +/// [`set_image_cache_budget`] changes it. pub const IMAGE_CACHE_MAX: usize = 1 << 30; /// The default budget on the web (a tab's memory is tight). @@ -31,10 +32,17 @@ fn default_budget() -> usize { #[cfg(not(target_arch = "wasm32"))] fn default_budget() -> usize { - physical_memory().map_or(IMAGE_CACHE_MAX, |m| usize::try_from(m / 8).unwrap_or(usize::MAX).min(IMAGE_CACHE_MAX)) + physical_memory().filter(|&m| m > 0).map_or(IMAGE_CACHE_MAX, budget_for) } -/// Installed memory in bytes, where the system says without a native call (Linux). +/// The default budget on a machine with `memory` bytes installed: a quarter of it, and at most a +/// quarter of the address space (on 32-bit builds). +#[cfg(not(target_arch = "wasm32"))] +fn budget_for(memory: u64) -> usize { + usize::try_from(memory / 4).unwrap_or(usize::MAX).min(usize::MAX / 4) +} + +/// Installed memory in bytes (`MemTotal` in `/proc/meminfo`). #[cfg(target_os = "linux")] fn physical_memory() -> Option { let info = std::fs::read_to_string("/proc/meminfo").ok()?; @@ -43,7 +51,22 @@ fn physical_memory() -> Option { kib.checked_mul(1024) } -#[cfg(not(any(target_os = "linux", target_arch = "wasm32")))] +/// Installed memory in bytes (`GlobalMemoryStatusEx`). +#[cfg(windows)] +fn physical_memory() -> Option { + winsafe::GlobalMemoryStatusEx().ok().map(|m| m.ullTotalPhys) +} + +/// Installed memory in bytes, as `sysctl` prints it (`hw.memsize` on macOS, `hw.physmem` on +/// FreeBSD). +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +fn physical_memory() -> Option { + let (sysctl, name) = if cfg!(target_os = "macos") { ("/usr/sbin/sysctl", "hw.memsize") } else { ("/sbin/sysctl", "hw.physmem") }; + let out = std::process::Command::new(sysctl).args(["-n", name]).output().ok().filter(|o| o.status.success())?; + String::from_utf8(out.stdout).ok()?.trim().parse().ok() +} + +#[cfg(not(any(target_os = "linux", windows, target_os = "macos", target_os = "freebsd", target_arch = "wasm32")))] fn physical_memory() -> Option { None } @@ -290,6 +313,32 @@ mod tests { assert_eq!(s.bytes(), 400); } + #[test] + #[cfg(target_pointer_width = "64")] + fn default_budget_is_a_quarter_of_installed_memory() { + assert_eq!(budget_for(16 << 30), 4 << 30); + assert_eq!(budget_for(64 << 30), 16 << 30, "no fixed ceiling"); + assert_eq!(budget_for(2 << 30), 512 << 20); + } + + #[test] + #[cfg(any(target_os = "linux", windows, target_os = "macos", target_os = "freebsd"))] + fn installed_memory_is_read() { + assert!(physical_memory().is_some_and(|m| m >= 256 << 20), "{:?}", physical_memory()); + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn two_images_of_a_tenth_of_memory_each_stay_cached() { + // 16 MiB installed (16 GiB scaled down): two images of 1,600 KiB (640 × 640 pixels) take + // about a fifth of it. + let mut s = ImageStore::new(budget_for(16 << 20)); + drop(s.insert(key("a"), Arc::new(Pixmap::new(640, 640)))); + drop(s.insert(key("b"), Arc::new(Pixmap::new(640, 640)))); + assert!(s.peek(&key("a")).is_some() && s.peek(&key("b")).is_some(), "neither image is decoded again"); + assert_eq!(s.bytes(), 2 * 640 * 640 * 4); + } + #[test] fn half_averages_and_rounds_up_odd_sizes() { let px = |v: u8| PremulRgba8 { r: v, g: v, b: v, a: 255 }; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6a77b40d2..f559fe7af 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -979,6 +979,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663). On macOS the tool cursor is painted only while the window has the focus. Over a window behind another app, or under the File › Open… sheet, the system pointer shows alone instead of next to the painted cursor (#2020). In a rotated view the resize and rotate cursors turn with the view, to the nearest 45°. - **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. Print-size images (more than 512 MB decoded, up to 2 GB) draw in the canvas and in raster exports and can be trimmed with Crop Image; an image with a side past 65,535 px is shrunk to fit, a raster export says when an image could not be decoded, and Crop Image's trim leaves CMYK images alone (#1517). On Windows `vectorcraft-cli perf` reports the cores busy and, with `--deep`, its working set, and warns on a busy machine as it does on macOS and Linux (#1502). + The renderers keep the decoded images they share within a quarter of the installed memory on Windows, macOS, Linux and FreeBSD, within 1 GiB where the installed memory can't be read, and within 256 MiB in the web build; while the decoded images add up to less than that budget, they are not decoded again when the view changes. With `--deep`, each memory row of `vectorcraft-cli perf` also gives the peak so far: the peak working set on Windows and the peak resident memory (`VmHWM`) on Linux. A new last row gives the memory after the SVG export and the save → open → save at the end of the run. The renderer composes the two ink planes of a CMYK document that shows transparency into screen colors on up to as many threads as it rasterizes with, instead of on one. At 2880 × 1800 on 8 threads, a page of 300 shapes at 20% to 90% opacity takes 30 ms a frame instead of 55 ms, and 50,000 paths with one at 99% opacity take 258 ms instead of 361 ms (`vectorcraft-cli bench`), with the same pixels. With Overprint Preview or Separations Preview on, the renderer keeps the art it prepared for the preview from one frame to the next. It prepares again the objects that changed, and all of the art when Discard White Overprint changes or, with Separations Preview, when the plates shown, the proof setup, the swatches, the Spot Colors option or the color settings change or a profile is loaded. It draws the other objects from the art, geometry and stroke outlines it kept for them.