macOS: cached CGMainDisplayID goes stale after display re-enumeration; all mouse input collapses to (0,0)

Open
#150 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
68/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
cpp, macos

Research direction

Start in macos_backend.cpp around line 387 and trace MacosInputState, submit_absolute_motion(), and post_mouse(), focusing on how the cached display ID is used after display re-enumeration. Reproduce the monitor reconnect scenario and verify that both absolute and relative mouse input continue to map to the current display rather than a zero-sized bounds rectangle.

Written by the indexing model from the issue text.

Description

Bug

On macOS, MacosInputState caches CGMainDisplayID() once at construction (macos_backend.cpp:387). When the display later re-enumerates (monitor power cycle, DisplayPort link renegotiation), macOS assigns a new display ID and the cached ID becomes invalid. From that point on:

  • submit_absolute_motion() calls CGDisplayBounds(state_->display) on the stale ID, which returns a zero rect. scale_absolute_axis() then returns 0 for display_size <= 0, so every absolute mouse event is mapped to (0,0) — the cursor is pinned to the top-left corner.
  • post_mouse() clamps the target location into that same zero rect (std::clamp(raw, origin, origin + size - 1)), so relative motion is also trapped — observed cursor positions oscillate only between (0,0) and (-1,-1).

Restarting the consumer process (re-creating the state, hence re-caching the current display ID) restores input until the next display re-enumeration.

Evidence (observed via Sunshine v2026.914.233613 on Mac mini M4, macOS arm64)

  • Main display ID changes across monitor reconnects: 10 → 12 → 1 over consecutive days.
  • While the bug is active:
    • CGDisplayBounds(staleId 12)(0,0,0,0)
    • CGDisplayBounds(CGMainDisplayID() = 1)(0,0,1920,1080)
    • Client input packets arrive (UDP control channel has traffic), video capture is unaffected (capture re-enumerates per session), but the cursor never leaves the origin.
  • After restarting Sunshine, input works again — until the display ID changes once more.

Reproduction

  1. Start a libvirtualhid consumer (e.g. Sunshine) and note the main display ID.
  2. Force the display to re-enumerate: power-cycle the monitor, or unplug/replug it (many DP/USB-C monitors do this on their own when entering deep sleep).
  3. Send absolute or relative mouse input — cursor stays pinned at (0,0).

Suggested fix

Either:

  • Resolve the display at event time (CGMainDisplayID() in submit_absolute_motion / post_mouse instead of the cached value), or
  • Register CGDisplayRegisterReconfigurationCallback and update the cached display / display_scaling on reconfiguration events.

Additionally, guard against zero-sized CGDisplayBounds results (invalid/offline display) instead of clamping into a degenerate rect — e.g. fall back to CGMainDisplayID() bounds.

Happy to provide more diagnostics if needed.

Dominant language
C++
Stars
57
Forks
15
Avg merge
11h 15m
Merged PRs (30d)
33

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from LizardByte/libvirtualhid

All issues in LizardByte/libvirtualhid

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.