Compare commits
No commits in common. "4ee58acf2caa5025edaa18a6a5818af50ce5edcc" and "0ba3c59cc1babdd47b6ddffb1a2e2d3295a36051" have entirely different histories.
4ee58acf2c
...
0ba3c59cc1
@ -90,7 +90,7 @@
|
|||||||
the round-trip metadata (width/height/format) so callers can confirm
|
the round-trip metadata (width/height/format) so callers can confirm
|
||||||
the protocol path works. The module docblock of `toplevel_export` and
|
the protocol path works. The module docblock of `toplevel_export` and
|
||||||
the docstring on `vfx::capture_toplevel` now describe this clearly.
|
the docstring on `vfx::capture_toplevel` now describe this clearly.
|
||||||
Real pixel read is shipped (commit `0ba3c59`) via a runtime dlopen of `libgbm.so.1`; setup needs no `libgbm-dev` build dep.
|
Real pixel read is a follow-up after the gbm_bo_map work.
|
||||||
- **T9 (overlay live render):** already shipped as part of the T7..T15
|
- **T9 (overlay live render):** already shipped as part of the T7..T15
|
||||||
batch (). creates a wl_shm pool from a
|
batch (). creates a wl_shm pool from a
|
||||||
temp file containing pixels,
|
temp file containing pixels,
|
||||||
|
|||||||
@ -113,7 +113,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_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 = stub (grim fallback) |
|
||||||
|
|
||||||
`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.
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
# In-game macros (no addon, retail-shaped)
|
# In-game macros (no addon, retail-shaped)
|
||||||
|
|
||||||
enBoxer only sends keystrokes. Bind these in WoW on **every** account. **The example profile targets retail (The War Within era, 2026).**
|
enBoxer only sends keystrokes. Bind these in WoW on **every** account. **The example profile targets retail (The War Within era, 2026).** Forever is not out yet, so the macro set is whatever current retail uses. If Interact with Target is missing on a client, the loot map will not walk or loot until a later helper (not shipped).
|
||||||
|
|
||||||
If `Interact with Target` is missing on a particular client (private servers, classic, custom UIs that hide it), open the per-character macro override in the GUI and rebind `game_binds.interact` + `game_binds.ctm_on` / `ctm_off` to whatever those clients accept. The daemon's routing logic is unchanged; only the per-client WoW-side keybind changes.
|
|
||||||
|
|
||||||
Match keys to `game_binds` in the profile.
|
Match keys to `game_binds` in the profile.
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,6 @@ It is not a second monitor and not a window swap. The other client can sit behin
|
|||||||
|
|
||||||
The dispatcher in `src/vfx.rs::capture_for_source` checks `is_window_visible(monitors, client)`. If the source window sits on a visible output, `grim` captures it. If the window is covered and the operator has opted in to compositor-side capture (`ENBOXER_ENABLE_TOPLEVEL=1`), the dispatcher routes to `capture_toplevel` (zwlr_export_dmabuf_unstable_v1). With the gate off, covered windows fall through to `grim` and the operator sees whatever is currently on top of those pixels — exactly the previous behaviour.
|
The dispatcher in `src/vfx.rs::capture_for_source` checks `is_window_visible(monitors, client)`. If the source window sits on a visible output, `grim` captures it. If the window is covered and the operator has opted in to compositor-side capture (`ENBOXER_ENABLE_TOPLEVEL=1`), the dispatcher routes to `capture_toplevel` (zwlr_export_dmabuf_unstable_v1). With the gate off, covered windows fall through to `grim` and the operator sees whatever is currently on top of those pixels — exactly the previous behaviour.
|
||||||
|
|
||||||
So: keep the source window **on a visible output** for the default `grim` path. Set `ENBOXER_ENABLE_TOPLEVEL=1` when you want a stacked layout where some sources are fully covered by another; the live toplevel-export client reads pixels through `gbm_bo_map` (dlopen'd at runtime from `libgbm.so.1`) and writes a real RGBA8 PNG via the `png` crate.
|
So: keep the source window **on a visible output** for the default `grim` path. Set `ENBOXER_ENABLE_TOPLEVEL=1` when you want a stacked layout where some sources are fully covered by another; the live toplevel-export client is a follow-up and the dispatcher is ready for it.
|
||||||
|
|
||||||
The overlay itself is an `mpv` window (`class: enboxer-vfx`) that reloads each `grim` frame.
|
The overlay itself is an `mpv` window (`class: enboxer-vfx`) that reloads each `grim` frame.
|
||||||
|
|||||||
145
src/session.rs
145
src/session.rs
@ -23,23 +23,6 @@ pub struct Session {
|
|||||||
pub vfx_source: Option<u32>,
|
pub vfx_source: Option<u32>,
|
||||||
pub mouse_broadcast: bool,
|
pub mouse_broadcast: bool,
|
||||||
pub mouse_follow: bool,
|
pub mouse_follow: bool,
|
||||||
/// Per-button press-and-hold repeat state. Keyed by Linux input
|
|
||||||
/// event code (272 = BTN_LEFT, 273 = BTN_RIGHT). Cancelled when
|
|
||||||
/// the matching `mouse-release` IPC verb arrives.
|
|
||||||
pub mouse_repeats: std::collections::HashMap<u32, RepeatState>,
|
|
||||||
/// Cadence in milliseconds between repeat clicks while a button is
|
|
||||||
/// held. Tunable via `ENBOXER_MOUSE_REPEAT_MS`; defaults to 50 ms
|
|
||||||
/// (20 Hz) which feels responsive without saturating the IPC socket.
|
|
||||||
pub mouse_repeat_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-button repeat-loop handle + cancel signal. Holding the cancel
|
|
||||||
/// `Arc<AtomicBool>` in the Session lets the release path flip it
|
|
||||||
/// without holding the Session mutex; the worker checks the bit at
|
|
||||||
/// each cadence tick and exits cleanly.
|
|
||||||
pub struct RepeatState {
|
|
||||||
pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
|
||||||
pub handle: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
@ -54,12 +37,6 @@ impl Session {
|
|||||||
vfx_source: None,
|
vfx_source: None,
|
||||||
mouse_broadcast: false,
|
mouse_broadcast: false,
|
||||||
mouse_follow: false,
|
mouse_follow: false,
|
||||||
mouse_repeats: std::collections::HashMap::new(),
|
|
||||||
mouse_repeat_ms: std::env::var("ENBOXER_MOUSE_REPEAT_MS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
|
||||||
.filter(|&n| (1..=2000).contains(&n))
|
|
||||||
.unwrap_or(50),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -827,124 +804,10 @@ pub fn others_clients(slots: &[(u32, Client)], leader: u32) -> Vec<Client> {
|
|||||||
/// click are forwarded from the cursor position in
|
/// click are forwarded from the cursor position in
|
||||||
/// `broadcast_mirror_click`.
|
/// `broadcast_mirror_click`.
|
||||||
///
|
///
|
||||||
/// Handle the IPC verb `mouse-press <button>`. Fires one click
|
/// Press-and-hold guard is OUT OF SCOPE for T13 (Master has not requested
|
||||||
/// immediately so the operator gets no perceptible delay, then starts
|
/// it yet); the caller should hold the mouse binds as a follow-up.
|
||||||
/// a per-button repeat loop. Cancels any prior repeat for the same
|
/// T13-todo: press-and-hold guard — repeated button-down without release
|
||||||
/// button before installing the new one.
|
/// should re-fire on a configurable cadence.
|
||||||
pub async fn mouse_press(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
|
||||||
// Fire the first click synchronously through whichever path the
|
|
||||||
// session is in (mouse-broadcast or mirror mode). mirror-click
|
|
||||||
// no-ops outside of Mode::Mirror so the dispatch is harmless.
|
|
||||||
{
|
|
||||||
let g = session.lock().await;
|
|
||||||
let mode = g.engine.mode;
|
|
||||||
let mouse_broadcast = g.mouse_broadcast;
|
|
||||||
drop(g);
|
|
||||||
let r = if mode == Mode::Mirror {
|
|
||||||
broadcast_mirror_click(session, button).await
|
|
||||||
} else if mouse_broadcast {
|
|
||||||
broadcast_click(session, button).await
|
|
||||||
} else {
|
|
||||||
// Neither mode nor mouse_broadcast is on; the bind was
|
|
||||||
// installed anyway. Just no-op.
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
if let Err(e) = r {
|
|
||||||
tracing::warn!("mouse-press initial click failed: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cancel any prior repeat for this button.
|
|
||||||
{
|
|
||||||
let mut g = session.lock().await;
|
|
||||||
if let Some(prev) = g.mouse_repeats.remove(&button) {
|
|
||||||
prev.cancel
|
|
||||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
if let Some(h) = prev.handle {
|
|
||||||
h.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Spawn the repeat loop.
|
|
||||||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
||||||
let cancel_clone = cancel.clone();
|
|
||||||
let cadence = {
|
|
||||||
let g = session.lock().await;
|
|
||||||
g.mouse_repeat_ms
|
|
||||||
};
|
|
||||||
let session_clone = session.clone();
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
mirror_repeat_loop(session_clone, button, cadence, cancel_clone).await;
|
|
||||||
});
|
|
||||||
let mut g = session.lock().await;
|
|
||||||
g.mouse_repeats.insert(
|
|
||||||
button,
|
|
||||||
RepeatState {
|
|
||||||
cancel,
|
|
||||||
handle: Some(handle),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle the IPC verb `mouse-release <button>`. Cancels the per-button
|
|
||||||
/// repeat loop; the most recent click is the last one delivered.
|
|
||||||
pub async fn mouse_release(session: &Arc<Mutex<Session>>, button: u32) -> Result<()> {
|
|
||||||
let mut g = session.lock().await;
|
|
||||||
if let Some(mut state) = g.mouse_repeats.remove(&button) {
|
|
||||||
state.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
if let Some(h) = state.handle.take() {
|
|
||||||
h.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-button repeat loop. Fires `broadcast_click` (or
|
|
||||||
/// `broadcast_mirror_click` in mirror mode) every `cadence_ms` until
|
|
||||||
/// either the cancel bit is flipped by the matching release handler or the
|
|
||||||
/// session shuts down. Each click is independent: a failure is logged and
|
|
||||||
/// the loop continues. Cadence is captured at spawn time; changing
|
|
||||||
/// `mouse_repeat_ms` mid-hold does not affect the active repeat.
|
|
||||||
async fn mirror_repeat_loop(
|
|
||||||
session: Arc<Mutex<Session>>,
|
|
||||||
button: u32,
|
|
||||||
cadence_ms: u64,
|
|
||||||
cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
|
||||||
) {
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::time::Duration;
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(Duration::from_millis(cadence_ms)).await;
|
|
||||||
if cancel.load(Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let (mode, mouse_broadcast) = {
|
|
||||||
let g = session.lock().await;
|
|
||||||
(g.engine.mode, g.mouse_broadcast)
|
|
||||||
};
|
|
||||||
let r = if mode == Mode::Mirror {
|
|
||||||
broadcast_mirror_click(&session, button).await
|
|
||||||
} else if mouse_broadcast {
|
|
||||||
broadcast_click(&session, button).await
|
|
||||||
} else {
|
|
||||||
// User disabled mouse_broadcast while holding the button;
|
|
||||||
// cancel the repeat so we don't fire forever.
|
|
||||||
cancel.store(true, Ordering::Relaxed);
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
if let Err(e) = r {
|
|
||||||
tracing::warn!("mouse repeat click failed: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns one `(target_client, button)` pair per non-leader slot so
|
|
||||||
/// the daemon can broadcast a single mirror-mode click. Repeated
|
|
||||||
/// clicks on a held mouse button are handled by [`mirror_repeat_loop`]:
|
|
||||||
/// a per-button timer re-fires this same broadcast at a configurable
|
|
||||||
/// cadence until the matching release IPC verb cancels it. The
|
|
||||||
/// initial press IPC verb still calls this once on its own so the first
|
|
||||||
/// click is delivered immediately, not after the cadence delay.
|
|
||||||
pub fn mirror_clicks_to(
|
pub fn mirror_clicks_to(
|
||||||
slots: &[(u32, Client)],
|
slots: &[(u32, Client)],
|
||||||
leader: u32,
|
leader: u32,
|
||||||
|
|||||||
@ -558,10 +558,23 @@ impl Dispatch<wl_buffer::WlBuffer, ()> for ExportState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the plane-0 dmabuf as an RGBA8 byte buffer and write a PNG
|
// ---- T10 follow-up note ----
|
||||||
/// of it to `dest`. This is the public entry point used by callers
|
//
|
||||||
/// that want the raw pixels (the live `capture_toplevel` path goes
|
// The actual `gbm_bo_map` (import dmabuf → gbm_bo → map → read pixels →
|
||||||
/// through `write_pixels_via_gbm` instead).
|
// copy into a PNG-encoded buffer) is a documented follow-up. Wiring it
|
||||||
|
// in is one new dep (`gbm` + `drm-fourcc` + a DRM device handle) plus a
|
||||||
|
// ~50-line renderer that:
|
||||||
|
// 1. opens /dev/dri/renderD128,
|
||||||
|
// 2. creates a gbm_device,
|
||||||
|
// 3. imports the dmabuf fd (we have it from the `object` event),
|
||||||
|
// 4. gbm_bo_map(...) → *mut u8,
|
||||||
|
// 5. reads stride*height bytes, swizzles into the PNG writer above.
|
||||||
|
//
|
||||||
|
// Until then `write_synthetic_frame` produces the right-sized, format-
|
||||||
|
// labelled placeholder so callers can verify the wire path.
|
||||||
|
|
||||||
|
/// Public stub: where the gbm_bo_map read belongs. Kept as a function
|
||||||
|
/// so the test below can assert its shape.
|
||||||
pub fn read_pixels_via_gbm(frame: &DmabufFrame, dest: &Path) -> anyhow::Result<Vec<u8>> {
|
pub fn read_pixels_via_gbm(frame: &DmabufFrame, dest: &Path) -> anyhow::Result<Vec<u8>> {
|
||||||
read_pixels_via_gbm_full(frame, dest)
|
read_pixels_via_gbm_full(frame, dest)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user