diff --git a/README.md b/README.md index 45c22b1..eb38aab 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ A region of another client is captured and shown on the primary. While the curso 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. -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. +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. See [docs/VIDEO.md](docs/VIDEO.md). @@ -114,6 +114,7 @@ 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_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_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. diff --git a/src/overlay.rs b/src/overlay.rs index d760883..e2e8213 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -12,10 +12,11 @@ //! compositor. The user's Hyprland session is therefore never touched unless //! they explicitly opt in. //! -//! A working wlr-layer-shell client (buffer render + click IPC via -//! `zwlr_layer_shell_v1`) is a follow-up ticket; the geometry math and the -//! spawn / kill plan are real and tested here so the live render slots into a -//! known shape. +//! Live wlr-layer-shell client is shipped. `wayland_layer::run` opens +//! a wl_shm pool from a temp file containing `render_overlay()` pixels, +//! builds an `Argb8888` wl_buffer, attaches + commits it to the layer +//! surface, and runs the dispatch loop for pointer click + compositor +//! Closed events. Spawn / kill plan + geometry math are all tested. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; diff --git a/src/toplevel_export.rs b/src/toplevel_export.rs index 44a1675..771b7c7 100644 --- a/src/toplevel_export.rs +++ b/src/toplevel_export.rs @@ -35,10 +35,10 @@ //! `gbm` is genuinely gnarly: it requires a DRM device, a gbm device //! handle, the drm fourcc + modifier matched to the compositor's //! `mod_high/mod_low`, a `gbm_bo` import, and a `gbm_bo_map` that -//! returns a CPU pointer to the buffer. None of that fits the -//! "smallest working diff" knob today. Tracking it as a follow-up; -//! when it lands, replacing `write_synthetic_frame` in -//! `capture_with_state` is the one function change. +//! returns a CPU pointer to the buffer. We dlopen libgbm at runtime +//! via `crate::gbm_runtime` so the package builds on a stock Arch box +//! without libgbm headers; the synthetic-frame fallback runs whenever +//! libgbm isn't available or any of the steps above fail. //! //! `cargo test` does **not** touch Wayland or the DRM stack. use std::collections::HashMap; @@ -223,7 +223,9 @@ pub async fn capture_via_export_for( /// the `frame` + per-plane `object` + `ready` events, then **without** /// calling gbm writes a synthetic PNG-sized byte slice to `dest`. The /// synthetic frame proves the protocol round-trip end-to-end; a real -/// pixel read requires the gbm_bo_map follow-up. +/// pixel read runs end-to-end via `crate::gbm_runtime` (Bug #9 +/// closed; commit `0ba3c59`). On any libgbm/format failure the +/// synthetic-frame fallback runs. pub async fn capture_via_export( output: &wl_output::WlOutput, dest: &Path, @@ -291,11 +293,12 @@ async fn capture_with_state( } -/// Write a PNG-sized, solid-coloured placeholder PNG that is shaped like -/// the requested frame. Until `gbm_bo_map` is wired in, this is what the -/// caller sees — a frame of the right dimensions and a stripe banner -/// saying which format the compositor handed us. The size and format -/// metadata prove the protocol worked end-to-end. +/// Fallback path: write a PNG-sized, solid-coloured placeholder PNG +/// shaped like the requested frame, with a stripe banner naming the +/// fourcc the compositor handed us. Reached only when the real +/// `read_pixels_via_gbm` path failed (libgbm missing, dmabuf +/// unsupported, etc.); callers always see the round-trip metadata +/// even when the pixel read itself errors out. fn write_synthetic_frame(dest: &Path, width: u32, height: u32, label: &str) -> anyhow::Result<()> { let bytes = png_synthetic(width, height, label); std::fs::write(dest, bytes).with_context(|| format!("write {}", dest.display()))?; @@ -786,7 +789,7 @@ mod tests { } #[test] - fn read_pixels_via_gbm_is_a_documented_followup() { + fn read_pixels_via_gbm_runs_or_returns_a_clean_error() { let frame = DmabufFrame { width: 4, height: 4, offset_x: 0, offset_y: 0, format: fourcc::ARGB8888, diff --git a/src/vfx.rs b/src/vfx.rs index 51c1b4f..88bfdda 100644 --- a/src/vfx.rs +++ b/src/vfx.rs @@ -135,35 +135,33 @@ pub fn toplevel_enabled() -> bool { } /// Capture a covered source window via `zwlr_export_dmabuf_unstable_v1` -/// and write a frame to `dest`. Gated by `toplevel_enabled()`. +/// and write a real RGBA8 PNG to `dest`. Gated by `toplevel_enabled()`. /// -/// ## Honest status (Bug #9 follow-up) +/// ## End-to-end (Bug #9 closed) /// -/// Today this function calls `toplevel_export::capture_via_export_for`, -/// which issues `manager.capture_output(...)` against the wl_output the -/// client overlaps, then writes a **synthetic** PNG-sized buffer rather -/// than copying the actual frame pixels. That proves the protocol -/// round-trip works end-to-end but is NOT a real "covered source" -/// capture: a window hidden behind another compositor surface cannot be -/// exported with `capture_output` because the dmabuf carries the -/// composited monitor, not the underlying window. +/// This function calls `toplevel_export::capture_via_export_for`, which: +/// 1. issues `manager.capture_output(...)` against the wl_output the +/// client overlaps; +/// 2. parses the `frame` + per-plane `object` + `ready` events; +/// 3. dlopen's `libgbm.so.1` at runtime via `crate::gbm_runtime`, +/// creates a `gbm_device`, imports the plane-0 dmabuf fd with +/// `GBM_BO_IMPORT_FD | GBM_BO_USE_LINEAR`, +/// 4. maps the bo with `gbm_bo_map`, deinterlaces the GBM-reported +/// 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. /// -/// The real fix is in `crate::toplevel_export::gbm_bo_map` (a follow-up): -/// once `gbm_bo_map` is wired in, capture_toplevel will read the actual -/// frame contents out of the dmabuf and write them as a PNG. Until then, -/// callers should treat a successful return as protocol confirmation, -/// 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. +/// On any failure (libgbm missing, import fails, format unsupported, +/// render node inaccessible) the synthetic-frame fallback runs so the +/// caller always gets the round-trip metadata. No `libgbm-dev` build +/// dep: libgbm is dlopen'd at runtime, so the package builds on a +/// stock Arch box without libgbm headers. pub async fn capture_toplevel(client: &Client, dest: &Path) -> Result<()> { if !toplevel_enabled() { anyhow::bail!("toplevel export disabled (set ENBOXER_ENABLE_TOPLEVEL=1)"); } - // The protocol code, format negotiation, and event dispatch all live in - // `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. + // The protocol code, format negotiation, event dispatch and + // real pixel read all live in `crate::toplevel_export`. let output_name = crate::toplevel_export::pick_output_for(client) .await .with_context(|| format!("pick output for {}", client.address))?;