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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Every panel is a tab you can drag, split, or stack.
| Scenes | Pick the scene in preview, and add, rename (double-click), reorder, remove, or tick **Publish** on one (see [Published scenes](#published-scenes)). A scene on program, or still leaving it in a transition, cannot be removed, even by an undo, until program cuts away from it. |
| History | See every edit, and undo or redo back to any of them. |
| Items | Place, show, hide, and order what the preview scene or the top layer draws, back to front. The top layer is edited over a scene of the program's size. Select an item to filter or copy it. |
| Sources | Add a source by hand, see whether each and its own track are live and where it is shown, clone one, and remove one. Select a source to filter it. A camera or broadcast without a new picture, or a microphone or system audio without new sound, for a second reads as stalled. A browser source shows whether its page is copied on the GPU or, as a fallback, uploaded from memory (CPU), in place of a track, and **Reload** restarts its page. A media file reads as ended once it plays to its end without looping, **Play** plays it from its start, **Stop** stops it at once, blank and silent, until played again, and **Reload** reads the file again. |
| Sources | Add a source by hand, see whether each and its own track are live and where it is shown, clone one, and remove one. Select a source to filter it. A camera or broadcast without a new picture, or a microphone or system audio without new sound, for a second reads as stalled. A browser source shows whether its page is copied on the GPU or, as a fallback, uploaded from memory (CPU, saying why on hover), in place of a track, and **Reload** restarts its page. A media file reads as ended once it plays to its end without looping, **Play** plays it from its start, **Stop** stops it at once, blank and silent, until played again, and **Reload** reads the file again. |
| Devices | List this machine's cameras, displays, windows, and audio inputs, and add one as a source. |
| Guests | Invite a guest, show their join link as a QR code, see who joined, and take a guest out of every scene. |
| Filters | Remove the selected source's background, crop the selected source or item, add, order, and set its filters, set an item's blend mode, and copy or paste filters. |
Expand Down Expand Up @@ -890,7 +890,8 @@ 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 and a display to paint through:
the Wayland one when `$WAYLAND_DISPLAY` is set, needing no XWayland, or else
the X one in `$DISPLAY`. Every other source, every source on Windows or on a
the X one in `$DISPLAY`, when the X server runs on the studio's GPU, since
CEF paints on the X server's there. 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. A browser source takes that path with no
display at all too, and there renders its page in software, WebGL included, so
Expand Down Expand Up @@ -986,8 +987,9 @@ congested or losing packets, the relay is far away, the encoder is behind or
stuck, a source stalled (a camera or broadcast the program draws, or a
microphone or system audio the mix on program plays: unmuted and not left
out of it), a source the program shows or plays failed or, as a media file
that does not loop, played to its end, or viewers are late, stalling, or cannot
decode a track. A relay lost while the other serves is
that does not loop, played to its end, a browser source the program shows is
uploaded from memory rather than copied on the GPU, or viewers are late,
stalling, or cannot decode a track. A relay lost while the other serves is
degraded, since viewers have nothing left to fall back to; with none serving
it is unhealthy. A problem with a published scene's broadcast names the scene.
Below that it shows:
Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ repository.
- [Web studio](/quest/m0/web.md) - the full studio in wasm on WebGPU and WebCodecs, served from this repository
- [Virtual camera](/quest/m0/vcam/README.md) - the program or a published scene exposed as a system camera on Linux, Windows, and macOS
- [Browser sources on the GPU under Mesa](/quest/m0/cef-headless.md) - shared-texture pages paint on Intel and AMD under Wayland, and a page that cannot paint fails loudly
- [Browser sources on the GPU under X11 on NVIDIA](/quest/m0/cef-x11-nvidia.md) - the X11 GPU path paints on NVIDIA, or falls back to the CPU path loudly
- [A browser helper exits with the studio](/quest/m0/browser-helper-exit.md) - no orphaned CEF helper after a crash, kill, or timed-out test
- [Nightly and the browser suite pass](/quest/m0/nightly-green.md) - render-sw, cookies, and the X11 clipboard test fixed at their causes
- [GPU tests exit cleanly on NVIDIA](/quest/m0/gpu-tests-exit.md) - compositor tests no longer crash on exit on NVIDIA
Expand Down
31 changes: 0 additions & 31 deletions quest/m0/cef-x11-nvidia.md

This file was deleted.

5 changes: 4 additions & 1 deletion rs/moq-scene/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ wuff = "0.2"
# `accelerated_osr` copies the page's shared texture into the studio's device.
cef = { version = "156.3", default-features = false, features = ["accelerated_osr"], optional = true }
postcard = { version = "1", default-features = false, features = ["use-std"] }
rustix = { version = "1", features = ["net", "process", "stdio"] }
rustix = { version = "1", features = ["fs", "net", "process", "stdio"] }
# The helper's CEF profile, removed once the helper has exited.
tempfile = { workspace = true }
# Which GPU the X server runs on, which CEF paints on under X11. Pure Rust, and
# 0.13 as the studio's hotkeys use.
x11rb = { version = "0.13", features = ["dri3"] }

[dev-dependencies]
# An encoder on every machine, for the source track and replay tests.
Expand Down
5 changes: 3 additions & 2 deletions rs/moq-scene/src/browser/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,9 @@ wrap_app! {
Display::X11 => "x11",
};
command_line.append_switch_with_value(Some(&"ozone-platform".into()), Some(&platform.into()));
// On X11, ANGLE otherwise draws through GLX, which on Mesa
// leaves shared textures blank. Wayland is always EGL.
// On X11, ANGLE otherwise draws through GLX, which leaves
// shared textures blank on Mesa, and on NVIDIA finds no GBM
// device to allocate them. Wayland is always EGL.
if *display == Display::X11 {
command_line.append_switch_with_value(Some(&"use-angle".into()), Some(&"gl-egl".into()));
}
Expand Down
113 changes: 92 additions & 21 deletions rs/moq-scene/src/browser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! become one; CEF's own subprocesses start the same way. The helper paints
//! into a shared GPU texture the studio copies on its device, or into memory
//! 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
//! display to paint them through ([`Paint`] says which, and why). Its popups, such as a
//! `<select>`'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.
//!
Expand Down Expand Up @@ -90,6 +90,9 @@ pub struct Helper {
/// Where pages paint.
#[cfg(target_os = "linux")]
raster: protocol::Raster,
/// Why a page that paints into memory does.
#[cfg(target_os = "linux")]
fallback: Arc<str>,
/// The helper, once a page has started it.
#[cfg(target_os = "linux")]
running: Arc<Mutex<Option<Arc<run::Instance>>>>,
Expand All @@ -113,13 +116,20 @@ impl Helper {
) -> Self {
#[cfg(not(target_os = "linux"))]
let _ = adapter;
#[cfg(target_os = "linux")]
let (raster, fallback) = match gpu(adapter, device) {
Ok(raster) => (raster, "its GPU process cannot share textures".into()),
Err(why) => (protocol::Raster::Cpu, why.into()),
};
Self {
program: program.into(),
profile: profile.into(),
device: device.clone(),
queue: queue.clone(),
#[cfg(target_os = "linux")]
raster: raster(adapter, device),
raster,
#[cfg(target_os = "linux")]
fallback,
#[cfg(target_os = "linux")]
running: Default::default(),
}
Expand Down Expand Up @@ -155,50 +165,111 @@ impl Helper {
}
}

/// Where pages paint, decided once from what the studio's device and session
/// offer.
/// The GPU path pages paint on, decided once from what the studio's device
/// and session offer, or why they cannot take it.
#[cfg(target_os = "linux")]
fn raster(adapter: &wgpu::Adapter, device: &wgpu::Device) -> protocol::Raster {
fn gpu(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Result<protocol::Raster, String> {
use protocol::{Display, Raster};

if !cfg!(feature = "cef") {
return Err("this build cannot share textures".into());
}
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);
if !shared {
return Raster::Cpu;
if info.device_type == wgpu::DeviceType::Cpu {
return Err(format!("the studio's GPU, {}, renders in software", info.name));
}
if !device
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF)
{
return Err(format!(
"the studio's GPU, {}, cannot import shared textures",
info.name
));
}
let set = |name: &str| std::env::var_os(name).is_some_and(|value| !value.is_empty());
let display = if set("WAYLAND_DISPLAY") {
Display::Wayland
} else if set("DISPLAY") {
x11_gpu(&info)?;
Display::X11
} else {
// CEF's GPU process cannot allocate shared textures without one.
return Raster::Cpu;
return Err("there is no display to paint through".into());
};
// A machine with two GPUs would otherwise paint on whichever CEF picks,
// and the studio cannot import from another GPU.
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()
.and_then(|node| node.into_os_string().into_string().ok());
Raster::Gpu { render_node, display }
Ok(Raster::Gpu { render_node, display })
}

/// Check that the X server in `$DISPLAY` runs on the studio's GPU, `info`.
/// On X11, CEF allocates shared textures on the device the X server hands out
/// over DRI3, whatever `--render-node-override` says, and another GPU's
/// tiling comes out scrambled on the studio's.
#[cfg(target_os = "linux")]
fn x11_gpu(info: &wgpu::AdapterInfo) -> Result<(), String> {
use std::os::unix::fs::MetadataExt;

use x11rb::connection::{Connection, RequestConnection};
use x11rb::protocol::dri3::{self, ConnectionExt};

let (connection, screen) = x11rb::connect(None).map_err(|err| format!("cannot connect to the X display: {err}"))?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let extension = connection
.extension_information(dri3::X11_EXTENSION_NAME)
.map_err(|err| format!("the X display: {err}"))?;
if extension.is_none() {
return Err("the X server offers no DRI3 device to share textures on".into());
}
let root = connection.setup().roots[screen].root;
let device = connection
.dri3_open(root, x11rb::NONE)
.map_err(|err| format!("the X display: {err}"))?
.reply()
.map_err(|err| format!("the X server's DRI3 device: {err}"))?
.device_fd;
let rdev = std::fs::File::from(device)
.metadata()
.map_err(|err| format!("the X server's DRI3 device: {err}"))?
.rdev();
let sys = format!(
"/sys/dev/char/{}:{}/device",
rustix::fs::major(rdev),
rustix::fs::minor(rdev)
);
let bus = std::fs::canonicalize(&sys)
.ok()
.and_then(|path| Some(path.file_name()?.to_str()?.to_owned()))
.ok_or_else(|| format!("cannot tell which GPU the X server runs on from {sys}"))?;
if info.device_pci_bus_id.is_empty() {
return Err(format!(
"cannot tell whether the studio's GPU, {}, is the one the X server runs on ({bus})",
info.name
));
}
if bus != info.device_pci_bus_id {
return Err(format!(
"the X server runs on another GPU ({bus}) than the studio's, {} ({}), and under X11 pages paint only on the X server's",
info.name, info.device_pci_bus_id
));
}
Ok(())
}

/// How a page's pictures reach the compositor.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Paint {
/// A shared texture, copied on the GPU.
Gpu,
/// Pixels in memory, uploaded every frame: the fallback when the studio
/// runs with no display, or the page's GPU process or the studio's device
/// cannot share textures.
Cpu,
/// Pixels in memory, uploaded every frame, and why: the fallback when the
/// studio's device, the session, or the page's GPU process cannot share
/// textures.
Cpu(Arc<str>),
}

/// A running browser source: how it paints, and the way to interact with it.
Expand Down Expand Up @@ -268,9 +339,9 @@ impl Page {
Arc::ptr_eq(&self.state, &other.state)
}

/// How the newest picture arrived, once one has.
/// How the newest picture arrived, while the page shows one.
pub fn paint(&self) -> Option<Paint> {
self.state.lock().unwrap().paint
self.state.lock().unwrap().paint.clone()
}

/// Paint at `rate`, the canvas's frame rate, from now on, without waiting on
Expand Down
6 changes: 3 additions & 3 deletions rs/moq-scene/src/browser/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ pub(crate) enum Raster {
/// The display server the GPU path connects to, the one the studio runs on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Display {
/// `$WAYLAND_DISPLAY`, preferred: under XWayland, CEF's GPU process finds
/// no GBM device on some drivers, NVIDIA's among them, and never paints.
/// `$WAYLAND_DISPLAY`, preferred: CEF paints there on the studio's GPU.
Wayland,
/// `$DISPLAY`.
/// `$DISPLAY`, whose X server must run on the studio's GPU, since CEF
/// paints on the X server's.
X11,
}

Expand Down
11 changes: 9 additions & 2 deletions rs/moq-scene/src/browser/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub(crate) async fn run(
// A stopped page leaves the canvas rather than freezing on its last
// picture, which for an alert or a ticker would show stale news.
picture.lock().unwrap().clear();
page.state.lock().unwrap().paint = None;
Err(Error::Browser(reason))
}

Expand Down Expand Up @@ -107,6 +108,8 @@ struct Routes {
/// What a page last copied or cut, until the studio takes it. One slot
/// for every page, so a newer copy always replaces an older one.
copied: Option<String>,
/// Why a page that paints into memory does.
fallback: Arc<str>,
}

/// One open page's source.
Expand Down Expand Up @@ -212,7 +215,10 @@ impl Instance {
commands
.send(&Command::Start(start), &[])
.map_err(|err| failed("the browser process's socket", err))?;
let routes = Arc::new(Mutex::new(Routes::default()));
let routes = Arc::new(Mutex::new(Routes {
fallback: helper.fallback.clone(),
..Default::default()
}));
let commands = writer(commands).map_err(|err| failed("cannot start the page writer", err))?;
let reader = {
// Not the helper, whose hold on this instance would keep it running
Expand Down Expand Up @@ -415,6 +421,7 @@ fn deliver(gpu: &Gpu, events: &Channel, commands: &Commands, routes: &Mutex<Rout
use std::io::ErrorKind::*;

let Gpu { device, queue, .. } = gpu;
let fallback = routes.lock().unwrap().fallback.clone();

loop {
let (event, fds) = match events.recv::<Event>() {
Expand All @@ -440,7 +447,7 @@ fn deliver(gpu: &Gpu, events: &Channel, commands: &Commands, routes: &Mutex<Rout
page,
layer,
super::upload(device, queue, width, height, &data),
Paint::Cpu,
Paint::Cpu(fallback.clone()),
),
Event::Popup(id, area) => {
routes.lock().unwrap().place(gpu, id, area);
Expand Down
14 changes: 12 additions & 2 deletions rs/moq-studio/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ impl Engine {
// published or recorded scene drawn.
// Stalled sound matters only if the mix on program plays it, not one
// only listened to. A source that stopped matters either way.
let (stalled, stopped) = {
let (stalled, stopped, uploaded) = {
let state = self.state.lock().unwrap();
let on_program = state.on_program();
let beside = state.beside();
Expand Down Expand Up @@ -1216,7 +1216,16 @@ impl Engine {
stopped.then(|| (name.to_string(), status, shown))
})
.collect();
(stalled, stopped)
let uploaded = state
.sources
.iter()
.filter(|(name, _)| shown(name).any())
.filter_map(|(name, feed)| match feed.page()?.paint()? {
moq_scene::browser::Paint::Cpu(why) => Some((name.to_string(), why)),
moq_scene::browser::Paint::Gpu => None,
Comment thread
kixelated marked this conversation as resolved.
Resolved
})
.collect();
(stalled, stopped, uploaded)
};
let video = |video: &VideoStats, failed: Option<String>| {
let full = &video.renditions[Role::Canvas as usize];
Expand All @@ -1240,6 +1249,7 @@ impl Engine {
grants: stats.grants(),
stalled,
stopped,
uploaded,
}
}

Expand Down
Loading