Skip to content

Tags: Ginger-Beard/ControllerManager

Tags

v2.2.7

Toggle v2.2.7's commit message
Update README

v2.2.6

Toggle v2.2.6's commit message
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.

v2.2.5

Toggle v2.2.5's commit message
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.

v2.2.4

Toggle v2.2.4's commit message
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.

v2.2.3

Toggle v2.2.3's commit message
Docs: mention CodeQL + dependency audit workflow in build notes

DEVELOPMENT.md described the release workflow but not the security
workflow added/enabled today. One bullet pointing readers to the
inline rationale in security.yml.

v2.2.2

Toggle v2.2.2's commit message
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

v2.2.1

Toggle v2.2.1's commit message
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.

v2.2.0

Toggle v2.2.0's commit message
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.

v2.1.0

Toggle v2.1.0's commit message
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.

v2.0.0

Toggle v2.0.0's commit message
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.