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:
2026-07-27 23:51:41 +02:00
co-authored by Claude Opus 5
parent ea0c61fb0f
commit a98174ebfa
3 changed files with 182 additions and 8 deletions
+130
View File
@@ -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(())
}
+14
View File
@@ -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),