Skip to content

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

Description

@Suluo0

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions