From f7dea7bc091ca22f14286334c7cc686c405f5a4b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 5 Nov 2026 10:59:51 -0800 Subject: [PATCH 1/3] chore(quest): claim quest/m0/cef-headless.md Quest-Claim: 07ca7edd149e7371 From 2af4f61ab7ae736881faccfe3bba006a04dd8160 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 5 Nov 2026 12:10:46 -0800 Subject: [PATCH 2/3] feat(scene): browser sources paint on the GPU under Wayland Pick the display the GPU path paints through at setup: Wayland Ozone when WAYLAND_DISPLAY is set, X11 when only DISPLAY is, and the CPU path with neither. CEF's own copy reaches no desktop under Wayland Ozone, so Copy and Cut always relay through the studio. libcef.so gets libEGL on its runpath, without which its GPU process never started in the dev shell. Co-Authored-By: Claude Opus 5.5 --- README.md | 18 +++--- flake.nix | 10 ++-- quest/m0/README.md | 2 +- quest/m0/cef-headless.md | 68 +++++++++++++--------- rs/moq-scene/src/browser/host.rs | 44 +++++++------- rs/moq-scene/src/browser/mod.rs | 85 +++++++++++++++++----------- rs/moq-scene/src/browser/protocol.rs | 18 +++++- rs/moq-scene/src/browser/run.rs | 13 +---- rs/moq-studio/src/ui.rs | 6 +- 9 files changed, 151 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 7e604373..12a94ff5 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,9 @@ Either shows at once, since the browser's processes dump no core. **Reload** in Sources restarts a page, failed or not. A page's dropdown lists, such as a ``'s list, arrive the same way apart -//! from the page, and the studio draws them over it. A page's sound joins the -//! program bus at the bus's rate. +//! the studio uploads when either side cannot share textures or there is no +//! display to paint them through ([`Paint`] says which). Its popups, such as a +//! ``'s, draw over it and take clicks under **Interact**, but it opens no other windows (`window.open` and new-tab links do nothing). Copy and Cut there put the page's selected text on the clipboard, -as plain text, which the page's own copy handlers cannot change, and copy -nothing from a password field. +and nothing from a password field. Except for a page copied on the GPU through +an X display, the text goes as plain text that the page's own copy handlers +cannot change. An overlay is a title, lower third, or other graphic written as HTML and CSS, drawn by the studio itself (through [Blitz](https://github.com/DioxusLabs/blitz)) diff --git a/rs/moq-scene/src/browser/host.rs b/rs/moq-scene/src/browser/host.rs index 31654e7d..c2c1af24 100644 --- a/rs/moq-scene/src/browser/host.rs +++ b/rs/moq-scene/src/browser/host.rs @@ -52,6 +52,9 @@ struct Shared { /// never another page's. painted: Mutex>, accelerated: bool, + /// Whether CEF's own copy reaches the desktop's clipboard: only through + /// an X display. + native_copy: bool, } impl Shared { @@ -115,6 +118,13 @@ fn serve() -> Result<(), String> { writer: Mutex::new(writer), painted: Mutex::new(painted), accelerated: matches!(start.raster, Raster::Gpu { .. }), + native_copy: matches!( + start.raster, + Raster::Gpu { + display: Display::X11, + .. + } + ), }); // The studio shows why, rather than only that the helper exited. browse(&shared, start, reader, painted_tx).inspect_err(|reason| _ = shared.send(&Event::Fatal(reason.clone()), &[])) @@ -313,7 +323,7 @@ fn retime(id: Id, rate: u32) { /// Hand one input event to page `id`. Runs on the UI thread. fn deliver(id: Id, input: Input) { - let Some(browser) = PAGES.with_borrow(|pages| pages.get(&id).map(|(browser, _)| browser.clone())) else { + let Some((browser, page)) = PAGES.with_borrow(|pages| pages.get(&id).cloned()) else { return; }; let Some(host) = browser.host() else { @@ -379,22 +389,30 @@ fn deliver(id: Id, input: Input) { host.send_key_event(Some(&event)); } } - Input::Copy => copy(&browser, false), - Input::Cut => copy(&browser, true), + Input::Copy => copy(&browser, &page, false), + Input::Cut => copy(&browser, &page, true), Input::Focus(focused) => host.set_focus(focused.into()), } } /// Copy, or cut if `cut`, the selection in the page's focused frame, which /// may be an iframe, to the desktop's clipboard. -/// -/// The frame's renderer answers with its selection, and the studio puts that -/// on the desktop's clipboard. CEF's own copy reaches no desktop: headless -/// Ozone, on the CPU path, has no clipboard, and Wayland's takes a selection -/// only with the serial of an input event, which an offscreen page never -/// receives. -fn copy(browser: &Browser, cut: bool) { +fn copy(browser: &Browser, page: &Paged, cut: bool) { let Some(frame) = browser.focused_frame() else { return }; + // Through an X display, CEF's clipboard is the desktop's, and the page's + // copy and cut handlers decide what lands there. + if page.shared.native_copy { + match cut { + true => frame.cut(), + false => frame.copy(), + } + return; + } + // Elsewhere CEF's own copy reaches no desktop: headless Ozone, on the CPU + // path, has no clipboard, and Wayland's takes a selection only with the + // serial of an input event, which an offscreen page never receives. So + // the frame's renderer answers with its selection, and the studio puts + // that on the desktop's clipboard. let Some(mut message) = process_message_create(Some(©.into())) else { return; }; diff --git a/rs/moq-scene/src/browser/mod.rs b/rs/moq-scene/src/browser/mod.rs index b88e254d..c16dbcfb 100644 --- a/rs/moq-scene/src/browser/mod.rs +++ b/rs/moq-scene/src/browser/mod.rs @@ -126,7 +126,7 @@ impl Helper { } /// Take what a page last copied or cut, once, for the desktop's - /// clipboard, which CEF cannot reach itself from an offscreen page. + /// clipboard, which CEF reaches itself only through an X display. #[cfg(target_os = "linux")] pub fn copied(&self) -> Option { let running = self.running.lock().unwrap(); @@ -337,9 +337,11 @@ pub enum Input { }, /// Text typed, after the keyboard layout and any input method. Text(String), - /// Copy the page's selection, which [`Helper::copied`] then returns. + /// Copy the page's selection to the clipboard, through + /// [`Helper::copied`] unless the page paints through an X display. Copy, - /// Cut the page's selection, which [`Helper::copied`] then returns. + /// Cut the page's selection to the clipboard, through + /// [`Helper::copied`] unless the page paints through an X display. Cut, /// The page gained or lost keyboard focus. Focus(bool), diff --git a/rs/moq-studio/src/ui.rs b/rs/moq-studio/src/ui.rs index b67b631a..188e79c5 100644 --- a/rs/moq-studio/src/ui.rs +++ b/rs/moq-studio/src/ui.rs @@ -197,8 +197,9 @@ impl eframe::App for Studio { let framerate = state.compositor.canvas().framerate; ui.ctx().request_repaint_after(Duration::from_secs(1) / framerate); self.panels.follow_reloads(&state); - // A page copies through the studio's clipboard, even if the preview let - // go of the page before the copy came back. + // A page not painting through an X display copies through the + // studio's clipboard, even if the preview let go of the page before + // the copy came back. if let Some(text) = state .sources .context()