Skip to content

Linux: import dma-bufs into atlas tiles, and display-rate frame callbacks - #18

Open
alexandre-vl wants to merge 3 commits into
zeronsh:zeron/drag-needs-held-buttonfrom
alexandre-vl:zeron/linux-native-browser
Open

alexandre-vl wants to merge 3 commits into
zeronsh:zeron/drag-needs-held-buttonfrom
alexandre-vl:zeron/linux-native-browser

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Oct 25, 2026 •

Copy link
Copy Markdown

Refs zeronsh/zeron#540. Zeron's Linux browser uses this branch: zeronsh/zeron#1039 (it pins b564a34).

Zeron's Linux browser now renders pages with WPE WebKit in a helper process, into dma-bufs. To show those frames at the display's rate without CPU copies, GPUI needs three things:

  1. Update a painted image in place.
  2. Copy only the damaged regions of another process's dma-buf into it, on the GPU.
  3. Keep calling back at the display's rate while the window is inactive.

The branch is stacked on 0dc69aa (zeron/drag-needs-held-button), the rev Zeron pins today, so this diff shows only the three commits below.

Commit What
5526f55 Update atlas tiles in place, from pixels or imported dma-bufs New atlas API and the Vulkan dma-buf import (Linux, FreeBSD)
5392ee1 Keep display-rate frame callbacks in inactive windows Window::on_next_display_frame
b564a34 Wayland: keep frames coming after a present outside the frame loop Bug fix: a forced draw could stop a window's frames for good

API

API Where Does
RenderImage::blank(size) all An image whose pixels only arrive through update_image
Window::insert_blank_image(image, frame) all Allocates its tile without uploading anything; returns whether the tile is new, its content undefined until filled
Window::update_image(image, frame, region, update, on_complete) all; only gpui_wgpu updates in place, the others return Ok(false) Overwrites region from CPU pixels (AtlasTileUpdate::Pixels) or from an imported dma-buf (AtlasTileUpdate::Dmabuf)
Window::dmabuf_target() Linux, FreeBSD The render node GPUI draws with, and the DRM modifiers it imports
Window::import_dmabuf(&Dmabuf) → DmabufImport Linux, FreeBSD Imports once; dropping it frees the image once no queued copy reads it
Window::on_next_display_frame(cb) all on_next_frame for content that follows the display: an inactive window, otherwise capped at ~30 fps, keeps the display's rate
PlatformAtlas::{insert_blank, update_tile, dmabuf_target, import_dmabuf} trait, default impls Metal and DirectX atlases compile unchanged

The update_tile contract

  • on_complete runs exactly once, errors included, never with the atlas locked, and only after the GPU has finished all work queued before it. A producer can recycle a buffer from that callback.
  • Updates land in the order they were queued, with one exception: pixels land before the frame's dma-buf copies and drop the copies they fully cover. A producer that switches from copies to pixels updates the whole tile.
  • If the device is lost, the pending completions run and the imports are dropped; producers import again.

What is imported

  • Format: AR24 (B8G8R8A8_UNORM), single plane.

  • The device opens with VK_EXT_image_drm_format_modifier when the GPU has it, so tiled layouts import too. NVIDIA's EGL renders only to its tiled layouts.

  • Only uncompressed modifiers are imported:

    Vendor Accepted
    Intel X, Y, Yf, 4 tiling
    AMD no DCC (bit 13 clear)
    NVIDIA compression bits 23–25 clear
    Vendor 0 LINEAR only
    Others none

    wgpu moves an imported image out of UNDEFINED without acquiring it from the producer's queue, which lets a driver reinitialize compression metadata and lose the pixels.

  • Before creating the image, the import checks each layout's importability and maximum extent (vkGetPhysicalDeviceImageFormatProperties2 with the modifier). It also checks the dma-buf is at least as large as the image's memory requirement, because an unsupported layout or size is undefined behavior, not an error.

  • The image binds the buffer with an explicit plane layout (offset, stride) in dedicated memory. The duplicated fd is closed if allocation fails; on success, Vulkan owns it.

Measured in Zeron

zeronsh/zeron#1039 measured Zeron's browser with this branch: release builds, a 240 Hz display, a 2849×1956 px viewport, 3 runs per cell.

Zeron main (WebKitGTK, CPU frames) With this branch (WPE WebKit, dma-buf imports)
2D canvas / WebGL 2 / scrolling / CSS, frames shown per second 7 / 5 / 21 / 22 240 / 240 / 240 / 240
Display refreshes without a new frame 94% 0 of 23,029
CPU per frame shown, 2D canvas 191.9 ms 2.4 ms
Bytes uploaded CPU→GPU per frame 22.29 MB 0 (22.29 MB copied on the GPU)
Press → frame painted, p50 36.8 ms 7.6 ms

The same path runs on the Radeon 610M iGPU (RADV, modifier 0x0200000000401b03) and through Xwayland. The PR has the full tables.

Tests

New: update_tile_overwrites_only_the_region, update_tile_needs_a_cached_tile_inside_its_bounds, dmabuf_regions_copy_into_the_tile, dmabuf_imports_refuse_unsupported_layouts_and_sizes, only_uncompressed_layouts_are_imported, inactive_windows_slow_frame_callbacks_but_not_display_frames.

Suite (Linux 7.2.8, RTX 3080 driver 615.78.08 open, Mesa 26.2.5) Result
cargo test --locked -p gpui --lib 229 passed
cargo test --locked -p gpui_wgpu --lib -- --include-ignored 35 passed (2 need a Vulkan adapter)
cargo test --locked -p gpui_linux --lib 24 passed
cargo check --locked -p gpui -p gpui_wgpu -p gpui_linux --tests on 5526f55 alone ok

Not covered by tests:

  • A pixel update that only partly covers a queued copy.
  • clear() with copies pending.
  • Completions after a real device loss.
  • Windows and macOS: compile-only through the default trait impls (this repo's Windows CI job).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Alexandre Vargas Lopez and others added 3 commits October 25, 2026 12:53
A producer that tracks damage, like an embedded browser, uploads only what
changed into an image it already painted instead of a whole new image per
frame: `Window::update_image` overwrites a region of the image's tile, and
`RenderImage::blank` with `Window::insert_blank_image` gives a frame-sized
tile without uploading blank pixels.

On Linux, a dma-buf another process renders into is imported into the
Vulkan device once (`Window::import_dmabuf`) and its damaged regions are
copied on the GPU, so frames never touch the CPU. The device opens with
VK_EXT_image_drm_format_modifier when the GPU has it, so tiled layouts
import too: GPUs whose EGL renders only to tiled layouts (NVIDIA) share
frames as well. `Window::dmabuf_target` names the render node and the
layouts the atlas imports: uncompressed ones only, since wgpu moves an
imported image out of an undefined layout without acquiring it from the
producer's queue, which may reinitialize compression metadata.

`on_complete` runs exactly once, outside the atlas lock, after the GPU has
finished all work queued before it, errors included, so a producer can
recycle a buffer as soon as the GPU stops reading it. Pixel uploads land
before the frame's copies and drop the copies they cover. A lost device
runs the pending completions and drops the imports; producers import
again.
An inactive window slows its frame callbacks to ~30 fps to save energy.
Content that follows the display, such as video or a web page, asks for
the next frame with `Window::on_next_display_frame` instead: the window
keeps the display's rate for that callback, and slows down again once
such requests stop.
A frame callback requests the next one, which only a commit sends. A draw
forced outside the frame loop left `renderer_presented` set, so the next
frame that drew nothing skipped that commit: the compositor never
answered and frames stopped. Reset it with each frame callback.
zeronsh Bot pushed a commit to alexandre-vl/zeron that referenced this pull request Oct 26, 2026
…PUI as dma-bufs

The sidebar browser on Linux ran WebKitGTK in an offscreen window: every
frame went through the CPU, over a pipe, into a new GPUI image, so pages
topped out near 60 fps whatever the display, and large ones fell well below.

Zeron now carries its own WPE WebKit platform (browser/linux/wpe.c) and runs
it in the helper when the build and the system have WPE WebKit 2.50+:

- WebKit renders on the GPU GPUI draws with, into dma-bufs in a DRM format
  modifier GPUI's Vulkan device imports. GPUI copies only each frame's
  damaged regions into the page's atlas tile, on the GPU (zeronsh/zui#18).
- Where the driver renders no layout Vulkan imports, the helper reads frames
  back with OpenGL ES into a sealed memfd. Without a GPU, WebKit renders on
  the CPU into shared memory. Both update the tile in place.
- GPUI's frame callbacks are the pages' vblank: WebKit hears a frame is on
  screen at most once per display refresh, so pages run at 60, 144 or 240 Hz
  and never render frames the display won't show. Hidden pages, and pages
  replaced by their error, get none.
- WebKit runs copy, cut and paste itself, sets the pointer's cursor, and sees
  click counts and the pointer leaving the page.

If the WPE helper can't start, the session uses WebKitGTK. Its helper now
renders at the output's scale (it rendered 4x the pixels at 1.5x on Wayland
and downscaled them) and sends Cairo's surface as-is instead of converting it
through GdkPixbuf, which took most of its CPU.

A helper that crashes, sends a message Zeron can't read or is still being
written by a forked child (ETXTBSY) is stopped, replaced or retried; the
tests run the real helper on every render node, in every layout its driver
renders, read back and in shared memory, at 60, 144 and 240 Hz.
zeronsh Bot pushed a commit to alexandre-vl/zeron that referenced this pull request Oct 27, 2026
…PUI as dma-bufs

The sidebar browser on Linux ran WebKitGTK in an offscreen window: every
frame went through the CPU, over a pipe, into a new GPUI image, so pages
topped out near 60 fps whatever the display, and large ones fell well below.

Zeron now carries its own WPE WebKit platform (browser/linux/wpe.c) and runs
it in the helper when the build and the system have WPE WebKit 2.50+:

- WebKit renders on the GPU GPUI draws with, into dma-bufs in a DRM format
  modifier GPUI's Vulkan device imports. GPUI copies only each frame's
  damaged regions into the page's atlas tile, on the GPU (zeronsh/zui#18).
- Where the driver renders no layout Vulkan imports, the helper reads frames
  back with OpenGL ES into a sealed memfd. Without a GPU, WebKit renders on
  the CPU into shared memory. Both update the tile in place.
- GPUI's frame callbacks are the pages' vblank: WebKit hears a frame is on
  screen at most once per display refresh, so pages run at 60, 144 or 240 Hz
  and never render frames the display won't show. Hidden pages, and pages
  replaced by their error, get none.
- WebKit runs copy, cut and paste itself, sets the pointer's cursor, and sees
  click counts and the pointer leaving the page.

If the WPE helper can't start, the session uses WebKitGTK. Its helper now
renders at the output's scale (it rendered 4x the pixels at 1.5x on Wayland
and downscaled them) and sends Cairo's surface as-is instead of converting it
through GdkPixbuf, which took most of its CPU.

A helper that crashes, sends a message Zeron can't read or is still being
written by a forked child (ETXTBSY) is stopped, replaced or retried; the
tests run the real helper on every render node, in every layout its driver
renders, read back and in shared memory, at 60, 144 and 240 Hz.
zeronsh Bot pushed a commit to alexandre-vl/zeron that referenced this pull request Oct 27, 2026
…PUI as dma-bufs

The sidebar browser on Linux ran WebKitGTK in an offscreen window: every
frame went through the CPU, over a pipe, into a new GPUI image, so pages
topped out near 60 fps whatever the display, and large ones fell well below.

Zeron now carries its own WPE WebKit platform (browser/linux/wpe.c) and runs
it in the helper when the build and the system have WPE WebKit 2.50+:

- WebKit renders on the GPU GPUI draws with, into dma-bufs in a DRM format
  modifier GPUI's Vulkan device imports. GPUI copies only each frame's
  damaged regions into the page's atlas tile, on the GPU (zeronsh/zui#18).
- Where the driver renders no layout Vulkan imports, the helper reads frames
  back with OpenGL ES into a sealed memfd. Without a GPU, WebKit renders on
  the CPU into shared memory. Both update the tile in place.
- GPUI's frame callbacks are the pages' vblank: WebKit hears a frame is on
  screen at most once per display refresh, so pages run at 60, 144 or 240 Hz
  and never render frames the display won't show. Hidden pages, and pages
  replaced by their error, get none.
- WebKit runs copy, cut and paste itself, sets the pointer's cursor, and sees
  click counts and the pointer leaving the page.

If the WPE helper can't start, the session uses WebKitGTK. Its helper now
renders at the output's scale (it rendered 4x the pixels at 1.5x on Wayland
and downscaled them) and sends Cairo's surface as-is instead of converting it
through GdkPixbuf, which took most of its CPU.

A helper that crashes, sends a message Zeron can't read or is still being
written by a forked child (ETXTBSY) is stopped, replaced or retried; the
tests run the real helper on every render node, in every layout its driver
renders, read back and in shared memory, at 60, 144 and 240 Hz.
zeronsh Bot pushed a commit to alexandre-vl/zeron that referenced this pull request Oct 27, 2026
…PUI as dma-bufs

The sidebar browser on Linux ran WebKitGTK in an offscreen window: every
frame went through the CPU, over a pipe, into a new GPUI image, so pages
topped out near 60 fps whatever the display, and large ones fell well below.

Zeron now carries its own WPE WebKit platform (browser/linux/wpe.c) and runs
it in the helper when the build and the system have WPE WebKit 2.50+:

- WebKit renders on the GPU GPUI draws with, into dma-bufs in a DRM format
  modifier GPUI's Vulkan device imports. GPUI copies only each frame's
  damaged regions into the page's atlas tile, on the GPU (zeronsh/zui#18).
- Where the driver renders no layout Vulkan imports, the helper reads frames
  back with OpenGL ES into a sealed memfd. Without a GPU, WebKit renders on
  the CPU into shared memory. Both update the tile in place.
- GPUI's frame callbacks are the pages' vblank: WebKit hears a frame is on
  screen at most once per display refresh, so pages run at 60, 144 or 240 Hz
  and never render frames the display won't show. Hidden pages, and pages
  replaced by their error, get none.
- WebKit runs copy, cut and paste itself, sets the pointer's cursor, and sees
  click counts and the pointer leaving the page.

If the WPE helper can't start, the session uses WebKitGTK. Its helper now
renders at the output's scale (it rendered 4x the pixels at 1.5x on Wayland
and downscaled them) and sends Cairo's surface as-is instead of converting it
through GdkPixbuf, which took most of its CPU.

A helper that crashes, sends a message Zeron can't read or is still being
written by a forked child (ETXTBSY) is stopped, replaced or retried; the
tests run the real helper on every render node, in every layout its driver
renders, read back and in shared memory, at 60, 144 and 240 Hz.
zeronsh Bot added a commit to zeronsh/zeron that referenced this pull request Oct 27, 2026
…PUI as dma-bufs (#1039)

* Linux browser: WPE WebKit at the display's rate, frames shared with GPUI as dma-bufs

The sidebar browser on Linux ran WebKitGTK in an offscreen window: every
frame went through the CPU, over a pipe, into a new GPUI image, so pages
topped out near 60 fps whatever the display, and large ones fell well below.

Zeron now carries its own WPE WebKit platform (browser/linux/wpe.c) and runs
it in the helper when the build and the system have WPE WebKit 2.50+:

- WebKit renders on the GPU GPUI draws with, into dma-bufs in a DRM format
  modifier GPUI's Vulkan device imports. GPUI copies only each frame's
  damaged regions into the page's atlas tile, on the GPU (zeronsh/zui#18).
- Where the driver renders no layout Vulkan imports, the helper reads frames
  back with OpenGL ES into a sealed memfd. Without a GPU, WebKit renders on
  the CPU into shared memory. Both update the tile in place.
- GPUI's frame callbacks are the pages' vblank: WebKit hears a frame is on
  screen at most once per display refresh, so pages run at 60, 144 or 240 Hz
  and never render frames the display won't show. Hidden pages, and pages
  replaced by their error, get none.
- WebKit runs copy, cut and paste itself, sets the pointer's cursor, and sees
  click counts and the pointer leaving the page.

If the WPE helper can't start, the session uses WebKitGTK. Its helper now
renders at the output's scale (it rendered 4x the pixels at 1.5x on Wayland
and downscaled them) and sends Cairo's surface as-is instead of converting it
through GdkPixbuf, which took most of its CPU.

A helper that crashes, sends a message Zeron can't read or is still being
written by a forked child (ETXTBSY) is stopped, replaced or retried; the
tests run the real helper on every render node, in every layout its driver
renders, read back and in shared memory, at 60, 144 and 240 Hz.

* Linux browser bench: shown frames, pacing, copies, press latency, memory

`ZERON_BROWSER_BENCH` measured one page's frame rate and CPU. It now loads
a fresh document for each of a 2D canvas, WebGL 2, scrolling, a CSS spinner
and, with `ZERON_BROWSER_BENCH_VIDEO`, a 60 fps video, and records for each:
the page's frame rate, when GPUI painted every new frame, the bytes copied
into the page's tile from CPU memory and between GPU buffers, CPU, PSS and
NVIDIA GPU memory for Zeron and for WebKit's processes, and a capture. It
then times 40 presses from GPUI's input to the frame showing them, and
measures an idle page. `ZERON_BROWSER_BENCH_WIDTH` sizes the browser.

The counts are kept only in fixture builds (`browser-fixture`), for both
engines, so WebKitGTK and every WPE path are measured the same way.

`ZERON_BROWSER_DMABUF=readback` keeps WebKit on the GPU but reads its frames
back, the path GPUs that render no importable layout take.

* Linux browser bench: WebGL page checks a WebGL 1 context too

The WebGL page's benchInfo() clears a WebGL 1 context and reads a pixel back, so a bench run records that both WebGL versions render.

* Linux browser: rustfmt the WPE engine and its tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>

* Linux browser: a paste without text clears WPE's clipboard copy

The WPE helper kept the last clipboard text Zeron shared, and Zeron shared
only text. Pasting with an image or nothing on the system clipboard then
inserted that earlier text, possibly into another page. Zeron now shares
the clipboard on every paste, and empty text clears the helper's copy.

build.rs embeds an empty WPE helper, as when WPE is missing, if wpe.c does
not compile against the installed development files (an older GBM), and
detects them again when PKG_CONFIG_PATH changes. probe_gpu releases the GBM
device and render node it opened when the GPU is unusable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>

* Linux browser fixture: rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>

* Linux browser docs: the WPE vblank check needs a compositor that answers idle frame callbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>

---------

Co-authored-by: Wing <44917547+wingleeio@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
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