feat(host): anchor-test, and libei says which output absolute input landed on
The absolute-region ladder existed for one case a unit test can only simulate — two heads of the SAME size, where matching a libei region by the streamed mode is a coin flip — and that case had never been run against a real compositor. It was also unobservable: the log said which regions the EIS server offered, never which one a coordinate went into, so "the pointer is on the wrong monitor" could only be discovered by looking at a monitor. libei now reports the chosen region once per distinct answer, beside the existing miss warning: one covers an anchor that named nothing, this one covers an anchor that matched something, by saying which. `anchor-test` is the gate that reads it, in the shape mirror-test established: it enumerates the heads, SAYS whether the rig actually has two of the same size (a green run on a single-head box proves nothing), anchors at a named one (or `--none` for the A/B that makes the anchored run mean anything), and walks the corners. It refuses to run off libei rather than emitting a green result about a ladder that isn't there. On-glass on KWin 6.7.3 with `kwin_wayland --virtual --output-count 2` (regions 1920x1080+0+0 and +1920+0): unanchored picks +0+0, anchored at Virtual-1 picks +1920+0, anchored at Virtual-0 picks +0+0. That is open item 3 of design/per-monitor-portal-capture.md §8b, closed. Clearing the pin now logs too — setting one always did, and the streamed screen going back to virtual is the same size of change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -597,3 +597,133 @@ pub fn mirror_test(args: &[String]) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Aim absolute input at a named monitor and prove where it landed — the on-glass gate for the
|
||||
/// input-region ladder (`design/per-monitor-portal-capture.md` §7.2), without needing a client.
|
||||
///
|
||||
/// The ladder exists for one case a unit test can only simulate: **two heads of the same size**,
|
||||
/// where matching a libei region by the streamed mode is a coin flip and the pointer silently ends
|
||||
/// up on the wrong screen. This drives the real thing — the compositor's own EIS regions, the real
|
||||
/// anchor, the real resolver — and prints the region absolute coordinates actually mapped into, so
|
||||
/// "it went to the right monitor" is something you read rather than infer.
|
||||
///
|
||||
/// `--monitor <CONNECTOR>` anchors at that head's origin (default: the `PUNKTFUNK_CAPTURE_MONITOR` /
|
||||
/// policy pin); `--none` deliberately runs UNANCHORED, which is the A/B that makes the anchored run
|
||||
/// mean something on a same-size pair. It then walks the corners and centre of a `--width`×`--height`
|
||||
/// client surface so an observer can watch the pointer.
|
||||
///
|
||||
/// Read the answer from the log line `libei: absolute input maps into this output`.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn anchor_test(args: &[String]) -> Result<()> {
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use std::time::Duration;
|
||||
let arg = |name: &str| {
|
||||
args.iter()
|
||||
.skip_while(|a| a.as_str() != name)
|
||||
.nth(1)
|
||||
.cloned()
|
||||
};
|
||||
let unanchored = args.iter().any(|a| a == "--none");
|
||||
let w: u32 = arg("--width").and_then(|s| s.parse().ok()).unwrap_or(1920);
|
||||
let h: u32 = arg("--height").and_then(|s| s.parse().ok()).unwrap_or(1080);
|
||||
|
||||
let compositor = crate::vdisplay::detect()?;
|
||||
let monitors = crate::vdisplay::monitors::list(compositor)?;
|
||||
println!(
|
||||
"anchor-test: {compositor:?} has {} monitor(s):",
|
||||
monitors.len()
|
||||
);
|
||||
for m in &monitors {
|
||||
println!(
|
||||
" {:<12} {:>13} at +{},+{}",
|
||||
m.connector,
|
||||
m.mode_label(),
|
||||
m.x,
|
||||
m.y
|
||||
);
|
||||
}
|
||||
// Two heads at the same size is the case the ladder exists for; say so when the rig is right,
|
||||
// and say so when it is NOT — a green run on a single-head box proves nothing about it.
|
||||
let same_size = monitors.iter().enumerate().any(|(i, a)| {
|
||||
monitors
|
||||
.iter()
|
||||
.skip(i + 1)
|
||||
.any(|b| a.width == b.width && a.height == b.height)
|
||||
});
|
||||
println!(
|
||||
"anchor-test: two same-size heads present: {} {}",
|
||||
same_size,
|
||||
if same_size {
|
||||
"— this run exercises the case the ladder exists for"
|
||||
} else {
|
||||
"— WEAK RIG: size matching would have picked correctly anyway"
|
||||
}
|
||||
);
|
||||
|
||||
if unanchored {
|
||||
crate::inject::set_absolute_anchor(None);
|
||||
println!("anchor-test: UNANCHORED (--none) — the size/first rungs decide");
|
||||
} else {
|
||||
let want = arg("--monitor")
|
||||
.or_else(crate::vdisplay::capture_monitor)
|
||||
.context("no monitor named — pass --monitor <CONNECTOR>, or --none for the A/B")?;
|
||||
let m = crate::vdisplay::monitors::resolve(&monitors, &want)?;
|
||||
crate::inject::set_absolute_anchor(Some(crate::inject::AbsoluteAnchor {
|
||||
origin: Some((m.x, m.y)),
|
||||
mapping_id: None,
|
||||
}));
|
||||
println!(
|
||||
"anchor-test: anchored at {} +{},+{} ({})",
|
||||
m.connector,
|
||||
m.x,
|
||||
m.y,
|
||||
m.mode_label()
|
||||
);
|
||||
}
|
||||
|
||||
let backend = crate::inject::default_backend();
|
||||
if backend != crate::inject::Backend::Libei {
|
||||
// The ladder is libei's; on any other backend this command would report nothing about it.
|
||||
// Say so rather than emitting a green run that means nothing (sway injects via WlrVirtual,
|
||||
// which is why the sway box cannot serve as this rig — set PUNKTFUNK_INPUT_BACKEND=libei on
|
||||
// a compositor that speaks EI).
|
||||
anyhow::bail!(
|
||||
"input backend is {backend:?}, not libei — the absolute-region ladder only exists on \
|
||||
the libei backend; set PUNKTFUNK_INPUT_BACKEND=libei"
|
||||
);
|
||||
}
|
||||
let mut inj = crate::inject::open(backend)?;
|
||||
// libei establishes its portal/EIS session + device resume asynchronously; events before then
|
||||
// are dropped (and it is the resume that publishes the regions we are testing).
|
||||
std::thread::sleep(Duration::from_secs(4));
|
||||
|
||||
let flags = (w << 16) | (h & 0xffff);
|
||||
let pts = [
|
||||
(w as i32 / 2, h as i32 / 2),
|
||||
(60, 60),
|
||||
(w as i32 - 60, 60),
|
||||
(w as i32 - 60, h as i32 - 60),
|
||||
(60, h as i32 - 60),
|
||||
(w as i32 / 2, h as i32 / 2),
|
||||
];
|
||||
println!("anchor-test: walking {w}x{h} — centre, four corners, centre (1s apart)");
|
||||
for (x, y) in pts {
|
||||
let e = InputEvent {
|
||||
kind: InputKind::MouseMoveAbs,
|
||||
_pad: [0; 3],
|
||||
code: 0,
|
||||
x,
|
||||
y,
|
||||
flags,
|
||||
};
|
||||
if let Err(err) = inj.inject(&e) {
|
||||
tracing::warn!(error = %format!("{err:#}"), "anchor-test: inject failed");
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
println!(
|
||||
"anchor-test: done — read the `libei: absolute input maps into this output` line above \
|
||||
for the region that was chosen"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -205,6 +205,16 @@ pub(crate) fn refresh_capture_monitor_anchor(context: &str) {
|
||||
let Some(want) = pf_vdisplay::capture_monitor() else {
|
||||
// No pin (or the console just cleared one): stop aiming input at a monitor we are no
|
||||
// longer mirroring, or a later virtual-display session inherits a stale anchor.
|
||||
// Setting a pin says so in the log; clearing one is the same size of change to what the
|
||||
// host streams, so say that too — but only when there WAS one, or every unpinned host
|
||||
// logs this line at startup for no reason.
|
||||
if pf_inject::absolute_anchor().is_some() {
|
||||
tracing::info!(
|
||||
context,
|
||||
"capture monitor: cleared — sessions create a virtual display again and absolute \
|
||||
input is no longer anchored"
|
||||
);
|
||||
}
|
||||
pf_inject::set_absolute_anchor(None);
|
||||
return;
|
||||
};
|
||||
@@ -532,6 +542,10 @@ fn real_main() -> Result<()> {
|
||||
// on-glass gate, with no client involved.
|
||||
#[cfg(target_os = "linux")]
|
||||
Some("mirror-test") => devtest::mirror_test(&args),
|
||||
// Aim absolute input at a named monitor and report which libei region it landed in — the
|
||||
// on-glass gate for the input-region ladder, with no client involved.
|
||||
#[cfg(target_os = "linux")]
|
||||
Some("anchor-test") => devtest::anchor_test(&args),
|
||||
// Create a virtual DualSense via UHID and exercise it (validation, no streaming session).
|
||||
#[cfg(target_os = "linux")]
|
||||
Some("dualsense-test") => devtest::dualsense_test(&args),
|
||||
|
||||
Reference in New Issue
Block a user