feat: cross-platform behaviour parity with dish-android - #12
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
FreedesktopScreenSaverInhibitor+ScreenWakeControllerflip theorg.freedesktop.ScreenSaver.InhibitD-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).PairResponse.reachable+PairingClient::classify(returnsstd::variant<Success, AuthRequired, Unreachable>) split the failure modes; the managerstd::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.pairAndConnectskips 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.GamepadInputProcessorcarries a per-deviceDeadzones { stickFlat, triggerFlat }table;SDLGamepadBridgepushes a default profile (~10 % stick / ~5 % trigger) on attach. Mirrors Android's per-deviceInputDevice.getMotionRange(axis).getFlat()pipeline.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-devalready pulls in QtDBus.Test plan
scripts/build.sh releasesucceeds on Ubuntu 22.04 / Fedora 38+ / Archscripts/build.sh debug testruns the full Catch2 suite — was 6 test binaries, this PR adds 2 (test_pairing_client_classify,test_screen_wake_controller) and extendstest_gamepad_input_processorwith 8 deadzone casesclang-format/clang-tidy -p build-debugclean (-Werrorbuild underdish_strict)qdbus org.freedesktop.ScreenSaver /org/freedesktop/ScreenSaver org.freedesktop.ScreenSaver.GetActivereturns true while streamingQT_LOGGING_RULES="dish.input.info=true"shows a singleDEVCAPSline per controller attachNotes
IOPMAssertioninstead of QtDBus).Qt6::DBus. On Debian/Ubuntu it's pulled in automatically byqt6-base-dev; Fedora/Arch ship it inqt6-qtbase-devel/qt6-baserespectively.