Tags: Ginger-Beard/ControllerManager
Tags
Kernel-driven hot-plug + enumerator cache + Profile-tab Refresh - DeviceChangeNotifier: CM_Register_Notification on the HID interface GUID with a 200ms debounce so composite-device arrival bursts collapse into one DevicesChanged event. Replaces the 2s DispatcherTimer in DevicesViewModel and the 1s Task.Delay loop in LaunchOrchestrator.StartHotPlugEnforcer. Steady-state Moonlight no longer hammers the HID stack with CreateFileW every 1-2s, which was driving PnP churn against ViGEm-backed virtual gamepads and input lag in the stream. - DeviceEnumerator: per-InstanceId cache for HidD_Get* results, link, and ContainerId. Stale entries auto-pruned. Invalidate(ids) / InvalidateAll() expose explicit refresh paths; ToggleEnabled invalidates touched devices (HidHide flip changes access state), and the user-facing Refresh paths flush the whole cache. - Profile-tab Refresh button next to "Show all devices" — manual fallback for the rare case a kernel notification misses something. - DevicesViewModel double-refresh: immediate refresh on notification for responsive UI, then a silent re-tap ~750ms later to catch HidHide's filter once it has fully settled for the new InstanceId. Without it, freshly hot-plugged devices showed IsEnabled=true even when persistently hidden, until manual Refresh. - README troubleshooting: note that Steam can still toast "Controller Detected" for HidHide-hidden devices (alt enum paths bypass HidHide), point users at Steam's per-device Disable Steam Input.
Live profile reapply + 2s device auto-refresh - LaunchOrchestrator.ReapplyActiveProfile: saving a profile while it's the active session now recomputes keep/hide against live devices and pushes the new set immediately. Lets users add a Moonlight/Artemis client's virtual gamepad to the running profile and have it unhide right away. Wired from GamesViewModel.SaveProfileCommand. - DevicesViewModel auto-refresh: DispatcherTimer ticks every 2s and runs a silent Refresh — no StatusText flicker, no Refresh-button enabled-state flicker (separate _refreshInFlight guard so silent ticks don't touch IsRefreshing). Picker piggybacks on Devices.CollectionChanged, so it rebuilds only when devices actually change. - README: explain ViGEm virtual gamepad's "show up after first input" quirk in the Sunshine/Apollo cookbook.
HidHide: fall back to system-wide hide for no-exe sessions ApplyState's session branch wrote Inverse=true with an empty deny list when gameExePath was empty (Sunshine/Apollo profiles after the --restore work), which means 'deny no one' — devices were blacklisted but visible to every process. Now: no-exe sessions switch to Inverse=false, Whitelist=[CM only], so the hide is system-wide. Trades the 'other apps still see the controllers' property for actually hiding from whoever the streaming host launches as the game.
Security: fix all CodeQL errors/warnings + bump deps Resolves the 1 error and 16 warnings from the first CodeQL scan: - Suppress invalid-dynamic-call on intentional WScript.Shell COM late-binding - Guard Dispose calls with try/finally or try/catch where exceptions could bypass cleanup (DeviceEnumerator, LaunchOrchestrator x2, DevicesViewModel) - using var on MemoryStream in HidHideClient - Remove dead dragVm assignment and redundant as-cast in GamesView - Remove unnecessary unchecked((int)...) casts in HidApi and HidHideClient - Use > 0 instead of != 0 for float comparison in ProfileEditorViewModel Also incorporates all 5 open Dependabot PRs: - codeql-action v3 → v4 (+ exclude app/obj/** from analysis) - actions/checkout v4 → v6 - actions/setup-dotnet v4 → v5 - softprops/action-gh-release v2 → v3 - System.Management 10.0.0 → 10.0.8
Docs + code review: align with current behavior, drop dead state DEVELOPMENT.md was carrying ~10 stale references from before the recent behavioral changes (additive reveal timing, checkbox-instead-of-dropdown, Always Hidden removal from UI, HidHide fork drop, button hides). All of those are now fixed: - 'Reveal phase triggers' section completely rewritten for the two-phase wait-then-loop architecture. The old 'early-fire optimization, not a gate' framing is gone (it IS the gate now, with a 60s timeout fallback to Timer-mode semantics). - 'What the app is' clarifies the UI has 2 roles; AlwaysHidden remains in the data model for backwards compat but isn't user-selectable. - 'Slot-commit grace' references the correct UI field name. - 'FirstDeviceAcquisitionWatcher path matching' notes the watcher has no timeout of its own; the 60s lives in RevealDisableThenRestore. - 'Four supported use cases', 'Profile editor (current UI shape)' lose AlwaysHidden role assignments and gain the new field/checkbox names, including the Explain expander and auto-trigger conflict prevention. - 'Steam Launch Options' notes the button is currently Collapsed. - 'IOCTL surface' drops the dangling HidHide/ folder reference. - 'Profile schema versions' clarifies the v2 stored value's runtime meaning is mode-dependent. - Testing checklist swaps Timer/FirstDeviceOpened terminology for the checkbox UI; uses current log strings; adds an acquisition-fallback row. - Unit-test plan acquisition-flow entry expanded with the three concrete cases (signal fast / never fires / cancel). LaunchOrchestrator code review found two real issues: - StartDiagnosticWatcher was gated on Logger.CurrentLevel == Verbose, which excluded Debug-level users (Debug > Verbose). Now uses >= so both levels keep the diagnostic observation active. - _firstDeviceAcquiredAtMs field was set in the watcher callback but never read anywhere — leftover from the old short-circuit model. Removed it and the duplicate 'Signal fired at watcher-start + Nms' log line (the watcher itself already logs the friendly device-name signal-fire line). Updated comments on RunFlow's watcher start and StartAcquisitionWatcher docstring to describe the actual two-phase wait instead of the old concurrent short-circuit behavior.
Reveal phase: additive per-row times in acquisition mode + UX polish
Behavioral change to the reveal phase. Previously when 'Wait until the
game opens the first device' was on, per-row Reveal At times acted only
as a safety-net upper bound — once the signal fired + grace expired, all
remaining devices fired back-to-back ignoring the row values. That made
the per-row field nearly useless in acquisition mode.
Now the per-row time is ADDITIVE to grace: targetTime = grace_end +
DelaySeconds. So with grace=1.5s, vJoy at 0s reveals 1.5s after the
wheel opens, shifter at 1s reveals at 2.5s, handbrake at 2s reveals at
3.5s — proper post-grace staggering for slot ordering, which was the
user's actual intent. RevealDisableThenRestore split into two phases:
wait-for-signal upfront (with 60s timeout fallback that drops back to
absolute Timer semantics), then a straightforward per-device timer loop.
Field semantics now differ by mode (absolute in Timer, relative-to-grace
in Acquisition); the tooltip on the field and the Explain panel section
description both spell this out.
Calibration Mode:
- Dialog now drives the game lifecycle itself: Start button captures
baseline then auto-launches the profile's exe; Stop button terminates
the game (CloseMainWindow with 3s grace then Kill) and captures final.
- Window pinned topmost so users can interact during gameplay without
alt-tabbing. Doesn't affect MainWindow pinning.
- Runner exposes BaselineCaptured event so the dialog launches the game
only after the baseline rundown is recorded.
- Entry point button still hidden — calibration relies on read-count
proxies and slot-detection is unsolved; surfacing now would create
false confidence (documented in DEVELOPMENT.md).
Logging:
- New LogLevel.Debug (above Verbose) for firehose-chatty HIDCLASS
rundown output. WriteDebug helper + early-return guard on the HID
diagnostic handler. HIDCLASS provider subscription itself is gated
to Debug so we don't pay for ETW collection at Verbose.
Explain-this-profile expander:
- Collapsed by default, scrolls internally (MaxHeight 220) so opening
it never pushes action buttons off-screen.
- Chips sorted by DelaySeconds for time-ordered reading.
- Per-section descriptions rewritten with hedged language ('attempts
to') and tweak-times advice; sim-rig vJoy/SimHub consolidation note
added as amber callout for users hitting Forza's multi-device limit.
- Static role-definition paragraph removed (redundant with chips +
descriptions).
Other UX:
- Top-of-Games-tab checkboxes (auto-trigger, wait-for-first-device)
no longer stretch to full form width — clicking empty space to the
right no longer toggles.
- About card: 'Built by Josh Racine' line removed; HidHide thanks
rephrased to credit Nefarius + other contributors (real name dropped).
Double-space artifact around nefarius.at link fixed via
xml:space=preserve.
- Picker caption ('Add devices to profile — Leave here to hide') and
README updated to drop now-hidden Steam Launch Options + Copy Steam
Command references; auto-trigger conflict-prevention behavior
documented.
Acquisition signal is now a short-circuit, not a gate User report: FH6 with acquisition mode — wheel got slot #1, but ETW never fired (FH6 likely uses RawInput/WGI/GameInputService and never calls CreateFile on the device file), so we hit the 30s timeout, fell back to "Timer mode," and revealed everything at T+0 of the reveal phase (30s after process start) — well past FH6's detection window. Other devices were lost. Root cause: acquisition mode and timer mode were mutually exclusive, with the UI hiding per-device T+Xs values in acquisition mode. If ETW didn't fire, the per-device times were zero, reveals fired useless-late, and there was no safety net. Architectural fix — combine the two modes: • Per-device T+Xs values are always the primary timing source. They're shown in the UI in both modes. Timer mode = strict T+Xs. • In acquisition mode, the ETW watcher runs CONCURRENT with the reveal phase (not as a blocking pre-phase wait). • If the watcher signals during a wait, the orchestrator short- circuits: applies the post-acquisition grace period once, then packs all remaining reveals back-to-back. • If the watcher never signals (game doesn't open device files observably), reveals still happen at the configured T+Xs. The acquisition signal is now an early-fire optimization on top of the timer, not a gate. Removed: the 30s timeout and the WaitForFirstDeviceOpen pre-phase. Added: StartAcquisitionWatcher (non-blocking, returns the watcher) called between LaunchGame and RevealDisableThenRestore. The reveal loop polls _firstDeviceAcquired in a 250ms granularity sub-loop so it can react to the signal without plumbing cancellation tokens through the watcher. UI: per-device T+Xs textbox now always visible for RevealAfterStart rows (was hidden in acquisition mode). Tooltip rewritten to explain the "safety net upper bound" semantics. README cookbook updated to recommend setting both — acquisition trigger AND sensible T+Xs. DEVELOPMENT.md "Reveal phase triggers" section rewritten to describe the combined model. For the user's FH6 + MOZA setup, the next test should set the profile to acquisition mode AND set per-device T+Xs values like wheel/pedals/etc at 11.0, 11.1, 11.2, 11.3 — those will fire even if ETW signal doesn't show up.
Cap WM_DEVICECHANGE broadcast at 100ms per window (was 1000ms) Each reveal step sends a system-wide WM_DEVICECHANGE so the game's hot-plug listener notices the newly-accessible device. SendMessageTimeout waits up to N ms per top-level window. WM_DEVICECHANGE handlers normally return in microseconds; only genuinely-hung windows hit the timeout, and SMTO_ABORTIFHUNG bails on those without waiting either way. 1000ms was unnecessarily generous — with several stuck windows in a session (Steam UI, Discord overlay, etc.), a six-device reveal could balloon by multiple seconds, pushing later reveals past the game's hot-plug detection window. 100ms gives well-behaved windows plenty of time while keeping the worst case bounded. NOT batching the broadcasts: that would break per-device slot ordering because the game's EnumDevices() picks up all currently-accessible devices at once on a single notification, in OS enumeration order rather than our reveal order. The per-reveal broadcast is essential for ordering.
PreviousNext