Skip to content

Architecture Overview

hifihedgehog edited this page Mar 19, 2026 · 58 revisions

Architecture Overview

This page describes the high-level architecture of the PadForge codebase: solution structure, project layout, design philosophy, threading model, data flow, dependencies, and build system.

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

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

    subgraph "Engine — PadForge.App/Common/Input"
        IM[InputManager<br/>Polling loop · Steps 1-6]
        ABD[AudioBassDetector<br/>WASAPI loopback · IIR filter]
    end

    subgraph "Virtual Controllers"
        X360[Xbox 360<br/>ViGEm]
        DS4C[DualShock 4<br/>ViGEm]
        VJ[vJoy<br/>DirectInput FFB]
        KBM[Keyboard+Mouse<br/>SendInput]
        MIDI[MIDI<br/>Windows MIDI Services]
    end

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

    subgraph "External Systems"
        SDL3[SDL3.dll<br/>Custom fork]
        VGM[ViGEmBus Driver]
        VJD[vJoy Driver]
        HH[HidHide Driver]
        WASAPI[Windows Audio<br/>WASAPI Loopback]
        DSU_CLIENT[DSU Clients<br/>Cemu · Dolphin]
        BROWSER[Web Browsers<br/>Phone · tablet]
    end

    UI --> VM
    VM --> IS
    VM --> SS
    IS --> IM
    IS --> ABD
    IS --> DS
    IS --> FMS
    RS --> IM
    IM --> X360
    IM --> DS4C
    IM --> VJ
    IM --> KBM
    IM --> MIDI
    IM --> SDL
    IM --> RIL
    IM --> IHM
    ABD --> WASAPI
    SDL --> SDL3
    X360 --> VGM
    DS4C --> VGM
    VJ --> VJD
    DS --> HH
    IM -.->|UDP 26760| DSU_CLIENT
    IM -.->|HTTP+WS| BROWSER

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

Solution Structure

PadForge is a two-project .NET 10 solution:

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

PadForge.App references PadForge.Engine. The Engine assembly contains no WPF dependencies and is designed to be reusable. Both projects use GenerateAssemblyInfo=false (version is in Properties/AssemblyInfo.cs).


Project Layout

PadForge.App

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

  Common/
    ControllerIcons.cs                # SVG path data for controller type icons (Xbox, DS4, vJoy)
    CurveLut.cs                       # Sensitivity curve lookup table generation (per-axis response curves)
    DriverInstaller.cs                # InstallVJoy(), UninstallVJoy() -- SetupAPI-based driver management
    HidHideController.cs              # HidHide IOCTL API: blacklist/whitelist/cloaking via \\.\HidHide
    MarqueeBehavior.cs                # WPF attached behavior for scrolling/marquee text animation
    SettingsManager.cs                # Slot arrays, profiles, PadSetting defaults, partial class (see below)
    StartupHelper.cs                  # Run-at-startup registry helper (HKCU\...\Run)
    VirtualKey.cs                     # Windows VK code → display name lookup table

    Input/
      AudioBassDetector.cs            # WASAPI loopback audio capture + 8th-order IIR bass extraction
      InputManager.cs                 # Core partial class: fields, Start/Stop, PollingLoop, IDisposable
      InputManager.Step1.UpdateDevices.cs       # Device enumeration (SDL3 + Raw Input)
      InputManager.Step2.UpdateInputStates.cs   # Input state reading + force feedback
      InputManager.Step3.UpdateOutputStates.cs  # Mapping engine (descriptor → OutputState)
      InputManager.Step4.CombineOutputStates.cs # Multi-device merge per slot
      InputManager.Step4b.EvaluateMacros.cs     # Macro trigger/action state machine
      InputManager.Step5.VirtualDevices.cs      # ViGEm + vJoy + MIDI + KBM virtual controller lifecycle
      InputManager.Step6.RetrieveOutputStates.cs # Copy combined states for UI display
      Xbox360VirtualController.cs     # IVirtualController for ViGEm Xbox 360
      DS4VirtualController.cs         # IVirtualController for ViGEm DualShock 4
      VJoyVirtualController.cs        # IVirtualController for vJoy (direct P/Invoke, FFB)
      MidiVirtualController.cs        # IVirtualController for Windows MIDI Services
      KeyboardMouseVirtualController.cs # IVirtualController for Win32 SendInput (KB+Mouse)
      InputEventArgs.cs               # Event args for input state change notifications
      InputException.cs               # Custom exception for input pipeline errors
      InputExceptionEventArgs.cs      # Event args wrapping InputException

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

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

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

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

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

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

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

  Controls/
    CurveEditor.xaml(.cs)             # Custom sensitivity curve editor control (interactive Bezier/linear curve)
    RangeSlider.cs                    # Custom dual-thumb range slider control (dead zone min/max)

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

  Resources/
    ControllerIcons.xaml              # XAML resource dictionary with controller icon geometries
    PadForge.ico                      # Application icon
    Xbox Series Controller - Front.png  # Controller reference image
    Xbox Series Controller - Top.png    # Controller reference image
    Strings/
      Strings.resx                    # Base (English) UI string resources
      Strings.Designer.cs            # Hand-written INotifyPropertyChanged resource accessor
      Strings.de.resx                # German
      Strings.es.resx                # Spanish
      Strings.fr.resx                # French
      Strings.it.resx                # Italian
      Strings.ja.resx                # Japanese
      Strings.ko.resx                # Korean
      Strings.nl.resx                # Dutch
      Strings.pt-BR.resx             # Brazilian Portuguese
      Strings.zh-Hans.resx           # Simplified Chinese
    SDL3/x64/SDL3.dll                 # SDL3 native library (custom fork)
    SDL3/x64/libusb-1.0.dll           # libusb for HIDAPI backend (Switch 2 support)
    ViGEmBus_1.22.0_x64_x86_arm64.exe # Embedded ViGEmBus installer
    HidHide_1.5.230_x64.exe           # Embedded HidHide installer
    vJoyDriver.zip                     # Embedded vJoy driver files (vjoy.sys, vjoy.inf, etc.)
    vJoySetup_v2.2.2.0_Win10_Win11.exe # Optional standalone vJoy installer

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

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

PadForge.Engine

PadForge.Engine/
  Properties/
    AssemblyInfo.cs

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

  Data/
    DeadZoneShape.cs            # Dead zone shape enum (circular, square, cross)
    MappingTranslation.cs       # Cross-layout mapping translation (Xbox/DS4/vJoy/MIDI/KBM positional equivalence)
    PadSetting.cs               # Per-device mapping configuration: button/axis descriptors, dead zones, vJoy mappings
    UserDevice.cs               # Physical device record: GUID, name, capabilities, runtime state
    UserSetting.cs              # Links a UserDevice (InstanceGuid) to a pad slot (MapTo) with a PadSetting

tools/

tools/
  DsuDiag/                      # Standalone DSU client for motion data diagnostics
  vJoy/
    Test/                       # vJoy axis/button/POV test tool with WinMM readback
    FfbTest/                    # SharpDX-based DirectInput FFB test tool
    SDK/                        # vJoy SDK reference (FeederDemoCS)
  capture_screenshots.ps1       # Automated screenshot capture script
  capture_all.ps1               # Full screenshot capture orchestration
  cleanup_vjoy.ps1              # vJoy device node cleanup utility
  deploy.ps1                    # Build + deploy to install directory
  deploy_and_restart.ps1        # Deploy + restart PadForge
  dump_ui_tree.ps1              # WPF visual tree dump for debugging
  overlay_positions.py          # 2D controller overlay coordinate generator
  (+ various vJoy test/diagnostic scripts)

Key Design Decisions

Why SDL3 (not DirectInput or raw XInput)

PadForge uses SDL3 as the sole input abstraction for all physical controllers, including Xbox/XInput gamepads. This was chosen over the traditional DirectInput/XInput split for several reasons:

  • Unified API: SDL3's Gamepad API normalizes button/axis layouts across controller types (Xbox, DualSense, Switch Pro, etc.), eliminating the need for separate code paths per controller family.
  • Cross-controller sensor support: SDL3 exposes gyroscope and accelerometer data from DualSense, DualShock 4, Switch Pro, and Joy-Con controllers through a single API. DirectInput has no gyro/accel support.
  • Background input: SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS provides out-of-focus input reading without window focus tricks.
  • Community gamepad database: SDL's gamecontrollerdb plus PadForge's own gamecontrollerdb_padforge.txt provide automatic button mapping for hundreds of controllers.
  • HIDAPI backend: SDL3's built-in HIDAPI support enables reading exotic controllers (Switch 2 Pro via custom fork) that no Windows API supports natively.

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

Key SDL3 hints:

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

Why ViGEm (not ScpVBus)

PadForge creates virtual Xbox 360 and DualShock 4 controllers via ViGEmBus rather than the older ScpVBus:

  • Maintained driver: ViGEmBus is actively maintained with signed drivers for Windows 10/11. ScpVBus is abandoned and has signing issues on modern Windows.
  • DS4 support: ViGEm supports virtual DualShock 4 controllers natively. ScpVBus only supports Xbox 360.
  • Rumble feedback: ViGEm provides a per-controller feedback callback (FeedbackReceived) that delivers game rumble values back to PadForge for forwarding to physical controllers. ScpVBus has no equivalent.
  • Slot control: ViGEm allows creating controllers at specific XInput slots and provides proper lifecycle management (connect/disconnect events).

Why Polling (not event-driven)

The input pipeline runs as a fixed-rate polling loop (~1000 Hz) rather than using event-driven input:

  • Deterministic timing: A fixed-rate loop guarantees consistent input latency regardless of controller type or OS scheduling. Event-driven approaches have variable latency depending on USB polling intervals and OS message queue behavior.
  • Multi-device merging: PadForge combines multiple physical devices into a single virtual controller per slot. Polling reads all devices synchronously each frame, ensuring the merged output reflects a consistent point in time. Event-driven merging would require complex synchronization to avoid reading stale state from non-event devices.
  • Macro evaluation: Macros need to evaluate their trigger conditions and execute actions on a consistent time base. A polling loop provides natural "frames" for the state machine.
  • Output rate control: Virtual controllers need steady-state updates. A polling loop naturally produces output at a fixed rate; event-driven output would burst on input activity and go silent during inactivity.

Design Philosophy

MVVM with CommunityToolkit.Mvvm

ViewModels use ObservableObject from CommunityToolkit.Mvvm as their base class, providing [ObservableProperty] and [RelayCommand] source generators. The ViewModelBase class in the project extends this base and adds the OnCultureChanged() hook for live language switching.

Command + Event Decoupling

ViewModels expose commands and raise events. They do not directly call services or touch the input pipeline. Instead:

  1. VMs raise events (e.g., MappingChanged, SlotAdded, SelectedDeviceChanged)
  2. MainWindow.xaml.cs or service classes subscribe to these events
  3. Event handlers call into 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. The typical lifecycle:

App.OnStartup()
  → Single-instance mutex check
  → Early language restore (regex parse of PadForge.xml before full load)
  → Auto-elevation if vJoy driver installed
  → new MainWindow()
      → Creates MainViewModel (root VM with Pads[] array)
      → Creates SettingsService, InputService, RecorderService, DeviceService
      → Wires ViewModel events → service handlers
      → SettingsService.Initialize() loads PadForge.xml
      → InputService.Start() creates InputManager, starts polling thread

SettingsManager vs SettingsService

These two classes have distinct roles despite both dealing with "settings":

SettingsManager (static class, Common/SettingsManager.cs)

SettingsManager is a static data container shared between the engine thread and the UI thread. It holds:

  • UserDevices and UserSettings collections (with SyncRoot locking)
  • Slot arrays: SlotCreated[], SlotEnabled[]
  • Profile data: Profiles, ActiveProfileId, EnableAutoProfileSwitching
  • Per-type slot limits: MaxXbox360Slots, MaxDS4Slots, MaxVJoySlots, etc.
  • Helper methods: CreateDefaultPadSetting(), SwapSlots(), FindSlotForDevice()

It is a partial class with declarations split across:

  • Common/SettingsManager.cs -- profiles, slot arrays, helper methods
  • InputManager.Step1.UpdateDevices.cs -- UserDevices/UserSettings property declarations and collection class definitions

SettingsManager has no knowledge of XML files, ViewModels, or the UI. It is the shared state that both threads operate on.

SettingsService (instance class, Services/SettingsService.cs)

SettingsService is the persistence and synchronization layer. It:

  • Loads PadForge.xml (or Settings.xml fallback) via XmlSerializer into SettingsManager collections
  • Saves SettingsManager state back to XML (manual save + auto-save timer)
  • Synchronizes SettingsManager data to/from ViewModels (bidirectional):
    • On load: populates PadViewModel mapping rows, dead zone configs, macro lists from PadSetting
    • On save: reads ViewModel state back into PadSetting objects for serialization
  • Manages profiles: save-as-profile, load profile, delete profile, default snapshot
  • Tracks dirty state: IsDirty flag, AutoSaved event

The separation means the engine thread can read SettingsManager data without any reference to the WPF-dependent SettingsService.


InputManager Partial Class Split

InputManager is a single partial class split across 8 files. The rationale is pipeline stage isolation: each file implements exactly one stage of the polling loop, with its own local fields, helper methods, and state. This avoids a single 5000+ line file while keeping all stages in the same class (they share per-slot arrays and virtual controller references).

File Stage Responsibility
InputManager.cs Core Fields, constants, Start()/Stop(), PollingLoop(), IDisposable, motion snapshots, DSU broadcast
InputManager.Step1.UpdateDevices.cs Step 1 SDL device enumeration, open/close, ViGEm instance filtering, UserDevices/UserSettings collection classes
InputManager.Step2.UpdateInputStates.cs Step 2 Read CustomInputState from each device, apply force feedback from VibrationStates[] + audio bass
InputManager.Step3.UpdateOutputStates.cs Step 3 Map CustomInputState to OutputState via PadSetting descriptors (dead zones, sensitivity curves, inversion, range clamping)
InputManager.Step4.CombineOutputStates.cs Step 4 Merge multiple device OutputStates into per-slot CombinedOutputStates[] (max-wins for axes, OR for buttons)
InputManager.Step4b.EvaluateMacros.cs Step 4b Evaluate macro triggers against combined state, execute macro actions (button presses, axis overrides, volume OSD)
InputManager.Step5.VirtualDevices.cs Step 5 Create/destroy IVirtualController instances, submit CombinedOutputStates[] to ViGEm/vJoy/MIDI/KBM, XInput slot detection
InputManager.Step6.RetrieveOutputStates.cs Step 6 Copy CombinedOutputStates[] to RetrievedOutputStates[] for UI consumption

The SettingsManager partial class is also split across files for the same reason: its collection types are declared alongside the Step 1 code that populates them.


Threading Model

PadForge uses up to seven distinct threads:

1. Engine Thread (InputManager, ~1000 Hz)

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

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

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

Drift compensation: Each cycle accumulates expectedTicks += targetTicks and compares against wallClock.ElapsedTicks. If the loop falls behind, subsequent cycles shorten; if ahead, they lengthen. This ensures the long-term average exactly matches the target Hz. Drift exceeding 10 cycles (e.g., after system sleep) triggers a wall-clock reset.

timeBeginPeriod(1) is set for the lifetime of the polling loop. Auto-idle mode (~20 Hz via Thread.Sleep(50)) activates when no virtual controller slots are created, reducing CPU usage to near zero. Timing target is recalculated each cycle from the runtime-adjustable PollingIntervalMs property (default: 1ms, configurable in Settings UI).

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

InputService creates a DispatcherTimer at ~33ms interval that:

  • Reads RetrievedOutputStates[] and RetrievedKbmRawStates[] from the engine for dashboard/schematic display
  • Reads UserDevice.InputState for the Devices page live visualization (only when visible)
  • Pushes macro snapshots from PadViewModels to MacroSnapshots[] for engine consumption
  • Updates SlotControllerTypes[], SlotVJoyConfigs[], SlotVJoyIsCustom[], _midiConfigs[] from ViewModel state
  • Syncs the device list from UserDevices to DevicesViewModel (via DeviceService)
  • Updates dashboard statistics (frequency, device count, online status)
  • Runs macro trigger recording (detects button/axis changes for macro binding)

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

3. WASAPI Audio Thread (AudioBassDetector)

AudioBassDetector uses NAudio's WasapiCapture in loopback mode to capture system audio. NAudio creates its own internal capture thread that delivers audio buffers via the DataAvailable callback. The callback runs an 8th-order cascaded IIR low-pass filter to extract bass frequencies (configurable cutoff, default 80 Hz) and updates a volatile _bassEnergy float (0.0--1.0). The engine thread reads this value in Step 2 via AudioBassDetector.BassEnergy and combines it with game rumble via max() for physical controller vibration.

The detector implements IMMNotificationClient to automatically restart capture when the default audio device changes.

4. Raw Input Thread (Hidden HWND_MESSAGE Window)

public static class RawInputListener

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

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

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

5. DSU Receive Thread

DsuMotionServer uses UdpClient.ReceiveAsync() on a background task to receive subscription packets from DSU clients (e.g., Cemu, Dolphin). Motion data is broadcast by the engine thread in the polling loop after Step 2 (same thread, no additional thread for sending). The DSU protocol is limited to 4 slots; slots 4-15 skip DSU broadcast.

6. Input Hook Thread (InputHookManager, on demand)

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

Created only when devices have "Consume mapped inputs" enabled. Installs WH_KEYBOARD_LL and WH_MOUSE_LL low-level hooks and runs a GetMessageW message pump. Suppression sets (which VKeys/mouse buttons to consume) are updated via volatile reference swap from the UI thread. The thread is stopped and hooks are removed when the engine stops or hiding is disabled.

7. Web Controller Server Thread

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

Created when the web controller server is enabled on the Dashboard. Runs an HttpListener accept loop. WebSocket connections spawn async tasks for per-client I/O. Each connected browser client creates a WebControllerDevice (implements ISdlInputDevice) that appears in the Step 1 device enumeration pipeline. The server also handles HTTP requests for static assets (HTML/CSS/JS/PNG) and a /api/layout JSON endpoint.

Thread Safety Summary

Shared State Written By Read By Sync Mechanism
UserDevices.Items Engine (Step 1) UI thread, Engine SyncRoot lock
UserSettings.Items UI thread Engine (Steps 2-5) SyncRoot lock
UserDevice.InputState Engine (Step 2) UI timer Atomic reference swap
CombinedOutputStates[] Engine (Step 4) Engine (Step 5, 6) Single-thread write
RetrievedOutputStates[] Engine (Step 6) UI timer Value copy (struct)
VibrationStates[] ViGEm callback thread Engine (Step 2) Volatile fields
MacroSnapshots[] UI timer Engine (Step 4b) Atomic reference swap
SlotControllerTypes[] UI timer Engine (Step 5) Volatile read
MotionSnapshots[] Engine (polling loop) DSU broadcast Same thread
RawInputListener key/mouse state Raw Input thread Engine (Step 2) ConcurrentDictionary
InputHookManager suppression sets UI thread Hook thread callbacks Volatile reference swap
WebControllerDevice.InputState WebSocket receive task Engine (Step 2) Atomic reference swap
AudioBassDetector._bassEnergy WASAPI callback thread Engine (Step 2) Volatile float

Data Flow

The following describes how data flows through the system from physical controller to game:

Physical Controller (USB/Bluetooth)
  │
  ▼
SDL3.dll (HID / XInput / HIDAPI backend)
  │
  ▼
Step 1: UpdateDevices()
  │  SDL_GetJoysticks() → open new devices → SdlDeviceWrapper
  │  Filter ViGEm virtual controllers (VID/PID + device path)
  │  Filter vJoy virtual controllers (VID 0x1234 / PID 0xBEAD)
  │  Update UserDevices collection (add new, mark disconnected)
  │
  ▼
Step 2: UpdateInputStates()
  │  For each online device:
  │    SdlDeviceWrapper.GetCurrentState() → CustomInputState
  │    Store as UserDevice.InputState (atomic swap)
  │  Apply force feedback: VibrationStates[] + AudioBass → physical rumble
  │
  ▼
Step 3: UpdateOutputStates()
  │  For each UserSetting (device→slot binding):
  │    Read PadSetting descriptors (axis/button/POV mappings)
  │    Apply dead zones, 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 VJoyRawState, MidiRawState, KbmRawState for non-gamepad types
  │
  ▼
Step 4b: EvaluateMacros()
  │  For each slot with macros:
  │    Check trigger conditions 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
  │    Submit CombinedOutputStates[i] → virtual controller
  │    Xbox 360: ViGEmBus → XInput slot
  │    DS4: ViGEmBus → DirectInput HID device
  │    vJoy: UpdateVJD() → vJoy driver → DirectInput HID device
  │    KBM: SendInput() → Win32 keyboard/mouse events
  │    MIDI: Windows MIDI Services → virtual MIDI port
  │
  ▼
Step 6: RetrieveOutputStates()
  │  Copy CombinedOutputStates[] → RetrievedOutputStates[]
  │  Copy CombinedKbmRawStates[] → RetrievedKbmRawStates[]
  │  (Value copy, consumed by UI timer at 30Hz)
  │
  ▼
Game reads virtual controller via XInput / DirectInput / SDL / raw HID

Reverse data flow (game rumble → physical controller):

Game sends rumble via XInput / DirectInput FFB
  │
  ▼
ViGEmBus FeedbackReceived callback (ViGEm thread)
  │  Writes to VibrationStates[slotIndex]
  │
  ▼ (or)
vJoy FFB callback (IOCTL thread)
  │  FfbCallback → FfbDeviceState → VibrationStates[slotIndex]
  │
  ▼
Step 2: ApplyForceFeedback() (engine thread)
  │  Reads VibrationStates[slot] for each device in that slot
  │  max(gameRumble, audioBass) → SdlDeviceWrapper.SetRumble()
  │  SDL3 → USB rumble command → physical controller

NuGet Dependencies

Package Version Purpose
ModernWpfUI 0.9.6 Fluent Design theme for WPF (Windows 11-style UI)
HelixToolkit.Core.Wpf 2.27.3 3D viewport for controller model visualization
CommunityToolkit.Mvvm 8.2.2 MVVM infrastructure: ObservableObject, RelayCommand, source generators
Nefarius.ViGEm.Client 1.21.256 ViGEmBus client for creating virtual Xbox 360 and DS4 controllers
NAudio.Wasapi 2.2.1 WASAPI loopback audio capture for bass-driven rumble
Microsoft.Windows.Devices.Midi2 1.0.16-rc.3.7 Windows MIDI Services SDK for virtual MIDI device creation

SDL3 is loaded via direct P/Invoke ([DllImport("SDL3")]) from SDL3Minimal.cs -- there is no SDL3 NuGet package. The SDL3.dll binary is a custom fork (with Switch 2 Pro Controller support) placed in Resources/SDL3/x64/.

vJoy is loaded via direct P/Invoke ([DllImport("vJoyInterface.dll")]) with NativeLibrary.TryLoad fallback from C:\Program Files\vJoy\.

XInput is loaded via direct P/Invoke ([DllImport("xinput1_4.dll")]) -- used only in Step 5 for XInput slot mask detection during ViGEm virtual controller creation.


Build System

PadForge must be built with dotnet publish, not dotnet build. The publish command produces a single-file self-contained executable:

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

Key publish properties (from PadForge.App.csproj):

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

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

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

Note: UseWindowsForms=true is set in the csproj -- this is required for System.Windows.Forms.NotifyIcon (system tray icon). WinForms implicit usings are removed to avoid type ambiguities with WPF.

Embedded resources (extracted at install time or runtime):

  • ViGEmBus_1.22.0_x64_x86_arm64.exe -- ViGEmBus installer
  • HidHide_1.5.230_x64.exe -- HidHide installer
  • vJoyDriver.zip -- vJoy driver files (vjoy.sys, vjoy.inf, vjoy.cat, hidkmdf.sys, vJoyInterface.dll)
  • 3DModels/**/*.obj -- 3D controller mesh assets
  • WebAssets/**/* -- Web controller frontend (served by WebControllerServer)
  • 2DModels/**/*.png -- 2D controller sprite assets (included as <Resource>, not <EmbeddedResource>)

Localization

PadForge supports 10 languages via .resx resource files in Resources/Strings/:

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

Strings.Designer.cs

Despite the <auto-generated> header, Strings.Designer.cs is hand-written (not generated by ResXFileCodeGenerator). It implements INotifyPropertyChanged so that XAML bindings update when the language changes.

Key design elements:

  • Singleton pattern: Strings.Instance is the binding source for all XAML string references.
  • CultureChanged event: Uses a weak event pattern for instance-method handlers. Static/lambda handlers are stored with strong references; instance-method handlers are stored as (WeakReference<object>, MethodInfo) pairs so they do not prevent GC of the subscribing object. Dead entries are pruned on every raise.
  • ChangeCulture(CultureInfo): Sets Thread.CurrentThread.CurrentUICulture and CultureInfo.DefaultThreadCurrentUICulture, raises PropertyChanged for all properties on the Instance (refreshing all XAML bindings), then raises CultureChanged so ViewModels can refresh their own computed strings.

XAML Binding Pattern

All UI strings bind to the singleton instance:

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

Because Strings implements INotifyPropertyChanged, WPF updates all bound text when ChangeCulture() fires PropertyChanged.

ViewModel Integration

ViewModelBase subscribes to Strings.CultureChanged in its constructor and exposes a virtual OnCultureChanged() method. Derived ViewModels override this to refresh culture-dependent computed properties (status text, titles, formatted strings, etc.).

Because CultureChanged uses weak references for instance handlers, ViewModels do not need to unsubscribe to avoid memory leaks.

Live Language Switching

Strings.ChangeCulture() applies the new culture immediately -- no application restart is needed. The sequence:

  1. CurrentUICulture is updated on the current thread.
  2. PropertyChanged fires for every resource property on Strings.Instance, updating all XAML bindings.
  3. CultureChanged fires, allowing ViewModels to recompute any non-bound culture-dependent strings.

Early Language Restore

App.OnStartup() applies the saved language before any UI is created. It reads the <Language> element from PadForge.xml via regex (avoiding full XML deserialization) and sets CurrentUICulture so the first window renders in the correct language.


Key Static / Singleton Classes

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

Slot System

PadForge supports up to 16 virtual controller slots (MaxPads = 16). Per-type limits are all derived from MaxPads:

Type Limit Constant
Xbox 360 16 MaxXbox360Slots = MaxPads
DualShock 4 16 MaxDS4Slots = MaxPads
vJoy 16 MaxVJoySlots = 16 (vJoy driver limit)
MIDI 16 MaxMidiSlots = MaxPads
Keyboard+Mouse 16 MaxKeyboardMouseSlots = MaxPads

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

API visibility note: The XInput API can only address 4 controllers (slots 0-3). Xbox 360 virtual controllers beyond the first 4 still function but may not be visible to XInput-only games. Games using DirectInput, SDL, or raw HID can see all 16. DualShock 4 and vJoy controllers are not affected by the XInput limit.

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 VJoyRawState[] CombinedVJoyRawStates;    // Step 4 output (custom vJoy)
public MidiRawState[] CombinedMidiRawStates;    // Step 4 output (MIDI)
public KbmRawState[] CombinedKbmRawStates;      // Step 4 output (KeyboardMouse)
public Gamepad[] RetrievedOutputStates;          // Step 6 output (UI display)
public KbmRawState[] RetrievedKbmRawStates;     // Step 6 output (KBM UI preview)
public Vibration[] VibrationStates;              // ViGEm feedback
public MotionSnapshot[] MotionSnapshots;         // DSU motion data
public MacroItem[][] MacroSnapshots;             // Macro definitions
public VirtualControllerType[] SlotControllerTypes;  // Configured type
internal VJoyVirtualController.VJoyDeviceConfig[] SlotVJoyConfigs;  // vJoy HID config
internal bool[] SlotVJoyIsCustom;                // Custom vs gamepad preset
internal MidiSlotConfig[] _midiConfigs;          // Per-slot MIDI configuration

Clone this wiki locally