Summary
Window transparency does not work on Linux/Wayland with the CEF host. The goal is semi-transparent windows (controlled by window:opacity setting, default 0.85) so the desktop is visible through the app. Electron achieves this on the same system; CEF does not.
Root Cause
The transparency pipeline has four layers. Three are solved; one remains:
1. Widget opacity — SOLVED (binary patch)
CEF hardcodes Widget::InitParams::opacity = kOpaque in libcef.so. Three binary patches fix this:
- NOP at file offset
0x2ec460a: prevents CefNativeWidgetDelegate::Init from setting kOpaque before Widget::Init
- Byte at
0x9d92a8d and 0x9d92b65: Widget::Init fallback paths changed from kOpaque(1) → kTranslucent(2)
2. Wayland surface opaque region — SOLVED (LD_PRELOAD shim)
scripts/wayland-alpha-shim.c intercepts wl_surface_set_opaque_region() and passes NULL, telling the compositor the surface has per-pixel alpha.
3. Wayland buffer format — ALREADY CORRECT
WaylandShmBuffer::Initialize passes with_alpha=true to CreateShmBuffer. The SHM buffer format is WL_SHM_FORMAT_ARGB8888. No fix needed.
4. Renderer compositor has_transparent_background — THE BLOCKER
The cc compositor's LayerTreeHost defaults to background_color = SK_PMColor4fWHITE. This makes has_transparent_background = false in every CompositorFrame. The viz Display::DrawAndSwap reads this flag and clamps all pixel alpha to 1.0 when false.
How Electron fixes this: Electron calls RenderWidgetHostViewBase::SetBackgroundColor(SK_ColorTRANSPARENT), which triggers:
RenderViewHostImpl::SetBackgroundOpaque(false)
- Mojo IPC → renderer process
WebFrameWidgetImpl::SetBackgroundOpaque(false) → SetBaseBackgroundColorOverrideTransparent(true)
has_transparent_background = true on LayerTreeHost
- All subsequent CompositorFrames preserve per-pixel alpha
Why CEF doesn't do this: CEF's Chrome-style browser calls ContentsWebView::SetBackgroundVisible(false) during creation. ContentsWebView::UpdateBackgroundColor should call RWHV::SetBackgroundColor(0) when RenderViewReady fires, but this either never fires in CEF's path, fires too late, or the call is a no-op because the default color is already 0.
Evidence
- CDP
Page.captureScreenshot with Emulation.setDefaultBackgroundColorOverride({color:{r:0,g:0,b:0,a:0}}) produces pixels with correct alpha: srgba(34,34,34,0.70). This proves the renderer CAN produce transparent pixels when told to.
- Electron 41 on the same system (GNOME/Wayland/Mutter) has fully working transparency.
gnome-screenshot always shows rgb(100,100,100) in the CEF window area — exactly rgba(34,34,34,0.7) composited over white (34*0.7 + 255*0.3 ≈ 100).
- The
Emulation.setDefaultBackgroundColorOverride CDP method only affects DevTools-captured screenshots, NOT the actual rendered output to the Wayland surface.
What was tried (binary patches)
| Patch |
Effect |
| Widget opacity (3 sites) |
kOpaque → kTranslucent — working |
CefContext::GetBackgroundColor alpha removal |
Removed or $0xff000000 — stops alpha clamping to 0xFF |
WebViewImpl::BaseBackgroundColor → kTransparent |
Changed Blink fallback — no visible effect |
BaseBackgroundColor always return kTransparent |
Forced unconditional jmp — no visible effect |
| RGBA buffer format (Skia + Presenter) |
Forced kPremul and needs_alpha=true — no visible effect |
| DRM format XRGB→ARGB |
Changed fourcc — inert (SHM path used, not DRM) |
Display::DrawAndSwap force has_transparent_background |
Forced r14b=1 in browser process — no effect (renderer-side flag is what matters) |
Trampoline SetPageBaseBackgroundColor(0) |
Code cave injection after tab creation — no visible effect |
CommitState background → transparent |
xorps zero — broke rendering (all white, NOTREACHED errors) |
LayerTreeHost constructor alpha NOP |
Crashed with bad_array_new_length |
SetBackgroundOpaque always transparent |
Only triggers when called — never called |
DirectRenderer::DrawFrame jne NOP |
Crashed GPU process |
Rust CDP setDefaultBackgroundColorOverride in on_load_end |
Only affects DevTools screenshots |
Proposed fix
CEF PR #4086 (unmerged) fixes this by setting params.opacity = kTranslucent when CefSettings.background_color has alpha=0, AND properly propagating transparency through SetBackgroundOpaque(false). This is the correct upstream fix.
Options:
- Build CEF from source with PR #4086 applied (~4-6 hours build, ~100GB disk)
- Upstream the fix — contribute to getting PR #4086 merged
- Find the binary patch that forces
RWHV::SetBackgroundColor(0) to be called early enough — difficult because the default color is already 0, causing the early-return check to skip the SetBackgroundOpaque call
Environment
- Ubuntu 24.04, GNOME on Wayland, Mutter compositor
- CEF 146 (Chromium 146),
cef = "146.4.1+146.0.9"
- CEF Views framework, Ozone/Wayland backend
- GPU process crashes (VA-API init failure) → software rendering via
WaylandCanvasSurface + WaylandShmBuffer
References
Summary
Window transparency does not work on Linux/Wayland with the CEF host. The goal is semi-transparent windows (controlled by
window:opacitysetting, default 0.85) so the desktop is visible through the app. Electron achieves this on the same system; CEF does not.Root Cause
The transparency pipeline has four layers. Three are solved; one remains:
1. Widget opacity — SOLVED (binary patch)
CEF hardcodes
Widget::InitParams::opacity = kOpaqueinlibcef.so. Three binary patches fix this:0x2ec460a: preventsCefNativeWidgetDelegate::Initfrom setting kOpaque beforeWidget::Init0x9d92a8dand0x9d92b65: Widget::Init fallback paths changed from kOpaque(1) → kTranslucent(2)2. Wayland surface opaque region — SOLVED (LD_PRELOAD shim)
scripts/wayland-alpha-shim.cinterceptswl_surface_set_opaque_region()and passes NULL, telling the compositor the surface has per-pixel alpha.3. Wayland buffer format — ALREADY CORRECT
WaylandShmBuffer::Initializepasseswith_alpha=truetoCreateShmBuffer. The SHM buffer format isWL_SHM_FORMAT_ARGB8888. No fix needed.4. Renderer compositor
has_transparent_background— THE BLOCKERThe cc compositor's
LayerTreeHostdefaults tobackground_color = SK_PMColor4fWHITE. This makeshas_transparent_background = falsein everyCompositorFrame. The vizDisplay::DrawAndSwapreads this flag and clamps all pixel alpha to 1.0 when false.How Electron fixes this: Electron calls
RenderWidgetHostViewBase::SetBackgroundColor(SK_ColorTRANSPARENT), which triggers:RenderViewHostImpl::SetBackgroundOpaque(false)WebFrameWidgetImpl::SetBackgroundOpaque(false)→SetBaseBackgroundColorOverrideTransparent(true)has_transparent_background = trueon LayerTreeHostWhy CEF doesn't do this: CEF's Chrome-style browser calls
ContentsWebView::SetBackgroundVisible(false)during creation.ContentsWebView::UpdateBackgroundColorshould callRWHV::SetBackgroundColor(0)whenRenderViewReadyfires, but this either never fires in CEF's path, fires too late, or the call is a no-op because the default color is already 0.Evidence
Page.captureScreenshotwithEmulation.setDefaultBackgroundColorOverride({color:{r:0,g:0,b:0,a:0}})produces pixels with correct alpha:srgba(34,34,34,0.70). This proves the renderer CAN produce transparent pixels when told to.gnome-screenshotalways showsrgb(100,100,100)in the CEF window area — exactlyrgba(34,34,34,0.7)composited over white (34*0.7 + 255*0.3 ≈ 100).Emulation.setDefaultBackgroundColorOverrideCDP method only affects DevTools-captured screenshots, NOT the actual rendered output to the Wayland surface.What was tried (binary patches)
CefContext::GetBackgroundColoralpha removalor $0xff000000— stops alpha clamping to 0xFFWebViewImpl::BaseBackgroundColor→ kTransparentBaseBackgroundColoralways return kTransparentDisplay::DrawAndSwapforcehas_transparent_backgroundSetPageBaseBackgroundColor(0)CommitStatebackground → transparentLayerTreeHostconstructor alpha NOPbad_array_new_lengthSetBackgroundOpaquealways transparentDirectRenderer::DrawFramejne NOPsetDefaultBackgroundColorOverrideinon_load_endProposed fix
CEF PR #4086 (unmerged) fixes this by setting
params.opacity = kTranslucentwhenCefSettings.background_colorhas alpha=0, AND properly propagating transparency throughSetBackgroundOpaque(false). This is the correct upstream fix.Options:
RWHV::SetBackgroundColor(0)to be called early enough — difficult because the default color is already 0, causing the early-return check to skip theSetBackgroundOpaquecallEnvironment
cef = "146.4.1+146.0.9"WaylandCanvasSurface+WaylandShmBufferReferences
docs/HANDOFF-CEF-TRANSPARENCY.mdscripts/wayland-alpha-shim.c