UI stack — chrome rendering decision
This ADR records the rendering stack chosen for buffr's native chrome — statusline, tab strip, command line, hint overlay.
Options
- A —
softbufferstrip in the samewinitwindow. Chrome lives in a CPU-blitted strip docked to the bottom (or top) of the buffr window. CEF's child window is sized to the remaining rectangle and reparented throughWindowInfo::parent_window. One window, no compositor placement, no GPU dependency. - B — separate top-level
winitwindows for chrome. Each chrome panel is its own OS window positioned over the CEF window. Avoids resizing CEF, but Linux compositors (especially Wayland) routinely refuse client-requested positioning and z-ordering. Fragile. - C — OSR +
wgpucompositor. CEF paints into a buffer viaCefRenderHandler::OnPaint; chrome is drawn aswgpuquads on top. Required for hint mode (per-pixel composition over the live page) and native Wayland. Pulls inwgpu,naga, shaders, plus the OSR plumbing theosrfeature already scaffolds.
Superseded: options A and B are historical alternatives — the decision below (option C, wgpu OSR) is the only path, and no windowed or softbuffer chrome mode remains in the tree.
Decision — Option C, wgpu OSR on every platform
CEF paints the page into an off-screen buffer, then the app composites that
buffer plus the tab strip, overlays, and statusline into the same winit window
with wgpu. This is now the path on all platforms — windowless_rendering
is enabled unconditionally and there is no native child-window mode left in the
tree.
Linux needs OSR because X11/XWayland child-window embedding is not supported. macOS also uses OSR because AppKit child views do not layer predictably with buffr's custom chrome: the native CEF child can cover the tabbar/statusline or land at a different origin than the chrome compositor.
Why OSR wins now
- One
winitwindow — no inter-window placement bugs. - Page and chrome share one coordinate system.
- Hints, command overlays, tabbar, statusline, and page content can be composed in z-order by the renderer.
- CEF child-view geometry does not need platform-specific AppKit/X11 resizing.
Historical: the windowed exception
Earlier revisions mapped RawWindowHandle::Win32(_) onto a HostMode::Windowed
path that parented CEF as a native child window. That mode has been removed —
HostMode no longer has a windowed variant and Windows composites through OSR
like everything else.
Layout
Constants below are the live values; crates/buffr-ui is the source of truth
(STATUSLINE_HEIGHT in src/lib.rs, TAB_STRIP_HEIGHT in src/tab_strip.rs,
INPUT_HEIGHT / SUGGESTION_ROW_HEIGHT / MAX_SUGGESTIONS in
src/input_bar.rs).
STATUSLINE_HEIGHT = 30pixels, docked to the bottom of the buffr window.TAB_STRIP_HEIGHT = 34pixels, sits above the CEF page area and below the optional input bar. Always painted (zero tabs renders an empty bar in the strip's bg colour).INPUT_HEIGHT = 28pixels, docked to the top when the command line or omnibar is open. The input strip is hidden when the overlay is closed and the page region reclaims those rows.- Suggestion dropdown: each row is
SUGGESTION_ROW_HEIGHT, which is defined asSTATUSLINE_HEIGHT(30 px);MAX_SUGGESTIONS = 8rows. Stacks below the input strip when populated; the dropdown rectangle also shrinks the CEF child rect so suggestions never overlap the page. - CEF page rect:
(0, overlay_h + TAB_STRIP_HEIGHT, w, h - overlay_h - TAB_STRIP_HEIGHT - STATUSLINE_HEIGHT), whereoverlay_hisINPUT_HEIGHT + dropdown_rows * STATUSLINE_HEIGHTwhen an overlay is open,0otherwise. In OSR mode this rect becomes the CEFview_rectand the renderer composites the painted buffer at the same position. Whenever overlays open or close, the app re-issues the resize so CEF re-flows the page area. - Renderer surface: a single
wgpusurface sized to the full window. Each frame composites the page, tab strip, statusline, overlays, hints, and popups in one pass.
Update — 2026-05-03
Dedicated wgpu-render worker thread (v0.3.0).
All wgpu mutating calls now run on a dedicated wgpu-render worker thread.
The UI thread does only:
surface.get_current_texture()— acquire the swapchain image.- Chrome paint closure (CPU-only rasterize of tab strip / statusline).
- OSR pixel
memcpyinto a staging buffer. try_send(RenderCommand)over a capacity-1 mailbox channel.
The worker thread handles queue.write_texture, queue.submit, and
surface_texture.present(). This decouples the UI event loop from Wayland
compositor backpressure: present() was previously blocking the main thread for
multiple seconds on Hyprland workspace switches, causing perceived freezes.
Additional constraints introduced alongside the worker:
frames_in_flightcounter gatesget_current_texture()— only one acquiredSurfaceTextureoutstanding at a time (desired_maximum_frame_latency = 1).Renderer::dropwrapssurfaceanddeviceinManuallyDrop; when the worker is mid-present()during shutdown, the wgpu state is leaked rather than triggering a "Surface in use" panic.resize()deferssurface.configure()into apending_resizeslot when the worker holds an outstandingSurfaceTexture; the nextframe()applies it once the worker drains.