Skip to content

vello_cpu/vello_hybrid: use rect fast paths for rect fills and clips - #114

Merged
nicoburns merged 3 commits into
mainfrom
devin/1791053769-vello-rect-fast-paths
Oct 3, 2026
Merged

nicoburns merged 3 commits into
mainfrom
devin/1791053769-vello-rect-fast-paths

Conversation

@nicoburns

@nicoburns nicoburns commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Summary

vello_cpu and vello_gpu 0.3 have rectangle fast paths (fill_rect, push_clip_rect; see linebender/vello#1916, #1924, #1926). AnyRender never used them, because every shape went through shape.into_path(..) and then fill_path / push_clip_path / push_clip_layer.

All three sparse-strip painters (vello_cpu, vello_hybrid, and vello_hybrid's WebGL painter) now check Shape::as_rect():

// fill
if let Some(rect) = shape.as_rect() { ctx.fill_rect(&rect) } else { ctx.fill_path(&shape.into_path(..)) }
// push_clip_layer (all three) and the clip of push_layer (hybrid / WebGL only)
if let Some(rect) = clip.as_rect() { ctx.push_clip_rect(&rect) } else { /* path */ }

vello_cpu push_clip_layer no longer always pushes an isolated clip layer:

if let Some(rect) = clip.as_rect()       { push_clip_rect(&rect) }   // pop_clip
else if ctx.is_multi_threaded()          { push_clip_layer(&path) }  // pop_layer
else                                     { push_clip_path(&path) }   // pop_clip
  • Single-threaded: non-isolated clips match vello_hybrid and Skia, and are faster.
  • Multi-threaded: isolated clip layers are kept for non-rect clips. The multi-threaded dispatcher flushes its pending batch on every push_clip_path / pop_clip, copies the clip into each batch, and rasterizes the clip on the main thread. In multi-threaded mode that costs more than the layer it saves. The check is RenderContext::is_multi_threaded() rather than the multithreading feature, because vello_cpu falls back to single-threaded when num_threads is 0.
  • Pop bookkeeping: VelloCpuScenePainter gains a small layer_stack to choose between pop_layer and pop_clip.
  • vello_cpu push_layer still passes its clip to push_layer as a path, because opacity and blend need a layer anyway.
  • Strokes are unchanged, because stroke_rect has no fast path.

Vello itself falls back to paths for skewed or rotated transforms. A new vello_cpu unit test, rect_clips_and_fills, covers a rect clip nested inside a rect-clipped layer.

Benchmark

paint_bench cpu backend through Blitz, 1366×768@2x, 8-core VM. Each figure is the median of 4 runs of 60 frames, on a synthetic page of 600 rounded overflow: hidden boxes (non-rect clips):

paint_scene rasterize
single-threaded, layer → push_clip_path 36.3 → 33.2 ms 7.9 → 6.8 ms
multi-threaded, layer → push_clip_path 18.4 → 23.4 ms 2.6 → 2.2 ms

Multi-threaded therefore keeps the layer. Real pages (bbc, google, github, servo) showed no clear change, because their clips are almost all rects.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/45dbc6c083354777aaa43442ce8ef8ab
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/45dbc6c083354777aaa43442ce8ef8ab?variant=devin-insiders
Requested by: @nicoburns

When the shape passed to fill, push_clip_layer or push_layer is a Rect
(Shape::as_rect), call fill_rect / push_clip_rect instead of converting
it to a path, so the sparse strip renderers can use their rectangle
fast paths.
@staging-devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

@nicoburns
nicoburns merged commit 3037fe0 into main Oct 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant