Skip to content

Architecture Overview

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

Note: 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-down cascade, inactivity timeout) is documented in HIDMaestro Deep Dive.

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]
        GMS[GameModServer #529<br/>DSX game mod UDP . loopback]
        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]
        RAS[RumbleAudioService<br/>Bass Shakers . WASAPI render]
        PH[PeripheralOutputHost #494<br/>HID++ . LampArray . Razer . SteelSeries . TouchSense<br/>Chroma . LED SDK . GameSense . Sensa workers]
        GTD[GameTelemetryDevice #531<br/>game telemetry UDP . Assetto Corsa shared memory]
    end

    subgraph "Virtual Controllers"
        MS[Xbox<br/>Xbox 360 / One / Series / Elite / Adaptive]
        SONY[PlayStation<br/>DS3 / DS4 / DualSense / DualSense Edge]
        NIN[Nintendo<br/>Switch Pro . Switch 2 Pro]
        EXT[Extended<br/>Sticks . Wheels . Custom HID]
        KBM[Keyboard+Mouse<br/>SendInput, no driver]
        MIDI[MIDI<br/>Windows MIDI Services or WinMM]
        VR[VR<br/>SteamVR left+right hand pair]
    end

    subgraph "Data Layer. PadForge.Engine"
        PS[PadSetting . UserSetting . UserDevice<br/>Gamepad . RawHidState . Vibration]
        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 + 16-XInput + Share button]
        OXI[OpenXInput<br/>xinput1_4 shim]
        HM[HIDMaestro<br/>UMDF2 user-mode driver<br/>ships 337 profiles, PadForge offers 239 with a captured descriptor<br/>+ native OpenVR driver for the VR slot]
        HH[HidHide Driver]
        WMS[Windows MIDI Services<br/>or legacy WinMM]
        WASAPI[Windows Audio<br/>WASAPI Loopback + Render]
        DSU_CLIENT[DSU Clients<br/>Cemu / Dolphin / Yuzu / Ryujinx]
        BROWSER[Web Browsers<br/>Phone / tablet]
        PEER[Remote Link Peers<br/>Paired PadForge PCs, LAN or internet relay]
        VEND[Peripheral vendors<br/>Razer Synapse . Logitech G HUB . SteelSeries GG<br/>Logitech HID++ devices]
        MODS[DSX game mods<br/>GTA V . RDR2 . Skyrim . Forza]
        GAMES[Driving games<br/>Forza . F1 . WRC . BeamNG . AMS2 . ACC]
    end

    UI --> VM
    VM --> IS
    VM --> SS
    IS --> IM
    IS --> ABD
    IS --> DS
    IS --> FMS
    IS --> PH
    IS --> GMS
    MODS -.->|UDP, default 6969, loopback| GMS
    IM --> GTD
    GAMES -.->|UDP, default 20777, every address| GTD
    IM -.->|levels, claims| PH
    PH -.->|REST, LED SDK, GameSense, HID++| VEND
    RS --> IM
    IM --> HMP
    IM --> KBM
    IM --> MIDI
    IM --> RAS
    HMP --> MS
    HMP --> SONY
    HMP --> NIN
    HMP --> EXT
    HMP --> VR
    IM --> SDL
    IM --> RIL
    IM --> IHM
    ABD --> WASAPI
    RAS --> WASAPI
    SDL --> SDL3
    SDL --> OXI
    MS --> HM
    SONY --> HM
    NIN --> HM
    EXT --> HM
    VR --> HM
    MIDI --> WMS
    DS --> HH
    IM -.->|UDP, default 26760| DSU_CLIENT
    IM -.->|HTTP+WS| BROWSER
    IM -.->|TCP+UDP, crypto-paired| PEER

    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

Solution Structure

Seven projects (PadForge.App, PadForge.Engine, PadForge.SteamWorkshop, PadForge.NativeChecks, PadForge.DsxStandIn, and the two test projects). All but PadForge.DsxStandIn target .NET 10:

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.SteamWorkshop net10.0-windows (Class Library) Steam Workshop config import (#9, v4.1): anonymous Steam clients over SteamKit2, VDF parser, Steam Input config model and translator, file cache. References Engine. See Steam Workshop Config Import Internals
PadForge.Tests net10.0-windows10.0.26100.0 (xUnit) Unit tests. References both App and Engine (Microsoft.NET.Test.Sdk + xunit + coverlet.collector)
PadForge.SteamWorkshop.Tests net10.0-windows (xUnit) Parser, client, cache, and translator tests, including 30 golden Workshop fixtures
PadForge.NativeChecks net10.0-windows (console Exe) Out-of-process checks the test suite runs against a bundled SDL3.dll (joystick, keyboard, and mouse enumeration, button mapping). References Engine
PadForge.DsxStandIn net462 (WinExe, assembly name DSX) The 5.5 KB DSX.exe stand-in for game mods that check for a running DSX (#529). It waits on PadForge's process and exits with it. PadForge.App builds it and embeds the exe as the resource PadForge.DsxStandIn.exe. See Game Mods Internals

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


Project Layout

PadForge.App

PadForge.App/
  App.xaml / App.xaml.cs              # Entry point: update-helper and pending-update checks, single-instance mutex, single-file OpenXInput SetDllDirectory, 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 (two entries: the DualShock 3 under DsHidMini SDF / SXS and the PS VR Aim Controller, #506)
  Properties/
    AssemblyInfo.cs                   # Assembly metadata only (title, copyright, theme, COM GUID). AssemblyVersion + AssemblyFileVersion live in ..\SharedVersion.cs, linked into the App, Engine, and SteamWorkshop csproj files.

  Common/
    BidiText.cs                       # Text and tooltips that keep a Latin name and its brackets together in right-to-left text
    ControllerIcons.cs                # SVG path data for controller type icons (Xbox, PlayStation, Nintendo, Extended, MIDI, KB+M)
    CurveLut.cs                       # Sensitivity curve LUT generation (per-axis response curves)
    DriverInstaller.cs                # HidHide install, Windows MIDI Services runtime install and uninstall, SteamVR install without Steam (steamcmd). Legacy ViGEmBus / vJoy uninstall
    HidHideController.cs              # HidHide IOCTL API: blacklist/whitelist/cloaking via \\.\HidHide
    MarqueeBehavior.cs                # WPF attached behavior for scrolling/marquee text animation
    MappingDisplayResolver.cs         # Descriptor → grid display label, including the contextual aux-motion labels
    ProfileTransfer.cs                # Profile export / import payloads
    SoundPackageManager.cs            # Sound package install, listing, and lookup for macro sound actions
    SetupApiInterop.cs                # SetupAPI P/Invoke shared by the device-ancestry walks
    SettingsManager.cs                # Slot arrays, profiles, PadSetting defaults, partial class (see below)
    StartupHelper.cs                  # Launch-at-logon via a schtasks ONLOGON task (/RL HIGHEST), migrating the legacy HKCU\...\Run entry
    TextDirection.cs                  # Gives each TextBlock in a right-to-left window the direction its own text needs
    VirtualKey.cs                     # Windows VK code → display name lookup table

    Telemetry/                        # Racing-telemetry sources feeding wheel RPM LEDs
      TelemetryHub.cs                 # Demand-driven owner of the sources, publishes one current snapshot for FFB dispatch. Keeps a source closed on a port the Game Telemetry row holds (YieldPort, ReclaimPort)
      ITelemetrySource.cs             # Source interface, and IUdpTelemetrySource for the sources that bind a port
      GameTelemetryRowSource.cs       # The Game Telemetry row's engine values, first in the hub's order (#531)
      ForzaUdpTelemetrySource.cs      # Forza Data Out (default port 5300)
      CodemastersUdpTelemetrySource.cs / OutGaugeTelemetrySource.cs / ScsTruckTelemetrySource.cs
      AssettoCorsaTelemetrySource.cs / IRacingTelemetrySource.cs / MadnessTelemetrySource.cs
      RFactor1TelemetrySource.cs / RFactor2TelemetrySource.cs / RaceRoomTelemetrySource.cs
      RpmLedMap.cs                    # RPM fraction → per-vendor LED bitmask

    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.Step1.UsbipVhciGuard.cs      # Self-readback guard: the test Step 1 runs on each device it opens (SelfVirtualSuppressions.PassOver), with the PnP-ancestry walk that keeps PadForge's own composite USB persona out
      InputManager.Step2.UpdateInputStates.cs   # Input state reading + force feedback
      InputManager.Step3.MappingSetEval.cs      # Per-VC MappingSet evaluation (rows, sources, combine modes, shift layers)
      InputManager.Step3.SteeringLockFeedback.cs # At-lock steering feedback for winding-stick / 2D steering (#94)
      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
      InputManager.MenuRuntime.cs     # Radial / touch menu runtime: per-(slot, device, menu) hover-commit contexts (#9)
      HMaestroVirtualController.cs    # IVirtualController for HIDMaestro (Xbox, PlayStation, Nintendo, Extended)
      HMaestroVRController.cs         # IVirtualController for the VR slot type (#49): one HMVRController drives both SteamVR hands
      HMaestroProfileCatalog.cs       # HIDMaestro profile lookup (HMProfile per VC subtype)
      HMaestroFfbDescriptor.cs        # HID PID output report ids the FFB decoder dispatches on (HM's AddPidFfbBlock emits the descriptor itself)
      HMaestroFfbDecoder.cs           # Decodes raw HM feedback packets into Vibration / FFB state
      SonyReportPackers.cs            # DS4 / DualSense Report 0x01 input passthrough packers
      UserEffectsDispatcher.cs        # Per-slot sole writer of Sony effect packets (rumble + lightbar + AT + mic LED), and the color of each lit mouse, keyboard and vendor row on the slot (#494)
      PlayStationEffectWriter.cs      # Low-level PlayStation effect packet write helper called by the dispatcher
      TouchpadPulseService.cs         # Sony-side swipe-haptic pulse delivery: 80 ms bursts max-combined into the dispatcher's rumble bytes (#219)
      DualSensePassthroughDispatcher.cs # Per-slot worker forwarding game-driven DS5 effect output reports (AT)
      Ds5EffectSynthesizer.cs         # Builds DS5 (DualSense) effect packets from the slot's DeviceSlotConfig
      Ds4EffectSynthesizer.cs         # Builds DS4 (DualShock 4) effect packets from the slot's DeviceSlotConfig
      XboxImpulseHidWriter.cs         # Raw HID writer for Xbox One+ rumble + impulse triggers (sole writer, bypasses SDL)
      SwitchHomeLedSetter.cs          # Switch HOME-button LED brightness via per-device SDL_SetJoystickLED, queued off-thread (#226)
      Ds3DirectService.cs             # Surfaces a BthPS3 RAW-PDO DualShock 3 (no DsHidMini) as an SDL virtual joystick
      Ds4SbcEncoder.cs                # Clean-room SBC encoder for the DS4 Bluetooth audio stream (32 kHz stereo, bitpool 48)
      AudioMuteService.cs             # Tracks system default-output mute state for audio-rumble gating
      SlotButtonSocd.cs               # Controller-button SOCD cleaner, applied to the slot's final combined output at the Step 5 submit (#240)
      RumbleAudioService.cs           # Rumble-to-audio (Bass Shakers) renderer: endpoint-keyed WASAPI players fed from the poll loop (#236)
      RumbleAudioSampleProvider.cs    # Allocation-free sine synthesizer for one rumble-to-audio endpoint (#236)
      MidiVirtualController.cs        # IVirtualController for MIDI: its own Windows MIDI Services port, or a picked port through WinMM
      MidiApiSelection.cs             # Picks the in-box Windows.Devices.Midi2 or the App SDK runtime, and predicts the API for the Settings card
      MidiBackend*.cs                 # IMidiBackend and its three implementations: in-box, App SDK runtime, legacy WinMM (MidiBackendLegacy)
      MidiRuntimeSwitch.cs            # The Settings card's runtime install and uninstall as MIDI sees them: the release before the uninstaller runs, its end once the uninstaller exits, and the switch onto a runtime whose install finished late
      KeyboardMouseVirtualController.cs # IVirtualController for Win32 SendInput (KB+Mouse)
      InputExceptionEventArgs.cs      # Event args wrapping an Exception raised on the polling thread
      MirrorDsp.cs                    # Controller-audio DSP chain for the DualSense speaker/jack mirror: crossfeed, parametric EQ, limiter, AutoEq import (#347)

  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 a configurable port (default 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
    CursorControlService.cs           # Samples the desktop cursor at 200 Hz for the Mouse Position X/Y sources (#107) while one is read or a pin/clamp is engaged, every 250 ms otherwise
    GyroCalibratorService.cs          # Averages held-still gyro readings into a per-(device, slot) at-rest bias
    NfcReaderService.cs               # PC/SC context + monitor thread raising TagDetected on tag presence (#150)
    WiiPairingService.cs              # In-app Bluetooth pairing ceremony for Wii controllers (#116)
    Ds3PairingService.cs             # Guided USB pairing ceremony for the DualShock 3 (#116)
    Ds3DriverInstaller.cs             # Installs/arms Nefarius BthPS3 + BthPS3PSM + DS3 WinUSB INF
    StarterProfileCatalog.cs          # The bundled starter profiles (#256): genre archetypes with no device GUIDs baked in
    WorkshopProfileMaterializer.cs    # Turns a translated Workshop config into a real ProfileData with its VC slots (#9)
    WorkshopTuningApplier.cs          # Moves a Workshop import's device tuning onto the device once one is assigned (#9)
    HeadsetTrackerRepair.cs           # Repairs a Sony headset whose head-tracker HID node never enumerates (#188)
    VoiceMacroService.cs              # Voice macro recognition (#317): one session per microphone, pulsing that device's phrase buttons
    VoskVoiceEngine.cs                # Vosk offline recognizer behind the same session surface SAPI uses (#317)
    WebControllerTls.cs               # HTTPS lane for the web controller (#296): motion sensors need a secure context
    WebControllerAccess.cs            # The plain HTTP address's access code: generation, cookie, admission, DPAPI storage
    WebCustomLayoutStore.cs           # Browser-built custom pad layouts (#296), machine-scoped on AppSettingsData
    WebMenusService.cs                # Web Menus snapshots per phone on the UI timer, icons served by token (#471)
    QrCode.cs                         # Byte-mode QR generator (Nayuki port) for the web-controller card's URL
    UpdateService.cs                  # In-app updates (#457): checks GitHub, downloads and verifies the build for this machine, replaces the running exe. Also holds BuildIdentity
    UpdateController.cs               # Runs the Settings > Updates card (#457): automatic checks, Check Now, background download, Install and Restart

  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
    PadViewModel.Mouse.cs             # Mouse tab state: per-(slot, device) mouse-gesture settings (#200)
    PadViewModel.Touchpad.cs          # Touchpad tab state: per-(slot, device, pad) gesture settings
    PadViewModel.AudioDsp.cs          # Audio tab state: crossfeed / EQ / limiter settings per (slot, device) (#347)
    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
    MappingSourceItem.cs              # One source row within a multi-source MappingItem (Engine.Data.MappingSource)
    MacroItem.cs                      # Macro definition: trigger, actions, repeat mode, state machine
    ShiftLayerInfo.cs                 # VM wrapper around one ShiftActivator on a slot's MappingSet (#61)
    MenuEditorItem.cs                 # Editor VM for one radial / touch menu on a slot's MappingSet, write-through like the mapping grid (#9)
    MidiSlotConfig.cs                 # Per-slot MIDI config: channel, velocity, CC/note counts, legacy output port
    DeviceSlotConfig.cs               # Per-slot, per-device output config (Adaptive Triggers + Lighting tabs, a mouse's or keyboard's Control This Device’s Lighting switch, #494)
    KbmSlotConfig.cs                  # Per-slot keyboard+mouse output config (SOCD / Snap Tap, #205)
    ExtendedSlotConfig.cs                 # Extended VC config: axis/button/POV/stick/trigger counts (HIDMaestro Extended profile)
    StickConfigItem.cs                # Thumbstick deadzone / anti-deadzone / linear config
    TriggerConfigItem.cs              # Trigger deadzone / anti-deadzone / max range config
    ProfileShortcutViewModel.cs       # Global button-combo → profile-switch shortcut row (GlobalMacroData)
    RemoteLinkNearbyPeer.cs           # One PadForge PC discovered on the LAN, shown in Dashboard "Nearby PCs" (#138)
    RemoteLinkTrustedPeer.cs          # One trusted peer row in the Settings paired-peer manager (#138)

  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, Updates card (#457)
    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
    VRPreviewView.xaml(.cs)           # VR slot preview: left and right hand controller art with live input regions (#49)
    MenuOverlayWindow.xaml(.cs)       # Click-through radial / touch menu HUD, pulled from ActiveMenuOverlay on the ~30 Hz UI timer (#9)
    IconPicker.xaml(.cs)              # Shared emoji and image picker for menu cells and shift layers, in a Popup (#471)
    CopyFromDialog.xaml(.cs)          # Copy mappings + every assigned device's tuning from another slot
    ProfileDialog.xaml(.cs)           # Save new profile (name + exe list)
    RemoteLinkPairDialog.xaml(.cs)    # First-contact pairing approval: short authentication string + fingerprint (#138)
    RemoteLinkPasswordDialog.xaml(.cs) # Set/enter the Remote Link portable-identity password (#138)

  Models3D/
    ControllerModelBase.cs            # Abstract base for 3D models (OBJ loading, part animation)
    ControllerModelXbox360.cs         # Xbox 360 3D model parts and animation bindings
    ControllerModelXboxSeries.cs      # Xbox Series 3D model, real Share button, replaces the Xbox One stand-in for Series profiles
    ControllerModelDS3.cs             # DualShock 3 mesh, one appearance
    ControllerModelDS4.cs             # DualShock 4 3D model parts and animation bindings
    ControllerModelDualSense.cs       # DualSense 3D model
    ControllerModelDualSenseEdge.cs   # DualSense Edge: DualSense body against the DualSenseEdge asset folder
    ControllerModelSwitch2Pro.cs      # Switch 2 Pro mesh, serves every Nintendo slot (switch-pro and switch2-pro profile families)
    ControllerModelSteamDeck.cs       # Steam Deck mesh (#337/#338)
    ControllerModelSteamController.cs # 2015 Steam Controller mesh (#337/#338)
    ControllerModelSteamController2.cs # 2026 Steam Controller mesh (#337/#338)

  Models2D/
    ControllerOverlayLayout.cs        # 2D overlay positioning data (button/stick coordinates)
    Controller2DColorways.cs          # Per-colorway sprite variants for the 2D overlays
    NintendoPreviewMap.cs             # Raw-HID button index to Nintendo preview target mapping

  2DModels/                           # PNG sprites from Gamepad-Asset-Pack (MIT)
    DS4/                              # DualShock 4 sprites
    DualSense/                        # DualSense sprites
    DUALSENSEEDGE/                    # DualSense Edge sprites
    XBOX360/                          # Xbox 360 sprites
    XBOXONE/                          # Xbox One S sprites (shared with Elite / Adaptive)
    XBOXSERIES/                       # Xbox Series sprites (adds Share button overlay)
    SWITCHPRO/                        # Switch Pro sprites
    SWITCH2PRO/                       # Switch 2 Pro sprites
    STEAMDECK/ STEAMCONTROLLER/ STEAMCONTROLLER2/  # Valve family sprites, dispatched by ControllerModel2DView like every other family
    VRCONTROLLER/                     # VR slot preview art (#49)
    MOUSE/                            # Mouse layers for the KBM preview

  3DModels/                           # OBJ meshes, loaded from the assembly's resource stream (MSBuild renames the folder to _3DModels)
    DS4/                              # DualShock 4 meshes
    DualSense/                        # DualSense meshes (Touchpad split out for click-mapping)
    DualSenseEdge/                    # DualSense Edge meshes
    XBOX360/                          # Xbox 360 meshes (Handheld Companion, CC BY-NC-SA 4.0)
    XboxSeries/                       # Xbox Series meshes, also serve Xbox One / Elite / Adaptive profiles
    Switch2Pro/                       # Switch 2 Pro meshes (every Nintendo slot)
    SteamDeck/                        # Steam Deck meshes, flat (#337/#338)
    SteamController/                  # 2015 Steam Controller meshes, flat, from Valve's STEP (#337/#338)
    SteamController2/                 # 2026 Steam Controller meshes, flat, from Valve's STEP (#337/#338)

  Controls/
    CurveEditor.xaml(.cs)             # Interactive sensitivity curve editor (Bezier/linear)
    RangeSlider.cs                    # Dual-thumb range slider (deadzone min/max)
    TriggerTravelArc.xaml(.cs)        # Trigger travel arc visual for the adaptive-trigger surfaces
    EqCurveControl.cs                 # Graphic EQ response curve for the controller-audio DSP tab (#347)

  Views/Controls/
    LabeledShapeIcon.xaml(.cs)        # Labeled shape glyph
    ProfilePill.xaml(.cs)             # Profile chip used in the shell and dialogs
    TriggerEffectGraph.xaml(.cs)      # Adaptive-trigger effect curve graph

  Converter/
    BoolToTriggerShapeKindConverter.cs # bool → trigger shape kind (left / right)
    BoolToVisibilityConverter.cs      # bool → Visible / Collapsed
    CrossGeometryConverter.cs         # Cross/X geometry for close buttons
    NormToCanvasConverter.cs          # Normalized float → Canvas pixel coordinate
    SignedNormToCanvasConverter.cs    # Signed normalized (-1..+1) → centered Canvas position
    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
    DzShapeNameConverter.cs           # DeadZoneShape → display name
    EnumEqualityVisibilityConverter.cs / EnumIndexConverter.cs  # Enum comparison and index binding
    IndexLessThanVisibilityConverter.cs / IndexToLetterConverter.cs / OneBasedIndexConverter.cs
    HexToBrushConverter.cs / RgbToBrushConverter.cs  # Color string or RGB triple → Brush
    ExeIconConverter.cs               # Executable path → its shell icon
    CultureLanguageConverter.cs       # Culture → the language its picker row declares (#504)
    UppercaseConverter.cs             # String → upper case
    EqBandTypeNameConverter.cs        # EQ band type enum → display name (#347)

  Resources/
    ControllerIcons.xaml              # XAML resource dictionary with controller icon geometries
    ZackslyIcons/                     # Vendor button-glyph icon set
    BthPS3/                           # Microsoft-signed BthPS3 + BthPS3PSM drivers and the DS3 WinUSB INF (embedded)
    PadForge.ico                      # Application icon
    PadForge-logo.png                 # App logo bitmap (WPF Resource)
    PadForge-icon.png                 # App icon bitmap (WPF Resource)
    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 + 16-XInput + Share button)
    SDL3/arm64/, OpenXInput/arm64/, VisualCpp/arm64/   # ARM64 copies for the win-arm64 build (4.5.1, preliminary)
    SDL3/x64/libusb-1.0.dll           # libusb for HIDAPI backend (Switch 2 support)
    VisualCpp/x64/                    # msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll: SDL3.dll's C++ runtime, bundled so no redistributable install is needed
    Vosk/arm64/libvosk.dll            # ARM64 Vosk recognizer, built by tools/build-libvosk-arm64.sh (the x64 build takes the Vosk package's own)
    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
    Interhaptics/x64/HAR.dll          # Interhaptics engine, P/Invoked lazily by SensaHapticsService, the Razer Sensa row's worker (#374, #494)
    Interhaptics/x64/Interhaptics.RazerProvider.dll  # HAR.dll's Razer Sensa backend
    HIDMaestro/HIDMaestro.Core.dll    # HIDMaestro SDK v2.1.0, x64 and ARM64 driver payloads in one assembly (HMContext, HMProfile, HMController, SubmitState, SubmitRawReport)
    HidHide_1.5.230_x64.exe           # Embedded HidHide installer (x64 build only)
    HidHideArm64/                     # HidHide_ARM64.zip (the Microsoft-signed ARM64 driver) + nefconc.exe, embedded in both builds for ARM64 machines

  WebAssets/
    index.html                        # Landing page with Xbox 360 and DS4 layout cards
    controller.html                   # Controller UI shell (dynamic overlay layout)
    touchpad.html                     # Touchpad surface UI
    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
    custom.html                       # Browser-side custom pad builder (#296)
    js/custom_client.js               # Builder client, persists layouts via /api/custom-layouts (#296)
    gamepad.html                      # Browser Gamepad: forwards a controller paired to the phone (#402, #415)
    js/gamepad_client.js              # Its client: Gamepad API reads, the ping echo, resync (#402)
    js/fullscreen.js                  # Fullscreen toggle where the browser offers element fullscreen (#402)
    menus.html                        # Web Menus: a slot's Touch Grid menus as tiles, with the profile picker (#471)

  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], per-pad touchpads[]
    DeviceObjectItem.cs         # Metadata for one axis/button/hat on a device
    ForceFeedbackState.cs       # Per-device rumble: change detection, haptic effect lifecycle
    GamepadTypes.cs             # Gamepad struct (XInput layout), TouchpadState, RawHidState, KbmRawState, MidiRawState
    LfeOutputState.cs           # Packed per-slot LFE voice state for rumble-to-audio: four ushort voices in one long, one Volatile read/write per side (#236)
    InputTypes.cs               # DeviceObjectTypeFlags + MapType enums, ObjectGuid + InputDeviceType constant classes
    VirtualControllerTypes.cs   # VirtualControllerType enum (Xbox 0, PlayStation 1, Extended 2, Midi 3, KeyboardMouse 4, Nintendo 5, Vr 6), IVirtualController interface
    VrRawState.cs               # One slot's VR output: VrHandRaw left + right, button bits mirroring HMVRButton exactly (#49)
    InputHookManager.cs         # WH_KEYBOARD_LL / WH_MOUSE_LL hooks for mapped input suppression
    PrecisionTouchpadReader.cs  # Windows Precision Touchpad reader (Raw Input, HID descriptors, tip-switch, multi-report frame assembly, HID-contact-id-stable slot assignment)
    TouchpadOverlayDevice.cs    # On-screen overlay touchpad surface (implements ISdlInputDevice)
    WebControllerDevice.cs      # Virtual input device from browser clients (implements ISdlInputDevice)

  Data/
    DeadZoneShape.cs            # Deadzone shape enum
    DeviceTuning.cs             # Per-pad-per-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) + Menus (#9)
    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/Nintendo/Extended/MIDI/KBM equivalence)
    PadSetting.cs               # Per-slot tuning (deadzones, force feedback, lighting, AT, MIDI, etc.)
    PassthroughCloneGenerator.cs # 1:1 passthrough clone of a physical device onto an Extended VC (#196)
    RumbleAudioConfig.cs        # Per-slot Rumble to Audio config carried on the MappingSet (#236)
    PerDeviceSettingsEntry.cs   # (v3.3) Clipboard payload. One per assigned device on Copy / Paste / Copy From, carrying a nested PadSettingJson
    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

  Menus/
    MenuDefinitionEntry.cs      # (v4.1) XML-serializable menu definition: kind (radial/grid), host surface, fire type, items with optional direct bindings (#9)
    MenuEvaluator.cs            # (v4.1) Hover-commit state machine: engage, hover, Click / ClickRelease / TouchRelease / Always fire types (#9)
    MenuSelectionMath.cs        # (v4.1) Pure radial-wedge / grid-cell selection math on the shipped #88 radial-zone convention (#9)

  Touchpad/
    GestureRecognizer.cs        # (v3.3) Per-tick touchpad recognizer: Tier 1 (swipes/radial/taps/longpress), Tier 2 (pinch/rotate/multi-finger), Tier 3 (shape matches via ShapeRecognizer + angular-margin)
    ShapeRecognizer.cs          # (v3.3) Canonical $Q point-cloud matcher (Magrofuoco/Vatavu/Anthony/Wobbrock 2018). Resample → Scale → TranslateTo, matched[]-tracked CloudDistance, LUT-driven ComputeLowerBound, bidirectional CloudMatch
    ShapeTemplate.cs            # (v3.3) Pre-processed template: PointCloud + LookupTable, FingerCount, ThresholdOverride, AngularSignature
    AngularMarginRecognizer.cs  # (v3.3) Per-segment angle-direction matcher that follows the algorithm GestureSign's PointPatternAnalyzer describes. Runs alongside ShapeRecognizer on single-finger templates and keeps the higher-confidence match
    InBoxShapeTemplates.cs      # (v3.3) Procedural builders for the in-box shapes (Circle, CircleCCW, Square, Triangle, Z, Checkmark)
    TouchpadCustomGesture.cs    # (v3.3) XML-serializable user-recorded gesture, compiled to a ShapeTemplate at profile load
    TouchpadGestureContext.cs   # (v3.3) Per-(slot, device, padIdx) gesture state: finger paths, timestamps, FiredGesturesThisFrame, cooldown
    TouchpadGestureSettings.cs  # (v3.3) Per-(slot, device, padIdx) toggles + thresholds (every feature off by default)
    TouchpadSettingsEntry.cs    # (v3.3) Serialization wrapper that pairs TouchpadGestureSettings with its (DeviceGuid, TouchpadIndex) key inside PadSetting
    SwipeHapticsEvaluator.cs    # (v4.1) Per-(slot, device, pad) finger-travel accumulator emitting a swipe-haptic tick per travel detent (#219)
    TouchpadGestureAutoArm.cs   # Turns on the gesture families an imported profile's mappings read

  Mouse/                        # Mouse-gesture recognition (#200)
    MouseGestureRecognizer.cs   # Per-tick recognizer over the accumulated mouse path
    MouseGestureContext.cs      # Per-(slot, device) gesture state
    MouseGestureSettings.cs     # Per-(slot, device) toggles + thresholds
    MouseGestureSettingsEntry.cs # Serialization wrapper keyed by device GUID

  RemoteLink/                   # Peer-to-peer controller sharing over LAN or the internet (#138, #294). Off by default
    LinkServer.cs               # TCP control listener (pairing handshake) + UDP input/feedback stream
    LinkDiscovery.cs            # LAN discovery of nearby PadForge PCs (Dashboard "Nearby PCs")
    LinkConnection.cs           # ILinkControlChannel: transport-agnostic duplex control channel
    LinkHandshake.cs            # SAS pairing handshake, yields the 32-byte session key + trust material
    LinkSession.cs              # AEAD seal/open of datagrams on the input/feedback channel
    TcpControlChannel.cs        # ILinkControlChannel over TCP: u32-length-prefixed framing with a size cap
    UdpControlChannel.cs        # ILinkControlChannel over a punched UDP path, same framing contract (#294)
    CustomInputStateCodec.cs    # Compact absolute-frame codec for CustomInputState datagrams
    OutputEffectCodec.cs        # Codec for the reverse output relay (rumble / effect, consumer → owner)
    PeerCrypto.cs               # Pinned suite: X25519, Ed25519, ChaCha20-Poly1305, HKDF-SHA256 (BouncyCastle)
    PeerIdentity.cs             # This PC's long-lived Ed25519 identity, SHA-256 fingerprint
    PeerTrust.cs                # One trusted-peer record (Ed25519 public key, serialized to PadForge.xml)
    PeerTrustStore.cs           # Trust list + admission decision (FirstContact vs known key)
    IdentityProtector.cs        # At-rest wrap of the private identity key (DPAPI or password)
    AntiReplayWindow.cs         # IPsec-style sliding replay window over the datagram sequence (RFC 6479)
    RemotePeerDevice.cs         # A peer-exposed device surfaced into the local pipeline (peer:// path)
    StunClient.cs               # STUN binding requests that learn this PC's reflexive address (#294)
    NatProfile.cs / PortPredictor.cs # NAT behavior classification and the port guess a symmetric NAT needs (#294)
    HolePuncher.cs              # UDP hole punch against the peer's candidate addresses (#294)
    PunchedConnection.cs        # The datagram path a landed punch yields (#294)
    RendezvousProtocol.cs       # Candidate exchange over the rendezvous lane (#294)
    IrohRelayClient.cs          # iroh relay client, the guaranteed lane when a punch cannot land (#294)
    LinkCode.cs                 # Short shareable link code that carries the peer identity (#294)
    Dht/                        # First-contact signaling over the public BitTorrent DHT (#294), because
                                #   an idle host's router drops the caller's probes and neither side can
                                #   punch first: Bencode, Krpc, UdpKrpcTransport, Bep44Record (BEP 44
                                #   mutable items), CodeRendezvous, IPresenceStore, DhtPresenceStore,
                                #   PresenceRecord, PresenceService, RemoteLinkInternetService

tools/

tools/
  DsuDiag/                      # Standalone DSU client for motion data diagnostics
  Ds4InputDump/                 # DS4 raw HID input dump (Report 0x01 passthrough debug)
  PersonaVerify/                # Composite USB persona audio verification harness
  SteamWorkshopSmoke/           # Manually-run live-network smoke harness for the Workshop client
  SteamWorkshopSweep/           # Wild-corpus regression sweep for the Workshop config translator
  WdgProbe/                     # Runs the handheld learner's ACPI _WDG / WMI path outside the app (#343)
  combomeasure/                 # WPF width-measurement harness for the lighting pickers in every language
  rowmeasure/                   # The same harness for fixed-size rows (Profiles shortcut row, raw hat strip, equalizer row, gesture recorder hint, pad page first tier) in every language
  build-libvosk-arm64.sh        # Builds the ARM64 libvosk.dll bundled at PadForge.App/Resources/Vosk/arm64
  capture_all.ps1               # Full screenshot capture orchestration (+ _wrapper for elevation)
  capture_vr.ps1 / capture_web.ps1 / capture_colorways.ps1 / capture_mouse_gestures.ps1
  prep_xml_for_capture.ps1      # Preps PadForge.xml with sample slots and macros for capture runs
  convert_screenshots.ps1 / add_slots_via_ui.ps1 / probe_macro_list.ps1
  mirror_screenshots.py         # Mirrors captured shots into the repo and the site, erroring on any gap
  diag-sweep.ps1                # Runtime self-diagnostics harvest via UI Automation, must run elevated
  btaudio-trace.ps1 / button-trace.ps1 / effect-trace.ps1  # Diag-mirror tails: DualSense BT audio,
                                #   button surface and edges, DS5 effect lane
  deploy.ps1                    # Copy the published exe to C:\PadForge and relaunch
  kill_padforge.ps1             # Elevated kill of a running instance
  verify_site_carousel.ps1      # Site carousel check
  overlay_positions.py          # 2D controller overlay coordinate generator
  gen_mouse_art.py / gen_2d_colorways.py / gen_dualsense_edge_art.py / gen_switchpro_s2_art.py
  steam_controller_2015_mesh.py / steam_controller_2026_mesh.py  # Valve STEP to per-part OBJs
  dualshock3_mesh.py / dualshock3_art.py  # DualShock 3 mesh split and its drawn 2D set
  steam_controller_2026_pads.py / steam_deck_stick_well.py       # Mesh fix-ups on those sets

New since the initial trees (through 4.0.0)

The Common/Input/, Services/, Engine/Data/, and Engine/RemoteLink/ trees above cover the core pipeline and the physical-device / Remote Link subsystems. Later feature work added these files, kept here so the core trees stay readable:

App Common/Input/

  • SocdCleaner.cs. SOCD / Snap Tap cleaner for the keyboard VC (#205)
  • XboxGipGuideLedWriter.cs. Guide-button LED brightness for Xbox One+ over the \\.\XboxGIP interface (#209)
  • SteamHomeLedSetter.cs. Home-button LED brightness for the 2015 Steam Controller via SDL's Steam HIDAPI hint (#209)
  • SwitchHomeLedSetter.cs. HOME-button LED brightness for the Switch Pro Controller and right Joy-Con via per-device SDL_SetJoystickLED, queued to a background worker because the Switch subcommand path waits for the controller's ACK under SDL's joystick lock (#226, the third #209 lane, since 4.1.0)
  • PlayerIdentityDefaults.cs. Console-style player color / DualSense pip idle-floor defaults (#191)
  • RemoteLinkOutputRouter.cs. Consumer-side hub for the reverse output relay (#138)
  • FanatecRawHidWriter.cs / LogitechRawHidWriter.cs / ThrustmasterRawHidWriter.cs. Per-vendor wheel/pedal FFB writers, over RawHidOutput.cs (shared overlapped raw-HID write)
  • SincResamplingSampleProvider.cs. Windowed-sinc resampler for the haptic-tone downmix (#147)
  • SoundMacroService.cs. Sound playback for macro actions (#83)
  • BluetoothBatteryService.cs. Bluetooth battery percent read from the device's own PnP battery property (#187)
  • BluetoothLinkHelper.cs. Host-side Bluetooth disconnect / power-off via IOCTL_BTH_DISCONNECT_DEVICE (#162)
  • HapticToneService.cs (HD haptic tones #147), MidiInputDevice.cs (MIDI input #128), NfcReaderDevice.cs / NfcTagRegistry.cs / WinScard.cs (NFC #150), AudioPassthroughService.cs, WiiSpeakerService.cs

Engine Common/

  • ConsumerControlWrapper.cs + ConsumerUsageTable.cs (Consumer Control #168), MidiInputState.cs, TouchpadInputState.cs, IdleInputDetector.cs, and the Common/Mapping/ subtree (SourceCoercion, SourceEvaluator, CombineHelper, MappingExpression, TargetKind, SourceKindRuntime)
  • ConstantForceEvaluator.cs / ConstantTriggerForceEvaluator.cs. Fill in the user-configured Constant Force / Constant Trigger Force override when game-driven rumble is silent
  • CustomControllerLayout.cs. Per-slot HID descriptor shape for the Extended VC path (replaces the v2 ExtendedDeviceConfig struct)
  • MacroRumbleOverride.cs. Per-slot ephemeral rumble override driven by the MacroActionType.Rumble action
  • ShiftCycleStepper.cs. Pure Next/Previous/wrap/Base stepping math for a Shift Cycle activator cursor (#119)
  • GlobalHotkeyParser.cs. Round-trips canonical keyboard-combo strings (e.g. Ctrl+Alt+T) to / from Win32 VK-code arrays for global hotkeys
  • DeviceNameSanitizer.cs. Cleans HID product-string device names (trailing NULs / fixed-size buffer junk)
  • StableXInputInstance.cs. Resolves a stable PnP instance ID for an XInput-backed Xbox pad when SDL reports a synthetic XInput#N path
  • XboxControllerIdentity.cs. Single source of truth for "is this an Xbox One+ pad that accepts the 9-byte impulse-trigger HID output report"
  • SdlDiagLog.cs. In-memory diagnostics ring (SDL DEBUG lines, poll-loop stall watchdogs, subsystem diagnostics). crash.log is the only sanctioned on-disk artifact
  • GamepadObjectNames.cs. Canonical display names for the gamepad axes / buttons the mapping surfaces show
  • SyntheticInstanceId.cs. Stable instance ID for devices SDL surfaces without a real PnP path
  • SpaceMouseDecoder.cs. Descriptor-driven decode of 3Dconnexion 6DoF reports (#288). Shape comes from the report descriptor, never from report length

Engine namespaces

  • Haptics/ (HapticToneEncoder, HapticToneReducer, WiiSpeakerAdpcm, and since 4.4.0 TritonPcmEncoder for the 2026 Steam Controller's native PCM haptics, #381). Since 4.1.0: Menus/ (MenuDefinitionEntry, MenuEvaluator, MenuSelectionMath) and Touchpad/SwipeHapticsEvaluator.cs. The RemoteLink/ subtree is listed in full above.

New InputDeviceType values Touchpad = 26, Midi = 27, Nfc = 28, ConsumerControl = 29 (append-only, serialized as ints in PadForge.xml) make NFC readers, Consumer Control collections, and MIDI devices device sources. Later cycles appended HeadsetMotion = 30, Microphone = 31, HandheldButtons = 32, SystemMotion = 33, HeadTracker = 34, Tablet = 35, VrController = 36, LogitechGKeys = 37, AnalogKeyboard = 38, WebMenus = 39, the vendor rows' PeripheralLighting = 40 and PeripheralHaptics = 41 (#494), KeyboardScreenKeys = 42 (#519), NovintFalcon = 43 (#521) and GameTelemetry = 44 (#531) under the same rule. A Remote Link peer's shared controller surfaces through RemotePeerDevice (an ISdlInputDevice), carrying the peer's own device type.

Since 4.1.0

The 4.1.0 cycle's Workshop import (#9) and its discussion spin-offs added:

  • Flick stick (#225). SourceKindRuntime.TickFlickStick turns stick rotation into calibrated mouse counts on the dedicated KbmRawState.MouseFlickX lane, resolved layer-aware off the KbmMouseX row and forwarded 1:1 by the KBM VC (no MouseSensitivity, no velocity accumulator)
  • Absolute touchpad pointer (#9 B-15). A "Touchpad N Pointer" source family that claims the MouseAbsX/Y absolute-cursor lane while a finger is engaged (Steam's mouse_region behavior). The KBM VC positions the cursor via SetCursorPos on the primary monitor
  • Touchpad swipe haptics (#219). Engine/Touchpad/SwipeHapticsEvaluator.cs accumulates finger travel into per-detent ticks. TouchpadPulseService.cs delivers 80 ms pulses through the Sony dispatcher's rumble bytes, HapticToneService.QueueTouchpadPulse covers the Steam Controller family
  • Radial / touch menus (#9 B-17). Engine/Menus/ (definition model on the slot's MappingSet), InputManager.MenuRuntime.cs (per-(slot, device, menu) hover-commit runtime), MenuEditorItem.cs (Menus tab VM), MenuOverlayWindow.xaml(.cs) (click-through HUD)
  • Mouse-gesture Custom activation (#216). A sixth gesture-activation session armed by a recorded cross-device input (keyboard key, gamepad button, wheel pedal) held while gesturing
  • Switch HOME LED (#226). SwitchHomeLedSetter.cs, the third #209 Guide LED lane (see the writer list above)
  • Nintendo virtual controllers (#215/#246). VirtualControllerType.Nintendo, a fourth HM-backed slot family. A console-family face like Xbox / PlayStation (own group, icon, Nintendo button lettering) riding the Extended raw-HID data path on a catalog profile (switch-pro by default, with the Switch 2 Pro's switch2-pro-controller and switch2-pro-controller-composite also in the picker). No Customize surface: the slot always deploys the catalog profile as-is
  • Rumble to Audio / Bass Shakers (#236). Routes the game feedback each slot's virtual controller receives to WASAPI render endpoints as low-frequency sine tones. RumbleAudioService + RumbleAudioSampleProvider (App) render, LfeOutputState (Engine) packs the four voices into one long, and the poll loop's UpdateRumbleAudioLane publishes once per tick from the slot's MappingSet.RumbleAudio config. Surfaced as the per-slot "Bass Shakers" tab
  • Controller-button SOCD (#240). SlotButtonSocd.cs extends #205's keyboard Snap Tap to gamepad and raw-HID slots: pair cleaning on the slot's final combined output right before the Step 5 submit, configured via MappingSet.SocdMode / SocdPairs

Since 4.2.0

App Common/Input/

  • HMaestroVRController.cs. The VR slot type (#49). One HMVRController pipe drives both SteamVR hands. Haptic pulses arrive as (hand, amplitude, duration) and fan into the slot's Vibration entry with a decay timer, because the rumble path latches motor speeds until someone writes zero
  • SonyHeadsetMotionDevice.cs / SonyHeadsetHid.cs / HeadTrackerHid.cs / HeadTrackerMath.cs. Sony headset head tracker (WH-1000XM5 family) over Bluetooth Classic as an Android Head Tracker HID sensor collection, surfaced as a motion-only ISdlInputDevice (#188). HeadTrackerMath synthesizes an angular rate from consecutive rotation vectors when the descriptor reports rotation but no gyro usage
  • AtToImpulseTranslator.cs. Adaptive-trigger blocks a game writes to a virtual DualSense, translated to Xbox impulse-trigger output when the physical pad is an Xbox One+ (#271 item 3)
  • MidiServiceRecovery.cs / MidiEndpointJanitor.cs. A midisrv restart, at most once every two minutes, for a hung create, a hung availability probe, a hung input session or connection, a hung endpoint query or legacy port listing, a release before a runtime uninstall whose worker was at work 15 s without finishing, or a refused session, and never for a hang in a classic driver, which no restart reaches, or on a PC without the MIDI service. Also removal of PadForge MIDI endpoint devnodes Windows MIDI Services stranded past their owner's death
  • PlayStationEffectWriter.cs. The former SonyEffectWriter.cs, renamed to match the PlayStation VC family naming

App Common/

  • Telemetry/. Racing-telemetry sources for wheel RPM LEDs, listed in the tree above
  • AmbientMotionProbe.cs, StickBoundary.cs, StickPeakHoldBehavior.cs, StickTrailBehavior.cs, EmberTheme.cs, DeviceTransport.cs, DeviceTypeGlyph.cs, MenuIconResolver.cs, ComboBoxWidthBehavior.cs

Engine

  • Common/VrRawState.cs. VrHandRaw left + right per slot. Button bits mirror HMVRButton exactly, so the wrapper's conversion is a cast rather than a table
  • Mouse/. Mouse-gesture recognition, listed in the tree above
  • Data/RumbleAudioConfig.cs, Touchpad/TouchpadGestureAutoArm.cs

VirtualControllerType.Vr = 6 is appended after Nintendo. Numeric values are persisted, so the enum is append-only.

Since 4.2.0, shipped in 4.3.0

App Common/Input/

  • OpenVrConsumerService.cs. Consumes real VR hardware as input sources (#287). The HMD pose becomes a "VR Headset" device (lean and orientation axes plus gyro / accel), and every tracked controller becomes a "VR Controller" device. Runs its own OpenVrConsumer thread
  • SpaceMouseService.cs. 3Dconnexion SpaceMouse bridge (#288). Opens the 6DoF HID puck directly and streams it into SDL as a virtual joystick, so the mapping grid, macros, curves, DSU, and per-app profiles consume it unchanged. Report shape comes from Engine/Common/SpaceMouseDecoder.cs
  • PsMoveDirectService.cs. PS Move over USB and Bluetooth (#277). Per-pad calibration blobs are read over USB and cached by MAC, and the Bluetooth lane scales its sensors from them, the psmoveapi architecture
  • MicrophoneInputDevice.cs. A Windows capture endpoint exposed to the pipeline as an ISdlInputDevice (#317). Its buttons are the registered voice phrases: button 0 is "Any Phrase", each phrase sits at its own stable index
  • VoicePhraseRegistry.cs / VoicePulse.cs. The persisted phrase-to-index registry, and the per-pad pulse store SdlDeviceWrapper.ExternalVoiceAugment applies each poll so a spoken phrase reads as an ordinary button press

App Services/

  • VoiceMacroService.cs / VoskVoiceEngine.cs. Voice macro recognition (#317), one session per microphone. Vosk is the shipped engine, behind the same session surface SAPI uses
  • WebControllerTls.cs / WebCustomLayoutStore.cs / QrCode.cs. The web controller's HTTPS lane, its browser-built custom pad layouts, and the QR generator for the pairing card (#296)
  • WebControllerAccess.cs. The access code that guards the web controller's optional plain HTTP address, its cookie, and its encrypted storage
  • WebMenusService.cs. The Web Menus page's content (#471): one snapshot per phone, built on the UI timer from the engine's own menu gates, and the token table /api/menuicon serves icons from

Engine

  • Common/SpaceMouseDecoder.cs, Common/GamepadObjectNames.cs, Common/SyntheticInstanceId.cs
  • RemoteLink/. The #294 internet lane, listed in full in the tree above: StunClient, NatProfile, PortPredictor, HolePuncher, PunchedConnection, RendezvousProtocol, UdpControlChannel, IrohRelayClient, LinkCode. Direct punch first, iroh relay as the guaranteed fallback

Peripheral outputs (#494)

A mouse, keyboard, lit unit or vendor row assigned to a virtual controller is an output device of that controller, the way a DualSense is. It takes the controller's lighting through its own Lighting tab when PadForge can light it, and its rumble through its own Force Feedback tab when it can rumble. A device is lit through its vendor's software while that software runs. While it does not, PadForge lights the device on its own where it has a path to it: Logitech HID++, a Razer or SteelSeries control collection, or a HID LampArray collection. This subsystem replaced the Razer Chroma (#373) and Logitech LIGHTSYNC (#382) lightbar mirror services and the Razer Sensa switch (#374), and none of it has a Dashboard setting. Detail on Peripheral Outputs Internals.

App Common/Input/Peripherals/

  • PeripheralOutputs.cs. The hub: the link table, each row's haptic level, each lighting claim and the ruling between claims, the Set Chroma Color assertions, the backend states. Every setter is lock-free or takes only the hub's own leaf state
  • PeripheralLinker.cs. Vendor software presence, read from the registry, files and process names, the watch on running vendor programs, and the pure rules that tie a row to its output paths
  • PeripheralOutputHost.cs. Runs with the engine: the link pass every 500 ms, the walk of the present HID collections, claim pruning, the workers' lifetimes, the crash path, and the Razer Sensa worker while its row is assigned or Identify buzzes it
  • PeripheralOutputRow.cs. The Razer Chroma, Logitech LIGHTSYNC, SteelSeries GG and Razer Sensa rows, synthetic devices with no inputs
  • PeripheralUnitRow.cs. The row of a lit unit no mouse or keyboard row reaches: a headset, a mousepad or a light strip that Windows lists as a HID LampArray
  • PeripheralLightingColor.cs / GameLightbarCapture.cs. One device's color for one slot under the game's lightbar, and that lightbar as a DualShock 4 or DualSense virtual controller receives it
  • PeripheralRouteText.cs. The line the Lighting and Force Feedback tabs show: where the output goes, and why it does not show
  • PeripheralProductIds.cs. Product IDs that name a Razer or SteelSeries device's kind
  • HidppBackend.cs / HidppUnits.cs / HidppLightPlan.cs. The Logitech HID++ worker: unit discovery, receiver pairing tables, battery reads, feature 0x19B0 waveforms, and the light plans for features 0x8070 and 0x8071 with their per-key partners 0x8080 and 0x8081
  • IDirectUnitBackend.cs / DirectBackends.cs. The contract of a backend that lights or rumbles one device at a time, and the one place those backends are made
  • LampArrayBackend.cs / LampArrayProtocol.cs / WindowsLightingState.cs. The HID LampArray worker, its reports and descriptor checks, and whether Windows Dynamic Lighting lights a collection
  • RazerDirectBackend.cs / RazerDirectProtocol.cs / RazerDirectTable.cs / RazerGuard.cs. The Razer worker, its 90-byte report, the product table, and the lock third-party Razer tools share
  • SteelSeriesDirectBackend.cs / SteelSeriesDirectProtocol.cs. The SteelSeries worker and the frames of each command family
  • TouchSenseBackend.cs / TouchSenseProtocol.cs / TouchSenseChannel.cs / TouchSenseWinUsb.cs / TouchSenseMouseDevice.cs. Rumble on a Logitech iFeel mouse bound to WinUSB: the worker, its frames, the one WinUSB handle per mouse, and the mouse row PadForge reads itself
  • ChromaBackend.cs / LedSdkBackend.cs / GameSenseBackend.cs. The Razer Chroma REST, Logitech LED SDK and SteelSeries GameSense workers

App Common/Input/

  • InputManager.PeripheralRows.cs. Step 1's phase 1m, which opens and retires the vendor rows and the unit rows
  • InputManager.TouchSenseMice.cs. Step 1's phase 1n, which opens and retires the rows of bound TouchSense mice
  • HidCollectionRuntime.cs / HidReportChannel.cs / RealHidAccess.cs. The walk of every present HID collection, the report channel with bounded calls, and the test host's refusal of real devices
  • HidppHaptics.cs. The HID++ 2.0 frames for the haptic feature 0x19B0
  • HapticRumbleShaper.cs. Rumble strength to a waveform and a repeat interval, for devices that play fixed waveforms

App Services/

  • GameSenseClient.cs. The client for the GameSense server in SteelSeries GG: one PADFORGE game with a tactile rumble event and a color event per device type
  • LogiLedEngineNative.cs. Finds and binds the LED engine G HUB or Logitech Gaming Software installs, so PadForge ships nothing of Logitech's
  • SensaHapticsService.cs. The Razer Sensa row's worker, through the bundled Interhaptics engine
  • TouchSenseBinding.cs. Which TouchSense mice are bound to PadForge's WinUSB package, and whether a mouse still on Windows' driver declares the buzz report
  • PeripheralSwitchMigration.cs. Turns the retired switches into vendor-row assignments, once per file

ChromaLightbarService.cs and LightsyncLightbarService.cs are gone.

Game mods (#529)

Game mods written for DSX send DualSense trigger programs and lights as JSON over UDP on the same PC. With Accept DSX Game Mods on, PadForge listens in DSX's place and plays what the mods send on the PlayStation controllers on its virtual controllers. Detail on Game Mods Internals.

App Common/Input/

  • GameModProtocol.cs. The packet parser, the trigger encoder for DSX's 27 trigger modes and 17 custom modes, the light conversions, and the reply
  • GameModEffects.cs. What the mods hold on each of their controllers, which virtual controller each controller number reaches, and the trigger thresholds Step 5 applies
  • GameModSender.cs. The program that owns a mod's UDP port, found in the system's UDP tables, and the process handle that says when it exits

App Services/

  • GameModServer.cs. The loopback UDP server: one receive thread, the reply to each packet, and the senders it watches
  • GameModPortFiles.cs. The two port files mods read, written when the server starts
  • DsxStandIn.cs. Extracts and runs the DSX.exe stand-in, tied to PadForge's lifetime by a kill-on-close job object
  • InputService.GameMods.cs. The server's lifetime, the Dashboard status line, and the Devices and pad page lines

PadForge.DsxStandIn/

  • Program.cs. The stand-in: it waits on the process ID passed as its one argument and exits when that process does

Driving games send their telemetry over UDP, and Assetto Corsa publishes it in shared memory. With Listen for Game Telemetry on, the Game Telemetry row reads it, Custom Expression variables read it in its own units, and the wheel RPM lights read the game from the row (#531). Detail on Game Telemetry Internals.

App Common/Input/Telemetry/

  • TelemetryFrame.cs, TelemetryDecoders.cs, TelemetryReader.cs. The frame, one decoder per game family, and the format choice with Auto-Detect's hold
  • KunosSharedMemory.cs. Assetto Corsa's three pages, opened read-only
  • CustomTelemetryFormat.cs. The user's packet formats and their readers
  • GameTelemetryRuntime.cs. The Dashboard mirror, the expression values and the wheel lights' engine values
  • GameTelemetryDevice.cs. The row: the socket, the forward socket, the axes and buttons

App Common/Input/

  • InputManager.GameTelemetry.cs. Step 1 phase 1i2
  • MacroTriggerEffects.cs. The adaptive trigger effects macros hold, one store per slot

App Services/

  • InputService.GameTelemetry.cs. The Dashboard status line and flame, and the Devices panel line

PadForge.SteamWorkshop

PadForge.SteamWorkshop/
  ISteamWorkshopGate.cs      # Opt-in gate. Every client constructor throws when it is off
  Api/                       # SteamWorkshopClient (SteamKit2 anonymous CM), Store / Community /
                             #   RemoteStorage / UGC / Artwork HTTPS clients, shared SteamHttp
  Vdf/                       # Original VDF parser (VdfParser, VdfNode, VdfSyntaxException)
  Model/                     # Typed SteamInputConfig model (groups, presets, activators, bindings)
  Translation/               # ConfigTranslator, TranslationReport + reason keys, slot resolver, VK tables
  Cache/                     # SteamWorkshopCache: %LOCALAPPDATA%\PadForge\SteamWorkshopCache, dual budgets
  Local/                     # LocalWorkshopConfigStore: legacy read from the local Steam install

Added for the Steam Workshop config import (#9, v4.1). Full detail on Steam Workshop Config Import Internals.


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

PadForge makes no XInput call to read controller input. XInput slot assignment (0–3) for a new HIDMaestro Xbox virtual controller is deterministic: Step 5 gates the driver's CreateController calls by visual position so xinputhid allocates kernel slots in creation order, and SDL's XInput backend never sees the HM slots, because the OpenXInput fork's xinput1_4.dll it loads skips HIDMaestro devices. The only direct xinput1_4.dll P/Invoke anywhere is XInputGetCapabilitiesEx / XInputPowerOff (ordinals 108 / 103) in BluetoothLinkHelper, for host-side Bluetooth power-off (#162).

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)

Why HIDMaestro (not ViGEmBus + vJoy)

Virtual Xbox, PlayStation, Nintendo, Extended, and VR controllers all come from HIDMaestro. v2 used ViGEmBus for Xbox / DS4 virtuals and vJoy for everything else. v3 replaces both:

Advantage Detail
One driver, two roles HIDMaestro covers Xbox-family, DS4 / DualSense, Switch Pro, and arbitrary HID descriptors (flight sticks, wheels, HOTAS). v2 needed ViGEmBus + vJoy side by side
337 profiles in HIDMaestro's catalog HIDMaestro ships 337 profiles for the long tail of DirectInput devices, keyboards and mice, and PadForge offers 239 that carry a captured HID descriptor (22 Xbox, 14 PlayStation, 4 Nintendo, 96 Extended, 51 keyboards, 52 mice), plus a profile builder for custom HID descriptors. vJoy was generic-only
DualSense native A real virtual DualSense (lightbar, adaptive triggers, mic LED, touchpad). ViGEmBus only emulated Xbox 360 and DS4
Per-controller OutputReceived One callback per virtual device delivers full game output (rumble, lightbar, AT, FFB). Feeds Sony's UserEffectsDispatcher and the FFB decoder
One marker to filter Every HM device carries HIDMAESTRO in its path or hardware IDs (a composite persona on its emulated host controller), so one classifier filters them from SDL enumeration. v2's two drivers needed two filters and produced N² phantom controllers at edge cases
User-mode (UMDF2) No kernel-mode HID stack to fight with, no SetupAPI device-node rebuild rules, no vJoyInterface.dll handle generation logic

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). Properties are hand-written and raise change notifications through SetProperty, and commands are RelayCommand instances built in code. The [ObservableProperty] and [RelayCommand] source generators are not used. ViewModelBase adds OnCultureChanged() for live language switching.

Command + Event Decoupling

VMs expose commands and raise events instead of calling services or the input pipeline, with a few static-service calls as the exception (PadViewModel drives RumbleAudioService's Bass Shakers test tones directly):

  1. VMs raise events (e.g. PadViewModel.SelectedDeviceChanged, MainViewModel.StartEngineRequested, DevicesViewModel.AssignToSlotRequested)
  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()
  → UpdateService.TryRunApplyMode (a copy started with --apply-update is the update installer: it replaces the old copy and exits)
  → UpdateService.IsUpdateInProgress (a launch while an update helper holds the exe exits quietly)
  → Single-instance mutex check
  → UpdateService.TryStartPendingInstall (an update downloaded last session installs now, then this copy exits)
  → SetDllDirectory on the single-file extraction directory (OpenXInput's xinput1_4.dll ahead of System32)
  → HIDMaestro orphan sweep on a background task
  → 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() (when Auto-Start Engine on Launch is on) creates InputManager, starts polling thread
      → UpdateController.Start() arms the automatic update checks (#457)

See Updates Internals for the update steps.

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 ExtendsContentIntoTitleBar to blend the hamburger menu button and application icon into the window chrome. The branding bar draws no background of its own: the window's Mica backdrop, and in the dark theme the Ember steel layer under all content (#175), show through it, so it matches the window in light and dark mode.


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(), AssignDeviceToSlot() / UnassignDevice(), GetAssignedSlots(), CanSlotTakeType() (slot swapping is InputService.SwapSlots())

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 21 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(int generation, int pollRun), IDisposable, motion snapshots, DSU broadcast
InputManager.MenuRuntime.cs Steps 2–4b Radial / touch menu runtime (#9): per-(slot, device, menu) hover-commit contexts ticked in Step 2, fired items read by Step 3 rows / activators / macro triggers, direct bindings delivered in Step 4b
InputManager.Step1.UpdateDevices.cs Step 1 SDL device enumeration, open/close, HIDMaestro filtering, UserDevices/UserSettings collection classes
InputManager.KeyboardScreens.cs Step 1 Keyboard screens (#519), Phase 1j2: a row per screen the screen host publishes, with the keys beside it
InputManager.Falcon.cs Steps 1, 2 Novint Falcon (#521), Phase 1j3: the worker sweep that opens Falcons, their rows, and Step 2's hand-off of each Falcon's combined force to its haptic thread
InputManager.BlissBox.cs Step 1 Bliss-Box adapters (#469), Phase 1l: an API sidecar beside each online port row while Read Bliss-Box Adapters is on
InputManager.PeripheralRows.cs Step 1 Peripheral outputs (#494), Phase 1m: the Razer Chroma, Logitech LIGHTSYNC, SteelSeries GG and Razer Sensa rows, each open while its vendor's software is installed, and the unit rows of lit devices no mouse or keyboard row reaches
InputManager.TouchSenseMice.cs Step 1 Peripheral outputs (#494), Phase 1n: the rows of Logitech iFeel mice bound to WinUSB, read over WinUSB on a thread of their own
InputManager.Step1.UsbipVhciGuard.cs Step 1 Self-readback guard, so PadForge never ingests its own virtual controller. It holds the test Step 1 runs on each device it opens (SelfVirtualSuppressions.PassOver). A device is passed over on an HM-CTL- serial, a HIDMAESTRO path, a HIDMaestro hardware id other than the host controller's stamp on its own node or one above it, a place under the usbip-win2 host controller below a USB device whose instance id ends in HIDMaestro's serial (HM and twelve hex digits), or a place under that controller with the vendor and product id of a composite persona this process has live, is creating or retired within 10 s, or of any composite profile while the launch sweep runs. An id held on that last ground alone is opened again once it lapses while its device is still in the device tree (ReleaseLapsed). Under a host controller that carries the ROOT\HIDMAESTRO_UDE stamp, the SDL and OpenXInput forks drop the HID and XUSB interfaces of all of these before Step 1, a real pad forwarded over usbip-win2 included, except a pad xboxgip serves, whose XUSB interface sits past OpenXInput's walk and keeps its slot. An Xbox One instrument that GameInput reports is listed whatever host controller it sits under, and so is what hidapi reads through libusb
InputManager.Step2.UpdateInputStates.cs Step 2 Read CustomInputState per device, apply FFB from VibrationStates[] + audio bass
InputManager.Step3.MappingSetEval.cs Step 3 Evaluate the per-VC MappingSet (rows, sources, combine modes, shift layers) into OutputState
InputManager.Step3.SteeringLockFeedback.cs Step 3 At-lock steering feedback for the winding-stick / 2D steering path (#94)
InputManager.Step3.UpdateOutputStates.cs Step 3 Map CustomInputState → OutputState 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 HMController.SubmitState / SubmitRawReport (HM) or per-VC paths (VR / MIDI / KBM), XInput slot detection
InputManager.Step6.RetrieveOutputStates.cs Step 6 Copy CombinedOutputStates[] → RetrievedOutputStates[] for UI
InputManager.GyroTilt.cs Steps 2–3 helper Gyro Tilt, the degree-ranged hold mode beside the usual rate mode: Step 2 updates the gravity estimate per (device, slot), Step 3 reads it
InputManager.GyroSimulation.cs Steps 2–3 helper Pitch and Roll Simulation for a DualShock 3 (#474): Step 2 updates the accelerometer rate estimate per (device, slot), Step 3 reads it
InputManager.MenuPublication.cs Steps 2-5 The publication scope Steps 2 through 5 run inside, so a menu cannot observe a half-written frame
InputManager.SteeringAngleRumble.cs Steps 6, 2 helper Steering Angle Rumble: Step 6 publishes each Xbox / PlayStation slot's combined frame, and the next Step 2 force-feedback pass turns the chosen virtual stick axis into rumble
InputManager.Tablets.cs Step 1 helper Windows pen and drawing tablet rows, and their capture state
InputManager.MotionRows.cs Steps 3–4 and the motion stage The Motion Pitch, Yaw and Roll rows (#475): Step 3 reads them per device, Step 4 combines them, and ApplyMotionRows composes the slot's motion after the macro pass for Step 5 and DSU
InputManager.PressureRows.cs Steps 3–5 The button pressure rows (discussion #476): Step 3 reads them per device, Step 4 keeps the harder press per button, and Step 5 hands them to the DualShock 3 (SIXAXIS): Full virtual controller

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


Threading Model

Fourteen documented execution contexts. Some run whenever the engine runs, the rest start on demand. The on-demand device services added since 4.2.0 spin their own named threads on top of these (OpenVrConsumer for #287, SpaceMouseMonitor / SpaceMouseRead for #288, PsMoveDirectRead / PsMoveDirectWrite for #277, VoiceMacro for #317, PadForge.HeadTrackerUdp for #355), each publishing through the same ISdlInputDevice or SDL virtual-joystick seam Step 1 already reads. Head tracking (#355) is a Dashboard service: HeadTrackingRuntime is a static mirror the Dashboard view model writes on the UI thread and the poll thread reads. Its UDP and FreeTrack inputs switch independently. Each has a global value (AppSettingsData.HeadTrackingEnabled, AppSettingsData.HeadTrackingFreeTrack) and a nullable per-profile opinion (ProfileData.EnableHeadTracking, ProfileData.EnableHeadTrackingFreeTrack), and the global value stands whenever the active profile has no opinion. The OpenXR input (#403), the port, and the ranges are global only. With every input off there is no device row, no UDP socket, no FreeTrack mapping, and no thread.

1. Engine Thread (InputManager, 1000 Hz)

_pollingThread = new Thread(() => PollingLoop(generation, pollRun))
{
    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). A periodic callback at the polling interval (1 ms by default) 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 created+enabled slot has an online mapped device, unless a Remote Link peer is connected or a virtual controller is still waiting out its inactivity timeout. 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[], SlotCustomLayouts[], SlotRawHidSurface[], _midiConfigs[]
  • Syncs device list from UserDevices → DevicesViewModel (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.MotorValue (the 0..65535 projection of BassEnergy) and merges it with game rumble via max(). The UI meter reads BassEnergy.

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
  • Consumer Control (usage page 0x0C, usage 0x01) with RIDEV_INPUTSINK (#168)

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

Async enumeration: Keyboard, mouse, and Consumer Control discovery runs on a Task.Run background thread on each Step 1 pass (the first pass runs synchronously on the poll thread so devices are ready for Step 2), so slow HID enumeration never stalls the poll loop. Results merge into the device list on the next pass after the task completes.

Mouse path notes: lLastX/lLastY are accumulated as relative deltas. For reports whose usFlags has MOUSE_MOVE_ABSOLUTE (bit 0) set, the lLastX/lLastY delta is discarded (buttons and wheel in the same report are still consumed): RDP virtual mice, Wacom tablets in absolute mode, and some KVMs deliver 0..65535 absolute coordinates that would inject huge spurious motion into the gamepad-mapping aim and scroll paths. PadForge's mouse-as-source path is delta-only by design. RI_MOUSE_HWHEEL (horizontal scroll) is currently dropped. Only RI_MOUSE_WHEEL is consumed.

5. Precision Touchpad Reader Thread

public sealed class PrecisionTouchpadReader

A separate hidden message-only window background thread receives WM_INPUT for the digitizer touchpad top-level collection (0x0D / 0x05) with RIDEV_INPUTSINK. Each enumerated PTP device shows up as a UserDevice with CapType = Touchpad and ud.Device == null. The reader's per-device state replaces an ISdlInputDevice wrapper.

The reader implements four spec-mandatory behaviors on top of the basic HidP_GetUsageValue parse: tip-switch (usage 0x42) for "is this contact touching," multi-report frame assembly across the 2-contacts-per-report fragmentation typical of certified PTP hardware, HID-contact-id-stable slot assignment so engine-side paths don't extend with the wrong physical finger when a low-slot finger lifts, and a 100 ms staleness clear. Field-level reference: Engine Library.

Per-device state is held in PtpDeviceState keyed by RAWINPUTHEADER.hDevice. The engine reads it in Step 2 via _ptpReader.ReadInto(hDevice, state).

6. DSU Receive Thread

DsuMotionServer runs a dedicated background thread named PadForge.DsuServer on a blocking Socket.ReceiveFrom loop, handling DSU client subscriptions (Cemu, Dolphin). Motion data is broadcast by the engine thread after Step 4b and the motion stage (no separate send thread). DSU protocol is limited to 4 slots. Slots 4–15 skip broadcast.

7. Input Hook Thread (InputHookManager, on demand)

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

Created when a device has Consume Mapped Inputs (Hooks) enabled, or when a global hotkey is registered. 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.

8. Web Controller Server Thread

var acceptThread = new Thread(() => AcceptLoop(ownedListener, token, generation))
{
    Name = "PadForge.WebServer",
    IsBackground = true
};

Created when the web controller server is enabled. Runs an HttpListener accept loop, one loop for the main address and the optional plain HTTP address, and rules on each plain-address request's access code before dispatching it. 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 the /api/layout and /api/info JSON.

9. Mouse Injector Thread (InputManager, ~500 Hz)

_mouseInjectorThread = new Thread(() => MouseInjectorLoop(generation))
{
    Name = "PadForge.MouseInjector",
    IsBackground = true,
    Priority = ThreadPriority.AboveNormal
};

Started in Start() right after the polling thread, so it runs whenever the engine runs. The poll thread and the KBM virtual controller accumulate macro mouse-move deltas with Interlocked.Add into _pendingMouseDx / _pendingMouseDy (scroll into _pendingScroll / _pendingScrollH). This thread drains them with Interlocked.Exchange, issues SendInput (one call per lane: movement, wheel, horizontal wheel), then Thread.Sleep(2) while input keeps arriving (~500 Hz cap, held near 2 ms by the poll loop's timeBeginPeriod(1)). With nothing pending it parks on a wait handle until the poll thread signals a new delta.

Injected mouse movement is processed synchronously: it traverses every process's low-level mouse hook chain. A SendInput on the poll thread itself could collapse the 1000 Hz poll rate to ~200 Hz, which is why the syscall is offloaded here. Accumulated delta is batched, never dropped. On shutdown the loop drains one final flush.

LinkServer (issue #138) is not a dedicated Thread. Start(port) opens a TCP listener and a UDP socket, then launches AcceptLoopAsync and UdpLoopAsync as fire-and-forget tasks on the thread pool, plus a 3-second Timer for keepalive and dead-connection reaping. TCP carries the pairing handshake, UDP carries the input/feedback datagram stream. Each paired peer's shared controller becomes a RemotePeerDevice visible in Step 1 enumeration. Off by default. Nothing listens until Remote Link is enabled, and the crypto identity gate runs before any peer input reaches the pipeline.

11. Bass Shakers WASAPI Render (RumbleAudioService, on demand)

RumbleAudioService (issue #236) renders each slot's inbound game feedback as low-frequency sine tones. Players are keyed by audio endpoint, not by slot: every slot routed to one endpoint shares a single NAudio WasapiOut (shared mode, event-sync, 30 ms buffers), one sample clock, and one composite limiter. Each WasapiOut runs its own render thread, and a 5-second reconcile Timer resolves configured endpoints and prunes dead players. The poll loop's UpdateRumbleAudioLane publishes a packed four-voice LfeOutputState long per slot each tick (Volatile.Write). RumbleAudioSampleProvider reads it on the render thread (Volatile.Read). On a composite DualSense persona the lane also merges the loudness of the persona's haptic channels, which HIDMaestro's pacing thread follows in AudioPassthroughService.FollowPersonaHaptics (discussion #522). The slot's rumble readers take the same level through InputManager.WithPersonaHaptics. Silence is an explicit edge, never an inference from callback inactivity: idle entry, engine stop, and slot delete publish it synchronously. A configured-but-unresolved endpoint fails closed (no fallback device).

12. Peripheral Outputs (PeripheralOutputHost, with the engine)

InputService.Start() starts PeripheralOutputHost (#494) beside the engine, and Stop() disposes it. The host owns a PeripheralLink thread, which reads vendor software presence every 5 s, walks the present HID collections for the direct backends, and runs the link pass every 500 ms. It starts eight workers: the PeripheralHidpp and PeripheralGameSense threads, the Razer Chroma and Logitech LED SDK loops on the thread pool, and one thread for each direct backend, PeripheralLampArray, PeripheralSteelSeries, PeripheralRazer and PeripheralTouchSense. A SensaHaptics thread runs only while the Razer Sensa row is assigned to a virtual controller or Identify buzzes it, and each Logitech iFeel mouse bound to WinUSB has a TouchSenseMouse thread that reads its input reports. Neither the poll thread nor an effects dispatcher calls a vendor's software or writes to a mouse or keyboard. Step 2 stores haptic levels and the dispatchers store lighting claims in PeripheralOutputs, each behind a lock-free store or a leaf lock, and every device or vendor call runs on the worker that owns it.

13. Game Mod Server Thread (GameModServer, on demand)

GameModServer (#529) runs a background thread named PadForge.GameModServer while Accept DSX Game Mods is on and the engine runs. It blocks in Socket.ReceiveFrom on a loopback socket with a 500 ms timeout, answers each packet, and applies its instructions to GameModEffects. Between packets, at most every 500 ms, it checks the senders: one whose process exited, or one with no process found that has been quiet for 15 seconds, loses everything it set. A change to a hold queues one effects pass on the thread pool for the slot it reaches, so the receive thread never waits on a HID write.

14. Game Telemetry Receive Thread (GameTelemetryDevice, on demand)

The Game Telemetry row (#531) runs a background thread named PadForge.GameTelemetryUdp while Listen for Game Telemetry is on, the engine runs, and the Game list holds anything but Assetto Corsa, which only shared memory carries. It blocks in Socket.ReceiveFrom, copies each packet to the forward port, and decodes it under the row's state lock. The poll thread reads the frame in Step 2 and polls Assetto Corsa's shared memory there itself. Closing the socket ends the thread, and retiring the row waits up to 50 ms for it.

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 (Steps 4, 4b) Engine (Steps 5, 6) Single-thread write
RetrievedOutputStates[] Engine (Step 6) UI timer Value copy (struct)
VibrationStates[] HIDMaestro callbacks, UI test rumble, Step 5 teardown Engine (Step 2) Volatile fields
MacroSnapshots[] UI timer Engine (Step 4b) Atomic ref swap
SlotControllerTypes[] UI timer Engine (Step 5) Volatile read
MotionSnapshots[] / DsuMotionSnapshots[] Engine (loop) Step 5 / 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
_pendingMouseDx / _pendingMouseDy Engine (Step 4b), KBM VC Mouse Injector thread Interlocked.Add / Exchange
RemotePeerDevice state Remote Link UDP loop Engine (Step 2) _stateLock (double-buffered)
RumbleAudioService voice packs Engine (poll lane) WASAPI render thread Volatile long (four packed voices)
PeripheralOutputs link table (#494) PeripheralLink thread Engine (Step 2), effects dispatchers, peripheral workers, UI Immutable table swapped whole (Volatile.Write)
PeripheralOutputs haptic levels Engine (Step 2), Remote Link relay, Identify HID++, GameSense, TouchSense and Sensa workers Leaf lock per row on write, volatile bits on read
PeripheralOutputs lighting claims Effects dispatchers Chroma, LED SDK, GameSense, HID++, LampArray, Razer and SteelSeries workers, the Pad page's route line ConcurrentDictionary, leaf lock per claim
PeripheralOutputs direct units and unit rows (#494) PeripheralLink thread Engine (Step 1 phase 1m), peripheral workers, UI Immutable snapshots swapped whole (Volatile.Write)
A direct backend's snapshot and crash record Its worker thread PeripheralLink thread, the crash path Immutable, swapped whole
GameModEffects holds (#529) Game mod server thread, UI (Clear Mod Effects), server stop Effects dispatchers, Engine (Step 5 thresholds), UI timer Copy-on-write array published whole under a leaf lock, read with Volatile.Read
Game Telemetry row frame (#531) Game telemetry receive thread, Engine (Step 2, shared memory) Engine (Step 2), UI timer (status) The row's _stateLock
GameTelemetryRuntime engine values (#531) Engine (Step 2) TelemetryHub poll thread Leaf lock (_engineLock)
MacroTriggerEffectState per slot (#531) Engine (Step 4b) Effects dispatchers, Engine (Step 2, Xbox trigger translation) Leaf lock per slot, volatile flags for the empty check

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
  │  HIDMaestro virtuals are already filtered upstream by the SDL3 fork
  │  (HID paths) and the OpenXInput fork (XInput slots), with a
  │  self-readback guard behind both
  │  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):
  │    Evaluate the slot's MappingSet rows (PadSetting descriptors when the set is empty)
  │    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 RawHidState, MidiRawState, KbmRawState, VrRawState
  │
  ▼
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](hidmaestro-deep-dive.md))
  │    Submit CombinedOutputStates[i] → virtual controller
  │    Xbox / PlayStation / Nintendo / Extended: HMaestroVirtualController
  │      → HMController.SubmitState (gamepad path)
  │      → SubmitRawHidState → HMController.SubmitState (raw-HID surface: Extended custom layouts + Nintendo)
  │      → HMController.SubmitRawReport (Sony Report 0x01 passthrough on DS4 / DualSense)
  │    VR: HMaestroVRController → HMVRController (both SteamVR hands from one slot)
  │    KBM: SendInput() → Win32 keyboard/mouse events
  │    MIDI: Windows MIDI Services → virtual MIDI port, or WinMM → the port the slot picked
  │
  ▼
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 → HMController.OutputReceived)
  │  HMaestroFfbDecoder parses packet → Vibration / FFB state
  │  Writes to VibrationStates[slotIndex]
  │
  ▼
Step 2: ApplyForceFeedback() (engine thread)
  │  Reads VibrationStates[slot] for each device in that slot
  │  Merges a DualSense persona's haptics played as audio (max, #522), unless
  │    the device plays them itself
  │  Merges with audio-bass rumble (max), macro rumble (max), and Constant Force
  │  Routes by source-pad VID/PID to the per-family writer:
  │    Sony (DS4/DualSense) → UserEffectsDispatcher (sole writer, SDL skipped)
  │    Xbox One+ (One/Elite/Series) → XboxImpulseHidWriter raw HID (sole writer, SDL skipped)
  │    Haptic mice and keyboards, the Razer Sensa row → PeripheralOutputs level, played by
  │      the HID++, GameSense, TouchSense or Interhaptics worker (sole writer, SDL skipped, #494)
  │    Everything else → SDL_RumbleJoystick or SDL haptic effects

Since 4.1.0 the same inbound feedback also feeds the optional Rumble to Audio path (#236): once per tick the poll loop's UpdateRumbleAudioLane publishes each slot's packed voice state to RumbleAudioService, which renders it as low-frequency tones on the configured audio endpoint (the "Bass Shakers" tab).

Lighting takes a parallel path (#494). A DualShock 4 or DualSense virtual controller records the lightbar color a game writes (GameLightbarCapture). The slot's UserEffectsDispatcher turns each lit mouse's, keyboard's, unit row's or vendor row's Lighting tab mode into a color under that game color, and stores it in PeripheralOutputs as the slot's claim on the device, with the device's Lighting Hold. The workers paint the claim that rules each device or shared path: a device's own claim beats a vendor row, then the smaller displayed player number wins. Which worker paints a device follows the link table. While the vendor's program runs that is the Razer Chroma, Logitech LED SDK or SteelSeries GameSense worker, and otherwise the HID++, Razer, SteelSeries or LampArray worker that reaches the device on its own. See Input Pipeline.


NuGet Dependencies

Package Version Project Purpose
CommunityToolkit.Mvvm 8.2.2 App MVVM: ObservableObject, SetProperty, RelayCommand
Concentus 2.2.2 App Pure-C# Opus encoder for the DualSense Bluetooth speaker
HelixToolkit.Core.Wpf 2.27.3 App 3D viewport for controller model visualization
Microsoft.Windows.CsWinRT 2.2.0 App Generates the projection of the in-box Windows.Devices.Midi2 from Resources/WinMD
Microsoft.Windows.Devices.Midi2 1.0.16-rc.3.7 App Projection of the Windows MIDI Services App SDK runtime: Microsoft's install, or the build the Settings card installs
NAudio.Wasapi 2.2.1 App WASAPI loopback capture for bass-driven rumble, plus the WASAPI capture and render behind Bass Shakers, macro sounds, controller audio, haptic tones, and voice macros
Nefarius.Utilities.DeviceManagement 6.1.0 App Driver-store install, class filters, and USB CyclePort for the BthPS3 DualShock 3 stack (#116) and for releasing a hidden USB controller
System.Management 10.0.11 App ACPI-WMI event subscriptions for handheld hidden buttons and their learner (#343)
System.Speech 10.0.0 App SAPI recognizer session for voice macros (#317)
Vosk 0.3.38 App Offline voice-macro recognizer (Apache-2.0). Phrase-list grammar with an [unk] bucket
WPF-UI 4.3.0 App Fluent Design theme (Windows 11-style UI)
BouncyCastle.Cryptography 2.6.2 Engine Remote Link crypto: X25519, Ed25519, ChaCha20-Poly1305, HKDF-SHA256 (#138)
System.Security.Cryptography.ProtectedData 10.0.9 Engine DPAPI wrap of the Remote Link private identity key at rest (#138)
SteamKit2 3.4.0 SteamWorkshop Anonymous Steam CM session for Workshop search (#9). LGPL-2.1: its license.txt grants 2.1 or any later version, and its package metadata says LGPL-2.1-only. protobuf-net and ZstdSharp.Port arrive transitively

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. The shipped build reports file version 1.9.0.0. See HIDMaestro Deep Dive.

Native libraries folded into PadForge.exe and extracted at first launch to the single-file extraction directory (%TEMP%\.net\PadForge\<hash>\). Each architecture folder below has an arm64 twin the ARM64 build takes, except Interhaptics, which ships x64 only:

Library Caller Notes
SDL3.dll SDL3Minimal.cs Custom fork: HM filter + Switch 2 Pro + 16-XInput + Share button support. Resources/SDL3/x64/
libusb-1.0.dll SDL3's HIDAPI backend WinUSB access for the Switch 2 Pro Controller. Resources/SDL3/x64/
msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll SDL3.dll The C++ runtime SDL3.dll imports (its Elite paddle reader is C++), bundled so no redistributable install is needed. vcruntime140_1.dll is x64 only. Resources/VisualCpp/x64/
xinput1_4.dll SDL3's XInput backend, BluetoothLinkHelper OpenXInput fork. Single-file-embedded. App.OnStartup calls SetDllDirectory on the extraction directory, so the loader resolves the extracted copy ahead of System32. Filters HM virtuals from PadForge's own XInput view. Resources/OpenXInput/x64/
HAR.dll SensaHapticsService (#374), the Razer Sensa row's worker (#494) Interhaptics engine, P/Invoked lazily, so a missing DLL degrades to a diagnostics line. Resources/Interhaptics/x64/
Interhaptics.RazerProvider.dll HAR.dll The Razer Sensa backend HAR.dll loads. Resources/Interhaptics/x64/
libvosk.dll + libgcc_s_seh-1.dll, libstdc++-6.dll, libwinpthread-1.dll Vosk.dll, behind VoskVoiceEngine (#317) x64: added by the Vosk 0.3.38 package's own targets file, not by the csproj, and the three MinGW DLLs are what that libvosk.dll links against. ARM64: a <Content> item, Resources/Vosk/arm64/libvosk.dll, built by tools/build-libvosk-arm64.sh with its runtime linked in, so it has no companions

The Logitech LED engine is not in the bundle. LogiLedEngineNative loads the one G HUB or Logitech Gaming Software registers under its ServerBinary key (#494). The Razer Chroma and SteelSeries GameSense paths are HTTP calls to servers Razer Synapse and SteelSeries GG run, so they load no library at all.


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):

<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifier Condition="'$(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, or win-arm64/publish/ with -r win-arm64. See Building for ARM64.

SDL3.dll, libusb-1.0.dll, msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll (x64 only), xinput1_4.dll, HAR.dll, and Interhaptics.RazerProvider.dll are declared as <Content> items with CopyToOutputDirectory=PreserveNewest and Link="filename" (flattened to root), each read from the $(NativeArch) folder (x64 or arm64) of its resource directory. A publish without SDL3.dll, libusb-1.0.dll, or xinput1_4.dll is refused (the RequireBundledNatives target). With PublishSingleFile=true plus IncludeNativeLibrariesForSelfExtract=true they get folded into the single-file EXE and extracted to a %TEMP%\.net\PadForge\<hash>\ directory at first launch, so PadForge.exe ships standalone with no adjacent DLLs required at deploy time. The <Content> declaration is what makes the build pick them up at all. Without it the publish output would lack them entirely. On x64, libvosk.dll and the three MinGW runtime DLLs ride in the same way, added by the Vosk package's targets file rather than by the csproj. The ARM64 build drops those four and takes its own libvosk.dll from a <Content> item.

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 (x64 build only)
HidHideArm64/HidHide_ARM64.zip, HidHideArm64/nefconc.exe The ARM64 HidHide driver and the tool that installs it (both builds)
BthPS3/**/* BthPS3 + BthPS3PSM drivers and the DS3 WinUSB INF
gamecontrollerdb_padforge.txt PadForge SDL gamepad mapping additions
3DModels/**/*.obj, *.png, *.jpg 3D controller meshes and atlases. The EmbedPackedArt target Brotli-packs the meshes and PNG atlases (.objbr, .pngbr), and the JPEG atlases embed as they are
VoiceModels/*.zip The Vosk voice model, packed by EmbedPackedArt
WebAssets/**/* Web controller frontend (served by WebControllerServer)
2DModels/**/*.png 2D controller sprites (included as <Resource>, not <EmbeddedResource>)

See Build and Publish for the full list.

HIDMaestro's user-mode UMDF2 driver and signing assets ship inside HIDMaestro.Core.dll (referenced as a project-local <Reference HintPath> and bundled into the single-file EXE). DriverInstaller does not handle HM. On first engine start, InputManager.Step5.VirtualDevices.EnsureHMaestroContext calls HMContext.InstallDriver() to register the driver with Windows via pnputil. 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

The interface text lives in Resources/Strings/. Strings.resx holds American English, the text every lookup falls back to, and each Strings.<tag>.resx beside it holds one translation. Languages.tsv is the one list of languages: tag, English name, own name, direction, script, and whether the translation holds every string. Common/LanguageCatalog.cs reads it, and the language picker, the tests and the measuring tools all read the catalog. Settings lists the languages.

A partial translation shows English wherever it has no string. Strings.en-GB.resx holds only the strings British English writes differently and reads the American text for the rest, so the catalog counts it complete.

Satellite Resources

MSBuild names the culture of Strings.<tag>.resx itself only for a tag it knows. A file tagged chk or tzo it embeds in the main assembly under a name no culture looks up, and that language would show English. PadForge.App.csproj takes the culture and the resource name from the file name for every translation, and RespectAlreadyAssignedItemCulture keeps the build from undoing it. LanguageCatalogTests fails on a translation the build left out of its satellite.

Finding a Language

LanguageCatalog.Find(tag) returns the row for a tag, else the row an alias names: a saved en, from before American and British English each had a row, reads en-US, bare pt reads pt-PT, zh reads zh-Hans, nb and nn read no, tl reads fil, and Windows' ku-Arab reads ckb. Resolve(culture) walks the culture's parents to the first row that answers (de-AT reads de, zh-TW reads zh-Hant) and returns null for a language with no row, which shows English.

English follows CLDR. A region whose parent is en-001, directly or through en-150, reads British English: the United Kingdom, Ireland, Australia, New Zealand, India, South Africa, most of the Commonwealth and the English of continental Europe. The United States and its territories, Canada and the Philippines read American English. .NET's CultureInfo.Parent hands every English region straight to en, so the catalog keeps the regions in BritishEnglishRegions, and LanguageCatalogTests holds the list to the CLDR data in Windows' own ICU, read through icu.dll.

ResourceManager looks a string up through CultureInfo.Parent alone, which never reaches a row the catalog finds through an alias or a CLDR parent. So the first thing App.OnStartup() does after its known-folder lookup is swap the Windows display language for the culture of its row (LanguageCatalog.InterfaceCulture), ahead of the update helper's windows. Reset to Defaults goes through the same call. Without it, Windows set to nb-NO showed English, and en-AU showed American English while the picker named British English.

Right to Left

A language whose row says rtl lays every window out right to left: Arabic, Persian, Urdu and Central Kurdish. UiLanguage.Direction sets the window's FlowDirection from the catalog, so Hebrew Windows with no Hebrew translation shows English left to right. WPF mirrors every element that inherits the direction, drawings included, so every Canvas, Path, Polygon, Polyline, Line, Image and 3D viewport in the markup declares left to right itself or sits under an element that does, and the flames and platform logos the main window builds in code pin themselves. RightToLeftPinsTests reads the markup and fails on a drawing with no pin. The touchpad overlay, the on-screen menus and the light gun's calibration window call UiLanguage.KeepLeftToRight and stay left to right whole: mirrored, a calibration target drawn at the top left would sit at the top right while the calibration still read it as the left. The welcome tour's canvas is the one drawing that mirrors, because it cuts its spotlight out at the window coordinates of the control it points at.

Unicode does not mirror arrows, so a string that shows a flow with an arrow points it from source to receiver in the reading direction of its language.

WPF's bidi pass has no bracket-pair rule. In a right-to-left paragraph, the closing bracket of "Igor Budzhak (MIT)" followed by Arabic takes the paragraph's direction and is drawn mirrored on the far side of the name. Common/BidiText.cs puts each Latin stretch that holds a bracket pair into a Run of its own with FlowDirection set to left to right, an embedding that keeps both brackets inside it, and the translation rules keep the textbook fix, a direction mark, out of every string. Its Text property binds in place of TextBlock.Text, and its ToolTip property in place of ToolTip. Every string on the About page, the formula chips' tooltips and the pad page strings whose translations held such a pair go through it. BidiTextTests draws a credit both ways and checks the closing bracket's position.

Every other TextBlock in a right-to-left window is handled by Common/TextDirection.cs, data and template parts included. UiLanguage sets its inherited Watch property on each right-to-left window, so WPF calls back for every element that joins the tree. A class handler for Loaded would miss most TextBlocks, because WPF raises Loaded only on elements with handlers of their own. Text with no right-to-left letter that the window's direction would draw differently, such as a device named "Controller (XBOX 360 For Windows)" or text that starts with a digit, takes the direction of its first letter, left to right, as HTML's dir="auto" does. Its alignment and padding are mirrored with it, so it stays in place. Text that mixes right-to-left letters with Latin bracketed stretches gets those stretches as left-to-right Runs. New Inlines cost a laid-out TextBlock the binding on its Text, so the class first takes the text over: it copies the binding on Text, the binding a template or style put there, or the Content of the ContentPresenter that shows the string, and builds the Runs from that source on every change. Text a trigger sets and Inlines written with formatting are left alone. Tooltips and context menus are watched as they open. TextDirectionTests draws device names, bound text, template text, ContentPresenter strings and a tooltip in a right-to-left window, and an unwatched copy of the device name as the positive control.

Translation Rules

tools/l10n/check_translation.py holds a translation to the rules in tools/l10n/rules.py: the numbered placeholders, braces and line breaks of the English, digits 0 to 9, no invisible character or special space (a joiner between two letters of a word excepted), the names in tools/l10n/names.txt in Latin letters, decimal numbers written with a period, and the strings that must agree with each other. Number boxes read and write the en-US notation in every language, and a box reads a typed 1,0 as 10. TranslationRulesTests holds every file in the tree to the same rules. tools/l10n/write_resx.py writes a translation, given as a JSON object of keys and text, into Strings.<tag>.resx in the layout of Strings.resx and refuses one that breaks a rule.

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.

Window Language and Fonts

Common/UiLanguage.cs declares the interface language on every window, as xml:lang does in markup, and sets the window's layout direction (see Right to Left). App.OnStartup() calls UiLanguage.Start() right after it swaps in the interface culture, ahead of the update helper, whose windows open before anything else. From then on a class handler for the Loaded event of Window sets Window.Language and FlowDirection as each window loads, and a Strings.CultureChanged handler sets them again on every open window when the language changes. Menus, tooltips and drop-downs take the language and direction of the window they open from.

Chinese, Japanese and Korean share their characters, and WPF picks the font for one from the language its text is declared in: Microsoft YaHei UI under zh-Hans, Microsoft JhengHei UI under zh-Hant, Yu Gothic UI under ja, Malgun Gothic under ko. Text with no declared language is en-US, which takes the last row of WPF's fallback font table, where the Japanese font comes first. Until this was added, a Simplified Chinese interface was drawn in Yu Gothic UI (#504).

Only those three languages are declared. A window's language is also the culture its bindings format and parse numbers in, and the pad page has some 250 number boxes that read and write the en-US notation. Declared German would read a typed 0.5 as 5. The Chinese, Japanese and Korean cultures write numbers as en-US does. UiLanguageTests checks that against every number format in the markup and every such culture .NET knows.

Each row of the language picker declares its own language through CultureLanguageConverter, so each language's name is drawn in its own font whatever language the interface is in. The binding names ConverterCulture, because WPF refuses a binding on Language that would take its culture from the property being set.


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
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
PeripheralOutputs Common/Input/Peripherals/PeripheralOutputs.cs Static Peripheral output hub (#494): link table, haptic levels, lighting claims, Set Chroma Color, backend states

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
Nintendo 16 MaxNintendoSlots = MaxPads
Extended 16 MaxExtendedSlots = 16
MIDI 16 MaxMidiSlots = MaxPads
Keyboard+Mouse 16 MaxKeyboardMouseSlots = MaxPads
VR 1 MaxVrSlots = 1

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

VR is the one type capped below the global count. One slot already drives both SteamVR hands, so a second would fight the first over the same pair of devices. SettingsManager.CanSlotTakeType() enforces the cap in one place, because a type switch on an existing slot could otherwise mint a second VR slot past the add-popup's own check.

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, Nintendo, 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 RawHidState[] CombinedRawHidStates;      // Step 4 output (raw-HID axes/buttons/POVs: Extended custom + Nintendo)
public MidiRawState[] CombinedMidiRawStates;    // Step 4 output (MIDI)
public KbmRawState[] CombinedKbmRawStates;      // Step 4 output (KeyboardMouse)
public VrRawState[] CombinedVrRawStates;        // Step 4 output (VR: the slot's left+right hand pair)
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[] SlotRawHidSurface;               // Raw-HID pipeline (custom axis/button counts) vs Gamepad preset pipeline
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 / Nintendo / 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 / Nintendo / Extended, HMaestroVRController for VR, plus MIDI and KB+M)
  • HIDMaestro Deep Dive: HM SDK surface, thread-pool lifecycle, OpenXInput shim, bubble-down cascade
  • SDL3 Integration: SDL3 P/Invoke, device enumeration, state reading, haptic
  • Build and Publish: Build commands, publish configuration, CI/CD
  • Peripheral Outputs Internals: Mice, keyboards and vendor rows as output devices of a virtual controller

Last updated for PadForge 5.2.0.