mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
184 lines
7.9 KiB
Rust
184 lines
7.9 KiB
Rust
//! Copying a GPU buffer back to the CPU, so a screenshot is not limited to
|
|
//! clients that render in software.
|
|
//!
|
|
//! nescope hands dmabuf straight through to `nescapture` and never looks at it,
|
|
//! which is right for a game and useless for everything else. XWayland with
|
|
//! glamor — which is every XWayland smithay spawns, since it passes no
|
|
//! `-noglamor` — always presents dmabuf, so a Steam client under XWayland was
|
|
//! unreadable by construction.
|
|
//!
|
|
//! This imports such a buffer as a texture and copies it back. It is the only
|
|
//! GPU work nescope does, and it is deliberately not compositing: one buffer
|
|
//! in, one image out, no output, no swapchain, no presentation.
|
|
//!
|
|
//! # Why the renderer is created lazily and kept
|
|
//!
|
|
//! Building an EGL context costs enough to notice, and a login screen is polled
|
|
//! every second or so. Building one per capture would spend most of the poll
|
|
//! interval on setup. It is created on the first capture that needs it, so a
|
|
//! box that only ever runs games never pays for it at all.
|
|
//!
|
|
//! Thread-local rather than in `NescopeState`: the compositor is single
|
|
//! threaded, `GlesRenderer` is not `Send`, and this keeps a debugging aid out
|
|
//! of the state every other part of the compositor passes around.
|
|
|
|
use std::cell::{Cell, RefCell};
|
|
use std::path::PathBuf;
|
|
|
|
use smithay::backend::allocator::Buffer;
|
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
|
use smithay::backend::renderer::gles::GlesRenderer;
|
|
use smithay::backend::renderer::{ExportMem, ImportDma};
|
|
use smithay::utils::{Point, Rectangle, Size};
|
|
|
|
thread_local! {
|
|
/// `None` until the first attempt; the inner `None` means the attempt
|
|
/// failed and should not be retried on every poll.
|
|
static RENDERER: RefCell<Option<Option<GlesRenderer>>> = const { RefCell::new(None) };
|
|
/// Whether the reason for a failed read has been said out loud yet.
|
|
static WARNED: Cell<bool> = const { Cell::new(false) };
|
|
/// The render device to import on, from `--render-device`.
|
|
static RENDER_DEVICE: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
/// Tell the readback which GPU to use, once, at startup.
|
|
///
|
|
/// The device cannot be taken from the buffer: a dmabuf handed over by a
|
|
/// client carries no `DrmNode` — smithay only fills that in when the
|
|
/// compositor put it there, and nescope never does. The operator already names
|
|
/// the GPU for the game's sake, so the same answer serves here.
|
|
pub fn set_render_device(path: Option<String>) {
|
|
RENDER_DEVICE.with(|d| *d.borrow_mut() = path.map(PathBuf::from));
|
|
}
|
|
|
|
/// The configured render node, or the first one on the system.
|
|
///
|
|
/// Scanning is a fallback rather than the plan: on a single-GPU box it is
|
|
/// right, and on a multi-GPU box picking the wrong one fails at import with a
|
|
/// message naming the device, which is better than refusing to try.
|
|
fn render_device() -> Result<PathBuf, String> {
|
|
if let Some(path) = RENDER_DEVICE.with(|d| d.borrow().clone()) {
|
|
return Ok(path);
|
|
}
|
|
let mut nodes: Vec<PathBuf> = glob::glob("/dev/dri/renderD*")
|
|
.map_err(|e| format!("could not scan /dev/dri: {e}"))?
|
|
.filter_map(Result::ok)
|
|
.collect();
|
|
nodes.sort();
|
|
nodes
|
|
.into_iter()
|
|
.next()
|
|
.ok_or_else(|| "no render node found in /dev/dri; pass --render-device".to_string())
|
|
}
|
|
|
|
/// Why a read did not happen.
|
|
enum ReadError {
|
|
/// The GPU path could not be set up at all. Reported once, at setup.
|
|
Unavailable,
|
|
/// Setup worked and this particular buffer could not be read.
|
|
Failed(String),
|
|
}
|
|
|
|
/// Build a renderer on the configured render device.
|
|
fn build_renderer() -> Result<GlesRenderer, String> {
|
|
let path = render_device()?;
|
|
let file = std::fs::OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open(&path)
|
|
.map_err(|e| format!("could not open {}: {e}", path.display()))?;
|
|
let gbm = smithay::backend::allocator::gbm::GbmDevice::new(file)
|
|
.map_err(|e| format!("could not create a GBM device on {}: {e}", path.display()))?;
|
|
|
|
// SAFETY: the GBM device stays alive for the life of the display, which
|
|
// lives in the thread-local below for the life of the process.
|
|
let display = unsafe { EGLDisplay::new(gbm) }
|
|
.map_err(|e| format!("could not open an EGL display: {e}"))?;
|
|
let context =
|
|
EGLContext::new(&display).map_err(|e| format!("could not create an EGL context: {e}"))?;
|
|
// SAFETY: the context is current only on this thread, and the renderer is
|
|
// thread-local so it can never be used from another.
|
|
unsafe { GlesRenderer::new(context) }.map_err(|e| format!("could not create a renderer: {e}"))
|
|
}
|
|
|
|
/// Copy a dmabuf back to CPU memory as RGBA8.
|
|
///
|
|
/// Returns `Err` with something worth printing when the GPU path is not
|
|
/// available at all — that is a configuration answer, not a transient one, so
|
|
/// the caller should say it rather than retry silently.
|
|
fn read_dmabuf(dmabuf: &Dmabuf) -> Result<(u32, u32, Vec<u8>), ReadError> {
|
|
RENDERER.with(|cell| {
|
|
let mut slot = cell.borrow_mut();
|
|
if slot.is_none() {
|
|
match build_renderer() {
|
|
Ok(renderer) => *slot = Some(Some(renderer)),
|
|
Err(e) => {
|
|
// Remembered as a failure so the next poll does not repeat
|
|
// the whole EGL setup just to fail the same way.
|
|
tracing::warn!("GPU readback unavailable: {e}");
|
|
*slot = Some(None);
|
|
}
|
|
}
|
|
}
|
|
let Some(renderer) = slot.as_mut().and_then(|r| r.as_mut()) else {
|
|
// Already explained at setup; saying it again per capture would
|
|
// report one problem twice in two different words.
|
|
return Err(ReadError::Unavailable);
|
|
};
|
|
|
|
let size = dmabuf.size();
|
|
let texture = renderer
|
|
.import_dmabuf(dmabuf, None)
|
|
.map_err(|e| ReadError::Failed(format!("could not import the buffer: {e}")))?;
|
|
|
|
let region = Rectangle::new(Point::from((0, 0)), Size::from((size.w, size.h)));
|
|
let mapping = renderer
|
|
.copy_texture(
|
|
&texture,
|
|
region,
|
|
smithay::backend::allocator::Fourcc::Abgr8888,
|
|
)
|
|
.map_err(|e| ReadError::Failed(format!("could not copy the texture back: {e}")))?;
|
|
let bytes = renderer
|
|
.map_texture(&mapping)
|
|
.map_err(|e| ReadError::Failed(format!("could not map the copied texture: {e}")))?;
|
|
|
|
Ok((size.w as u32, size.h as u32, bytes.to_vec()))
|
|
})
|
|
}
|
|
|
|
/// Read a `wl_buffer` that is backed by a dmabuf.
|
|
///
|
|
/// `None` when the buffer is not a dmabuf at all, or when the GPU path is
|
|
/// unavailable — the caller has already tried shm, so there is nothing left to
|
|
/// distinguish and a failure here means "not readable by any route".
|
|
pub fn from_wl_buffer(
|
|
buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
|
|
) -> Option<crate::screenshot_wire::Capture> {
|
|
let dmabuf = smithay::wayland::dmabuf::get_dmabuf(buffer).ok()?;
|
|
match read_dmabuf(dmabuf) {
|
|
Ok((width, height, rgba)) => Some(crate::screenshot_wire::Capture {
|
|
width,
|
|
height,
|
|
rgba,
|
|
}),
|
|
// Setup already said why, once, and repeating it per capture would
|
|
// report one problem twice in two different words.
|
|
Err(ReadError::Unavailable) => None,
|
|
Err(ReadError::Failed(e)) => {
|
|
// Loudly the first time and quietly afterwards. A capture polled
|
|
// every few hundred milliseconds would otherwise either bury the
|
|
// log or -- at debug level, which is off by default -- never say
|
|
// anything at all, leaving `Unreadable` with no explanation
|
|
// anywhere.
|
|
if !WARNED.with(|w| w.replace(true)) {
|
|
tracing::warn!("GPU readback failed: {e}");
|
|
} else {
|
|
tracing::debug!("GPU readback failed: {e}");
|
|
}
|
|
None
|
|
}
|
|
}
|
|
}
|