Files
vectorcraft/crates/render/src/freeform.rs
T
arhammusheerandClaude Opus 5.5 c4214be9d7 #1244: render caches bounded by bytes, evicting the least recently used instead of clearing
Text geometry, clips, clip paints, effect art, brushes, live effects, pattern tiles and freeform
grids were capped by entry count and cleared wholesale on overflow, so a document past the cap
(e.g. more than 4,096 type objects) recomputed every entry on every frame. A FrameCache keeps
per-entry byte sizes and last-used frames, drops the least recently used entries of earlier
frames, and sweeps stale ones. Text geometry keeps its glyph outlines once instead of twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-12 11:02:17 +07:00

93 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Freeform gradients in the renderer.
//!
//! vello has no freeform shading, so the colour field ([`vectorcraft_color::freeform`]) is
//! sampled on a grid of cells over the painted box and drawn as an image paint (bilinear, edges
//! padded): the fill or stroke being painted clips it. The grid is sized from the box's size on
//! screen (a cell per few device pixels, in power-of-two steps so zooming reuses it) and cached by
//! the gradient's content, so panning and redraws don't resample it.
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use vectorcraft_color::freeform::{grid_size, painted_box, spread_scale};
use vectorcraft_color::{Color, Freeform, GradientPaint};
use vectorcraft_geom::{Affine, Rect};
use vello_cpu::peniko::{Extend, ImageQuality, ImageSampler};
use vello_cpu::{Pixmap, RenderContext};
use crate::ink::Ink;
use crate::lru::{self, FrameCache};
thread_local! {
/// Grids by [`key`], within [`lru::FREEFORM_BUDGET`], least recently used first out.
static GRIDS: RefCell<FrameCache<u64, Arc<Pixmap>>> = RefCell::new(FrameCache::new(lru::FREEFORM_BUDGET, usize::MAX));
}
fn hash_color(c: &Color, h: &mut impl Hasher) {
match *c {
Color::Rgb { r, g, b } => (0u8, r.to_bits(), g.to_bits(), b.to_bits()).hash(h),
Color::Cmyk { c, m, y, k } => (1u8, c.to_bits(), m.to_bits(), y.to_bits(), k.to_bits()).hash(h),
Color::Gray { k } => (2u8, k.to_bits()).hash(h),
Color::Lab { l, a, b } => (3u8, l.to_bits(), a.to_bits(), b.to_bits()).hash(h),
}
}
/// Cache key of the grid of `f` on box `b` at `cols` × `rows` painted with `ink` (and the active
/// colour settings, which turn colours into display RGB or inks).
fn key(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
for p in &f.points {
(p.at.x.to_bits(), p.at.y.to_bits(), p.opacity.to_bits(), p.spread.to_bits()).hash(&mut h);
hash_color(&p.color, &mut h);
}
f.lines.hash(&mut h);
(b.x0.to_bits(), b.y0.to_bits(), b.x1.to_bits(), b.y1.to_bits(), cols, rows).hash(&mut h);
(Arc::as_ptr(&vectorcraft_color::cms::active()) as usize, ink).hash(&mut h);
h.finish()
}
/// Sample `f` at the centres of a `cols` × `rows` grid over `b` (premultiplied, colours as `ink`
/// paints them).
pub(crate) fn rasterize(f: &Freeform, b: Rect, cols: u16, rows: u16, ink: Ink) -> Pixmap {
let field = f.field_with(spread_scale(b), &|c| ink.rgb(c));
let data = field
.grid(b, cols, rows)
.map(|([r, g, bl], a)| {
let a = a.clamp(0.0, 1.0);
let q = |v: f32| (v.clamp(0.0, 1.0) * a * 255.0).round() as u8;
vello_cpu::color::PremulRgba8 { r: q(r), g: q(g), b: q(bl), a: (a * 255.0).round() as u8 }
})
.collect();
Pixmap::from_parts(data, cols, rows)
}
/// Set freeform gradient `g` on box `bounds` (in the paint's space, which the context's current
/// transform maps to pixels) as the context paint. Returns false for an empty box.
pub(crate) fn set_freeform_paint(ctx: &mut RenderContext, g: &GradientPaint, bounds: Rect, ink: Ink) -> bool {
let Some(b) = painted_box(bounds) else { return false };
let device = b.width().max(b.height()) * ctx.transform().determinant().abs().sqrt();
let (cols, rows) = grid_size(b, device);
let f = g.freeform_on(b);
let k = key(&f, b, cols, rows, ink);
let pm = GRIDS
.with(|c| {
let mut c = c.borrow_mut();
let t = c.tick();
c.get(&k, t).cloned()
})
.unwrap_or_else(|| {
let pm = Arc::new(rasterize(&f, b, cols, rows, ink));
GRIDS.with(|c| {
let mut c = c.borrow_mut();
let t = c.tick();
c.insert(k, pm.clone(), cols as usize * rows as usize * 4, t);
});
pm
});
let sampler = ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad, quality: ImageQuality::Medium, alpha: 1.0 };
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler });
ctx.set_paint_transform(Affine::translate(b.origin().to_vec2()) * Affine::scale_non_uniform(b.width() / cols as f64, b.height() / rows as f64));
true
}