Skip to content

feat: cross-platform behaviour parity with dish-android - #12

Merged
emir-hasanbegovic merged 2 commits into
mainfrom
feature/cross-platform-parity
May 14, 2026
Merged

feat: cross-platform behaviour parity with dish-android#12
emir-hasanbegovic merged 2 commits into
mainfrom
feature/cross-platform-parity

Conversation

@emir-hasanbegovic

Copy link
Copy Markdown
Contributor

Summary

Brings the Linux client up to parity with the four desktop-applicable gaps identified against the dish-android reference (the PR #38#47 era of behaviour fixes). Each gap ships with parallel unit tests so the suite pins the contract instead of trusting it.

  • Display-sleep inhibitor while streamingFreedesktopScreenSaverInhibitor + ScreenWakeController flip the org.freedesktop.ScreenSaver.Inhibit D-Bus cookie on 0↔positive streaming-slot transitions. Works under every modern DE that implements the freedesktop portal (GNOME, KDE, Xfce, MATE, Cinnamon, Sway/swayidle).
  • Connection-state recovery (dish-android PR fix(packaging): bundle the Wayland client buffer integrations in the AppImage; 0.1.1 #43 parity)PairResponse.reachable + PairingClient::classify (returns std::variant<Success, AuthRequired, Unreachable>) split the failure modes; the manager std::visits into the three arms. A moved server now surfaces "Server unreachable — has it moved networks?" instead of trapping the user behind an unanswerable PIN.
  • Auto-reconnect saved-key fast pathpairAndConnect skips the TCP pair handshake entirely when the PIN is empty and a 64-char shared key is already on disk; a moved server then fails fast in the HTTP layer.
  • Per-device deadzonesGamepadInputProcessor carries a per-device Deadzones { stickFlat, triggerFlat } table; SDLGamepadBridge pushes a default profile (~10 % stick / ~5 % trigger) on attach. Mirrors Android's per-device InputDevice.getMotionRange(axis).getFlat() pipeline.
  • DEVCAPS log on attach — one-shot qCInfo(lcDishInput) line carrying SDL controller type, USB VID/PID, and SDL GUID for diagnostics on unfamiliar pads.

The README's new "Cross-platform behaviour parity" section narrates each item; the Testing section lists the new suites; the Debian build-deps note flags that qt6-base-dev already pulls in QtDBus.

Test plan

  • scripts/build.sh release succeeds on Ubuntu 22.04 / Fedora 38+ / Arch
  • scripts/build.sh debug test runs the full Catch2 suite — was 6 test binaries, this PR adds 2 (test_pairing_client_classify, test_screen_wake_controller) and extends test_gamepad_input_processor with 8 deadzone cases
  • clang-format / clang-tidy -p build-debug clean (-Werror build under dish_strict)
  • CodeQL / OSV-Scanner / gitleaks workflows green
  • Manual: pair, bind, confirm qdbus org.freedesktop.ScreenSaver /org/freedesktop/ScreenSaver org.freedesktop.ScreenSaver.GetActive returns true while streaming
  • Manual: bind a controller, drift the sticks slightly, confirm the noise is filtered (look at the telemetry counters in the footer)
  • Manual: take a paired satellite offline, re-launch the app, confirm the error toast reads "Server unreachable — has it moved networks?" instead of pulling up the PIN dialog
  • Manual: QT_LOGGING_RULES="dish.input.info=true" shows a single DEVCAPS line per controller attach

Notes

  • Authored from a Windows workstation; build/test verification is on the reviewer.
  • The reciprocal dish-mac PR ships the same parity work in parallel — same architecture, different platform primitives (IOKit IOPMAssertion instead of QtDBus).
  • New CMake dependency: Qt6::DBus. On Debian/Ubuntu it's pulled in automatically by qt6-base-dev; Fedora/Arch ship it in qt6-qtbase-devel / qt6-base respectively.

Brings the Linux client up to parity with the four desktop-applicable gaps
identified against the dish-android reference (PR #38-#47 era). All four
ship with parallel unit tests so the suite pins the contract instead of
trusting it.

* Display-sleep inhibitor while streaming.
  - New DisplaySleepInhibitor abstract + FreedesktopScreenSaverInhibitor
    backed by org.freedesktop.ScreenSaver.Inhibit on the session bus —
    the portal every modern desktop environment honours (GNOME, KDE,
    Xfce, MATE, Cinnamon, Sway/swayidle).
  - New ScreenWakeController owns the streamingSlotCount derivation
    (hub.bindings x hub.connections) and flips the D-Bus cookie on
    0<->positive transitions only — same-count emissions are no-ops so a
    noisy hub feed doesn't thrash the session bus.
  - Wired into AppModel::rebuild() so every binding / state transition
    feeds an update.
  - CMakeLists: Qt6::DBus added to find_package + linked into dish_core.
  - Mirrors dish-android :: WakeStateController and dish-mac ::
    IOKitDisplaySleepInhibitor.

* Connection-state recovery (dish-android PR #43 parity).
  - PairResponse gains a client-side reachable flag (not on the wire);
    fromJson sets it to true after a successful body parse, the
    makeError helper in PairingClient leaves it false.
  - New PairingClient::classify -> std::variant<Success, AuthRequired,
    Unreachable>. The manager fans those out via std::visit so a
    moved/offline server surfaces "Server unreachable — has it moved
    networks?" instead of trapping the user behind an unanswerable PIN.

* Auto-reconnect saved-key fast path.
  - pairAndConnect now checks for a 64-char shared key on disk before
    starting the async pair watcher. If present and the PIN is empty
    (auto-reconnect path), we skip pair entirely and go straight to
    openSession. A moved server then fails fast in the HTTP layer
    rather than bouncing through pair -> PairingRequired.

* Per-device deadzones.
  - GamepadInputProcessor carries a per-device Deadzones { stickFlat,
    triggerFlat } table; reports are filtered (|v| <= flat -> 0)
    before they leave the processor.
  - Pure applyDeadzones helper extracted so tests can pin the
    arithmetic without the lock plumbing.
  - SDLGamepadBridge pushes a default profile (~10% stick / ~5%
    trigger) when each controller attaches; remove() now clears
    deadzones too.
  - Mirrors Android's InputDevice.getMotionRange(axis).getFlat()
    pipeline; SDL2 has no OS-level equivalent so the default is the
    noise floor we ship.

* DEVCAPS one-shot log on attach.
  - SDLGamepadBridge now emits a qCInfo(lcDishInput) line on every
    SDL_CONTROLLERDEVICEADDED carrying id, name, SDL controller type
    (negotiated mapping), USB VID/PID, and SDL GUID. Same diagnostic
    shape as Android's SatelliteJNI DEVCAPS log.

Tests
-----
* test_pairing_client_classify (8 cases) — pins every classify arm,
  the empty-sharedKey-with-ok=true defensive guard, and the
  fromJson-sets-reachable invariant.
* test_screen_wake_controller (11 cases) — streamingCount derivation
  across the 0/1/N + idle/connecting/connected matrix, then the
  acquire/release lifecycle through a FakeInhibitor; includes a
  null-inhibitor tolerance case.
* test_gamepad_input_processor (+8 cases) — applyDeadzones across
  sticks, triggers, and buttons; per-device application via publish;
  remove clears the deadzone state too.

Docs
----
* README "Cross-platform behaviour parity" section explains each gap +
  its remediation; testing section lists the new suites; the Debian
  build-deps note flags that qt6-base-dev already pulls in QtDBus.

Build verification is on the user — this commit was authored from a
Windows workstation that can't run cmake/ctest.
test_screen_wake_controller already covers the abstract
DisplaySleepInhibitor contract via a FakeInhibitor, but the production
QtDBus-backed impl was previously runtime-only. This adds direct coverage
so the inhibit-cookie lifecycle isn't a "tested on the user's desktop
when they pair a controller" surface.

The freedesktop ScreenSaver service may not be present on a headless CI
runner (no session bus, no DE) — the inhibitor handles that gracefully
by silently failing the acquire when QDBusInterface.isValid() returns
false. The test pins the bits that don't depend on the bus actually
answering: default state, idempotent release, no-op re-acquire, dtor
behaviour on a never-held instance.

A QtAppSingleton helper provides a single shared QCoreApplication for
the whole test binary so QDBus initialisation doesn't spam stderr.

Cases pinned (4):
* default-unheld state
* never-crashes-regardless-of-session-bus
* dtor on a never-acquired instance is a no-op
* dtor on a held (or acquire-attempted) instance is safe
@emir-hasanbegovic
emir-hasanbegovic merged commit ca5be37 into main May 14, 2026
0 of 7 checks passed
@emir-hasanbegovic
emir-hasanbegovic deleted the feature/cross-platform-parity branch May 14, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant