Repository navigation
vello_common: Generate rectangular clip strips lazily - #1989
Thenewmanator15 wants to merge 5 commits into
Conversation
A rectangular clip pushed onto the viewport or onto another rectangular clip no longer generates its strips when it is pushed. They are generated when a path is drawn through the clip, and only for the tile rows that path covers. The cost of such a clip then depends on what is drawn through it instead of on the height of the clip. The multi-threaded dispatcher keeps the previous behaviour through `ClipContext::new_eager`, because it copies the clip's strips into each render task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sounds like whatever CAD program created those should be optimized. :D I can see it being useful to generate clip rects lazily (especially for GPU where we might not need them at all), but I need to carefully think about how this should be implemented. In the meanwhile, could you not just detect such viewport-sized rects and don't emit them in the first place? Or are the clip paths really limited to the size of the artwork instead of the whole viewport? |
|
Ha, yeah, it's an Inkscape export of a CAD drawing. 7,434 clips that clip nothing. :D Turns out it's a known Inkscape bug. The source PDF has one clip for about 6,500 strokes, and Inkscape 1.4's PDF import made a copy of it for every stroke (inkscape/inkscape#5345 on GitLab, fixed for 1.5). So you're right that the exporter's at fault, but files from 1.4 and earlier will be around for a while. They're the size of the artwork, not the viewport. The drawing's an image on a page, so the clips only cover its box. And most aren't exactly that size: 934 are the drawing's box, the other 6,500 are inset by 0.3 units. So checking against the viewport or the parent clip doesn't catch them. Your suggestion works though. I skip them on my side now by checking each clip rect against the bounding box of what it clips. Page takes 21.7 ms that way, 20.0 ms with this PR and nothing skipped, so I'm not blocked. Only catch is you need the content's bounds up front. I get those from usvg, not every caller will have them. Happy for you to take the idea and do it however suits Vello, especially if the GPU can skip the strips altogether. I can close this or leave it open as a reference, whichever's more useful. |
|
Cool, thanks! I definitely see some value in adding more smartness into Vello for this, but need to think about how to best achieve it. |
…`intersect` `ClipContext::new` keeps the previous behaviour again, and the lazy one is `ClipContext::new_lazy`, so code that reads a clip's strips doesn't find them silently empty after an upgrade. `ClipState` asks for lazy contexts, including the ones it takes from its pool. The multi-threaded dispatcher is back on `ClipContext::new` and asserts that it never sees a lazy clip. `intersect` now handles a lazy clip on either side itself, by generating the rectangle's strips for the rows it needs, with the same code `render_with_clip` uses. The bounding box of a lazy clip is rounded to whole tiles, using the tile height for y, and stays inside the viewport. New tests compare lazy and eager clips for stroked paths, a path clip on top of a lazy rectangle, appending and replacing modes, paths without strips, `ClipState` with a shifted root viewport, and `intersect` with either or both sides lazy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out std `tile_bbox` rounds with `floor` and `ceil`, which f64 only has with std. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rectangular clip currently generates its strips when it is pushed, for the full height of the clip. With many large clips around small content, that cost dominates rendering. This PR defers the strips for a rectangular clip until a path is drawn through it, and generates them only for the tile rows that path covers.
Motivation
I hit this with an Inkscape export of a CAD drawing. It has 7,434
<clipPath>elements. Each one is a rectangle the size of the whole drawing, wrapped around one short stroke, so none of them clips anything. Rendering the page throughvello_cpu0.3.0 with those clips sent throughpush_clip_recttook 37.5 ms at 150 ppi. With the clips removed by the caller it took 21.7 ms.What changes
ClipContextmade with the newClipContext::new_lazy,push_clip_rectno longer generates strips when the clip sits directly on the viewport or on another rectangular clip. It records the clamped rectangle and a tile-aligned bounding box.ClipStateuses lazy contexts, so the single-threaded dispatcher invello_cpuandvello_gpuget this.ClipContext::newbehaves as before.PathDataRefgets alazy_rectfield. When it is set,stripsandalphasare empty, andrender_with_clipandintersectrender the rectangle for the rows between the other path's first and last strip and intersect with that. Each tile row of a rectangle's strips is independent of the others, so those rows are the same as in the strips for the whole rectangle.ClipContext::new, and asserts in debug builds that it never gets a lazy clip. It copies the clip's strips into every render task, so it needs a different change and I have left it alone.PathDataRef::lazy_rectandClipContext::new_lazyare additions to the public API ofvello_common, and the new field breaks struct literals ofPathDataRef. Code that usesClipContext::newstill gets strips, so nothing that reads them changes unless it opts in. I'm happy to restructure this if you would prefer a different shape, for example an enum for the clip data.Results
Intel i7-12700K, Windows 11, single-threaded, release build.
N short strokes on a square canvas, each inside its own
push_clip_rectcovering the whole canvas, best of 7 runs:The CAD page, cold, median of 10 passes, measured with the patch applied to the 0.3.0 crates:
vello_cpu, 150 ppivello_cpu, 300 ppivello_gpu, 150 ppiThe timings on the larger canvases vary by 10 to 20% between runs on this machine.
Testing
cargo test -p vello_tests --release --testsgives the same result asmainon my machine: 4,801 passed, 2 failed, 1,346 ignored. The two failures arefilter_blur_with_image_gpuandfilter_with_clip_rects_outside_of_viewport_gpu, which also fail for me on unmodifiedmain(NVIDIA RTX 5070 Ti). The ignored tests are mostly AVX-512 variants, which this CPU can't run.rectangular_clip_metadata_tracks_intersections_and_popsnow uses a lazy context and asserts that the strips are deferred.eager_rectangular_clip_stores_its_stripskeeps the old assertion forClipContext::new.ClipStatewith a shifted root viewport; and throughintersectwith either or both sides lazy. Where a path's rows miss an eager clip, intersecting still appends a lone sentinel strip and the lazy version appends nothing. The tests treat that as equal, since neither draws anything.vello_cpu0.3.0 and the patched crate. All 1,200 match.I have not run the AVX-512 variants, macOS or Linux, or the
no_stdbuild.Not included
push_clip_pathare unchanged, including paths that happen to be rectangles.🤖 Generated with Claude Code