Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,9 @@ driver that imports them, and NV12 capture and VideoToolbox decode surfaces on
macOS. A browser source's page is copied on the GPU from the shared texture CEF
paints, given the same Vulkan DMA-BUF import. Every other source, every source
on Windows or on a GPU that cannot import, and every still image delivers its
frames from the CPU, which the compositor uploads.
frames from the CPU, which the compositor uploads. A browser source on that
path renders its page in software, WebGL included, so a WebGL page runs
slower there.

## Replay

Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ repository.
- [Virtual camera](/quest/m0/vcam/README.md) - a canvas's program exposed as a system camera on Linux, Windows, and macOS
- [Recover crashed recordings](/quest/m0/recording-repair.md) - a recording a crash left unfinished is repaired beside the original at launch
- [Premultiplied source sampling](/quest/m0/premultiply.md) - scaled transparent sources draw without edge fringes
- [Deterministic browser source startup](/quest/m0/cef-startup.md) - CEF on the CPU path paints its first frame without the GPU fallback hang
- [A browser renderer that dies on its first page](/quest/m0/browser-first-crash.md) - a renderer lost during the first navigation fails its source
- [Browser sources without an X display](/quest/m0/cef-headless.md) - CEF runs headless, so Wayland sessions and tests need no X server
- [Signing identities are available](/quest/m0/signing.md) - the Apple and Windows code-signing credentials packaging and the macOS camera need
Expand Down
4 changes: 0 additions & 4 deletions quest/m0/cef-headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,3 @@ Check first, and record the result in the PR:
Then drop the `xvfb-run` wrapper from the `render-sw` recipe in the
`justfile` (the nightly job inherits it through the recipe), and say in
`README.md` that browser sources need no X server.

## Required

- [Deterministic browser source startup](/quest/m0/cef-startup.md) - the `--disable-gpu` CPU path this builds on
32 changes: 0 additions & 32 deletions quest/m0/cef-startup.md

This file was deleted.

28 changes: 21 additions & 7 deletions rs/moq-scene/src/browser/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex, mpsc};
use cef::*;

use super::outbox::Commands;
use super::protocol::{Area, Channel, Command, Event, Format, Id, Layer, Open, Plane, Start, Texture};
use super::protocol::{Area, Channel, Command, Event, Format, Id, Layer, Open, Plane, Raster, Start, Texture};
use super::{Button, Input, Modifiers};

/// Run one of CEF's subprocesses (renderer, GPU, utility, zygote).
Expand Down Expand Up @@ -101,7 +101,7 @@ fn serve() -> Result<(), String> {
let shared = Arc::new(Shared {
writer: Mutex::new(writer),
painted: Mutex::new(painted),
accelerated: start.accelerated,
accelerated: matches!(start.raster, Raster::Gpu { .. }),
});
// 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()), &[]))
Expand All @@ -112,7 +112,7 @@ fn serve() -> Result<(), String> {
fn browse(shared: &Arc<Shared>, start: Start, reader: Channel, painted_tx: mpsc::Sender<()>) -> Result<(), String> {
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = args::Args::new();
let mut app = HelperApp::new(HelperProcess::new(), start.render_node);
let mut app = HelperApp::new(HelperProcess::new(), start.raster);
let profile = CefString::from(start.profile.as_str());
let settings = Settings {
windowless_rendering_enabled: 1,
Expand Down Expand Up @@ -405,7 +405,7 @@ wrap_task! {
wrap_app! {
struct HelperApp {
process: BrowserProcessHandler,
render_node: Option<String>,
raster: Raster,
}

impl App {
Expand All @@ -426,9 +426,23 @@ wrap_app! {
Some(&"autoplay-policy".into()),
Some(&"no-user-gesture-required".into()),
);
// Paint on the studio's GPU, which is the only one it imports from.
if let Some(node) = &self.render_node {
command_line.append_switch_with_value(Some(&"render-node-override".into()), Some(&node.as_str().into()));
match &self.raster {
// Paint on the studio's GPU, which is the only one it imports
// from.
Raster::Gpu { render_node: Some(node) } => {
command_line.append_switch_with_value(Some(&"render-node-override".into()), Some(&node.as_str().into()));
}
Raster::Gpu { render_node: None } => {}
// The pictures come back in memory anyway. Left to find a GPU,
// Chromium walks its fallbacks, and one can hang until its
// watchdog kills it seconds later, delaying the first paint.
// WebGL then needs SwiftShader, which Chromium runs only when
// told to, since it JITs page shaders in the GPU process. A
// browser source runs the pages its user picked, not the web.
Raster::Cpu => {
command_line.append_switch(Some(&"disable-gpu".into()));
command_line.append_switch(Some(&"enable-unsafe-swiftshader".into()));
}
}
}
}
Expand Down
11 changes: 0 additions & 11 deletions rs/moq-scene/src/browser/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@

use std::os::fd::OwnedFd;

use super::Helper;
use super::protocol::Texture;
use crate::wgpu;

/// Whether the studio's device can import the helper's shared textures, so
/// the helper should paint into them.
pub(super) fn supported(helper: &Helper) -> bool {
cfg!(feature = "cef")
&& helper
.device
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
}

/// Copy the DMA-BUF `texture` describes, whose plane descriptors are `fds`,
/// into a texture of the studio's own on `device`, waiting until the copy is
/// done.
Expand Down
17 changes: 14 additions & 3 deletions rs/moq-scene/src/browser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ pub struct Helper {
profile: PathBuf,
device: wgpu::Device,
queue: wgpu::Queue,
/// Whether pages paint into shared textures the device imports, rather
/// than into memory it uploads.
shared: bool,
/// The device's DRM render node, which CEF must paint on for the studio to
/// import what it paints.
render_node: Option<PathBuf>,
Expand All @@ -111,17 +114,25 @@ impl Helper {
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Self {
let info = adapter.get_info();
// A software device holds a shared texture in memory anyway, so there
// the page paints in software too.
let shared = cfg!(feature = "cef")
&& info.device_type != wgpu::DeviceType::Cpu
&& device
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF);
// A machine with two GPUs would otherwise paint on whichever CEF
// picks, and the studio cannot import from another GPU.
let bus = adapter.get_info().device_pci_bus_id;
let render_node = (!bus.is_empty())
.then(|| std::fs::canonicalize(format!("/dev/dri/by-path/pci-{bus}-render")).ok())
let render_node = (!info.device_pci_bus_id.is_empty())
.then(|| std::fs::canonicalize(format!("/dev/dri/by-path/pci-{}-render", info.device_pci_bus_id)).ok())
.flatten();
Self {
program: program.into(),
profile: profile.into(),
device: device.clone(),
queue: queue.clone(),
shared,
render_node,
#[cfg(target_os = "linux")]
running: Default::default(),
Expand Down
18 changes: 13 additions & 5 deletions rs/moq-scene/src/browser/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,24 @@ pub(crate) enum Command {
/// How a helper runs every page it opens.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct Start {
/// Whether to paint into shared textures the studio imports, rather than
/// into memory it uploads.
pub accelerated: bool,
/// The DRM render node to paint on: the studio's GPU.
pub render_node: Option<String>,
pub raster: Raster,
/// The profile every page shares: cookies, storage, and cache. CEF lets
/// one process at a time hold it.
pub profile: String,
}

/// Where a helper's pages paint.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) enum Raster {
/// On the GPU, into shared textures the studio imports: on the DRM render
/// node named, the studio's GPU, or else on the one CEF picks.
Gpu { render_node: Option<String> },
/// In software, into memory the studio uploads. Startup never waits on a
/// GPU process CEF cannot start, and WebGL runs on SwiftShader.
#[default]
Cpu,
}

/// A page to open.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct Open {
Expand Down
18 changes: 11 additions & 7 deletions rs/moq-scene/src/browser/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use moq_audio::bus::Channel as Sound;

use super::layers::{Gpu, Layers};
use super::outbox::Commands;
use super::protocol::{Area, Channel, Command, Event, Id, Layer, Open, Start};
use super::protocol::{Area, Channel, Command, Event, Id, Layer, Open, Raster, Start};
use super::{ARG, Helper, Page, Paint};
use crate::Error;
use crate::source::{Mailbox, Picture, Status};
Expand Down Expand Up @@ -198,12 +198,16 @@ impl Instance {
.and_then(|()| std::fs::set_permissions(&profile, std::fs::Permissions::from_mode(0o700)))
.map_err(|err| failed(&format!("cannot create the browser profile {}", profile.display()), err))?;
let start = Start {
accelerated: super::import::supported(helper),
render_node: helper
.render_node
.as_ref()
.and_then(|node| node.to_str())
.map(str::to_string),
raster: match helper.shared {
true => Raster::Gpu {
render_node: helper
.render_node
.as_ref()
.and_then(|node| node.to_str())
.map(str::to_string),
},
false => Raster::Cpu,
},
profile: profile
.to_str()
.ok_or_else(|| Error::Browser(format!("the browser profile {} is not UTF-8", profile.display())))?
Expand Down
18 changes: 18 additions & 0 deletions rs/moq-studio/tests/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ async fn a_page_draws_and_plays() {
.await;
}

/// A page gets WebGL and WebGL2 contexts, and what it draws with them shows.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs CEF and a Vulkan device; run by just render-sw"]
async fn a_webgl_page_draws() {
local(async {
// Red unless both contexts exist, and then green from WebGL2.
let html = "<body style='margin:0;background:red'>\
<canvas id=c width=320 height=240 style='display:block'></canvas><script>\
const gl = document.getElementById('c').getContext('webgl2');\
const gl1 = document.createElement('canvas').getContext('webgl');\
if (gl && gl1) { gl.clearColor(0, 1, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); }\
</script></body>";
let mut studio = Studio::new(&[("page", page(html))]).await;
studio.until_center(GREEN, &[]).await;
})
.await;
}

/// A click reaches the page where the preview put it, and a popup it asks for
/// opens nothing.
#[tokio::test(flavor = "multi_thread")]
Expand Down