Render: a filter layer's offscreen culls art and crops long strokes to its own area

On a multithreaded context, a drop shadow or glow drawn through a filter
layer is drawn into an offscreen that covers the art plus the blur's
spread, but its frame kept the view's visible rect. Art outside the view
but within the spread was culled, so it cast no shadow, and a long stroke
was outlined only near the view, so its far parts no longer fed the blur.
The offscreen's frame now takes its visible rect from its own area, and
the frame of content kept from an earlier split takes the window it is
drawn over.
This commit is contained in:
dotCooCoo
2026-11-04 16:55:30 +07:00
committed by bflatastic
parent 676cc0ee8e
commit d6190e79e0
2 changed files with 64 additions and 2 deletions
+5 -2
View File
@@ -54,7 +54,7 @@ impl Renderer {
let split_frame;
let (f, screen) = match content.kept.and_then(|k| k.frame.map(|frame| (k.window, frame))) {
Some((window, (view, px))) => {
split_frame = Frame { view, px, ..*f };
split_frame = Frame { view, visible: window, px, ..*f };
(&split_frame, view.transform_rect_bbox(window))
}
None => (f, Rect::new(0.0, 0.0, ctx.width() as f64, ctx.height() as f64)),
@@ -580,7 +580,10 @@ impl Renderer {
None => (x0, y0, w, h, k),
};
let mut off = f.offscreen_context(w, h);
let shifted = Frame { mt: false, view: Affine::scale(k) * Affine::translate((-x0, -y0)) * f.view, px: f.px / k, ..*f };
let view = Affine::scale(k) * Affine::translate((-x0, -y0)) * f.view;
// Art is culled against, and long strokes outlined around, the offscreen's own area.
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
let shifted = Frame { mt: false, view, visible, px: f.px / k, ..*f };
self.inside_layer(|r| draw(r, &mut off, &shifted, None));
off.flush();
let mut pm = vello_cpu::Pixmap::new(w, h);
+59
View File
@@ -98,6 +98,65 @@ fn a_long_stroke_draws_the_same_pixels_outlined_near_the_view() {
}
}
/// A zigzag of 3,000 anchors 8 pt apart between y = 60 and y = 550, through the bottom of a view
/// on y = 0.
fn tall_zigzag() -> BezPath {
let mut bp = BezPath::new();
let at = |i: usize| Point::new(i as f64 * 8.0, if i.is_multiple_of(2) { 60.0 } else { 550.0 });
bp.move_to(at(0));
for i in 1..3000 {
bp.line_to(at(i));
}
bp
}
#[test]
fn a_long_stroke_under_a_wide_blur_draws_the_same_pixels_while_dragging_on_threads() {
// While dragging on threads, a drop shadow this large is drawn through a filter layer whose
// offscreen reaches 602 px past the view: the parts of the stroke that far away feed the blur.
let mut d = Document::new(1000.0, 1000.0);
let shadow = serde_json::json!({"x": 0, "y": 0, "blur": 400, "opacity": 100, "mode": "normal"});
let look = Appearance { items: vec![AppearanceItem::Stroke(stroke(20.0, LineJoin::Round, LineCap::Butt))], ..Default::default() };
let n = Node::path(d.alloc_id(), PathData::from_bezpath(&tall_zigzag()), look);
// The shadow is the group's: the path itself draws as a plain path.
let mut g = Node::group(d.alloc_id(), vec![n.into()]);
g.appearance.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: shadow, visible: true });
let l = d.layers[0].id;
d.insert(Some(l), 0, g).unwrap();
let at = Point::new(12_000.0, 0.0);
let opts = RenderOptions { background: Some([255, 255, 255, 255]), dragging: true, ..Default::default() };
let draw = |crops: bool| {
CROP_STROKES.with(|c| c.set(crops));
let mut r = Renderer::new();
r.threads = 2;
let img = r.render(&d, SIZE, SIZE, view_at(at), &opts);
CROP_STROKES.with(|c| c.set(true));
(img, r.strokes.values().any(|e| e.crop.is_some()))
};
let ((near, cropped), (all, _)) = (draw(true), draw(false));
assert!(cropped, "outlined near the view");
assert!(all.pixel(100, 0)[0] < 250, "the shadow shows above the stroke: {:?}", all.pixel(100, 0));
assert!(near.pixels == all.pixels, "the same pixels as the whole outline");
}
#[test]
fn a_member_below_the_view_casts_its_wide_shadow_while_dragging_on_threads() {
// A 400 pt shadow of a group whose only member, a rectangle, lies 50 pt below the view.
let mut d = Document::new(1000.0, 1000.0);
let shadow = serde_json::json!({"x": 0, "y": 0, "blur": 400, "opacity": 100, "mode": "normal"});
let look = Appearance { items: vec![AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(Paint::solid(Color::BLACK)))], ..Default::default() };
let n = Node::path(d.alloc_id(), PathData::from_bezpath(&Rect::new(-1000.0, 150.0, 1000.0, 550.0).to_path(0.1)), look);
let mut g = Node::group(d.alloc_id(), vec![n.into()]);
g.appearance.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: shadow, visible: true });
let l = d.layers[0].id;
d.insert(Some(l), 0, g).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), dragging: true, ..Default::default() };
let mut r = Renderer::new();
r.threads = 2;
let img = r.render(&d, SIZE, SIZE, view_at(Point::ZERO), &opts);
assert!(img.pixel(100, 199)[0] < 200, "the shadow shows at the bottom of the view: {:?}", img.pixel(100, 199));
}
#[test]
fn a_long_stroke_is_outlined_near_the_view_only() {
let d = doc(&zigzag(), stroke(6.0, LineJoin::Miter, LineCap::Square));