Describe the bug
In windowless/off-screen rendering (OSR), OnPaint can intermittently deliver a full-size image buffer that is visually incomplete after the page has reported that its JavaScript state is ready and the native host has invalidated the view.
The buffer dimensions and byte size are correct, and this does not appear to be a dirty-rect interpretation issue. The bad frame looks like an intermediate compositor result: the background is present, but some DOM/canvas/composited layers are missing or only partially updated, sometimes with a tiled/stair-step boundary. A later Invalidate / OnPaint for the same page state produces the complete image.
This is visible when using CEF OSR as a deterministic frame capture backend for video export, where each animation frame is driven by JavaScript and then captured from OnPaint.
To Reproduce
I have not reduced this to cefclient or cefsimple yet. The current reproduction is in a custom OSR application using CEF APIs through Rust bindings, but the flow maps directly to CEF windowless rendering APIs:
- Create a windowless browser with:
external_begin_frame_enabled = true on CefWindowInfo
external_message_pump = true
windowless_rendering_enabled = true
windowless_frame_rate = 30
shared_texture_enabled = false / regular OnPaint buffer path
- Use command-line switches including:
disable-gpu
disable-gpu-compositing
disable-dev-shm-usage
- Load an HTML page with complex animated composited content:
- gradient background
- DOM overlays
- canvas-drawn elements
- CSS clip/reveal effects
- filters and/or 3D transforms
- Expose a JS method similar to
setTime(ms) that updates the page to a deterministic animation time and reports a "frame ready" signal after the JS/React/canvas state update has completed.
- Native frame loop:
- call JS
setTime(ms)
- wait for the JS ready signal
- drain previously queued paint callbacks
- call
CefBrowserHost::Invalidate(PET_VIEW)
- pump the external message loop / external begin frames
- capture the resulting
OnPaint buffer
- Repeat this for a sequence of frames.
Intermittently, the first OnPaint after the JS ready signal and invalidation contains an incomplete visual composite. Capturing another candidate for the same JS state usually produces the correct image.
Expected behavior
After the page has reached a stable JS state and the view is invalidated, there should be a reliable way for an OSR client to know when CEF has delivered a fully composited frame for that state.
Alternatively, if the first OnPaint after invalidation is allowed to represent an intermediate compositor state, the expected synchronization mechanism for deterministic frame capture should be documented.
Actual behavior
OnPaint sometimes returns a valid full-size BGRA buffer that is not visually complete for the current page state. Some composited layers appear missing or partially updated. A subsequent invalidation/capture for the same state produces the expected complete frame.
Workarounds tried
- Waiting for JavaScript / React / canvas completion before invalidating the view.
- Waiting for
requestAnimationFrame on the page side before signaling readiness.
- Draining stale paint callbacks before each capture.
- Pumping external begin frames while waiting for
OnPaint.
The reliable workaround so far is to capture multiple OnPaint candidates for the same JS state and choose the most complete candidate. This avoids the visual corruption, but it adds extra capture cost and suggests the first paint can be an intermediate compositor result.
Versions (please complete the following information):
- OS: macOS 15.7.4 (Darwin 24.6.0, x86_64)
- CEF Version: CEF 146.0.12 via
cef-dll-sys 146.7.0+146.0.12 / Rust cef crate 146.7.0
Additional context
- The page renders correctly in an interactive browser context; the issue is specific to OSR frame capture timing.
- I have not yet reproduced this in
cefclient or cefsimple.
- The observed
OnPaint buffer is the full image buffer, not only the dirty rect data.
- The main question is whether this behavior is expected for OSR with
external_begin_frame_enabled, and what synchronization point CEF recommends for deterministic capture of a fully composited frame.
Describe the bug
In windowless/off-screen rendering (OSR),
OnPaintcan intermittently deliver a full-size image buffer that is visually incomplete after the page has reported that its JavaScript state is ready and the native host has invalidated the view.The buffer dimensions and byte size are correct, and this does not appear to be a dirty-rect interpretation issue. The bad frame looks like an intermediate compositor result: the background is present, but some DOM/canvas/composited layers are missing or only partially updated, sometimes with a tiled/stair-step boundary. A later
Invalidate/OnPaintfor the same page state produces the complete image.This is visible when using CEF OSR as a deterministic frame capture backend for video export, where each animation frame is driven by JavaScript and then captured from
OnPaint.To Reproduce
I have not reduced this to
cefclientorcefsimpleyet. The current reproduction is in a custom OSR application using CEF APIs through Rust bindings, but the flow maps directly to CEF windowless rendering APIs:external_begin_frame_enabled = trueonCefWindowInfoexternal_message_pump = truewindowless_rendering_enabled = truewindowless_frame_rate = 30shared_texture_enabled = false/ regularOnPaintbuffer pathdisable-gpudisable-gpu-compositingdisable-dev-shm-usagesetTime(ms)that updates the page to a deterministic animation time and reports a "frame ready" signal after the JS/React/canvas state update has completed.setTime(ms)CefBrowserHost::Invalidate(PET_VIEW)OnPaintbufferIntermittently, the first
OnPaintafter the JS ready signal and invalidation contains an incomplete visual composite. Capturing another candidate for the same JS state usually produces the correct image.Expected behavior
After the page has reached a stable JS state and the view is invalidated, there should be a reliable way for an OSR client to know when CEF has delivered a fully composited frame for that state.
Alternatively, if the first
OnPaintafter invalidation is allowed to represent an intermediate compositor state, the expected synchronization mechanism for deterministic frame capture should be documented.Actual behavior
OnPaintsometimes returns a valid full-size BGRA buffer that is not visually complete for the current page state. Some composited layers appear missing or partially updated. A subsequent invalidation/capture for the same state produces the expected complete frame.Workarounds tried
requestAnimationFrameon the page side before signaling readiness.OnPaint.The reliable workaround so far is to capture multiple
OnPaintcandidates for the same JS state and choose the most complete candidate. This avoids the visual corruption, but it adds extra capture cost and suggests the first paint can be an intermediate compositor result.Versions (please complete the following information):
cef-dll-sys 146.7.0+146.0.12/ Rustcefcrate 146.7.0Additional context
cefclientorcefsimple.OnPaintbuffer is the full image buffer, not only the dirty rect data.external_begin_frame_enabled, and what synchronization point CEF recommends for deterministic capture of a fully composited frame.