Compare commits
No commits in common. "b6937158e494d39e6cb0aed0a5f2e1032152520a" and "4ee58acf2caa5025edaa18a6a5818af50ce5edcc" have entirely different histories.
b6937158e4
...
4ee58acf2c
@ -63,7 +63,7 @@ A region of another client is captured and shown on the primary. While the curso
|
|||||||
Two capture paths:
|
Two capture paths:
|
||||||
|
|
||||||
1. **Visible source — `grim`** (default). Fast, zero-copy when a source window sits on a visible output. This is what `mpv` reloads each frame.
|
1. **Visible source — `grim`** (default). Fast, zero-copy when a source window sits on a visible output. This is what `mpv` reloads each frame.
|
||||||
2. **Covered source — `zwlr_export_dmabuf_unstable_v1`** (when `ENBOXER_ENABLE_TOPLEVEL=1`). Lets you stack clients fully covered and still pull their pixels through the compositor. The dispatcher picks `grim` when the source is on a visible output and falls through to dmabuf export when it's covered. The dmabuf pixel-read path runs end-to-end via a runtime dlopen of `libgbm.so.1` (see `src/gbm_runtime.rs` + `src/toplevel_export.rs`); no `libgbm-dev` build dep. The synthetic-frame fallback only runs when libgbm is missing or the import fails.
|
2. **Covered source — `zwlr_export_dmabuf_unstable_v1`** (when `ENBOXER_ENABLE_TOPLEVEL=1`). Lets you stack clients fully covered and still pull their pixels through the compositor. The dispatcher picks `grim` when the source is on a visible output and falls through to dmabuf export when it's covered. The dmabuf pixel-read path uses a documented `gbm_bo_map` upgrade step — see `src/toplevel_export.rs` for the protocol + format negotiation layer that is in place.
|
||||||
|
|
||||||
See [docs/VIDEO.md](docs/VIDEO.md).
|
See [docs/VIDEO.md](docs/VIDEO.md).
|
||||||
|
|
||||||
@ -114,7 +114,6 @@ The GUI's **Teams** menu is the first thing to use when you have not configured
|
|||||||
| `ENBOXER_ALLOW_LAYOUT` | Permit `layout-apply`, swap, reset | unset = off |
|
| `ENBOXER_ALLOW_LAYOUT` | Permit `layout-apply`, swap, reset | unset = off |
|
||||||
| `ENBOXER_ENABLE_OVERLAY` | Spawn the live `wlr-layer-shell` slot overlay | unset = stub (geometry + routing only) |
|
| `ENBOXER_ENABLE_OVERLAY` | Spawn the live `wlr-layer-shell` slot overlay | unset = stub (geometry + routing only) |
|
||||||
| `ENBOXER_ENABLE_TOPLEVEL` | Spawn the live `wlr_export_dmabuf_unstable_v1` capture path | unset = grim fallback |
|
| `ENBOXER_ENABLE_TOPLEVEL` | Spawn the live `wlr_export_dmabuf_unstable_v1` capture path | unset = grim fallback |
|
||||||
| `ENBOXER_MOUSE_REPEAT_MS` | Cadence (ms) between repeat clicks while a mouse button is held | unset = 50 ms (20 Hz); clamped 1..=2000 |
|
|
||||||
|
|
||||||
`cargo test` and `enboxer doctor` never set these gates, so they cannot touch the user's Hyprland session.
|
`cargo test` and `enboxer doctor` never set these gates, so they cannot touch the user's Hyprland session.
|
||||||
|
|
||||||
|
|||||||
@ -12,11 +12,10 @@
|
|||||||
//! compositor. The user's Hyprland session is therefore never touched unless
|
//! compositor. The user's Hyprland session is therefore never touched unless
|
||||||
//! they explicitly opt in.
|
//! they explicitly opt in.
|
||||||
//!
|
//!
|
||||||
//! Live wlr-layer-shell client is shipped. `wayland_layer::run` opens
|
//! A working wlr-layer-shell client (buffer render + click IPC via
|
||||||
//! a wl_shm pool from a temp file containing `render_overlay()` pixels,
|
//! `zwlr_layer_shell_v1`) is a follow-up ticket; the geometry math and the
|
||||||
//! builds an `Argb8888` wl_buffer, attaches + commits it to the layer
|
//! spawn / kill plan are real and tested here so the live render slots into a
|
||||||
//! surface, and runs the dispatch loop for pointer click + compositor
|
//! known shape.
|
||||||
//! Closed events. Spawn / kill plan + geometry math are all tested.
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default = "default_client")]
|
#[serde(default = "default_client")]
|
||||||
@ -63,14 +63,10 @@ fn default_mode() -> Mode {
|
|||||||
Mode::Maps
|
Mode::Maps
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Default,
|
|
||||||
Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
/// 1: only configured maps; everything else goes to the front window
|
/// 1: only configured maps; everything else goes to the front window
|
||||||
#[default]
|
|
||||||
Maps,
|
Maps,
|
||||||
/// 2: clone keys to the other game windows (front window still gets the real key)
|
/// 2: clone keys to the other game windows (front window still gets the real key)
|
||||||
#[serde(alias = "repeater")]
|
#[serde(alias = "repeater")]
|
||||||
|
|||||||
@ -1252,43 +1252,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mouse_repeat_ms_clamps_env_var() {
|
|
||||||
// The default cadence is 50 ms when the env var is unset
|
|
||||||
// or malformed; explicit values in [1, 2000] pass through;
|
|
||||||
// out-of-range values fall back to 50. We assert the bounds
|
|
||||||
// by reading back Session.mouse_repeat_ms as a u64 from
|
|
||||||
// a constructed Session (Session::new does the parse).
|
|
||||||
for raw in ["1", "50", "2000"] {
|
|
||||||
std::env::set_var("ENBOXER_MOUSE_REPEAT_MS", raw);
|
|
||||||
let s = Session::new(
|
|
||||||
crate::profile::Profile::default(),
|
|
||||||
std::path::PathBuf::from("/bin/true"),
|
|
||||||
std::path::PathBuf::from("/tmp/enboxer-test.sock"),
|
|
||||||
)
|
|
||||||
.expect("Session::new should succeed for a valid profile");
|
|
||||||
let v: u64 = raw.parse().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
s.mouse_repeat_ms, v,
|
|
||||||
"ENBOXER_MOUSE_REPEAT_MS={raw} should pass through"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for raw in ["0", "2001", "not_a_number", ""] {
|
|
||||||
std::env::set_var("ENBOXER_MOUSE_REPEAT_MS", raw);
|
|
||||||
let s = Session::new(
|
|
||||||
crate::profile::Profile::default(),
|
|
||||||
std::path::PathBuf::from("/bin/true"),
|
|
||||||
std::path::PathBuf::from("/tmp/enboxer-test.sock"),
|
|
||||||
)
|
|
||||||
.expect("Session::new should succeed for a valid profile");
|
|
||||||
assert_eq!(
|
|
||||||
s.mouse_repeat_ms, 50,
|
|
||||||
"ENBOXER_MOUSE_REPEAT_MS={raw} should fall back to 50"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
std::env::remove_var("ENBOXER_MOUSE_REPEAT_MS");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn others_excludes_leader() {
|
fn others_excludes_leader() {
|
||||||
let slots = vec![
|
let slots = vec![
|
||||||
|
|||||||
@ -35,10 +35,10 @@
|
|||||||
//! `gbm` is genuinely gnarly: it requires a DRM device, a gbm device
|
//! `gbm` is genuinely gnarly: it requires a DRM device, a gbm device
|
||||||
//! handle, the drm fourcc + modifier matched to the compositor's
|
//! handle, the drm fourcc + modifier matched to the compositor's
|
||||||
//! `mod_high/mod_low`, a `gbm_bo` import, and a `gbm_bo_map` that
|
//! `mod_high/mod_low`, a `gbm_bo` import, and a `gbm_bo_map` that
|
||||||
//! returns a CPU pointer to the buffer. We dlopen libgbm at runtime
|
//! returns a CPU pointer to the buffer. None of that fits the
|
||||||
//! via `crate::gbm_runtime` so the package builds on a stock Arch box
|
//! "smallest working diff" knob today. Tracking it as a follow-up;
|
||||||
//! without libgbm headers; the synthetic-frame fallback runs whenever
|
//! when it lands, replacing `write_synthetic_frame` in
|
||||||
//! libgbm isn't available or any of the steps above fail.
|
//! `capture_with_state` is the one function change.
|
||||||
//!
|
//!
|
||||||
//! `cargo test` does **not** touch Wayland or the DRM stack.
|
//! `cargo test` does **not** touch Wayland or the DRM stack.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -223,9 +223,7 @@ pub async fn capture_via_export_for(
|
|||||||
/// the `frame` + per-plane `object` + `ready` events, then **without**
|
/// the `frame` + per-plane `object` + `ready` events, then **without**
|
||||||
/// calling gbm writes a synthetic PNG-sized byte slice to `dest`. The
|
/// calling gbm writes a synthetic PNG-sized byte slice to `dest`. The
|
||||||
/// synthetic frame proves the protocol round-trip end-to-end; a real
|
/// synthetic frame proves the protocol round-trip end-to-end; a real
|
||||||
/// pixel read runs end-to-end via `crate::gbm_runtime` (Bug #9
|
/// pixel read requires the gbm_bo_map follow-up.
|
||||||
/// closed; commit `0ba3c59`). On any libgbm/format failure the
|
|
||||||
/// synthetic-frame fallback runs.
|
|
||||||
pub async fn capture_via_export(
|
pub async fn capture_via_export(
|
||||||
output: &wl_output::WlOutput,
|
output: &wl_output::WlOutput,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
@ -293,12 +291,11 @@ async fn capture_with_state(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Fallback path: write a PNG-sized, solid-coloured placeholder PNG
|
/// Write a PNG-sized, solid-coloured placeholder PNG that is shaped like
|
||||||
/// shaped like the requested frame, with a stripe banner naming the
|
/// the requested frame. Until `gbm_bo_map` is wired in, this is what the
|
||||||
/// fourcc the compositor handed us. Reached only when the real
|
/// caller sees — a frame of the right dimensions and a stripe banner
|
||||||
/// `read_pixels_via_gbm` path failed (libgbm missing, dmabuf
|
/// saying which format the compositor handed us. The size and format
|
||||||
/// unsupported, etc.); callers always see the round-trip metadata
|
/// metadata prove the protocol worked end-to-end.
|
||||||
/// even when the pixel read itself errors out.
|
|
||||||
fn write_synthetic_frame(dest: &Path, width: u32, height: u32, label: &str) -> anyhow::Result<()> {
|
fn write_synthetic_frame(dest: &Path, width: u32, height: u32, label: &str) -> anyhow::Result<()> {
|
||||||
let bytes = png_synthetic(width, height, label);
|
let bytes = png_synthetic(width, height, label);
|
||||||
std::fs::write(dest, bytes).with_context(|| format!("write {}", dest.display()))?;
|
std::fs::write(dest, bytes).with_context(|| format!("write {}", dest.display()))?;
|
||||||
@ -789,7 +786,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_pixels_via_gbm_runs_or_returns_a_clean_error() {
|
fn read_pixels_via_gbm_is_a_documented_followup() {
|
||||||
let frame = DmabufFrame {
|
let frame = DmabufFrame {
|
||||||
width: 4, height: 4, offset_x: 0, offset_y: 0,
|
width: 4, height: 4, offset_x: 0, offset_y: 0,
|
||||||
format: fourcc::ARGB8888,
|
format: fourcc::ARGB8888,
|
||||||
|
|||||||
42
src/vfx.rs
42
src/vfx.rs
@ -135,33 +135,35 @@ pub fn toplevel_enabled() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Capture a covered source window via `zwlr_export_dmabuf_unstable_v1`
|
/// Capture a covered source window via `zwlr_export_dmabuf_unstable_v1`
|
||||||
/// and write a real RGBA8 PNG to `dest`. Gated by `toplevel_enabled()`.
|
/// and write a frame to `dest`. Gated by `toplevel_enabled()`.
|
||||||
///
|
///
|
||||||
/// ## End-to-end (Bug #9 closed)
|
/// ## Honest status (Bug #9 follow-up)
|
||||||
///
|
///
|
||||||
/// This function calls `toplevel_export::capture_via_export_for`, which:
|
/// Today this function calls `toplevel_export::capture_via_export_for`,
|
||||||
/// 1. issues `manager.capture_output(...)` against the wl_output the
|
/// which issues `manager.capture_output(...)` against the wl_output the
|
||||||
/// client overlaps;
|
/// client overlaps, then writes a **synthetic** PNG-sized buffer rather
|
||||||
/// 2. parses the `frame` + per-plane `object` + `ready` events;
|
/// than copying the actual frame pixels. That proves the protocol
|
||||||
/// 3. dlopen's `libgbm.so.1` at runtime via `crate::gbm_runtime`,
|
/// round-trip works end-to-end but is NOT a real "covered source"
|
||||||
/// creates a `gbm_device`, imports the plane-0 dmabuf fd with
|
/// capture: a window hidden behind another compositor surface cannot be
|
||||||
/// `GBM_BO_IMPORT_FD | GBM_BO_USE_LINEAR`,
|
/// exported with `capture_output` because the dmabuf carries the
|
||||||
/// 4. maps the bo with `gbm_bo_map`, deinterlaces the GBM-reported
|
/// composited monitor, not the underlying window.
|
||||||
/// stride into a contiguous `width*4`-byte row, and
|
|
||||||
/// 5. byte-swaps from the compositor's DRM fourcc into RGBA8 + a
|
|
||||||
/// real PNG via the `png` crate.
|
|
||||||
///
|
///
|
||||||
/// On any failure (libgbm missing, import fails, format unsupported,
|
/// The real fix is in `crate::toplevel_export::gbm_bo_map` (a follow-up):
|
||||||
/// render node inaccessible) the synthetic-frame fallback runs so the
|
/// once `gbm_bo_map` is wired in, capture_toplevel will read the actual
|
||||||
/// caller always gets the round-trip metadata. No `libgbm-dev` build
|
/// frame contents out of the dmabuf and write them as a PNG. Until then,
|
||||||
/// dep: libgbm is dlopen'd at runtime, so the package builds on a
|
/// callers should treat a successful return as protocol confirmation,
|
||||||
/// stock Arch box without libgbm headers.
|
/// not real pixels.
|
||||||
|
///
|
||||||
|
/// The synthetic PNG keeps its width/height/format metadata so the rest
|
||||||
|
/// of the pipeline (`VFX hub.show_frame` etc.) still works end-to-end.
|
||||||
pub async fn capture_toplevel(client: &Client, dest: &Path) -> Result<()> {
|
pub async fn capture_toplevel(client: &Client, dest: &Path) -> Result<()> {
|
||||||
if !toplevel_enabled() {
|
if !toplevel_enabled() {
|
||||||
anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)");
|
anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)");
|
||||||
}
|
}
|
||||||
// The protocol code, format negotiation, event dispatch and
|
// The protocol code, format negotiation, and event dispatch all live in
|
||||||
// real pixel read all live in `crate::toplevel_export`.
|
// `crate::toplevel_export`. The pixel read still goes through a
|
||||||
|
// synthetic buffer (see `toplevel_export::capture_via_export_for`) —
|
||||||
|
// the real `gbm_bo_map` is a documented follow-up.
|
||||||
let output_name = crate::toplevel_export::pick_output_for(client)
|
let output_name = crate::toplevel_export::pick_output_for(client)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("pick output for {}", client.address))?;
|
.with_context(|| format!("pick output for {}", client.address))?;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user