Skip to content

Architecture Overview

hifihedgehog edited this page May 20, 2026 · 58 revisions

Architecture Overview

High-level architecture: solution structure, project layout, design philosophy, threading model, data flow, dependencies, and build system.

Note for v3: This page describes the cross-cutting architecture and project layout. The day-to-day virtual-controller lifecycle (HIDMaestro thread-pool create/destroy, OpenXInput shim, bubble-up cascade, inactivity timeout) is documented in HIDMaestro Deep Dive — that's the canonical v3 source. Some implementation detail in the deeper subsections of this page still describes v2 mechanics under v3 names; treat the diagrams and high-level flow as current, and cross-check specific class names against the source.

graph TB
    subgraph "Presentation Layer. PadForge.App"
        UI[WPF Views<br/>Dashboard . Pad . Devices . Settings . Profiles . About]
        VM[ViewModels<br/>PadViewModel . DashboardViewModel . DevicesViewModel . SettingsViewModel]
    end

    subgraph "Services Layer. PadForge.App"
        IS[InputService<br/>Engine lifecycle . 30Hz UI sync]
        SS[SettingsService<br/>XML load/save . auto-save . profiles]
        DS[DeviceService<br/>Device list sync . HidHide]
        RS[RecorderService<br/>Input mapping recorder]
        FMS[ForegroundMonitorService<br/>Per-app profile switching]
    end

    subgraph "Engine. PadForge.App/Common/Input"
        IM[InputManager<br/>Polling loop . Steps 1-6]
        HMP[HM lifecycle thread pool<br/>Create / Destroy off polling thread]
        ABD[AudioBassDetector<br/>WASAPI loopback . IIR filter]
    end

    subgraph "Virtual Controllers"
        MS[Xbox<br/>Xbox 360 / One / Series / Elite / Adaptive]
        SONY[PlayStation<br/>DS3 / DS4 / DualSense / DualSense Edge]
        EXT[Extended<br/>Sticks . Wheels . Custom HID]
        KBM[Keyboard+Mouse<br/>SendInput, no driver]
        MIDI[MIDI<br/>Windows MIDI Services]
    end

    subgraph "Data Layer. PadForge.Engine"
        PS[PadSetting . UserSetting . UserDevice<br/>Gamepad . Vibration . MotionSnapshot]
        SDL[SdlDeviceWrapper<br/>SDL3 P/Invoke]
        RIL[RawInputListener<br/>Keyboard . Mouse]
        IHM[InputHookManager<br/>LL hooks . input suppression]
    end

    subgraph "External Systems"
        SDL3[SDL3.dll<br/>Custom fork: HM filter + Switch 2 Pro]
        OXI[OpenXInput<br/>xinput1_4 / devobj shim]
        HM[HIDMaestro<br/>UMDF2 user-mode driver, 225+ profiles]
        HH[HidHide Driver]
        WMS[Windows MIDI Services]
        WASAPI[Windows Audio<br/>WASAPI Loopback]
        DSU_CLIENT[DSU Clients<br/>Cemu . Dolphin]
        BROWSER[Web Browsers<br/>Phone . tablet]
    end

    UI --> VM
    VM --> IS
    VM --> SS
    IS --> IM
    IS --> ABD
    IS --> DS
    IS --> FMS
    RS --> IM
    IM --> HMP
    IM --> KBM
    IM --> MIDI
    HMP --> MS
    HMP --> SONY
    HMP --> EXT
    IM --> SDL
    IM --> RIL
    IM --> IHM
    ABD --> WASAPI
    SDL --> SDL3
    SDL --> OXI
    MS --> HM
    SONY --> HM
    EXT --> HM
    MIDI --> WMS
    DS --> HH
    IM -.->|UDP 26760| DSU_CLIENT
    IM -.->|HTTP+WS| BROWSER

    style UI fill:#e1f5fe
    style IM fill:#f3e5f5
    style HMP fill:#f3e5f5
    style PS fill:#e8f5e9
    style SDL3 fill:#fff3e0
    style OXI fill:#fff3e0
    style HM fill:#fff3e0
Loading

Solution Structure

Two-project .NET 10 solution:

Project Target Role
PadForge.App net10.0-windows10.0.26100.0 (WPF, WinExe) UI, input pipeline, virtual controllers, services
PadForge.Engine net10.0-windows (Class Library) Shared data types, SDL3 P/Invoke, device wrappers, input state structures

PadForge.App references PadForge.Engine. The Engine has no WPF dependencies and is reusable. Both projects use GenerateAssemblyInfo=false and share AssemblyVersion / AssemblyFileVersion via the repo-root SharedVersion.cs linked into both csproj files (<Compile Include="..\SharedVersion.cs" />). Per-project Properties/AssemblyInfo.cs carries no version attributes.


Project Layout

PadForge.App

PadForge.App/
  App.xaml / App.xaml.cs              # Entry point, auto-elevation, global exception handling
  MainWindow.xaml / MainWindow.xaml.cs # Shell: app branding bar, sidebar navigation, page hosting, event wiring
  gamecontrollerdb_padforge.txt       # Custom SDL gamepad mappings (DS3 SDF, etc.)
  Properties/
    AssemblyInfo.cs                   # Version, metadata (GenerateAssemblyInfo=false)

  Common/
    ControllerIcons.cs                # SVG path data for controller type icons (Xbox, PlayStation, Extended, MIDI, KB+M)
    CurveLut.cs                       # Sensitivity curve LUT generation (per-axis response curves)
    DriverInstaller.cs                # HidHide / Windows MIDI Services install. Legacy ViGEmBus / vJoy uninstall
    HidHideController.cs              # HidHide IOCTL API: blacklist/whitelist/cloaking via \\.\HidHide
    MarqueeBehavior.cs                # WPF attached behavior for scrolling/marquee text animation
    SettingsManager.cs                # Slot arrays, profiles, PadSetting defaults, partial class (see below)
    StartupHelper.cs                  # Run-at-startup registry helper (HKCU\...\Run)
    VirtualKey.cs                     # Windows VK code → display name lookup table

    Input/
      AudioBassDetector.cs            # WASAPI loopback capture + 8th-order IIR bass extraction
      InputManager.cs                 # Core partial class: fields, Start/Stop, PollingLoop, IDisposable
      InputManager.Step1.UpdateDevices.cs       # Device enumeration (SDL3 + Raw Input)
      InputManager.Step2.UpdateInputStates.cs   # Input state reading + force feedback
      InputManager.Step3.UpdateOutputStates.cs  # Mapping engine (descriptor → OutputState)
      InputManager.Step4.CombineOutputStates.cs # Multi-device merge per slot
      InputManager.Step4b.EvaluateMacros.cs     # Macro trigger/action state machine
      InputManager.Step5.VirtualDevices.cs      # HIDMaestro + MIDI + KBM virtual controller lifecycle (HM lifecycle on thread pool)
      InputManager.Step6.RetrieveOutputStates.cs # Copy combined states for UI display
      HMaestroVirtualController.cs    # IVirtualController for HIDMaestro (Xbox, PlayStation, Extended)
      HMaestroProfileCatalog.cs       # HIDMaestro profile lookup (HMProfile per VC subtype)
      HMaestroFfbDescriptor.cs        # Feedback descriptor for HM controllers (rumble + FFB ranges)
      HMaestroFfbDecoder.cs           # Decodes raw HM feedback packets into Vibration / FFB state
      SonyReportPackers.cs            # DS3 / DS4 / DualSense Report 0x01 input passthrough packers
      MidiVirtualController.cs        # IVirtualController for Windows MIDI Services
      KeyboardMouseVirtualController.cs # IVirtualController for Win32 SendInput (KB+Mouse)
      InputEventArgs.cs               # Event args for input state change notifications
      InputException.cs               # Custom exception for input pipeline errors
      InputExceptionEventArgs.cs      # Event args wrapping InputException

  Services/
    InputService.cs                   # Bridge: InputManager (engine thread) ↔ UI (30Hz timer)
    SettingsService.cs                # Settings load/save, applies PadSettings to InputManager
    DeviceService.cs                  # Device list UI sync (ObservableCollection from UserDevices)
    DsuMotionServer.cs                # UDP server on port 26760. DSU/Cemuhook motion protocol
    ForegroundMonitorService.cs       # Polls GetForegroundWindow at 30Hz for per-app profile switching
    RecorderService.cs                # Input mapping recorder (physical input → mapping descriptors)
    WebControllerServer.cs            # Embedded HTTP+WebSocket server for browser-based virtual controllers

  ViewModels/
    ViewModelBase.cs                  # Base class: INotifyPropertyChanged, OnCultureChanged hook
    MainViewModel.cs                  # Shell VM: navigation, sidebar items, profile list, Pads[] array
    DashboardViewModel.cs             # Per-slot status cards, virtual controller status
    PadViewModel.cs                   # Per-slot mapping/settings/deadzone/macro configuration
    DevicesViewModel.cs               # Physical device list with live input visualization
    DeviceRowViewModel.cs             # Single device card in the Devices page
    SettingsViewModel.cs              # App-level settings (polling rate, driver status, etc.)
    MappingItem.cs                    # Single axis/button mapping row in PadPage
    MacroItem.cs                      # Macro definition: trigger, actions, repeat mode, state machine
    MidiSlotConfig.cs                 # Per-slot MIDI config: channel, velocity, CC/note counts
    StickConfigItem.cs                # Thumbstick deadzone / anti-deadzone / linear config
    TriggerConfigItem.cs              # Trigger deadzone / anti-deadzone / max range config
    ExtendedSlotConfig.cs                 # Extended VC config: axis/button/POV/stick/trigger counts (HIDMaestro Extended profile)

  Views/
    DashboardPage.xaml(.cs)           # Card-based dashboard with per-slot status and 3D/2D preview
    PadPage.xaml(.cs)                 # Mapping grid, deadzone sliders, macros
    DevicesPage.xaml(.cs)             # Physical device list with live input visualization
    SettingsPage.xaml(.cs)            # Polling rate, driver install, DSU toggle
    ProfilesPage.xaml(.cs)            # Profile management: save/load/delete
    AboutPage.xaml(.cs)               # Version, credits, license
    ControllerModelView.xaml(.cs)     # 3D controller visualization (HelixToolkit viewport)
    ControllerModel2DView.xaml(.cs)   # 2D controller overlay (Canvas-based)
    ControllerSchematicView.xaml(.cs) # Schematic controller diagram (vector-based)
    KBMPreviewView.xaml(.cs)          # Keyboard+Mouse interactive preview
    MidiPreviewView.xaml(.cs)         # MIDI piano keyboard + CC slider preview
    MousePreviewControl.xaml(.cs)     # Read-only mouse graphic for Devices page detail pane
    CopyFromDialog.xaml(.cs)          # Copy mappings from another slot
    ProfileDialog.xaml(.cs)           # Save new profile (name + exe list)

  Models3D/
    ControllerModelBase.cs            # Abstract base for 3D models (OBJ loading, part animation)
    ControllerModelXbox360.cs         # Xbox 360 3D model parts and animation bindings
    ControllerModelDS4.cs             # DualShock 4 3D model parts and animation bindings

  Models2D/
    ControllerOverlayLayout.cs        # 2D overlay positioning data (button/stick coordinates)

  2DModels/
    DS4/                              # DualShock 4 PNG sprites (Gamepad-Asset-Pack, MIT)
    XBOX360/                          # Xbox 360 PNG sprites

  3DModels/
    DS4/                              # DualShock 4 OBJ meshes (from Handheld Companion, CC BY-NC-SA 4.0)
    XBOX360/                          # Xbox 360 OBJ meshes

  Controls/
    CurveEditor.xaml(.cs)             # Interactive sensitivity curve editor (Bezier/linear)
    RangeSlider.cs                    # Dual-thumb range slider (deadzone min/max)

  Converter/
    BoolToColorConverter.cs           # bool → SolidColorBrush
    BoolToVisibilityConverter.cs      # bool → Visible / Collapsed
    CrossGeometryConverter.cs         # Cross/X geometry for close buttons
    NormToCanvasConverter.cs          # Normalized float → Canvas pixel coordinate
    NormToTriggerHeightConverter.cs   # Trigger value → bar height
    NullToCollapsedConverter.cs       # null → Collapsed, non-null → Visible
    PercentToSizeConverter.cs         # Percentage → pixel size
    SlopedWedgeGeometryConverter.cs   # Wedge geometry for trigger visuals
    StringToVisibilityConverter.cs    # Non-empty string → Visible, empty → Collapsed

  Resources/
    ControllerIcons.xaml              # XAML resource dictionary with controller icon geometries
    PadForge.ico                      # Application icon
    Xbox Series Controller - Front.png  # Controller reference image
    Xbox Series Controller - Top.png    # Controller reference image
    Strings/
      Strings.resx                    # Base (English) UI string resources
      Strings.Designer.cs            # Hand-written INotifyPropertyChanged resource accessor
      Strings.de.resx                # German
      Strings.es.resx                # Spanish
      Strings.fr.resx                # French
      Strings.it.resx                # Italian
      Strings.ja.resx                # Japanese
      Strings.ko.resx                # Korean
      Strings.nl.resx                # Dutch
      Strings.pt-BR.resx             # Brazilian Portuguese
      Strings.zh-Hans.resx           # Simplified Chinese
    SDL3/x64/SDL3.dll                 # SDL3 native library (custom fork: HM filter + Switch 2 Pro)
    SDL3/x64/libusb-1.0.dll           # libusb for HIDAPI backend (Switch 2 support)
    OpenXInput/x64/xinput1_4.dll      # OpenXInput fork. Single-file-embedded into PadForge.exe; SetDllDirectory at launch resolves it ahead of System32. Filters HM virtuals from PadForge's own XInput view
    HIDMaestro/HIDMaestro.Core.dll    # HIDMaestro SDK (HMContext, HMProfile, HMController, SubmitState, SubmitRawReport)
    HidHide_1.5.230_x64.exe           # Embedded HidHide installer

  WebAssets/
    index.html                        # Landing page with Xbox 360 and DS4 layout cards
    controller.html                   # Controller UI shell (dynamic overlay layout)
    css/controller.css                # Responsive dark theme with touch-optimized zones
    js/controller_client.js           # WebSocket client, touch handling, layout renderer
    js/nipplejs.min.js                # Virtual joystick library for analog sticks

  Themes/
    Generic.xaml                      # Custom control default styles (RangeSlider)

PadForge.Engine

PadForge.Engine/
  Properties/
    AssemblyInfo.cs

  Common/
    SDL3Minimal.cs              # SDL3 P/Invoke declarations (init, joystick, gamepad, haptic, sensor)
    ISdlInputDevice.cs          # Interface: GetCurrentState(), GetDeviceObjects(), rumble
    SdlDeviceWrapper.cs         # Joystick/Gamepad open, state reading, GUID construction, haptic
    SdlKeyboardWrapper.cs       # Keyboard via Raw Input, ISdlInputDevice adapter
    SdlMouseWrapper.cs          # Mouse via Raw Input with delta accumulation, ISdlInputDevice adapter
    RawInputListener.cs         # Hidden HWND_MESSAGE window, RIDEV_INPUTSINK, per-device state
    CustomInputState.cs         # API-agnostic input snapshot: axes[24], sliders[8], povs[4], buttons[256], gyro[3], accel[3]
    CustomInputHelper.cs        # Diff computation between two CustomInputState snapshots
    CustomInputUpdate.cs        # Single input change (axis delta, button press/release)
    DeviceObjectItem.cs         # Metadata for one axis/button/hat on a device
    DeviceEffectItem.cs         # Metadata for a force feedback effect on a device
    ForceFeedbackState.cs       # Per-device rumble: change detection, haptic effect lifecycle
    GamepadTypes.cs             # Gamepad struct (XInput layout), ExtendedRawState, MidiRawState, KbmRawState, Vibration, MotionSnapshot
    InputTypes.cs               # Enums: DeviceObjectTypeFlags, ObjectAspect, MapType, ObjectGuid, InputDeviceType
    VirtualControllerTypes.cs   # VirtualControllerType enum, IVirtualController interface
    RumbleLogger.cs             # Diagnostic logger for rumble/FFB (disabled by default)
    InputHookManager.cs         # WH_KEYBOARD_LL / WH_MOUSE_LL hooks for mapped input suppression
    WebControllerDevice.cs      # Virtual input device from browser clients (implements ISdlInputDevice)

  Data/
    DeadZoneShape.cs            # Deadzone shape enum
    DeviceTuning.cs             # Per-(device, slot) tuning bag (gyro, etc.)
    MappingRow.cs               # One output row inside a MappingSet (Target, Sources, CombineMode, LayerMask)
    MappingSet.cs               # Per-VC mapping table — rows + ShiftActivators (Issue #61)
    MappingSetMigrator.cs       # Loads v2 per-device PadSetting mapping fields into the v3 MappingSet shape
    MappingSource.cs            # One physical input feeding a row — Kind, DeviceGuid, Descriptor, Invert, Half, …
    MappingTranslation.cs       # Cross-layout mapping translation (Xbox/PlayStation/Extended/MIDI/KBM equivalence)
    PadSetting.cs               # Per-slot tuning (deadzones, force feedback, lighting, AT, MIDI, etc.)
    ShiftActivator.cs           # One activator on a MappingSet — input descriptor, Mode, Kind, LayerMask, color
    UserDevice.cs               # Physical device record: GUID, name, capabilities, runtime state
    UserSetting.cs              # Links a UserDevice (InstanceGuid) to a pad slot (MapTo) with a PadSetting

tools/

tools/
  DsuDiag/                      # Standalone DSU client for motion data diagnostics
  Ds4InputDump/                 # DS4 raw HID input dump (Sony Report 0x01 passthrough debug)
  vJoy/                         # Legacy v2 vJoy test/SDK assets (kept for reference, unused by v3)
  capture_screenshots.ps1       # Automated screenshot capture script
  capture_all.ps1               # Full screenshot capture orchestration
  deploy.ps1                    # Build + deploy to install directory
  deploy_and_restart.ps1        # Deploy + restart PadForge
  dump_ui_tree.ps1              # WPF visual tree dump for debugging
  overlay_positions.py          # 2D controller overlay coordinate generator
  (+ various legacy vJoy diagnostic scripts from v2)

Key Design Decisions

Why SDL3 (not DirectInput or raw XInput)

SDL3 is the sole input abstraction for all physical controllers, including Xbox/XInput gamepads.

Advantage Detail
Unified API Gamepad API normalizes button/axis layouts across Xbox, DualSense, Switch Pro, etc.. No per-family code paths
Sensor support Gyro and accelerometer from DualSense, DS4, Switch Pro, and Joy-Con via one API. DirectInput has no gyro/accel
Background input SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS reads input without window focus
Gamepad database SDL's gamecontrollerdb + PadForge's gamecontrollerdb_padforge.txt auto-map hundreds of controllers
HIDAPI backend Reads exotic controllers (Switch 2 Pro via custom fork) that no Windows API supports natively

The native XInput P/Invoke (xinput1_4.dll) in Step 5 is used only for detecting which XInput slot (0–3) a new HIDMaestro Xbox virtual controller occupies. It never reads input.

Key SDL3 hints:

SDL_SetHint(SDL_HINT_JOYSTICK_XINPUT, "1");                // Xbox enumeration via XInput backend
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); // Background input without focus
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH2, "1");         // Switch 2 Pro (custom fork)
// NEVER set SDL_HINT_JOYSTICK_RAWINPUT. Conflicts with XInput enumeration

Why HIDMaestro (not ScpVBus)

Virtual Xbox and PlayStation controllers use HIDMaestro:

Advantage Detail
Maintained driver Signed for Windows 10/11. ScpVBus is abandoned with signing issues
DS4 support Native virtual DS4. ScpVBus only supports Xbox 360
Rumble feedback Per-controller FeedbackReceived callback delivers game rumble back to PadForge. ScpVBus has no equivalent
Slot control Create controllers at specific XInput slots with proper lifecycle management

Why Polling (not event-driven)

The input pipeline is a fixed-rate polling loop (stable 1000 Hz):

Reason Detail
Deterministic timing Consistent latency regardless of controller type or OS scheduling
Multi-device merging Reads all devices synchronously each frame. Merged output reflects a single point in time
Macro evaluation Natural "frames" for the macro trigger/action state machine
Steady output rate Virtual controllers get constant updates; event-driven output would burst on activity and go silent on idle

Design Philosophy

MVVM with CommunityToolkit.Mvvm

ViewModels extend ObservableObject (CommunityToolkit.Mvvm), using [ObservableProperty] and [RelayCommand] source generators. ViewModelBase adds OnCultureChanged() for live language switching.

Command + Event Decoupling

VMs expose commands and raise events but never call services or touch the input pipeline directly:

  1. VMs raise events (e.g., MappingChanged, SlotAdded, SelectedDeviceChanged)
  2. MainWindow.xaml.cs or service classes subscribe
  3. Handlers call InputService, SettingsService, etc.

This keeps VMs testable and decoupled from the engine thread.

No DI Container

Dependencies are wired manually in App.xaml.cs and MainWindow.xaml.cs:

App.OnStartup()
  → Single-instance mutex check
  → Early language restore (regex parse of PadForge.xml before full load)
  → Admin elevation (declared in app.manifest. requireAdministrator)
  → new MainWindow()
      → Creates MainViewModel (root VM with Pads[] array)
      → Creates SettingsService, InputService, RecorderService, DeviceService
      → Wires ViewModel events → service handlers
      → SettingsService.Initialize() loads PadForge.xml
      → InputService.Start() creates InputManager, starts polling thread
      → Async Raw Input enumeration (keyboard/mouse on background thread)

App Branding Bar

The custom title bar was replaced with an app branding bar. A styled bar at the top of the window that uses ExtendViewIntoTitleBar to blend the hamburger menu button and application icon into the window chrome. The branding bar background is pixel-sampled from the current theme to stay consistent across light/dark mode switches.


SettingsManager vs SettingsService

Two classes with distinct roles:

SettingsManager (static, Common/SettingsManager.cs)

A static data container shared between engine and UI threads:

  • UserDevices / UserSettings collections (SyncRoot locking)
  • Slot arrays: SlotCreated[], SlotEnabled[]
  • Profile data: Profiles, ActiveProfileId, EnableAutoProfileSwitching
  • Per-type limits: MaxXbox360Slots, MaxPlayStationSlots, MaxExtendedSlots, etc.
  • Helpers: CreateDefaultPadSetting(), SwapSlots(), FindSlotForDevice()

Partial class split across:

  • Common/SettingsManager.cs. Profiles, slot arrays, helpers
  • InputManager.Step1.UpdateDevices.cs. UserDevices/UserSettings declarations and collection classes

Has no knowledge of XML, ViewModels, or UI.

SettingsService (instance, Services/SettingsService.cs)

The persistence and sync layer:

  • Loads PadForge.xml (or Settings.xml fallback) via XmlSerializer into SettingsManager
  • Saves SettingsManager state to XML (manual + auto-save timer)
  • Syncs SettingsManager data bidirectionally with ViewModels
  • Manages profiles: save-as, load, delete, default snapshot
  • Tracks dirty state: IsDirty flag, AutoSaved event

The engine thread reads SettingsManager without referencing the WPF-dependent SettingsService.


InputManager Partial Class Split

InputManager is a partial class split across 8 files for pipeline stage isolation. Each file owns one stage's fields, helpers, and state. This avoids a 5000+ line monolith while keeping stages in a single class (they share per-slot arrays and virtual controller references).

File Stage Responsibility
InputManager.cs Core Fields, constants, Start()/Stop(), PollingLoop(), IDisposable, motion snapshots, DSU broadcast
InputManager.Step1.UpdateDevices.cs Step 1 SDL device enumeration, open/close, HIDMaestro filtering, UserDevices/UserSettings collection classes
InputManager.Step2.UpdateInputStates.cs Step 2 Read CustomInputState per device, apply FFB from VibrationStates[] + audio bass
InputManager.Step3.UpdateOutputStates.cs Step 3 Map CustomInputStateOutputState via PadSetting descriptors (deadzones, curves, inversion, range clamping)
InputManager.Step4.CombineOutputStates.cs Step 4 Merge device OutputStates per slot into CombinedOutputStates[] (max-wins axes, OR buttons)
InputManager.Step4b.EvaluateMacros.cs Step 4b Evaluate macro triggers, execute actions (button/axis overrides, volume OSD, toggle)
InputManager.Step5.VirtualDevices.cs Step 5 Create/destroy IVirtualController (HM lifecycle on thread pool, see HIDMaestro Deep Dive), submit CombinedOutputStates[] via HMContext.SubmitState / SubmitRawReport (HM) or per-VC paths (MIDI/KBM), XInput slot detection
InputManager.Step6.RetrieveOutputStates.cs Step 6 Copy CombinedOutputStates[]RetrievedOutputStates[] for UI

SettingsManager is also a partial class. Its collection types are declared alongside the Step 1 code that populates them.


Threading Model

Up to seven threads:

1. Engine Thread (InputManager, 1000 Hz)

_pollingThread = new Thread(PollingLoop)
{
    Name = "PadForge.InputManager",
    IsBackground = true,
    Priority = ThreadPriority.AboveNormal
};

Runs the 6-step pipeline (see Input Pipeline). Uses a 3-tier sleep strategy with wall-clock drift compensation:

  1. Tier 1. HR Waitable Timer: CreateWaitableTimerExW with CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Windows 10 1803+). Sub-ms kernel sleep, near-zero CPU. Sleeps remaining - 0.1ms, leaving the tail for spin-wait.
  2. Tier 2. Multimedia Timer: timeSetEvent + ManualResetEvent.WaitOne (x360ce-style fallback). Periodic 1ms callback signals the event.
  3. Tier 3. Thread.Sleep(1): Legacy fallback when remaining > 1.5ms and both timers unavailable.
  4. Final spin-wait: Thread.SpinWait(1) loop against Stopwatch.ElapsedTicks for sub-ms cycle boundary.

Drift compensation: Each cycle accumulates expectedTicks += targetTicks and compares against wallClock.ElapsedTicks. Late cycles shorten; early cycles lengthen. Drift exceeding 10 cycles (e.g., after system sleep) resets the wall clock.

timeBeginPeriod(1) is set for the polling loop's lifetime. Auto-idle (~20 Hz via Thread.Sleep(50)) activates when no slots are created. Timing target recalculates each cycle from the adjustable PollingIntervalMs (default 1ms, configurable in Settings).

2. UI Thread (WPF Dispatcher, ~30 Hz)

InputService runs a DispatcherTimer at ~33ms interval:

  • Reads RetrievedOutputStates[] / RetrievedKbmRawStates[] for dashboard/schematic display
  • Reads UserDevice.InputState for Devices page live visualization (only when visible)
  • Pushes macro snapshots from PadViewModels to MacroSnapshots[]
  • Updates SlotControllerTypes[], SlotExtendedConfigs[], SlotExtendedIsCustom[], _midiConfigs[]
  • Syncs device list from UserDevicesDevicesViewModel (via DeviceService)
  • Updates dashboard statistics (frequency, device count, online status)
  • Runs macro trigger recording

All WPF binding occurs on this thread. Engine results use atomic reference swap (Volatile.Read/write) or value copy semantics.

3. WASAPI Audio Thread (AudioBassDetector)

AudioBassDetector uses NAudio's WasapiCapture in loopback mode. NAudio's internal capture thread delivers audio buffers via DataAvailable. The callback runs an 8th-order cascaded IIR low-pass filter (configurable cutoff, default 80 Hz) and updates a volatile _bassEnergy float (0.0–1.0). The engine reads this in Step 2 via AudioBassDetector.BassEnergy and merges it with game rumble via max().

Implements IMMNotificationClient to restart capture on default audio device change.

4. Raw Input Thread (Hidden HWND_MESSAGE Window)

public static class RawInputListener

A background thread creates a message-only window (HWND_MESSAGE) and runs a GetMessageW pump. Registered for:

  • HID_USAGE_GENERIC_KEYBOARD with RIDEV_INPUTSINK (background capture)
  • HID_USAGE_GENERIC_MOUSE with RIDEV_INPUTSINK

Per-device state tracked via RAWINPUT.header.hDevice in concurrent dictionaries. The engine reads state in Step 2 via GetKeyboardState() / ConsumeMouseDelta().

Async enumeration: Keyboard and mouse device discovery runs on a Task.Run background thread during startup, preventing slow HID enumeration from blocking the UI thread. Results merge into the device list when the task completes.

5. DSU Receive Thread

DsuMotionServer uses UdpClient.ReceiveAsync() on a background task for DSU client subscriptions (Cemu, Dolphin). Motion data is broadcast by the engine thread after Step 2 (no separate send thread). DSU protocol limited to 4 slots; slots 4–15 skip broadcast.

6. Input Hook Thread (InputHookManager, on demand)

_hookThread = new Thread(() => HookThreadProc(ready))
{
    Name = "InputHookManager",
    IsBackground = true
};

Created only when "Consume mapped inputs" is enabled. Installs WH_KEYBOARD_LL / WH_MOUSE_LL hooks and runs a GetMessageW pump. Suppression sets update via volatile reference swap from the UI thread. Stopped when the engine stops or hiding is disabled.

7. Web Controller Server Thread

_acceptThread = new Thread(AcceptLoop)
{
    Name = "WebControllerServer",
    IsBackground = true
};

Created when the web controller server is enabled. Runs an HttpListener accept loop. WebSocket connections spawn async tasks per client. Each browser client creates a WebControllerDevice (implements ISdlInputDevice) visible in Step 1 enumeration. Also serves static assets and /api/layout JSON.

Thread Safety Summary

Shared State Writer Reader Sync
UserDevices.Items Engine (Step 1) UI, Engine SyncRoot lock
UserSettings.Items UI thread Engine (Steps 2–5) SyncRoot lock
UserDevice.InputState Engine (Step 2) UI timer Atomic ref swap
CombinedOutputStates[] Engine (Step 4) Engine (Steps 5, 6) Single-thread write
RetrievedOutputStates[] Engine (Step 6) UI timer Value copy (struct)
VibrationStates[] HIDMaestro callback Engine (Step 2) Volatile fields
MacroSnapshots[] UI timer Engine (Step 4b) Atomic ref swap
SlotControllerTypes[] UI timer Engine (Step 5) Volatile read
MotionSnapshots[] Engine (loop) DSU broadcast Same thread
RawInputListener state Raw Input thread Engine (Step 2) ConcurrentDictionary
InputHookManager sets UI thread Hook callbacks Volatile ref swap
WebControllerDevice.InputState WebSocket task Engine (Step 2) Atomic ref swap
AudioBassDetector._bassEnergy WASAPI callback Engine (Step 2) Volatile float

Data Flow

Physical controller to game:

Physical Controller (USB/Bluetooth)
  │
  ▼
SDL3.dll (HID / XInput / HIDAPI backend)
  │
  ▼
Step 1: UpdateDevices()
  │  SDL_GetJoysticks() → open new devices → SdlDeviceWrapper
  │  Filter HIDMaestro virtual controllers (VID/PID + device path + container ID walk)
  │  Update UserDevices collection (add new, mark disconnected)
  │
  ▼
Step 2: UpdateInputStates()
  │  For each online device:
  │    SdlDeviceWrapper.GetCurrentState() → CustomInputState
  │    Store as UserDevice.InputState (atomic swap)
  │  Apply force feedback: VibrationStates[] + AudioBass → physical rumble
  │
  ▼
Step 3: UpdateOutputStates()
  │  For each UserSetting (device → slot binding):
  │    Read PadSetting descriptors (axis/button/POV mappings)
  │    Apply per-mapping deadzones (activation threshold per mapping row)
  │    Apply global deadzones, sensitivity curves, inversion, range clamping
  │    CustomInputState → per-device OutputState (Gamepad struct)
  │
  ▼
Step 4: CombineOutputStates()
  │  For each slot (0–15):
  │    Merge all device OutputStates mapped to this slot
  │    Axes: max absolute value wins
  │    Buttons: OR (any device pressing = pressed)
  │    Also produces ExtendedRawState, MidiRawState, KbmRawState
  │
  ▼
Step 4b: EvaluateMacros()
  │  For each slot with macros:
  │    Check triggers against CombinedOutputStates
  │    Execute actions: button overrides, axis overrides, volume OSD, toggle
  │    Modify CombinedOutputStates in-place
  │
  ▼
Step 5: UpdateVirtualDevices()
  │  For each created slot:
  │    Create/destroy IVirtualController if type changed or slot toggled
  │      (HM lifecycle dispatched to thread pool, see [[HIDMaestro Deep Dive]])
  │    Submit CombinedOutputStates[i] → virtual controller
  │    Xbox / PlayStation / Extended: HMaestroVirtualController
  │      → HMContext.SubmitState (gamepad path)
  │      → HMContext.SubmitRawReport (Sony Report 0x01 passthrough on DS4 / DualSense)
  │    KBM: SendInput() → Win32 keyboard/mouse events
  │    MIDI: Windows MIDI Services → virtual MIDI port
  │
  ▼
Step 6: RetrieveOutputStates()
  │  Copy CombinedOutputStates[] → RetrievedOutputStates[]
  │  Copy CombinedKbmRawStates[] → RetrievedKbmRawStates[]
  │  (Value copy, consumed by UI timer at 30 Hz)
  │
  ▼
Game reads virtual controller via XInput / DirectInput / SDL / raw HID

Reverse data flow (game rumble → physical controller)

Game sends rumble via XInput / DirectInput FFB
  │
  ▼
HIDMaestro feedback packet (UMDF2 driver → HM SDK callback)
  │  HMaestroFfbDecoder parses packet → Vibration / FFB state
  │  Writes to VibrationStates[slotIndex]
  │
  ▼
Step 2: ApplyForceFeedback() (engine thread)
  │  Reads VibrationStates[slot] for each device in that slot
  │  max(gameRumble, audioBass) → SdlDeviceWrapper.SetRumble()
  │  SDL3 → USB rumble command → physical controller

NuGet Dependencies

Package Version Purpose
WPF-UI 4.2.0 Fluent Design theme (Windows 11-style UI)
HelixToolkit.Core.Wpf 2.27.3 3D viewport for controller model visualization
CommunityToolkit.Mvvm 8.2.2 MVVM: ObservableObject, RelayCommand, source generators
NAudio.Wasapi 2.2.1 WASAPI loopback capture for bass-driven rumble
Microsoft.Windows.Devices.Midi2 1.0.16-rc.3.7 Windows MIDI Services SDK for virtual MIDI devices

HIDMaestro.Core.dll is referenced as a project-local <Reference> (Resources/HIDMaestro/HIDMaestro.Core.dll), not a NuGet package. The DLL is copied from a tagged HIDMaestro release build to keep PadForge pinned to a known-good HIDMaestro snapshot. See HIDMaestro Deep Dive.

Native libraries adjacent to PadForge.exe:

Library Caller Notes
SDL3.dll SDL3Minimal.cs Custom fork: HM filter + Switch 2 Pro support. Resources/SDL3/x64/
xinput1_4.dll XInput-consuming code paths OpenXInput fork. Single-file-embedded; SetDllDirectory at launch resolves the extracted copy ahead of System32. Filters HM virtuals from PadForge's own XInput view

Build System

PadForge must be built with dotnet publish (not dotnet build). Produces a single-file self-contained executable:

dotnet publish PadForge.App/PadForge.App.csproj -c Release

Key publish properties (PadForge.App.csproj):

<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>

Output: PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/PadForge.exe

SDL3.dll and libusb-1.0.dll are copied to output via <Content> items with CopyToOutputDirectory=PreserveNewest and Link="filename" (flattened to root). They are not embedded in the single-file exe. They must sit adjacent to PadForge.exe.

UseWindowsForms=true is set in the csproj. Required for System.Windows.Forms.NotifyIcon (system tray). WinForms implicit usings are removed to avoid WPF type ambiguities.

Embedded resources (extracted at install/runtime):

Resource Content
HidHide_1.5.230_x64.exe HidHide installer
gamecontrollerdb_padforge.txt PadForge SDL gamepad mapping additions
3DModels/**/*.obj 3D controller mesh assets
WebAssets/**/* Web controller frontend (served by WebControllerServer)
2DModels/**/*.png 2D controller sprites (included as <Resource>, not <EmbeddedResource>)

HIDMaestro's user-mode driver is installed by the bundled installer (downloaded and extracted on first launch by DriverInstaller). The driver is not embedded in the EXE. OpenXInput's xinput1_4.dll is embedded in the single-file EXE via <Content> + IncludeNativeLibrariesForSelfExtract; App.xaml.cs calls SetDllDirectory on the extract directory so the loader resolves PadForge's copy ahead of C:\Windows\System32\xinput1_4.dll. devobj.dll is deliberately not bundled — a stub devobj.dll from OpenXInput's source tree would hijack setupapi.dll's own DevObj* imports and crash HID class enumeration. The system devobj.dll resolves from System32 unaided.


Localization

10 languages via .resx files in Resources/Strings/:

File Language
Strings.resx English (base/fallback)
Strings.de.resx German
Strings.es.resx Spanish
Strings.fr.resx French
Strings.it.resx Italian
Strings.ja.resx Japanese
Strings.ko.resx Korean
Strings.nl.resx Dutch
Strings.pt-BR.resx Brazilian Portuguese
Strings.zh-Hans.resx Simplified Chinese

Strings.Designer.cs

Despite the <auto-generated> header, this file is hand-written (not ResXFileCodeGenerator). It implements INotifyPropertyChanged so XAML bindings update on language change.

Key design:

  • Singleton: Strings.Instance is the binding source for all XAML strings.
  • Weak event pattern: Instance-method handlers stored as (WeakReference<object>, MethodInfo) to avoid preventing GC. Static/lambda handlers use strong references. Dead entries pruned on every raise.
  • ChangeCulture(CultureInfo): Sets CurrentUICulture + DefaultThreadCurrentUICulture, raises PropertyChanged for all properties (refreshing bindings), then raises CultureChanged for ViewModel-side refresh.

XAML Binding Pattern

{Binding PropName, Source={x:Static strings:Strings.Instance}}

WPF updates all bound text when ChangeCulture() fires PropertyChanged.

ViewModel Integration

ViewModelBase subscribes to Strings.CultureChanged and exposes virtual OnCultureChanged(). Derived VMs override it to refresh computed strings. Weak references mean VMs need not unsubscribe.

Live Language Switching

Strings.ChangeCulture() applies immediately. No restart needed:

  1. CurrentUICulture updated.
  2. PropertyChanged fires for every resource property, refreshing all XAML bindings.
  3. CultureChanged fires, letting VMs recompute culture-dependent strings.

Early Language Restore

App.OnStartup() applies the saved language before creating UI. Reads <Language> from PadForge.xml via regex (no full deserialization) and sets CurrentUICulture so the first window renders correctly.


Key Static / Singleton Classes

Class Location Lifetime Role
SettingsManager Common/SettingsManager.cs + partial in Step1 Static Shared state: slot arrays, UserDevices/UserSettings, profiles
RawInputListener Engine/Common/RawInputListener.cs Static Per-device keyboard/mouse input via hidden window
RumbleLogger Engine/Common/RumbleLogger.cs Static Diagnostic logger (disabled by default)
InputManager Common/Input/InputManager.cs Singleton Polling loop, 6-step pipeline, virtual controller lifecycle
InputService Services/InputService.cs Singleton UI–engine bridge, 30Hz timer, macro recording, DSU/web lifecycle
SettingsService Services/SettingsService.cs Singleton XML persistence, auto-save, profile CRUD, ViewModel sync
DeviceService Services/DeviceService.cs Singleton Device list sync, HidHide whitelist management

Slot System

Up to 16 virtual controller slots (MaxPads = 16). Per-type limits:

Type Limit Constant
Xbox 16 MaxXbox360Slots = MaxPads (constant name preserved from v2)
PlayStation 16 MaxPlayStationSlots = MaxPads
Extended 16 MaxExtendedSlots = 16
MIDI 16 MaxMidiSlots = MaxPads
Keyboard+Mouse 16 MaxKeyboardMouseSlots = MaxPads

The "Add Controller" button hides when all 16 slots are in use.

XInput visibility: The XInput API addresses only slots 0–3. Xbox VCs beyond slot 3 still work but may be invisible to XInput-only games. DirectInput, SDL, and raw HID see all 16. PlayStation and Extended VCs are unaffected by the XInput slot cap.

Per-slot state arrays in SettingsManager:

public static bool[] SlotCreated;   // Slot exists in config
public static bool[] SlotEnabled;   // Slot is active (user toggle)

Per-slot state arrays in InputManager:

public Gamepad[] CombinedOutputStates;          // Step 4 output
public ExtendedRawState[] CombinedExtendedRawStates;    // Step 4 output (Extended VC raw axes/buttons/POVs)
public MidiRawState[] CombinedMidiRawStates;    // Step 4 output (MIDI)
public KbmRawState[] CombinedKbmRawStates;      // Step 4 output (KeyboardMouse)
public Gamepad[] RetrievedOutputStates;          // Step 6 output (UI display)
public KbmRawState[] RetrievedKbmRawStates;     // Step 6 output (KBM UI preview)
public Vibration[] VibrationStates;              // HIDMaestro feedback
public MotionSnapshot[] MotionSnapshots;         // DSU motion data
public MacroItem[][] MacroSnapshots;             // Macro definitions
public VirtualControllerType[] SlotControllerTypes;  // Configured type
internal bool[] SlotExtendedIsCustom;                // Custom Extended config vs gamepad preset
internal MidiSlotConfig[] _midiConfigs;          // Per-slot MIDI configuration

Pad indices are data identity. A pad's mappings, profile, devices, and settings live at its pad index and never move. Visual position within an HM-backed group (Xbox / PlayStation / Extended) is the kernel-slot anchor: drag-reorder mutates SettingsManager.SlotOrders and reroutes the pad-index pointer in _virtualControllers[] so the data at the new pad-at-position-V feeds into V's kernel slot. Same-profile reorders pointer-swap; different-profile positions destroy + recreate. See Services Layer#slot-reordering.


See Also

  • Input Pipeline: 6-step polling loop, mapping engine, macro evaluation
  • Services Layer: InputService, SettingsService, DeviceService, RecorderService, ForegroundMonitorService
  • Engine Library: Gamepad, CustomInputState, IVirtualController, PadSetting, UserDevice
  • ViewModels: PadViewModel, DashboardViewModel, DevicesViewModel, SettingsViewModel
  • XAML Views: DashboardPage, PadPage, DevicesPage, SettingsPage
  • Settings and Serialization: XML persistence, SettingsManager, SettingsService data flow
  • Virtual Controllers: IVirtualController implementations (HMaestroVirtualController for Xbox / PlayStation / Extended, plus MIDI and KB+M)
  • HIDMaestro Deep Dive: HM SDK surface, thread-pool lifecycle, OpenXInput shim, bubble-up cascade
  • SDL3 Integration: SDL3 P/Invoke, device enumeration, state reading, haptic
  • Build and Publish: Build commands, publish configuration, CI/CD

Clone this wiki locally