Skip to content

Background blur for layer surfaces and translucent windows - #3

Open
wayne-tbl wants to merge 7 commits into
FuriLabs:forkyfrom
wayne-tbl:glass-theme
Open

Background blur for layer surfaces and translucent windows#3
wayne-tbl wants to merge 7 commits into
FuriLabs:forkyfrom
wayne-tbl:glass-theme

Conversation

@wayne-tbl

@wayne-tbl wayne-tbl commented Aug 16, 2026

Copy link
Copy Markdown

Background blur for layer surfaces that ask for it, and for translucent
application windows.

  • layer-shell-effects: add a background blur request — bumps
    zphoc_layer_shell_effects_v1 to version 4 and adds
    get_blur_layer_surface. The radius is double buffered and takes effect on
    wl_surface.commit, following the alpha surface it is modelled on.
  • layer-surface: track a background blur radius
  • layer-shell-effects: implement the blur layer surface — mirrors
    PhocAlphaLayerSurface: same lifecycle, same listeners.
  • output: report the blur radius of mapped layer surfaces — the maximum over
    mapped TOP and OVERLAY. While anything asks for blur the whole output is
    damaged, since the filter spreads pixels sideways and a partial redraw leaves
    blurred content disagreeing with what is around it.
  • renderer: blur the backdrop of translucent surfaces
  • tests: exercise the blur layer surface

The renderer commit is the large one and does not split further — the filter,
the capture and the two call sites are one mechanism. Its shape is forced by the
hardware rather than chosen: on the android renderer a wlr_allocator buffer
cannot back a render target (no dmabuf import, no native window), and an earlier
revision that rendered into one produced incomplete framebuffers and aborted the
compositor at startup. phoc says as much itself, in the "Do not use
wlr_allocator on android"
path of phoc_renderer_render_view_to_buffer(). So
the dual-Kawase pyramid runs on plain GL textures and FBOs, with the source
copied out of the output's own framebuffer with glCopyTexSubImage2D from
inside the render pass. The pyramid halves resolution per level rather than
widening the tap offset: past roughly a texel of separation the samples stop
overlapping and read as discrete copies — a visible star pattern on diagonal
taps.

Application windows are blurred without asking, decided from their opaque
region, since a toplevel has no way to request it over the layer shell effects
protocol.

Nothing is blurred unless a surface asks for it, so that request is the opt-in.
On top of that the blur gsetting switches the whole effect off, for a device
whose GL path misbehaves, without downgrading the compositor. It defaults to
on, is read at startup and followed while phoc runs -- the same shape as the
touch-points key in the screen-capture PR, damaging the outputs on change
because the backdrop is captured from the render pass. An earlier revision of
this branch used a PHOC_BLUR environment variable for that; it has been
replaced by the setting at review request, and is gone.

On testing: the GL path only runs on a device. The suite forces
WLR_RENDERER=pixman and no gles2 renderer can be created under Xvfb, so what
the new test covers is the protocol plumbing and the fallback when blur is
unavailable. The renderer itself has been exercised on an FLX1 (Halium/hybris,
Mali-G68) across many boots, including rotation.

How it reaches the GPU

Nothing is read back to the CPU anywhere in this path -- there is no
glReadPixels and no buffer mapping.

phoc_renderer_capture_blur() runs inside the output's existing
wlr_render_pass, called from render_layer() just before a surface that
asked for blur is drawn. It does not begin a render pass of its own, and that
is deliberate: the android renderer has no dmabuf import and no native window
for an offscreen wlr_allocator buffer, so a second render target is not
available here -- phoc_renderer_render_view_to_buffer() already says as much.

  • Capture -- glCopyTexSubImage2D copies the output framebuffer, as it
    stands at that moment, into a GL texture. Driver-side blit.
  • Blur -- a Kawase pyramid over plain GL textures and FBOs, down-sample and
    up-sample shaders drawing a 4-vertex triangle strip. The pyramid starts at
    quarter resolution, cutting the fill rate of every iteration by 16.
    Depth follows the radius: levels = round(log2(max(radius, 8) / 8)), clamped
    to 0..4, so radius 8 is one level and each doubling buys one more.
  • Composite -- render_blur_backdrop() draws the result as a raw GL quad
    under the surface.
  • State -- one PhocBlurState per output (capture texture, five pyramid
    textures and their FBOs) in a hashtable on PhocRenderer, dropped by
    phoc_renderer_forget_output(). Allocated once per output, not per frame.
    The FBO binding and surrounding GL state are saved and restored, since this
    borrows wlroots' render pass rather than owning it.

Cost when idle is nil, because phoc_output_draw() renders no frame at all
when nothing is damaged. The cost that is real: when blur is active and the
damage ring is non-empty the whole output is damaged, since a blurred pixel
depends on a hundred pixels around it and partial redraw artifacts. So anything
that damages a small region every frame now repaints the whole output. A CPU
comparison against stock phoc is still outstanding; gsettings set sm.puri.phoc blur false gives a live A/B on the same device.

Pairs with FuriLabs/phosh#4, which requests the blur. phosh binds this
protocol only if the compositor offers it and is deliberately outside its
"lacks needed globals" fatal check, so it still starts against a stock phoc and
simply gets no blur. The two can land in either order.

7 commits, off forky.

The blur request needs somewhere to live. Adding it to
zphoc_layer_shell_effects_v1 would mean editing a protocol phoc owns:
our version 4 and any version 4 phoc later defines would be two
different things under one name, and a client could not tell them
apart.

So define zphoc_furios_layer_shell_effects_v1 instead, in a file of our
own, with get_blur_layer_surface and the zphoc_furios_blur_layer_surface_v1
it hands back. phoc's protocol is left exactly as it is.

Signed-off-by: wayne <wayne@furilabs.com>
Signed-off-by: wayne <wayne@furilabs.com>
Mirrors PhocAlphaLayerSurface: same lifecycle, same listeners, pending
radius applied on commit. Changing the radius damages the whole output,
since blurred pixels do not follow the surface's own damage.

The request arrives on our own global rather than phoc's, so this binds
zphoc_furios_layer_shell_effects_v1 alongside the existing one and leaves
zphoc_layer_shell_effects_v1 at version 3, exactly as phoc defines it.

Signed-off-by: wayne <wayne@furilabs.com>
Take the maximum over the mapped TOP and OVERLAY surfaces. While anything
asks for blur the whole output is damaged: the filter spreads pixels
sideways, so a partial redraw leaves the blurred content disagreeing with
what is around it.

Signed-off-by: wayne <wayne@furilabs.com>
A dual-Kawase pyramid over plain GL textures and framebuffers. The source
is the output's own framebuffer, copied with glCopyTexSubImage2D from
inside the render pass.

That shape is forced by the hardware rather than chosen: on the android
renderer a wlr_allocator buffer cannot back a render target -- no dmabuf
import, no native window -- and an earlier revision that rendered into
one produced incomplete framebuffers and aborted the compositor at
startup. phoc says as much itself, in the "Do not use wlr_allocator on
android" path of phoc_renderer_render_view_to_buffer().

The pyramid halves resolution per level rather than widening the tap
offset, because past roughly a texel of separation the samples stop
overlapping and read as discrete copies -- on diagonal taps, a visible
star pattern.

Application windows are blurred without asking, decided from their opaque
region, since a toplevel has no way to request it over the layer shell
effects protocol.

Nothing is blurred unless a surface asks for it, so that request is the
opt in; PHOC_BLUR=0 switches the whole thing off for a session, for a
device whose GL path misbehaves.

Signed-off-by: wayne <wayne@furilabs.com>
Blur is on by default and a surface only gets it by asking over the layer
shell effects protocol, but a device whose GL path misbehaves needs a way
to switch the effect off without downgrading the compositor. Read at
startup and followed while phoc runs; the backdrop is captured from the
render pass, so nothing frosts or clears until a frame is asked for.

Signed-off-by: wayne <wayne@furilabs.com>
Covers the request and that a blurred surface still renders correctly.
The GL path itself only runs on a device: the suite forces
WLR_RENDERER=pixman and no gles2 renderer can be created under Xvfb, so
what runs here is the protocol plumbing and the fallback when blur is
unavailable.

Signed-off-by: wayne <wayne@furilabs.com>
@wayne-tbl

Copy link
Copy Markdown
Author

Blur cost, measured. FLX1s (720x1600), six runs, two per configuration.

phoc, % of one core:

phase glass off glass on
idle 0.53 0.50
drawer 6.78 12.08
app grid 11.02 22.42
app open 13.80 27.00

Zero at idle, roughly double during animation, peak near half a core. phosh unaffected.

Repeat runs of the same configuration reproduce phoc to 0.3-6% on drawer and app grid, so the doubling sits well outside noise. Governor pinned to performance, background package activity killed, every run started from the same temperature.

Caveats: the FLX1 has 2.3x the pixels and is not yet measured, and this is CPU only. The kawase pass is GPU work and was not measured.

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