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
66 changes: 33 additions & 33 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,38 @@ name: Build
on: [push, pull_request]

env:
CARGO_TERM_COLOR: always
CARGO_TERM_COLOR: always

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- name: all-features
cargo_args: --workspace --release --all-features
- name: default
cargo_args: --workspace --release
- name: minimal
cargo_args: --workspace --release --no-default-features --features "png"

name: ${{ matrix.name }}
runs-on: ubuntu-latest
container:
image: archlinux:latest

steps:
- name: Checkout
uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable

- name: Build Cache
uses: Swatinem/rust-cache@v2

- name: Install wayland dependencies
run: |
pacman -Syu --noconfirm egl-wayland egl-gbm wayland base-devel mesa pango cairo libjxl

- name: Build (${{ matrix.name }})
run: cargo build ${{ matrix.cargo_args }}
build:
strategy:
fail-fast: false
matrix:
include:
- name: all-features
cargo_args: --workspace --release --all-features
- name: default
cargo_args: --workspace --release
- name: minimal
cargo_args: --workspace --release --no-default-features

name: ${{ matrix.name }}
runs-on: ubuntu-latest
container:
image: archlinux:latest

steps:
- name: Checkout
uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable

- name: Build Cache
uses: Swatinem/rust-cache@v2

- name: Install wayland dependencies
run: |
pacman -Syu --noconfirm egl-wayland egl-gbm wayland base-devel mesa pango cairo libjxl

- name: Build (${{ matrix.name }})
run: cargo build ${{ matrix.cargo_args }}
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ version = "1.4.6"
edition = "2024"

[workspace.dependencies]
libwayshot = { version = "0.7.3", path = "./libwayshot" }
libwayshot = { version = "0.7.3", path = "./libwayshot", default-features = false }
tracing = "0.1"
tracing-subscriber = "0.3"
r-egl-wayland = "0.7"
Expand Down
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

[xdg-desktop-portal-luminous](https://github.com/waycrate/xdg-desktop-portal-luminous) is a xdg-desktop-portal backend for wlroots based compositors, providing screenshot and screencast capabilities.


# Some usage examples:

NOTE: Read `man 7 wayshot` for more examples.
Expand Down Expand Up @@ -89,16 +88,17 @@ features can be selectively disabled:
cargo build --no-default-features --features clipboard,logger,notifications
```

| Feature | What it adds | Extra dependency |
|----------------|-------------------------------------------------------|-----------------------|
| `avif` | AVIF encoding (`--encoding avif`) | rav1e (via image) |
| `clipboard` | `--clipboard` flag, copy to Wayland clipboard | wl-clipboard-rs |
| `color_picker` | `--color` flag, freeze screen and pick a pixel color | — |
| `jxl` | JPEG-XL encoding (`--encoding jxl`) | libjxl / jpegxl-rs |
| `logger` | `--log-level` flag, tracing output to stderr | tracing-subscriber |
| `notifications`| Desktop notifications after each capture | notify-rust |
| `selector` | `--geometry` flag, interactive region selection | libwaysip |
| `completions` | `--completions <SHELL>` flag, generate shell completion scripts | clap_complete |
| Feature | What it adds | Extra dependency |
| --------------- | --------------------------------------------------------------- | ------------------ |
| `avif` | AVIF encoding (`--encoding avif`) | rav1e (via image) |
| `clipboard` | `--clipboard` flag, copy to Wayland clipboard | wl-clipboard-rs |
| `color_picker` | `--color` flag, freeze screen and pick a pixel color | — |
| `egl` | EGL/OpenGL GPU capture backend (DMA-BUF → EGLImage) | gl, r-egl-wayland |
| `jxl` | JPEG-XL encoding (`--encoding jxl`) | libjxl / jpegxl-rs |
| `logger` | `--log-level` flag, tracing output to stderr | tracing-subscriber |
| `notifications` | Desktop notifications after each capture | notify-rust |
| `selector` | `--geometry` flag, interactive region selection | libwaysip |
| `completions` | `--completions <SHELL>` flag, generate shell completion scripts | clap_complete |

## Clipboard without the built-in feature

Expand All @@ -118,18 +118,18 @@ Alternatively, set `stdout = true` in your config file to always write to stdout

## Compile time dependencies:

- scdoc (If present, man-pages will be generated.)
- rustup
- make
- pkg-config
- libjxl _(optional — only needed when the `jxl` feature is enabled)_
- scdoc (If present, man-pages will be generated.)
- rustup
- make
- pkg-config
- libjxl _(optional — only needed when the `jxl` feature is enabled)_

## Compiling:

- `git clone https://github.com/waycrate/wayshot && cd wayshot`
- `make setup`
- `make`
- `sudo make install`
- `git clone https://github.com/waycrate/wayshot && cd wayshot`
- `make setup`
- `make`
- `sudo make install`

# Support:

Expand Down
2 changes: 1 addition & 1 deletion docs/wayshot.1.scd
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ To build with a reduced set, use *--no-default-features* together with *--featur

cargo build --no-default-features --features clipboard,logger

Available features: *avif*, *clipboard*, *color_picker*, *jxl*, *logger*, *notifications*, *selector*
Available features: *avif*, *clipboard*, *color_picker*, *completions*, *jxl*, *logger*, *notifications*, *selector*

# SEE ALSO
- wayshot(5)
Expand Down
7 changes: 4 additions & 3 deletions libwayshot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ repository.workspace = true
edition.workspace = true

[features]
default = ["png"]
default = ["png", "egl"]
png = ["image/png"]
jpeg = ["image/jpeg"]
qoi = ["image/qoi"]
webp = ["image/webp"]
avif = ["image/avif"]
egl = ["dep:gl", "dep:r-egl-wayland"]

[dependencies]
tracing.workspace = true
Expand All @@ -35,5 +36,5 @@ wayland-backend = { version = "0.3", features = ["client_system"] }
gbm = "0.18"
drm = "0.15"

gl.workspace = true
r-egl-wayland.workspace = true
gl = { workspace = true, optional = true }
r-egl-wayland = { workspace = true, optional = true }
28 changes: 28 additions & 0 deletions libwayshot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,31 @@ use libwayshot::WayshotConnection;
let wayshot_connection = WayshotConnection::new()?;
let image_buffer = wayshot_connection.screenshot_all()?;
```

# Optional features

All features are enabled by default. To opt out selectively:

```toml
libwayshot = { version = "...", default-features = false, features = ["png", "egl"] }
```

| Feature | What it enables | Extra dependencies |
|---------|----------------|--------------------|
| `png` | PNG encoding/decoding | — (via image) |
| `jpeg` | JPEG encoding/decoding | — (via image) |
| `qoi` | QOI encoding/decoding | — (via image) |
| `webp` | WebP encoding/decoding | — (via image) |
| `avif` | AVIF encoding/decoding | rav1e (via image) |
| `egl` | EGL/OpenGL GPU capture backend (DMA-BUF → EGLImage → GL texture) | gl, r-egl-wayland |

## EGL capture backend

The `egl` feature enables zero-copy GPU screen capture via the EGL/OpenGL path.
When enabled, the following APIs are available:

- `WayshotConnection::capture_target_frame_eglimage` — capture to an `EGLImage`
- `WayshotConnection::capture_target_frame_eglimage_on_display` — capture to an `EGLImage` on a given `EGLDisplay`
- `WayshotConnection::bind_target_frame_to_gl_texture` — capture and bind directly to the current GL texture
- `WayshotConnection::create_screencast_with_egl` — set up a screencast session with EGL display binding
- `EGLImageGuard` — RAII wrapper that owns and destroys the `EGLImage` on drop
136 changes: 136 additions & 0 deletions libwayshot/src/egl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! EGL/OpenGL-based capture path: DMA-BUF → EGLImage → GL texture.
//!
//! This module is available when the `egl` feature is enabled. It provides:
//! - [`EGLImageGuard`] – owns an EGLImage created from a DMA-BUF capture
//! - [`get_egl_display_wl`] / [`initialize_egl`] – EGL display from Wayland
//! - [`create_egl_image_from_dmabuf`] – create an EGLImage from a GBM buffer
//! - [`bind_egl_image_to_gl_texture`] – bind an EGLImage to the current GL texture (OES_EGL_image)
//! - [`create_egl_image_and_bind_to_gl_texture`] – one-shot create + bind (e.g. for screencast)

use std::os::fd::IntoRawFd;

use gbm::BufferObject;
use r_egl_wayland::{EGL_INSTALCE, WayEglTrait, r_egl as egl};
use wayland_client::protocol::wl_display::WlDisplay;

use crate::error::{Error, Result};
use crate::screencopy::DMAFrameFormat;

/// EGL display type (re-exported for API use).
pub type EglDisplay = egl::Display;

/// Guard that owns an EGLImage created from a DMA-BUF. Destroys the image on drop.
pub struct EGLImageGuard {
pub image: egl::Image,
pub(crate) egl_display: egl::Display,
}

impl Drop for EGLImageGuard {
fn drop(&mut self) {
EGL_INSTALCE
.destroy_image(self.egl_display, self.image)
.unwrap_or_else(|e| {
tracing::error!("EGLimage destruction had error: {e}");
});
}
}

/// Obtain an EGL display from the Wayland display. Call [`initialize_egl`] before use.
pub fn get_egl_display_wl(display: &WlDisplay) -> Result<egl::Display> {
EGL_INSTALCE.get_display_wl(display).map_err(Error::from)
}

/// Initialize EGL for the given display.
pub fn initialize_egl(display: egl::Display) -> Result<()> {
EGL_INSTALCE
.initialize(display)
.map(|_| ())
.map_err(Error::from)
}

/// Create an EGLImage from a DMA-BUF (GBM buffer object). Returns a guard that destroys the image on drop.
pub fn create_egl_image_from_dmabuf(
egl_display: egl::Display,
bo: &BufferObject<()>,
frame_format: &DMAFrameFormat,
) -> Result<EGLImageGuard> {
type Attrib = egl::Attrib;
let modifier: u64 = bo.modifier().into();
let image_attribs = [
egl::WIDTH as Attrib,
frame_format.size.width as Attrib,
egl::HEIGHT as Attrib,
frame_format.size.height as Attrib,
egl::LINUX_DRM_FOURCC_EXT as Attrib,
bo.format() as Attrib,
egl::DMA_BUF_PLANE0_FD_EXT as Attrib,
bo.fd_for_plane(0)?.into_raw_fd() as Attrib,
egl::DMA_BUF_PLANE0_OFFSET_EXT as Attrib,
bo.offset(0) as Attrib,
egl::DMA_BUF_PLANE0_PITCH_EXT as Attrib,
bo.stride_for_plane(0) as Attrib,
egl::DMA_BUF_PLANE0_MODIFIER_LO_EXT as Attrib,
(modifier as u32) as Attrib,
egl::DMA_BUF_PLANE0_MODIFIER_HI_EXT as Attrib,
(modifier >> 32) as Attrib,
egl::ATTRIB_NONE as Attrib,
];
tracing::debug!(
"Calling eglCreateImage with attributes: {:#?}",
image_attribs
);
unsafe {
let image = EGL_INSTALCE
.create_image(
egl_display,
egl::Context::from_ptr(egl::NO_CONTEXT),
egl::LINUX_DMA_BUF_EXT as u32,
egl::ClientBuffer::from_ptr(std::ptr::null_mut()),
&image_attribs,
)
.map_err(|e| {
tracing::error!("eglCreateImage call failed with error {e}");
Error::from(e)
})?;
Ok(EGLImageGuard { image, egl_display })
}
}

/// Bind the EGLImage to the current GL texture (TEXTURE_2D) via OES_EGL_image.
/// The caller must have bound the target texture before calling.
pub fn bind_egl_image_to_gl_texture(guard: &EGLImageGuard) -> Result<()> {
let gl_egl_image_texture_target_2d_oes = unsafe {
let f = match EGL_INSTALCE.get_proc_address("glEGLImageTargetTexture2DOES") {
Some(f) => {
tracing::debug!("glEGLImageTargetTexture2DOES found at address {:#?}", f);
f
}
None => {
tracing::error!("glEGLImageTargetTexture2DOES not found");
return Err(Error::EGLImageToTexProcNotFoundError);
}
};
std::mem::transmute::<
extern "system" fn(),
unsafe extern "system" fn(gl::types::GLenum, gl::types::GLeglImageOES) -> (),
>(f)
};
unsafe {
gl_egl_image_texture_target_2d_oes(gl::TEXTURE_2D, guard.image.as_ptr());
tracing::trace!("glEGLImageTargetTexture2DOES called");
}
Ok(())
}

/// Create an EGLImage from the DMA-BUF and bind it to the current GL texture, then destroy the EGLImage.
/// Used by screencast when updating the texture each frame.
pub fn create_egl_image_and_bind_to_gl_texture(
egl_display: egl::Display,
bo: &BufferObject<()>,
frame_format: &DMAFrameFormat,
) -> Result<()> {
let guard = create_egl_image_from_dmabuf(egl_display, bo, frame_format)?;
bind_egl_image_to_gl_texture(&guard)?;
drop(guard);
Ok(())
}
6 changes: 5 additions & 1 deletion libwayshot/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{io, result};

use drm::buffer::UnrecognizedFourcc;
use gbm::InvalidFdError;
#[cfg(feature = "egl")]
use r_egl_wayland::r_egl as egl;
use thiserror::Error;
use wayland_client::{
Expand Down Expand Up @@ -46,8 +47,10 @@ pub enum Error {
NoDMAStateError,
#[error("dmabuf color format provided by compositor is invalid")]
UnrecognizedColorCode(#[from] UnrecognizedFourcc),
#[error("dmabuf device has been destroyed")]
#[cfg(feature = "egl")]
#[error("EGL error: {0}")]
EGLError(#[from] egl::Error),
#[cfg(feature = "egl")]
#[error("No EGLImageTargetTexture2DOES function located, this extension may not be supported")]
EGLImageToTexProcNotFoundError,
#[error("Capture failed: {0}")]
Expand Down Expand Up @@ -263,6 +266,7 @@ mod tests {
}
}

#[cfg(feature = "egl")]
#[test]
fn test_from_egl_error() {
let egl_error = egl::Error::ContextLost;
Expand Down
Loading