Skip to content

Services Layer

hifihedgehog edited this page Apr 28, 2026 · 42 revisions

Services Layer

Five service classes bridge PadForge.Engine with the WPF UI layer. All live in PadForge.App/Services/ and run on the WPF dispatcher thread unless noted otherwise.

graph TB
    MW[MainWindow]
    IS[InputService]
    SS[SettingsService]
    DS[DeviceService]
    RS[RecorderService]
    FMS[ForegroundMonitorService]
    IM[InputManager<br/>Polling Thread]
    DSU[DsuMotionServer<br/>UDP Thread]
    WCS[WebControllerServer<br/>HTTP Thread]
    ABD[AudioBassDetector<br/>WASAPI Thread]

    MW --> IS
    MW --> SS
    MW --> DS
    MW --> RS
    IS --> IM
    IS --> DSU
    IS --> WCS
    IS --> ABD
    IS --> FMS
    SS -->|Load/Save XML| IS
    DS -->|Device events| IS

    IS -->|30Hz UI timer| MW
    IM -->|1000Hz polling| IS

    style IS fill:#f3e5f5
    style IM fill:#e1f5fe
    style DSU fill:#e8f5e9
    style WCS fill:#e8f5e9
    style ABD fill:#fff3e0
    style FMS fill:#fff3e0
Loading

Table of Contents


Architecture Overview

+-------------------+     30Hz Timer      +-------------------+
|   InputManager    | ==================> |   InputService    |
|  (background,     |   reads engine      |  (UI thread,      |
|   ~1000Hz poll)   |   state arrays      |   30Hz timer)     |
+-------------------+                     +---------+---------+
        ^                                           |
        |  writes PadSettings,                      | pushes to ViewModels
        |  slot types, macro snapshots              v
+-------------------+                     +-------------------+
| SettingsManager   |                     |   MainViewModel   |
|  (static, shared) | <================= |   PadViewModels   |
+-------------------+                     |   DashboardVM     |
        ^                                 |   DevicesVM       |
        |                                 |   SettingsVM      |
        |                                 +-------------------+
+-------------------+
| SettingsService   |
|  (XML load/save)  |
+-------------------+

Data flow

Direction Mechanism Frequency
Engine -> UI InputService reads CombinedOutputStates[], VibrationStates[], CombinedExtendedRawStates[], CombinedMidiRawStates[], CombinedKbmRawStates[] 30 Hz (UI timer)
UI -> Engine InputService writes SlotControllerTypes[], SlotExtendedConfigs[], MacroSnapshots[], _midiConfigs[] 30 Hz (SyncViewModelToPadSettings)
UI -> PadSetting InputService pushes deadzone, force feedback, mapping values to PadSetting objects 30 Hz (SyncViewModelToPadSettings)
Engine event -> UI DevicesUpdated, FrequencyUpdated, ErrorOccurred marshalled via Dispatcher.BeginInvoke On engine event
Settings file -> Memory SettingsService deserializes XML into SettingsManager collections On load
Memory -> Settings file SettingsService serializes SettingsManager + ViewModel state to XML On MarkDirty (250ms debounce)

Threading Model

PadForge uses three primary threads. Knowing which thread owns what prevents race conditions.

Thread Owner Rate Responsibilities
UI thread (WPF Dispatcher) MainWindow 30 Hz timer All ViewModel property writes, device list sync, dashboard updates, macro recording, profile switching, settings forwarding
Polling thread InputManager ~1000 Hz SDL input read, mapping, deadzone processing, virtual controller output, rumble, DSU broadcast
Subsystem threads Various Varies DSU server (UDP), Web controller (HTTP/WebSocket), Audio bass detector (WASAPI), HidHide controller

Thread-safety conventions

Data Strategy
SettingsManager collections (UserDevices, UserSettings) SyncRoot lock on both UI and polling threads
PadSetting string properties Atomic reference assignment; UI writes at 30 Hz, polling reads at ~870 Hz
InputManager arrays (CombinedOutputStates[], VibrationStates[], etc.) Simple value copies, no locking
Macro snapshots Atomic array reference swap by UI thread; polling thread reads the reference
Engine events (DevicesUpdated, FrequencyUpdated) Fire on polling thread, marshalled to UI via Dispatcher.BeginInvoke

InputService

File: PadForge.App/Services/InputService.cs Implements: IDisposable

Central service bridging the InputManager engine with WPF ViewModels. Owns the InputManager instance, runs the 30 Hz UI timer, and manages all subsystem lifecycles (DSU, web server, audio bass detector, foreground monitor, device hiding).

Constructor and Initialization

public InputService(MainViewModel mainVm)

Constructor:

  1. Stores MainViewModel reference, captures Dispatcher.CurrentDispatcher.
  2. Subscribes to Strings.CultureChanged for language-change status refresh.
  3. Subscribes to SelectedDeviceChanged and MappingsRebuilt on every PadViewModel.
  4. Subscribes to DevicesViewModel.PropertyChanged for offline device detail display.
  5. Initializes _previousSelectedDevice dictionary (tracks per-pad device GUID for save-before-switch).

Start / Stop / Dispose

Start()

Startup sequence:

  1. Cleanup stale HIDMaestro nodes. Removes USB device nodes from previous sessions (crash recovery).
  2. Create InputManager. Sets PollingIntervalMs from SettingsViewModel.PollingRateMs.
  3. Copy slot config. Copies SlotControllerTypes[], Extended/MIDI configs from PadViewModels to engine. Calls PreInitializeVigemCounts() so the HIDMaestro device filter catches VCs on the first cycle.
  4. Subscribe to engine events. DevicesUpdated, FrequencyUpdated, ErrorOccurred.
  5. Subscribe to ViewModel changes. SettingsViewModel.PropertyChanged, DashboardViewModel.PropertyChanged.
  6. Create ForegroundMonitorService. Subscribes to ProfileSwitchRequired.
  7. Capture default profile snapshot. Uses PendingDefaultSnapshot (from prior XML) or creates one via SnapshotCurrentProfile().
  8. Async Raw Input enumeration. Keyboard and mouse devices are discovered on a background thread via Task.Run, preventing UI thread stalls from slow HID enumeration. Results are merged into the device list when the task completes.
  9. Start engine. _inputManager.Start() launches the polling thread.
  10. Start subsystems. DSU, web controller, audio bass detector (each conditional on settings).
  11. Clear stale HidHide state. HidHideController.ClearAll() removes leftover entries.
  12. Apply device hiding. HidHide blacklist + input hooks.
  13. Start UI timer. 30 Hz DispatcherTimer at DispatcherPriority.Render.
  14. Update state. Sets IsEngineRunning = true, enters idle if no slots created.

Stop(bool preserveExtendedNodes = false)

  1. Stops UI timer and unsubscribes from its Tick event.
  2. Unsubscribes from all ViewModel property change events.
  3. Unsubscribes from per-pad events (SelectedDeviceChanged, MappingsRebuilt).
  4. Disposes ForegroundMonitorService.
  5. Stops DSU server, web controller server, audio bass detector.
  6. Calls RemoveDeviceHiding() (HidHide blacklist cleanup + input hook teardown).
  7. Unsubscribes from engine events, calls _inputManager.Stop(preserveExtendedNodes) and _inputManager.Dispose().
  8. Updates MainViewModel state (sets engine status to "Stopped", marks all device rows offline).
  9. If !preserveExtendedNodes, tears down lingering Extended HM virtuals so they are not left in joy.cpl after shutdown.

preserveExtendedNodes is set when the engine will restart immediately (e.g., output type change), avoiding a tear-down/recreate cycle for Extended HM virtuals. The flag name is preserved from v2's vJoy-node lifecycle for compatibility; in v3 it gates Extended HM teardown.

Dispose()

Calls Stop() in a try/catch (best-effort shutdown).

30Hz UI Timer Tick

UiTimer_Tick is the service layer heartbeat. Called ~30 times/second on the UI thread, it runs these steps in sequence:

UiTimer_Tick
  |-- Update Pad ViewModels (gamepad state, vibration, Extended/MIDI/KBM raw state)
  |-- UpdateDashboard()
  |-- UpdateDevicesRawState()         [only if Devices page visible]
  |-- UpdateMappingLiveValues()       [only if a Pad page visible]
  |-- UpdateMacroTriggerRecording()   [only if recording active]
  |-- SyncViewModelToPadSettings()    [always, 30Hz]
  |-- SyncMacroSnapshots()            [always, 30Hz]
  |-- Audio rumble level meters       [only if detector active]
  |-- UpdateIdleState()               [auto-idle when no active slots]
  |-- ForegroundMonitor.CheckForegroundWindow()  [auto-profile switching]

Pad ViewModel updates (per slot)

For each of the 16 slots:

Slot type Source array Update call
All CombinedOutputStates[i], VibrationStates[i] padVm.UpdateFromEngineState()
Custom Extended CombinedExtendedRawStates[i] padVm.UpdateFromExtendedRawState()
MIDI CombinedMidiRawStates[i] padVm.UpdateFromMidiRawState()
KB+M CombinedKbmRawStates[i] Sets padVm.KbmOutputSnapshot

Per-device stick/trigger previews read either KBM pre-deadzone values (synthesized into a Gamepad struct) or the selected device's RawMappedState.

Visibility gating

Two flags gate expensive per-frame work:

Flag When true
IsDevicesPageVisible Calls UpdateDevicesRawState()
IsPadPageVisible Calls UpdateMappingLiveValues()

Both are set by MainWindow navigation.

Dashboard Updates

UpdateDashboard() (private, 30Hz)

Pushes engine statistics to DashboardViewModel: state key ("Running"/"Idle"/"Stopped"), localized status, PollingFrequency, and device counts (TotalDevices, OnlineDevices, MappedDevices) computed under UserDevices.SyncRoot lock. Calls RefreshSlotSummaryProperties() and RefreshNavItemConnectedCounts().

RefreshSlotSummaryProperties(IEnumerable<UserDevice> devices = null) (public)

Updates all SlotSummary items on the dashboard with per-slot status (IsActive, DeviceName, MappedDeviceCount, ConnectedDeviceCount, IsVirtualControllerConnected, IsInitializing, IsEnabled, StatusText) and per-type numbering (e.g., "Xbox 1", "DS4 2").

RefreshNavItemConnectedCounts(IEnumerable<UserDevice>) (private)

Updates sidebar NavControllerItem.ConnectedDeviceCount and IsInitializing for power icon color logic.

Devices Page Raw State

UpdateDevicesRawState() (private, 30Hz, gated by IsDevicesPageVisible)

Updates the raw input state display for the selected device:

  1. Finds UserDevice for the selected DeviceRowViewModel.
  2. On device change: rebuilds axis/button/POV collections via devVm.RebuildRawStateCollections().
  3. Updates axes in-place (NormalizedValue = Axis[i] / 65535.0), buttons, keyboard keys, POV hats, mouse motion/scroll, and gyro/accel (if supported).

OnDevicesVmPropertyChanged() (private)

Handles SelectedDevice changes when the engine is not running. Populates the detail panel from cached UserDevice capabilities so the layout is visible offline.

Mapping Live Values

UpdateMappingLiveValues() (private, 30Hz, gated by IsPadPageVisible)

For the active Pad page: finds the selected device, parses each MappingItem.SourceDescriptor, reads the raw value from CustomInputState, and sets mapping.CurrentValueText.

ReadMappedValue(CustomInputState, string descriptor) (private, static)

Simplified Step 3 parser for display. Strips I/H prefixes, parses "Axis N", "Button N", "Slider N", "POV N" descriptors, and reads from the state arrays.

Settings Sync (ViewModel to PadSetting)

SyncViewModelToPadSettings() (private, 30Hz)

Primary runtime sync path. For each pad slot:

  1. Always synced (even with no device selected):

    • SlotControllerTypes[i] from padVm.OutputType
    • Extended config via SyncExtendedConfigToSlot()
    • MIDI config via _inputManager._midiConfigs[i]
  2. Per-device sync (when a device is selected):

    • Calls SaveViewModelToPadSetting(padVm, instanceGuid, syncMappings: false)
    • Pushes deadzones (independent X/Y), anti-deadzones, linear, center offsets, max range (independent directions), trigger deadzones, force feedback gains, audio rumble settings
    • Mapping descriptors are NOT synced at 30 Hz to avoid a race condition. ClearMappingDescriptors() creates a window where the polling thread sees empty mappings
  3. Audio bass detector lifecycle: detects when AudioRumbleEnabled toggles on any slot and calls SyncAudioBassDetector().

SaveViewModelToPadSetting(PadViewModel, Guid, bool syncMappings) (private, static)

Writes all tuning parameters from ViewModel to PadSetting. When syncMappings is true (explicit save, preset change, device switch), also clears and rewrites all mapping descriptors.

LoadPadSettingToViewModel(PadViewModel, Guid) (private, static)

Reverse direction: reads PadSetting and populates the PadViewModel (deadzones, sensitivity curves, max ranges, center offsets, triggers, force feedback, audio rumble, mapping descriptors).

Settings Forwarding (OnSettingsPropertyChanged)

private void OnSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)

Propagates SettingsViewModel changes to the engine at runtime:

Property Action
PollingRateMs Sets _inputManager.PollingIntervalMs
EnableInputHiding Calls ApplyDeviceHiding() or RemoveDeviceHiding()

Dashboard Forwarding (OnDashboardPropertyChanged)

private void OnDashboardPropertyChanged(object sender, PropertyChangedEventArgs e)

Propagates DashboardViewModel changes:

Property Action
EnableDsuMotionServer Starts or stops DSU server
DsuMotionServerPort Restarts DSU server if enabled
EnableWebController Starts or stops web controller server
WebControllerPort Restarts web controller server if enabled

Engine Event Handlers

All fire on the polling thread and are marshalled to UI via Dispatcher.BeginInvoke.

Handler Action
OnDevicesUpdated SyncDevicesList(), UpdatePadDeviceInfo(), ApplyDeviceHiding()
OnFrequencyUpdated No-op (frequency read on next UI tick)
OnErrorOccurred Sets _mainVm.StatusText

Device List Sync

SyncDevicesList() (private)

Synchronizes DevicesViewModel.Devices with SettingsManager.UserDevices:

  1. Snapshots under lock.
  2. Updates existing rows, adds new ones (skips virtual/shadow devices).
  3. Removes stale or virtual rows.
  4. Sorts by name, then VID:PID.
  5. Calls devVm.RefreshCounts().

IsVirtualOrShadowDevice(UserDevice) (private, static)

Filters virtual controllers from the device list (defense-in-depth; Step 1 already filters HIDMaestro). Checks name for "HIDMaestro"/"Virtual Gamepad", path for "vigem"/"virtual", and the IsHidden flag.

PopulateDeviceRow(DeviceRowViewModel, UserDevice) (private)

Maps UserDevice properties to the ViewModel row: name, VID/PID, online status, capabilities, device type, slot assignments, HidHide state, instance path.

UpdatePadDeviceInfo() (public)

Rebuilds each PadViewModel's MappedDevices from UserSettings.FindByPadIndex(). Handles multi-device slots, auto-selects first device, refreshes sidebar and dashboard.

Per-Device Settings Swap

PopulateAvailableInputs() (private)

Builds the input source dropdown for a pad slot's selected device. Uses Math.Max(CapButtonCount, RawButtonCount) to generate button entries, ensuring the full button list is available regardless of whether the device is in gamepad or raw joystick mode. When the device is offline, falls back to cached DeviceObjects and stored capability counts so the mapping UI remains functional without a live connection.

RefreshMappingDropdowns() (internal)

Called when ForceRawJoystickMode is toggled on a device. Rebuilds the input source dropdown and mapping descriptors for all pad slots using that device, reflecting the change in available raw vs. gamepad inputs.

OnSelectedDeviceChanged(PadViewModel, MappedDeviceInfo)

When the user selects a different device in a pad slot's dropdown:

  1. Saves current ViewModel values to the previous device's PadSetting (skipped when re-adding the same device).
  2. Loads the new device's PadSetting via LoadPadSettingToViewModel().
  3. Populates input dropdown via PopulateAvailableInputs().
  4. Updates the _previousSelectedDevice tracker.

OnMappingsRebuilt(PadViewModel)

When mappings are rebuilt (OutputType or HIDMaestro profile change), reloads mapping descriptors from PadSetting without touching deadzone or force feedback settings.

Copy / Paste Settings

ApplyPadSettingToCurrentDevice(int padIndex, PadSetting source) (public)

Applies a source PadSetting to the selected device. Used by Paste and "Copy From".

ApplyPadSettingToCurrentDeviceTranslated(...) (public)

Applies with cross-layout translation (e.g., Xbox to Extended mapping key conversion).

FlushAllPadViewModels() (public)

Flushes all active PadViewModel state to PadSettings. Call before reading PadSettings across slots (e.g., Copy From dialog).

GetCurrentPadSetting(int padIndex) (public)

Returns the PadSetting for the selected device after syncing ViewModel state.

Macro Snapshot Sync

SyncMacroSnapshots() (private, 30Hz)

Creates a snapshot array of MacroItem objects per slot and assigns it to _inputManager.MacroSnapshots[i]. The engine reads these atomically each cycle. Empty lists set the snapshot to null.

Macro Trigger Recording

StartMacroTriggerRecording(MacroItem macro, int padIndex) (public)

Starts recording button/axis/POV presses for a macro trigger combo. Captures axis baselines for delta detection.

StopMacroTriggerRecording() (public)

Finalizes recording. Writes accumulated data to the MacroItem by trigger path:

Path Data written
InputDevice Raw device buttons (TriggerRawButtons) + device GUID
Custom Extended Numbered button words (TriggerCustomButtonWords)
OutputController Xbox button bitmask (TriggerButtons)

Also writes axis targets/directions and POV triggers.

UpdateMacroTriggerRecording() (private, 30Hz)

Called each UI tick during recording. Reads state per TriggerSource:

Source Behavior
InputDevice Scans raw buttons/POVs from mapped devices; first press locks _recordingDeviceGuid
Numbered (custom Extended) Accumulates from CombinedExtendedRawStates
OutputController Accumulates from CombinedOutputStates Xbox button bitmask

Axis detection: 25% threshold, 3-cycle hold confirmation (same as RecorderService).

DSU Server Lifecycle

StartDsuServerIfEnabled() (private)

  1. Checks Dashboard.EnableDsuMotionServer and engine existence.
  2. Creates DsuMotionServer, subscribes to StatusChanged.
  3. Validates port (1024–65535; default 26760).
  4. Starts server. On success assigns to _inputManager.DsuServer; on failure disposes.

StopDsuServer() (private)

  1. Clears _inputManager.DsuServer.
  2. Disposes server instance.

Web Controller Server Lifecycle

StartWebServerIfEnabled() (private)

  1. Checks Dashboard.EnableWebController and engine existence.
  2. Creates WebControllerServer, subscribes to StatusChanged, DeviceConnected, DeviceDisconnected.
  3. Device connect/disconnect calls _inputManager.RegisterExternalDevice() / UnregisterExternalDevice().
  4. Validates port (1024–65535; default 8080), starts server.

StopWebServer() (private)

Unsubscribes from StatusChanged, disposes server, clears status and client count.

Audio Bass Detector Lifecycle

SyncAudioBassDetector() (internal)

Called on engine start, slot changes, and during 30 Hz sync when AudioRumbleEnabled toggles. Starts the detector if any slot enables audio rumble; stops it if none do.

StartAudioBassDetector() (private)

Creates and starts AudioBassDetector. On success, assigns to _inputManager.AudioBassDetector; on failure, disposes.

StopAudioBassDetector() (private)

Clears _inputManager.AudioBassDetector, disposes detector, zeros all pad level meters.

Audio rumble level meter update (in UiTimer_Tick)

When _audioBassDetector != null, reads BassEnergy and pushes to padVm.AudioRumbleLevelMeter for each created slot with AudioRumbleEnabled.

Device Hiding

ApplyDeviceHiding() (public)

Only acts if EnableInputHiding is on. Two mechanisms:

HidHide (driver-level):

  1. Builds whitelist (PadForge exe + user paths). SyncWhitelist() adds/removes only PadForge-managed entries.
  2. For each UserDevice with HidHideEnabled: converts DevicePath to HID instance ID (fallback: VID/PID lookup for synthetic paths like "XInput#0"). Caches resolved IDs for offline pre-emptive blacklisting.
  3. HidHideController.SyncManagedDevices(desiredIds). Atomic diff-based sync.
  4. Activates cloaking if any devices are blacklisted.

Input hooks (keyboard/mouse):

  1. For each device with ConsumeInputEnabled and a slot assignment: parses "Button {index}" descriptors to collect VKey codes or mouse button IDs.
  2. Creates/updates InputHookManager if inputs need suppressing; otherwise stops and disposes it.

RemoveDeviceHiding() (public)

Calls HidHideController.RemoveManagedDevices() (best-effort), stops and disposes InputHookManager.

SyncWhitelist(HashSet<string> desiredWinPaths) (private)

Converts Windows paths to DOS device paths. Only modifies PadForge-managed entries. Entries from HidHide Client or other tools are left untouched. Tracked via _managedWhitelistDosPaths.

Auto-Idle

UpdateIdleState() (private, 30Hz)

Sets _inputManager.IsIdle based on whether any slot is created, enabled, and has a device assigned. Idle mode skips input/mapping/output and sleeps at ~20 Hz, reducing CPU to ~0%.

Profile Switching

SnapshotCurrentProfile() (public) -> ProfileData

Captures current runtime state:

  1. Flushes all PadViewModel values to PadSettings.
  2. Collects ProfileEntry (InstanceGuid, ProductGuid, MapTo, checksum) and deduplicated PadSetting clones.
  3. Captures SlotCreated[], SlotEnabled[], SlotControllerTypes[], SlotProfileIds[] (per-slot HIDMaestro profile slug), Extended/MIDI configs, DSU/web server settings.

ApplyProfile(ProfileData profile) (public)

Restores a profile:

  1. Topology. Sets SlotCreated[], SlotEnabled[], OutputType, ProfileId (per-slot HM profile slug); unassigns devices from destroyed slots. The HM slug update gates Step 5's per-slot diff at InputManager.Step5.VirtualDevices.cs:514-527 — slots whose new slug matches the live HMaestroVirtualController.ProfileId stay live untouched, slots whose slug differs are destroyed and recreated with the new identity.
  2. Device assignments (single-pass transition). Builds the desired final assignment map from profile.Entries first, then transitions each UserSetting directly old → new MapTo (or → -1 for entries dropped from the new profile). The "find UserSetting" gate is "not yet consumed by a prior entry in this same apply pass," not the previous reset-MapTo-to-negative gate. This avoids the reset window where the polling thread could observe HasAnyDeviceMapped == false for surviving slots and fall into the immediate-destroy path at Step 5:590-600 — slots whose mapping is unchanged across profiles transition with zero teardown.
  3. Extended/MIDI configs. Restores preset and custom counts.
  4. Server settings. Sets DSU and web controller enable/port.
  5. Rebuilds UI. UpdatePadDeviceInfo(), reloads PadSettings, refreshes Devices page.

OnProfileSwitchRequired(string profileId) (private)

Called by ForegroundMonitorService when the foreground process matches a different profile. Skips if same profile already active. Saves outgoing state via SaveActiveProfileState(), then applies the target profile (or reverts to default if profileId is null).

SaveActiveProfileState() (public)

Snapshots current state. Default profile: updates _defaultProfileSnapshot and PendingDefaultSnapshot. Named profile: updates stored data in SettingsManager.Profiles.

RefreshDefaultSnapshot() (public)

Refreshes the default profile snapshot from current state. Called after saving when no profile is active.

ApplyDefaultProfile() (public)

Applies _defaultProfileSnapshot to revert to the pre-profile state.

Slot Reordering

The reorder model rests on five rules:

  • Pad indices are data identity. A pad's mappings, profile, devices, settings, and dirty flags live at its pad index and never move on reorder.
  • Visual position is the kernel-slot anchor. Within an HM-backed group (Xbox / PlayStation / Extended), the VC at visual position V holds kernel slot V. SlotOrders[group][V] = padIndex says which pad's data the VC at slot V is serving.
  • Reorder repoints, not rebuilds. SwapSlots / MoveSlot mutate SlotOrders (visual order). The kernel VC at each visual position stays put; the pad-index pointer in _virtualControllers[] moves so the data at the new pad-at-position-V feeds into V's kernel slot.
  • Same-profile reorders are zero-flicker. Pointer swap in _virtualControllers[] plus FeedbackPadIndex update on the moved VC. Per-VC state arrays move with the VC.
  • Different-profile positions destroy + recreate. Only the specific positions whose profile changed. Matching positions in the same reorder still pointer-swap.

Per-pad state (_slotInactiveCounter, _createFailed, _hmInactivityFired, _slotInitializing, _pendingDisposeTask, _pendingConnectTask) describes the pad's lifecycle and stays at the pad index. Per-VC state (_loggedFirstSubmit, _extendedAppliedProductString, _extendedAppliedLayout, _oemOverrideClaimedVidPid, _lastAppliedOemLabel) moves with the VC.

SwapSlots(int padIndexA, int padIndexB) (public)

Swaps two slots' visual positions within their (shared) group. Snapshots the pre-swap order, mutates SlotOrders via SwapWithinGroup, then calls RebuildKernelOrderAfterReorder(groupType, oldOrder). Cross-group calls are rejected. Refreshes UI after.

MoveSlot(int sourcePadIndex, int targetVisualPosition) (public)

Moves a slot from its current visual position to a new visual position within the same group. Snapshots the pre-move order, mutates SlotOrders via MoveWithinGroup, then calls RebuildKernelOrderAfterReorder(groupType, oldOrder).

RebuildKernelOrderAfterReorder(VirtualControllerType groupType, IReadOnlyList<int> oldOrder) (private)

Thin delegator. Reads the new order from SettingsManager.SlotOrders.GetOrderFor(groupType) and calls _inputManager.RerouteVirtualControllersForReorder(groupType, oldOrder, newOrder). Non-HM groups (KBM, MIDI) are filtered inside the engine method since their slot order is not tied to a kernel-side index allocation.

InputManager.RerouteVirtualControllersForReorder(VirtualControllerType groupType, IReadOnlyList<int> oldOrder, IReadOnlyList<int> newOrder) (public, engine)

Walks oldOrder against newOrder position by position and decides per visual position whether to reuse the existing VC at that kernel slot or destroy it. Three-step implementation:

  1. Decide per position. For each visual position V, compare the profile of the VC at oldOrder[V] against SlotProfileIds[newOrder[V]]. Same profile: the VC at V is reused. Different profile: the old VC is queued for destruction. While walking, snapshot the per-VC state at each oldPad so it can travel with the VC.
  2. Destroy mismatched VCs. Each goes through DestroyVirtualController(oldPad, asyncDispose: true), which releases OEM override claims and queues HIDMaestro teardown to the thread pool. Per-pad state at these old pads is cleared.
  3. Re-route reused VCs. For each reused VC, write the VC pointer plus its per-VC state snapshot into the destination pad's slot in the engine arrays, and update FeedbackPadIndex so vibration callbacks land in the right VibrationStates[] entry. Per-VC state cleared at the old pad if it differs from the new pad.

Same-profile cycles (Example: insert a Profile-A slot at the top of an all-Profile-A group) collapse to a pure pointer rotation across _virtualControllers[] with no kernel teardown. Zero-flicker for the game side. Different-profile positions go through the regular destroy + recreate path; Pass 2's visual-order gate plus ApplyAscendingIndexPreemption recreate them with the new pad's profile, taking the lowest free kernel slot (which is V, because surviving VCs at positions < V keep theirs). The swap-only path does not engage Pass 2's preemption.

Non-HM group types are rejected at the entry. Cross-group moves do not route through here at all. MoveSlotToGroupTail changes SlotControllerTypes[padIndex], which Pass 1 detects as a type change and destroys the old-group VC; the new group's ordinary creation logic spins up the new VC at the tail.

This replaced the v2-era ShouldRebuildKernelOrder predicate and the live-subsequence walk that destroyed every live VC at and below the lowest changed position. Reorders now make per-position reuse decisions, so a same-profile rotation involves zero VC destroys.

EnsureTypeGroupOrder(bool silent = false) (public) -> bool

Groups slots by type order: Xbox, PlayStation, Extended, KB+M, MIDI. Within a group, drag-and-drop reorder is honored via SettingsManager.SlotOrders. Uses bubble sort with SwapSlotData (data-only, no VC destruction). Returns true if reordering occurred.

Test Rumble

SendTestRumble(int padIndex, Guid? deviceGuid) (public)

Sets vibration to 32768 on left/right motors. Optional device GUID filter. Clears after 500 ms via a one-shot DispatcherTimer.

// Overload for selective motors
public void SendTestRumble(int padIndex, Guid? deviceGuid, bool left, bool right)

InputService All Public Methods

Method Signature Description
Start void Start() Creates engine, starts all subsystems, begins UI timer
Stop void Stop(bool preserveExtendedNodes = false) Stops engine and all subsystems
Dispose void Dispose() Calls Stop() for cleanup
RefreshSlotSummaryProperties void RefreshSlotSummaryProperties(IEnumerable<UserDevice> devices = null) Updates dashboard slot summary cards
RefreshDeviceList void RefreshDeviceList() Full re-sync of device list UI
UpdatePadDeviceInfo void UpdatePadDeviceInfo() Refreshes PadViewModel device info for all pads
ApplyDeviceHiding void ApplyDeviceHiding() Applies HidHide + input hooks based on settings
RemoveDeviceHiding void RemoveDeviceHiding() Removes all device hiding
SendTestRumble void SendTestRumble(int padIndex, Guid? deviceGuid) Sends brief test rumble
SendTestRumble void SendTestRumble(int padIndex, Guid? deviceGuid, bool left, bool right) Sends selective test rumble
ApplyPadSettingToCurrentDevice void ApplyPadSettingToCurrentDevice(int padIndex, PadSetting source) Applies copied PadSetting
ApplyPadSettingToCurrentDeviceTranslated void ApplyPadSettingToCurrentDeviceTranslated(int padIndex, PadSetting source, VirtualControllerType sourceType, bool sourceIsCustomExtended, VirtualControllerType targetType, bool targetIsCustomExtended) Applies with cross-layout translation
FlushAllPadViewModels void FlushAllPadViewModels() Saves all ViewModel state to PadSettings
GetCurrentPadSetting PadSetting GetCurrentPadSetting(int padIndex) Gets PadSetting for selected device
StartMacroTriggerRecording void StartMacroTriggerRecording(MacroItem macro, int padIndex) Starts macro trigger recording
StopMacroTriggerRecording void StopMacroTriggerRecording() Stops macro trigger recording
SnapshotCurrentProfile ProfileData SnapshotCurrentProfile() Captures current state as profile
ApplyProfile void ApplyProfile(ProfileData profile) Loads a profile into runtime state
SaveActiveProfileState void SaveActiveProfileState() Saves current state into active profile
RefreshDefaultSnapshot void RefreshDefaultSnapshot() Refreshes default profile from current state
ApplyDefaultProfile void ApplyDefaultProfile() Reverts to default profile
RefreshProfileTopology void RefreshProfileTopology() Refreshes active profile topology label
SwapSlots void SwapSlots(int padIndexA, int padIndexB) Swaps two controller slots
MoveSlot void MoveSlot(int sourcePadIndex, int targetVisualPosition) Moves slot to visual position
EnsureTypeGroupOrder bool EnsureTypeGroupOrder(bool silent = false) Groups slots by type

InputService All Events

InputService exposes no custom events. All UI updates flow through ViewModel properties and InputManager's marshalled events.

Properties used as communication channels:

Property Type Description
Engine InputManager (get-only) Access to underlying InputManager
IsDevicesPageVisible bool (get/set) Gates Devices page raw state updates
IsPadPageVisible bool (get/set) Gates mapping live value updates
SettingsService SettingsService (set-only) For triggering saves on cache updates
ToggleMainWindow Action (get/set) Callback to show/hide the main window. Set by MainWindow at startup.

Window Toggle via Global Macro

The ToggleMainWindow property is an Action delegate set by MainWindow during initialization. It handles three visibility states:

  1. Hidden (system tray). Calls RestoreFromTray() + ForceToForeground().
  2. Minimized or inactive. Restores WindowState and calls ForceToForeground().
  3. Foreground and visible. Minimizes (or hides to tray if MinimizeToTray enabled).

The engine thread sets InputManager.PendingToggleWindow = true when a global macro with SwitchProfileMode.ToggleWindow fires. The UI thread consumes this volatile flag inside UiTimer_Tick, immediately after pending profile switch handling:

if (_inputManager.PendingToggleWindow)
{
    _inputManager.PendingToggleWindow = false;
    ToggleMainWindow?.Invoke();
}

Profile Switch Overlay

ShowProfileSwitchOverlay(string profileId) creates (or reuses) a ProfileSwitchOverlay window and wires two callbacks that the overlay polls at ~30 Hz to track virtual controller initialization:

Callback Signature Purpose
CheckAllSlotsInitState Func<(bool anyInitializing, bool allReady)> Returns whether any created+enabled slots are still initializing, and whether all are ready. Checks each VC's lifecycle state.
CheckAnyControllerOffline Func<bool> Returns true if any created+enabled slot has no online physical devices assigned. Used to show a warning after the "Active" state.

CheckAllSlotsInitState() iterates all 16 slots. If SlotCreated[i] && SlotEnabled[i], it checks whether the virtual controller exists and is connected. CheckAnyControllerOffline() similarly iterates slots and checks whether any assigned UserSetting references a device that is offline.

Profile Shortcut Recording (ProfilesPage Code-Behind)

Shortcut combo recording is implemented in ProfilesPage.xaml.cs, not InputService, because it requires direct access to the XAML ItemsControl and per-row ProfileShortcutViewModel instances.

ShortcutLearn_Click(object sender, RoutedEventArgs e). Starts a 5-second recording window:

  1. Cancels any in-progress recording on another shortcut row.
  2. Snapshots axis baselines from all online devices (_recordAxisBaselines).
  3. Sets InputService.SetSuppressGlobalMacros(true) to prevent the combo from firing during recording.
  4. Creates a 33 ms DispatcherTimer (_recordTimer).

RecordTimer_Tick(object sender, EventArgs e). Fires at ~30 Hz during recording:

  1. Updates the countdown display via RecordingCountdown.
  2. Scans all online devices for pressed buttons and axis deflections exceeding AxisRecordDeltaThreshold (0.25).
  3. Builds TriggerButtonEntry[] with per-button device tracking (DeviceInstanceGuid, DeviceProductGuid, IsAxis, AxisIndex, AxisThreshold, AxisDirection).
  4. Temporarily sets entries on the ViewModel for live display.
  5. Auto-stops after RecordTimeoutSeconds (5 seconds).

StopRecording(). Finalizes:

  1. Stops _recordTimer.
  2. If valid entries were captured, calls _recordingShortcut.SetLearnedButtons(entries).
  3. Otherwise calls CancelRecording() on the ViewModel.
  4. Clears _recordAxisBaselines and calls SetSuppressGlobalMacros(false).

SettingsService

File: PadForge.App/Services/SettingsService.cs

Loads and saves PadForge settings to XML. Handles bidirectional sync between SettingsManager data and WPF ViewModels.

SettingsService Constructor and Initialization

public SettingsService(MainViewModel mainVm)

Stores reference to MainViewModel.

Initialize()

  1. Ensures UserDevices and UserSettings collections exist.
  2. Finds settings file via FindSettingsFile().
  3. Loads if found; otherwise initializes with defaults.
  4. Sets SettingsFilePath, clears dirty flag.

File Discovery

Search order (all relative to AppDomain.CurrentDomain.BaseDirectory):

  1. PadForge.xml (primary)
  2. Settings.xml (fallback)
  3. If neither exists, creates PadForge.xml.

Load

LoadFromFile(string filePath) (public)

  1. Deserializes SettingsFileData from XML.
  2. Populates UserDevices and UserSettings under SyncRoot locks.
  3. PadSetting linking: finds PadSetting by checksum and clones it. Cloning is critical. Without it, devices sharing a checksum would share one object.
  4. Purges orphaned UserSettings (MapTo == -1).
  5. Calls LoadAppSettings(), LoadPadSettings(), LoadMacros(), LoadProfiles().

LoadAppSettings(AppSettingsData) (private)

Pushes to SettingsViewModel: AutoStartEngine, MinimizeToTray, StartMinimized, StartAtLogin, EnablePollingOnFocusLoss, PollingRateMs, theme, language, input hiding, auto-profile switching, slot types, Extended/MIDI configs, DSU/web server settings.

Critical load order: SlotCreated[] and SlotEnabled[] must load BEFORE OutputType, because OutputType fires PropertyChanged which reads SlotCreated.

LoadPadSettings(UserSetting[], PadSetting[]) (private)

For each slot (first device only), loads all tuning parameters into PadViewModel: deadzones, sensitivity curves, max ranges, center offsets, triggers, force feedback, audio rumble, Extended HID custom configs, and mapping descriptors. Per-mapping deadzones are loaded with a default of 50 (centered, no effect) for mappings that lack a stored value.

LoadMacros(MacroData[]) (private)

Rebuilds each PadViewModel's macro list from MacroData[], grouped by pad index.

Save

Save() (public)

Calls SaveToFile(_settingsFilePath).

SaveToFile(string filePath) (public)

  1. UpdatePadSettingsFromViewModels() pushes all ViewModel values to PadSettings.
  2. Flushes Extended/MIDI/KBM mapping dictionaries to serializable arrays, recomputes checksums.
  3. FlushMappingDeadZones(). Collects per-mapping deadzone values from all PadViewModels and writes them into the corresponding PadSetting objects before serialization.
  4. Updates active profile snapshot via UpdateActiveProfileSnapshot().
  5. Collects devices, user settings, deduplicated pad settings (under locks), app settings, macros, profiles.
  6. Serializes SettingsFileData to XML, clears dirty flag.

Note: When a named profile is active, BuildAppSettings() stores the default profile's slot state (from PendingDefaultSnapshot), not the current runtime state. This prevents the named profile's topology from contaminating the default.

MarkDirty and Autosave

MarkDirty() (public)

Sets IsDirty = true, starts a 250 ms debounce DispatcherTimer (restarted if already running). On tick: calls Save(), raises AutoSaved. The debounce batches rapid changes (e.g., slider drags) into a single save.

Reset and Reload

ResetToDefaults() (public)

Clears all SettingsManager collections, resets all ViewModels to defaults, clears profiles, marks dirty.

Reload() (public)

Reloads from disk, discarding unsaved changes.

Profile Loading

LoadProfiles(ProfileData[], AppSettingsData) (private)

  1. Adds the built-in Default profile at the top.
  2. Adds each saved profile with topology counts.
  3. If a named profile was active at shutdown, restores its slot config and captures the default snapshot from XML (PendingDefaultSnapshot).

UpdateActiveProfileSnapshot() (private)

Called during Save. If a named profile is active, updates its stored snapshot from current state (entries, PadSettings, topology, server settings).

UpdateTopologyCounts(ProfileListItem, bool[], int[]) (internal, static)

Counts Xbox/PlayStation/Extended/MIDI/KBM slots and sets topology label (e.g., "2x Xbox, 1x PlayStation").

SettingsService All Public Methods

Method Signature Description
Initialize void Initialize() Finds settings file, loads, initializes collections
LoadFromFile void LoadFromFile(string filePath) Loads settings from XML
Save void Save() Saves to active settings file
SaveToFile void SaveToFile(string filePath) Saves to specified XML file
MarkDirty void MarkDirty() Marks dirty, schedules 250ms autosave
Reload void Reload() Reloads from disk
ResetToDefaults void ResetToDefaults() Resets all settings to defaults

SettingsService All Events

Event Signature Description
AutoSaved event EventHandler AutoSaved Raised after autosave completes

Properties:

Property Type Description
SettingsFilePath string (get) Full path to active settings file
IsDirty bool (get) Whether unsaved changes exist

DeviceService

File: PadForge.App/Services/DeviceService.cs

Handles UI-triggered device management: assign/unassign, hide/show, create/delete virtual controller slots. Bridges DevicesViewModel commands to SettingsManager and SettingsService.

DeviceService Constructor and Initialization

public DeviceService(MainViewModel mainVm, SettingsService settingsService)

Stores MainViewModel and SettingsService references.

WireEvents() (public)

Subscribes to DevicesViewModel events:

Event Handler
AssignToSlotRequested OnAssignToSlot
ToggleSlotRequested OnToggleSlot
HideDeviceRequested OnHideDevice
RemoveDeviceRequested OnRemoveDevice
DeviceHidingChanged OnDeviceHidingChanged

UnwireEvents() (public)

Unsubscribes from all DevicesViewModel events.

Device Assignment

OnAssignToSlot(int slotIndex) (private)

Assigns the selected device to a slot:

  1. Auto-creates the slot if needed.
  2. SettingsManager.AssignDeviceToSlot(), populates ProductGuid.
  3. Creates default PadSetting if none exists.
  4. AutoEnableHidingDefaults() for newly assigned devices.
  5. Marks dirty, raises DeviceAssignmentChanged, DeviceHidingStateChanged, NavigateToSlotRequested.

AssignDeviceToSlot(Guid instanceGuid, int slotIndex) (public)

Public version for drag-and-drop. Same logic as OnAssignToSlot but takes a GUID directly.

OnToggleSlot(int slotIndex) (private)

Toggles device assignment for a slot (multi-slot support). If unassigning leaves no remaining slots, auto-disables hiding.

UnassignDevice(Guid instanceGuid) (public)

Removes all slot assignments for a device.

Slot Management

CreateSlot(VirtualControllerType type = Microsoft) (public) -> int

Creates the next available slot:

  1. Sets OutputType before SlotCreated (order matters for sidebar rebuild).
  2. Sets ProfileId = GetDefaultProfileId(type) so the profile picker shows a selection immediately. For Xbox slots the default comes from XboxProfiles.DefaultXboxProfileId. For Extended this is the Custom "PadForge Game Controller" profile.
  3. Returns slot index (0–15) or -1 if full.

DeleteSlot(int slotIndex) (public)

Clears SlotCreated[slotIndex], calls padVm.ResetAllSettings() to prevent stale leaks, removes all UserSettings mapped to this slot.

SetSlotEnabled(int slotIndex, bool enabled) (public)

Sets SettingsManager.SlotEnabled[slotIndex].

Device Hiding Toggle

OnHideDevice(Guid instanceGuid) (private)

Marks a device as hidden in SettingsManager and ViewModel.

OnRemoveDevice(Guid instanceGuid) (private)

Removes a device and all associated settings. The virtual controller slot persists empty.

OnDeviceHidingChanged(Guid instanceGuid) (private)

Handles HidHide/ConsumeInput/ForceRawJoystickMode toggles. Writes state to UserDevice, marks dirty, raises DeviceHidingStateChanged.

AutoEnableHidingDefaults(UserDevice, DeviceRowViewModel) (private)

Sets default hiding for newly assigned devices. Gamepads: auto-enables HidHide (if driver available). Keyboards/mice: does not auto-enable (blocking the only keyboard/mouse would lock out Windows).

DeviceService All Public Methods

Method Signature Description
WireEvents void WireEvents() Subscribes to DevicesViewModel events
UnwireEvents void UnwireEvents() Unsubscribes from events
AssignDeviceToSlot void AssignDeviceToSlot(Guid instanceGuid, int slotIndex) Public assignment for drag-and-drop
UnassignDevice void UnassignDevice(Guid instanceGuid) Removes all slot assignments
CreateSlot int CreateSlot(VirtualControllerType type = Microsoft) Creates next available slot
DeleteSlot void DeleteSlot(int slotIndex) Deletes a slot and unassigns devices
SetSlotEnabled void SetSlotEnabled(int slotIndex, bool enabled) Enables/disables a slot

DeviceService All Events

Event Signature Description
DeviceAssignmentChanged event EventHandler DeviceAssignmentChanged Fired after assign/unassign. MainWindow refreshes PadViewModel device info
NavigateToSlotRequested event EventHandler<int> NavigateToSlotRequested Fired after assignment. MainWindow navigates to the assigned slot's page
DeviceHidingStateChanged event EventHandler DeviceHidingStateChanged Fired when hiding toggles change. InputService re-applies device hiding

RecorderService

File: PadForge.App/Services/RecorderService.cs Implements: IDisposable

Handles input recording for mapping assignment. When the user clicks "Record", captures current state as a baseline, polls at 30 Hz for changes, and writes the detected input descriptor to the MappingItem.

RecorderService Constructor

public RecorderService(MainViewModel mainVm)

Stores reference to MainViewModel.

Recording Flow

User clicks Record
       |
StartRecording(mapping, padIndex, deviceGuid)
       |
       v
Capture baseline state (clone of CustomInputState)
       |
       v
Start 30Hz DispatcherTimer (PollTick)
       |
       v   [each tick]
PollTick:
  1. Check timeout (10 seconds)
  2. Read current state (clone)
  3. Wait-for-release phase (if neutralizeBaseline)
  4. Check buttons (instant detection)
  5. Check POV hats (instant detection)
  6. Check axes (3-cycle hold confirmation)
       |
       v   [on detection]
CompleteRecording:
  1. Build descriptor string ("Button 0", "Axis 1", "POV 0 Up")
  2. Auto-detect inversion based on movement direction + target type
  3. Call mapping.LoadDescriptor(descriptor)
  4. Raise RecordingCompleted event

StartRecording(MappingItem, int padIndex, Guid deviceGuid, bool neutralizeBaseline, bool negRecording) (public)

Cancels any existing recording, captures baseline CustomInputState, sets mapping.IsRecording = true, starts 30 Hz timer.

  • neutralizeBaseline: waits for all buttons/POVs to return to neutral before detecting (for auto-prompt follow-ups).
  • negRecording: records the negative direction of a bidirectional axis.

CancelRecording() (public)

Stops the timer, clears all recording state, sets IsRecording = false.

Detection Algorithm

Input type Detection Details
Buttons Instant Any button transitioning from unpressed to pressed
POV hats Instant Any POV transitioning from centered (-1) to a direction (8 sectors, 45 degrees each)
Axes 3-cycle hold Threshold: 16384 units (~25% of 65535 range). Largest absolute delta wins. Mouse exception: instant accept (deltas return to center)

Auto-inversion: ShouldAutoInvert() applies the "I" prefix based on target type and direction:

Target Inverts when
Stick axes User pushed wrong direction for target
Trigger axes Axis value decreased (reverse polarity)
KBM axes Never (screen convention is correct)
Other User pushed negative

Constants:

Constant Value Description
PollIntervalMs 33 ~30 Hz poll rate
TimeoutSeconds 10 Recording auto-cancels after this
AxisThreshold 16384 ~25% of full range
AxisHoldCycles 3 Cycles axis must be held

RecorderService All Public Methods

Method Signature Description
StartRecording void StartRecording(MappingItem mapping, int padIndex, Guid deviceGuid, bool neutralizeBaseline = false, bool negRecording = false) Starts input recording
CancelRecording void CancelRecording() Cancels without assigning
Dispose void Dispose() Cancels recording, disposes resources

Properties:

Property Type Description
IsRecording bool (get) True when recording is active

RecorderService All Events

Event Signature Description
RecordingCompleted event EventHandler<RecordingResult> RecordingCompleted Raised on successful detection
RecordingTimedOut event EventHandler RecordingTimedOut Raised after 10 second timeout

RecordingResult fields: Mapping, PadIndex, Descriptor, Type, Index, PovDirection.


ForegroundMonitorService

File: PadForge.App/Services/ForegroundMonitorService.cs

Monitors the foreground window and fires an event when the foreground process matches a profile's executable list. Not a standalone timer. Called at 30 Hz from InputService's UI timer tick.

How It Works

CheckForegroundWindow() (public)

Called at 30 Hz by UiTimer_Tick:

  1. Bails if EnableAutoProfileSwitching is false or no profiles exist.
  2. Gets foreground window handle via GetForegroundWindow(), then process ID and MainModule.FileName.
  3. Skips if exe path unchanged (_lastExePath deduplication).
  4. Matches against all profiles' ExecutableNames (pipe-separated full paths, case-insensitive).
  5. Fires ProfileSwitchRequired only when the matched profile changes (_lastMatchedProfileId). Null signals reversion to default.

ForegroundMonitorService All Public Methods

Method Signature Description
CheckForegroundWindow void CheckForegroundWindow() Polls foreground window and fires event on profile change

ForegroundMonitorService All Events

Event Signature Description
ProfileSwitchRequired event Action<string> ProfileSwitchRequired Fired with profile ID (or null for default) when foreground process matches a different profile

MainWindow Service Wiring

File: PadForge.App/MainWindow.xaml.cs

MainWindow owns the service instances and wires them to ViewModels. Two notable behaviors:

SyncBarBackgrounds

SyncBarBackgrounds() pixel-samples the current theme's background brush at runtime to match the sidebar and app branding bar backgrounds. Re-invoked on theme changes via the ThemeManager.Current.ActualApplicationThemeChanged handler, ensuring the bars stay consistent when the user switches between light and dark themes.

AddController Popup Dismiss

The "Add Controller" popup auto-dismisses on navigation, window move, window resize, and window deactivation. This prevents the popup from floating over stale content when the user interacts with other parts of the application.


Service Interaction Patterns

Startup Sequence

App.OnStartup
  |-- MainWindow constructor
  |     |-- Creates MainViewModel, SettingsService, InputService, DeviceService, RecorderService
  |     |-- SettingsService.Initialize()        [loads XML into SettingsManager]
  |     |-- DeviceService.WireEvents()          [subscribes to DevicesViewModel events]
  |     |-- InputService.SettingsService = ss   [for save triggers]
  |     |-- InputService.Start()                [creates engine, starts all subsystems]
  |     |     |-- InputManager.Start()          [launches polling thread]
  |     |     |-- StartDsuServerIfEnabled()
  |     |     |-- StartWebServerIfEnabled()
  |     |     |-- SyncAudioBassDetector()
  |     |     |-- ApplyDeviceHiding()
  |     |     |-- UI timer starts (30Hz)

Shutdown Sequence

MainWindow.OnClosing
  |-- InputService.Stop()
  |     |-- UI timer stops
  |     |-- Unsubscribe all events
  |     |-- StopDsuServer()
  |     |-- StopWebServer()
  |     |-- StopAudioBassDetector()
  |     |-- RemoveDeviceHiding()
  |     |-- InputManager.Stop() + Dispose()
  |     |-- Tear down Extended HM virtuals (unless preserveExtendedNodes is set)
  |-- SettingsService.Save()              [final save]
  |-- DeviceService.UnwireEvents()
  |-- RecorderService.Dispose()

Device Connected Flow

InputManager.UpdateDevices()              [polling thread, every 2s]
  |-- SDL enumerates devices
  |-- DevicesUpdated event fires
  |-- Dispatcher.BeginInvoke:             [UI thread]
        |-- SyncDevicesList()
        |-- UpdatePadDeviceInfo()
        |-- ApplyDeviceHiding()           [blacklist newly connected devices]

Settings Change Flow

User drags slider on Pad page
  |-- PadViewModel property updates (data binding)
  |-- [next 30Hz tick] SyncViewModelToPadSettings()
  |     |-- SaveViewModelToPadSetting(syncMappings: false)
  |     |     |-- PadSetting properties updated (string refs, atomic)
  |     |     |-- Polling thread reads new values next cycle

Profile Switch Flow

[30Hz tick] ForegroundMonitor.CheckForegroundWindow()
  |-- Detects new foreground matches different profile
  |-- ProfileSwitchRequired event fires
  |-- InputService.OnProfileSwitchRequired(profileId)
        |-- SaveActiveProfileState()        [snapshot outgoing profile]
        |-- ApplyProfile(target)            [load incoming profile]
        |     |-- Set SlotCreated/Enabled/OutputType
        |     |-- Reset all MapTo = -1, apply profile entries
        |     |-- Apply Extended/MIDI configs
        |     |-- Apply DSU/Web server settings
        |     |-- UpdatePadDeviceInfo()
        |     |-- Reload PadSettings into ViewModels

Recording Flow

User clicks Record button
  |-- MainWindow calls RecorderService.StartRecording(mapping, padIndex, deviceGuid)
  |-- [30Hz timer] PollTick detects input change
  |-- CompleteRecording(type, index, direction)
  |-- RecordingCompleted event fires
  |-- MainWindow receives result, optionally prompts for next mapping

See Also

Clone this wiki locally