Skip to content

perf(blur): keep blurred backdrops, clip damage, opaque regions - #28

Merged
nongio merged 3 commits into
mainfrom
feat/blur-cache
Sep 15, 2026
Merged

nongio merged 3 commits into
mainfrom
feat/blur-cache

Conversation

@nongio

@nongio nongio commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three changes that make a frosted layer (BlendMode::BackgroundBlur) cheaper to repaint when little has changed. The first is the main one, and the other two follow from it.

1. Keep a blurred backdrop until something beneath it changes (e0ece8b)

Before: any damage that reached a blur shape repainted the whole shape and ran the blur again. That included the layer's own content changing, or something drawn on top of it. So a frosted window blurred its backdrop again on every frame that its text or a scroll changed.

Now: the blurred result is a function of what lies beneath the shape, so the engine only invalidates it for that.

  • Paint order. The traversal cache now records each node's pre-order index (cached_paint_order), which is the scene's z-order.
  • Engine: blur_expansion replaces both older Phase 8 expansions (the whole-scene one in update_nodes and the one in subtree_damage). A blur shape is repainted whole, and its backdrop_generation bumped, only when damage lands:
    • within blur reach (3σ), and
    • on a node earlier in paint order that is not inside the blur layer's own subtree.
    • Damage from removed nodes has no known position, so it counts as beneath.
  • Drawing: a small per-thread cache of blurred backdrops (BLUR_CACHE, 32 entries, LRU).
    • Each entry is keyed by the device rect, matrix, size, bounds, opaque region, generation and external backdrop image ids.
    • On a hit, paint_node draws the kept pixels (BlendMode::Src) instead of running the save_layer backdrop blur.
    • A snapshot is stored only after a repaint that covered the whole shape. A partial repaint would capture last frame's content.
  • Cases that keep the old behaviour, repainted whole for any damage in reach: a translucent blur (fade group), and one under an image-cached ancestor (drawn offscreen). Their pixels aren't on the surface a snapshot would read.
  • Teardown. clear_blur_cache() is public so a render context can drop the images before it is torn down.

2. Clip damage to clipping ancestors (eff46de)

A node's damage is intersected with every clip_children ancestor's bounds before it is recorded. A scrolled band is content much taller than the clip it moves inside. Without this it reported its full height as damage on every scroll step, and that damage then reached blur shapes elsewhere on screen.

3. Opaque region (4f9731e)

New Layer::set_opaque_region(Vec<Rect>), in layer-local coordinates. The backdrop of a BackgroundBlur layer is clipped out (ClipOp::Difference) where the layer's own content is opaque, because that area gets painted over straight away. A window whose opaque listing covers most of it then seeds, blurs and replays only its translucent edges. The region is part of the blur cache key.

It's a promise about the pixels: a rect that isn't really opaque shows whatever was on the surface before.

Tests

New integration tests, each with its own scene:

  • tests/blur_damage.rs: the blur shape is repainted whole for damage beneath it, and when the frosted layer moves (old and new shape). It is not repainted for its own content, content above it, or damage out of reach.
  • tests/blur_cache_pixels.rs: a partial repaint (own content changed, or content above) draws the kept blur and matches a full render pixel for pixel. A changed backdrop blurs again.
  • tests/frosted_band.rs: a band moving inside a frosted window damages only its clip, both in the scene damage and in subtree_damage.
  • tests/clip_damage.rs: moving content inside a clip damages only what the clip shows. Without a clip the whole move is damaged, and a move entirely outside the clip damages nothing.
  • tests/opaque_region.rs: leaving the region out changes no pixel, a partial repaint with a region matches a full render, and changing the region does not replay a stale blur.

Test plan

  • cargo test for the five new test files (16 tests)
  • Used by Otto's scroll pane work (companion PR in nongio/otto): frosted Files, Settings, launcher and emoji windows scrolling on a live session, with no blur seams or stale frost

A BackgroundBlur shape was repainted whole for any damage in the scene,
and every repaint blurred again. Expand damage to a blur shape only for
damage painted beneath it (earlier in paint order) within reach, and keep
the pixels the blur produced so a repaint caused by the layer's own
content or by content above it draws them instead.

Claude-Session: https://claude.ai/code/session_01NQwfuVAHbrQx7UUyEUgd54
@nongio
nongio merged commit f3306ae into main Sep 15, 2026
5 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