fix(desktop): scale HiDPI pointer coords into logical space (VA-111) #119

Merged
David merged 1 commit from fix/VA-111-hidpi-pointer-scale into main 2026-07-06 06:33:30 +02:00
Owner

What

Absolute pointer positioning on the Desktop tab was off by the display-scale factor (2x on a scale=2 HiDPI output, desktop-02). The SPA sends pointer coordinates in the captured frame's physical pixel space (grim), but the Wayland input paths inject in the compositor's logical coordinate space, so every move travelled twice as far. This divides the incoming coordinate down to logical space before injection, keeping the agent authoritative over the input coordinate space. scale=1 sessions are bit-for-bit unchanged.

How

  • uinput path (kept a relative device per the libinput ABS+buttons reclassification warning in the module header): move_abs now takes the display scale and divides the frame-pixel coordinate by it (to_logical_coord) before computing the relative delta, instead of ignoring its old width/height args (VA-111 AC4).
  • portal path: new pointer_motion_absolute_from_frame maps the frame-pixel coordinate into the ScreenCast stream's own coordinate space via Stream::size() (map_frame_coord), rather than passing raw tab pixels to NotifyPointerMotionAbsolute (VA-111 AC3).
  • Scale discovery (resolve_pointer_scale, preference order): the SPA reports the compositor's logical desktop size via a new inbound command (cmd 90), the authoritative compositor-agnostic source; absent that, the wlroots-only wlr-randr Scale: value (primary_output_scale / parse_wlr_randr_scale); absent both, scale collapses to 1.0. Regression-safe by construction.

Decisions (confirmed with maintainer)

  • Keep the uinput pointer relative and divide by scale, rather than switching to an absolute device (which libinput would reclassify and drop, killing all input).
  • The SPA supplies the logical desktop size as the primary scale source (cmd 90); wlr-randr is the wlroots fallback.

Tests

Pure transforms carry unit tests covering scale 1.0 identity, 2.0 halving, and fractional 1.5 rounding: to_logical_coord, map_frame_coord, parse_wlr_randr_scale, resolve_pointer_scale. Full just check (clippy + fmt + test) green, plus cargo clippy --features vervain-agent-kvm/wayland-portal to cover the feature-gated portal path that the default gate skips.

Follow-ups (filed + linked)

  • VA-117 (is required for VA-111): SPA send-side of cmd 90. Needed to close AC1 on KWin / Mutter, where no in-process logical-size source exists. The agent receive-side is in this PR and is inert until the SPA sends it.
  • VA-118 (relates to VA-111): the MCP kvm_input_mouse_move action and the native VNC pointer path both hardcode the X11 XTEST backend and need the same session-based routing + scaling. Found in the completeness sweep.

Not covered here

Live cursor-coincidence validation on scale=1 / scale=2 / multi-monitor HiDPI hardware (VA-111 AC1/AC2/AC4 "Validate ...") is manual QA the CI sandbox cannot perform (no Wayland, no /dev/uinput, no HiDPI display).

## What Absolute pointer positioning on the Desktop tab was off by the display-scale factor (2x on a scale=2 HiDPI output, desktop-02). The SPA sends pointer coordinates in the captured frame's physical pixel space (grim), but the Wayland input paths inject in the compositor's logical coordinate space, so every move travelled twice as far. This divides the incoming coordinate down to logical space before injection, keeping the agent authoritative over the input coordinate space. scale=1 sessions are bit-for-bit unchanged. ## How - **uinput path** (kept a relative device per the libinput ABS+buttons reclassification warning in the module header): `move_abs` now takes the display scale and divides the frame-pixel coordinate by it (`to_logical_coord`) before computing the relative delta, instead of ignoring its old width/height args (VA-111 AC4). - **portal path**: new `pointer_motion_absolute_from_frame` maps the frame-pixel coordinate into the ScreenCast stream's own coordinate space via `Stream::size()` (`map_frame_coord`), rather than passing raw tab pixels to `NotifyPointerMotionAbsolute` (VA-111 AC3). - **Scale discovery** (`resolve_pointer_scale`, preference order): the SPA reports the compositor's logical desktop size via a new inbound command (cmd 90), the authoritative compositor-agnostic source; absent that, the wlroots-only `wlr-randr` `Scale:` value (`primary_output_scale` / `parse_wlr_randr_scale`); absent both, scale collapses to 1.0. Regression-safe by construction. ## Decisions (confirmed with maintainer) - Keep the uinput pointer relative and divide by scale, rather than switching to an absolute device (which libinput would reclassify and drop, killing all input). - The SPA supplies the logical desktop size as the primary scale source (cmd 90); wlr-randr is the wlroots fallback. ## Tests Pure transforms carry unit tests covering scale 1.0 identity, 2.0 halving, and fractional 1.5 rounding: `to_logical_coord`, `map_frame_coord`, `parse_wlr_randr_scale`, `resolve_pointer_scale`. Full `just check` (clippy + fmt + test) green, plus `cargo clippy --features vervain-agent-kvm/wayland-portal` to cover the feature-gated portal path that the default gate skips. ## Follow-ups (filed + linked) - VA-117 (is required for VA-111): SPA send-side of cmd 90. Needed to close AC1 on KWin / Mutter, where no in-process logical-size source exists. The agent receive-side is in this PR and is inert until the SPA sends it. - VA-118 (relates to VA-111): the MCP `kvm_input_mouse_move` action and the native VNC pointer path both hardcode the X11 XTEST backend and need the same session-based routing + scaling. Found in the completeness sweep. ## Not covered here Live cursor-coincidence validation on scale=1 / scale=2 / multi-monitor HiDPI hardware (VA-111 AC1/AC2/AC4 "Validate ...") is manual QA the CI sandbox cannot perform (no Wayland, no /dev/uinput, no HiDPI display).
fix(desktop): scale HiDPI pointer coords into logical space (VA-111)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 16m10s
Create release / Create release from merged PR (pull_request) Has been skipped
cc37e80b2e
Absolute pointer positioning on the Desktop tab was off by the display-scale factor (2x on a scale=2 output): the SPA sends coordinates in the captured frame's physical pixel space (grim), but the Wayland input paths inject in the compositor's logical coordinate space, so on a HiDPI output every move travelled twice as far. This keeps the agent authoritative over the input coordinate space and divides the incoming coordinate down to logical space before injection, so scale=1 sessions are bit-for-bit unchanged.

uinput path (relative device, kept relative per the libinput ABS+buttons reclassification warning): `move_abs` now takes the display scale and divides the frame-pixel coordinate by it (`to_logical_coord`) before computing the relative delta, instead of ignoring its old width/height args.

portal path: new `pointer_motion_absolute_from_frame` maps the frame-pixel coordinate into the ScreenCast stream's own coordinate space via `Stream::size()` (`map_frame_coord`), rather than passing raw tab pixels to `NotifyPointerMotionAbsolute`.

Scale discovery: the SPA reports the compositor's logical desktop size via a new inbound command (cmd 90), the authoritative compositor-agnostic source; absent that, the wlroots-only `wlr-randr` `Scale:` value is parsed as a fallback (`primary_output_scale` / `parse_wlr_randr_scale`); absent both, scale collapses to 1.0. `resolve_pointer_scale` encodes that preference order and is regression-safe by construction.

The pure transforms (`to_logical_coord`, `map_frame_coord`, `parse_wlr_randr_scale`, `resolve_pointer_scale`) carry unit tests covering scale 1.0 identity, 2.0 halving, and fractional 1.5 rounding. Live cursor-coincidence validation on scale=1 / scale=2 / multi-monitor HiDPI hardware is manual QA the CI sandbox cannot perform.

#VA-111

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David merged commit 38f0e83462 into main 2026-07-06 06:33:30 +02:00
David deleted branch fix/VA-111-hidpi-pointer-scale 2026-07-06 06:33:30 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!119
No description provided.