From d6190e79e0f05ae97351fbcc9f93001202252b4b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 00:49:00 -0800 Subject: [PATCH] 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. --- crates/render/src/fx/pixels.rs | 7 +++- crates/render/src/tests/longstroke.rs | 59 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/render/src/fx/pixels.rs b/crates/render/src/fx/pixels.rs index eb5c698019..e89f2bddac 100644 --- a/crates/render/src/fx/pixels.rs +++ b/crates/render/src/fx/pixels.rs @@ -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); diff --git a/crates/render/src/tests/longstroke.rs b/crates/render/src/tests/longstroke.rs index b8c7e7cf65..ef7a449d3f 100644 --- a/crates/render/src/tests/longstroke.rs +++ b/crates/render/src/tests/longstroke.rs @@ -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));