-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture Overview
High-level architecture: 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/>HIDMaestro]
DS4C[DualShock 4<br/>HIDMaestro]
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[HIDMaestro 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
Two-project .NET 10 solution:
| Project | Target | Role |
|---|---|---|
| PadForge.App |
net10.0-windows10.0.26100.0 (WPF, WinExe) |
UI, input pipeline, virtual controllers, services |
| PadForge.Engine |
net10.0-windows (Class Library) |
Shared data types, SDL3 P/Invoke, device wrappers, input state structures |
PadForge.App references PadForge.Engine. The Engine has no WPF dependencies and is reusable. Both projects use GenerateAssemblyInfo=false (version in Properties/AssemblyInfo.cs).
PadForge.App/
App.xaml / App.xaml.cs # Entry point, auto-elevation, global exception handling
MainWindow.xaml / MainWindow.xaml.cs # Shell: app branding bar, sidebar navigation, page hosting, event wiring
gamecontrollerdb_padforge.txt # Custom SDL gamepad mappings (DS3 SDF, etc.)
Properties/
AssemblyInfo.cs # Version, metadata (GenerateAssemblyInfo=false)
Common/
ControllerIcons.cs # SVG path data for controller type icons (Xbox, DS4, vJoy)
CurveLut.cs # Sensitivity curve LUT generation (per-axis response curves)
DriverInstaller.cs # InstallExtended(), UninstallExtended(). 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 capture + 8th-order IIR bass extraction
InputManager.cs # Core partial class: fields, Start/Stop, PollingLoop, IDisposable
InputManager.Step1.UpdateDevices.cs # Device enumeration (SDL3 + Raw Input)
InputManager.Step2.UpdateInputStates.cs # Input state reading + force feedback
InputManager.Step3.UpdateOutputStates.cs # Mapping engine (descriptor → OutputState)
InputManager.Step4.CombineOutputStates.cs # Multi-device merge per slot
InputManager.Step4b.EvaluateMacros.cs # Macro trigger/action state machine
InputManager.Step5.VirtualDevices.cs # HIDMaestro + vJoy + MIDI + KBM virtual controller lifecycle
InputManager.Step6.RetrieveOutputStates.cs # Copy combined states for UI display
Xbox360VirtualController.cs # IVirtualController for HIDMaestro Xbox 360
DS4VirtualController.cs # IVirtualController for HIDMaestro DualShock 4
ExtendedVirtualController.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: InputManager (engine thread) ↔ UI (30Hz timer)
SettingsService.cs # Settings load/save, applies PadSettings to InputManager
DeviceService.cs # Device list UI sync (ObservableCollection from UserDevices)
DsuMotionServer.cs # UDP server on port 26760. DSU/Cemuhook motion protocol
ForegroundMonitorService.cs # Polls GetForegroundWindow at 30Hz for per-app profile switching
RecorderService.cs # Input mapping recorder (physical input → mapping descriptors)
WebControllerServer.cs # Embedded HTTP+WebSocket server for browser-based virtual controllers
ViewModels/
ViewModelBase.cs # Base class: INotifyPropertyChanged, OnCultureChanged hook
MainViewModel.cs # Shell VM: navigation, sidebar items, profile list, Pads[] array
DashboardViewModel.cs # Per-slot status cards, virtual controller status
PadViewModel.cs # Per-slot mapping/settings/deadzone/macro configuration
DevicesViewModel.cs # Physical device list with live input visualization
DeviceRowViewModel.cs # Single device card in the Devices page
SettingsViewModel.cs # App-level settings (polling rate, driver status, etc.)
MappingItem.cs # Single axis/button mapping row in PadPage
MacroItem.cs # Macro definition: trigger, actions, repeat mode, state machine
MidiSlotConfig.cs # Per-slot MIDI config: channel, velocity, CC/note counts
StickConfigItem.cs # Thumbstick deadzone / anti-deadzone / linear config
TriggerConfigItem.cs # Trigger deadzone / anti-deadzone / max range config
ExtendedSlotConfig.cs # 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 grid, deadzone sliders, macros
DevicesPage.xaml(.cs) # Physical device list with live input visualization
SettingsPage.xaml(.cs) # Polling rate, driver install, DSU toggle
ProfilesPage.xaml(.cs) # Profile management: save/load/delete
AboutPage.xaml(.cs) # Version, credits, license
ControllerModelView.xaml(.cs) # 3D controller visualization (HelixToolkit viewport)
ControllerModel2DView.xaml(.cs) # 2D controller overlay (Canvas-based)
ControllerSchematicView.xaml(.cs) # Schematic controller diagram (vector-based)
KBMPreviewView.xaml(.cs) # Keyboard+Mouse interactive preview
MidiPreviewView.xaml(.cs) # MIDI piano keyboard + CC slider preview
MousePreviewControl.xaml(.cs) # Read-only mouse graphic for Devices page detail pane
CopyFromDialog.xaml(.cs) # Copy mappings from another slot
ProfileDialog.xaml(.cs) # Save new profile (name + exe list)
Models3D/
ControllerModelBase.cs # Abstract base for 3D models (OBJ loading, part animation)
ControllerModelXbox360.cs # Xbox 360 3D model parts and animation bindings
ControllerModelDS4.cs # DualShock 4 3D model parts and animation bindings
Models2D/
ControllerOverlayLayout.cs # 2D overlay positioning data (button/stick coordinates)
2DModels/
DS4/ # DualShock 4 PNG sprites (Gamepad-Asset-Pack, MIT)
XBOX360/ # Xbox 360 PNG sprites
3DModels/
DS4/ # DualShock 4 OBJ meshes (from Handheld Companion, CC BY-NC-SA 4.0)
XBOX360/ # Xbox 360 OBJ meshes
Controls/
CurveEditor.xaml(.cs) # Interactive sensitivity curve editor (Bezier/linear)
RangeSlider.cs # Dual-thumb range slider (deadzone min/max)
Converter/
BoolToColorConverter.cs # bool → SolidColorBrush
BoolToVisibilityConverter.cs # bool → Visible / Collapsed
CrossGeometryConverter.cs # Cross/X geometry for close buttons
NormToCanvasConverter.cs # Normalized float → Canvas pixel coordinate
NormToTriggerHeightConverter.cs # Trigger value → bar height
NullToCollapsedConverter.cs # null → Collapsed, non-null → Visible
PercentToSizeConverter.cs # Percentage → pixel size
SlopedWedgeGeometryConverter.cs # Wedge geometry for trigger visuals
StringToVisibilityConverter.cs # Non-empty string → Visible, empty → Collapsed
Resources/
ControllerIcons.xaml # XAML resource dictionary with controller icon geometries
PadForge.ico # Application icon
Xbox Series Controller - Front.png # Controller reference image
Xbox Series Controller - Top.png # Controller reference image
Strings/
Strings.resx # Base (English) UI string resources
Strings.Designer.cs # Hand-written INotifyPropertyChanged resource accessor
Strings.de.resx # German
Strings.es.resx # Spanish
Strings.fr.resx # French
Strings.it.resx # Italian
Strings.ja.resx # Japanese
Strings.ko.resx # Korean
Strings.nl.resx # Dutch
Strings.pt-BR.resx # Brazilian Portuguese
Strings.zh-Hans.resx # Simplified Chinese
SDL3/x64/SDL3.dll # SDL3 native library (custom fork)
SDL3/x64/libusb-1.0.dll # libusb for HIDAPI backend (Switch 2 support)
HIDMaestro_1.22.0_x64_x86_arm64.exe # Embedded HIDMaestro 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/
Properties/
AssemblyInfo.cs
Common/
SDL3Minimal.cs # SDL3 P/Invoke declarations (init, joystick, gamepad, haptic, sensor)
ISdlInputDevice.cs # Interface: GetCurrentState(), GetDeviceObjects(), rumble
SdlDeviceWrapper.cs # Joystick/Gamepad open, state reading, GUID construction, haptic
SdlKeyboardWrapper.cs # Keyboard via Raw Input, ISdlInputDevice adapter
SdlMouseWrapper.cs # Mouse via Raw Input with delta accumulation, ISdlInputDevice adapter
RawInputListener.cs # Hidden HWND_MESSAGE window, RIDEV_INPUTSINK, per-device state
CustomInputState.cs # API-agnostic input snapshot: axes[24], sliders[8], povs[4], buttons[256], gyro[3], accel[3]
CustomInputHelper.cs # Diff computation between two CustomInputState snapshots
CustomInputUpdate.cs # Single input change (axis delta, button press/release)
DeviceObjectItem.cs # Metadata for one axis/button/hat on a device
DeviceEffectItem.cs # Metadata for a force feedback effect on a device
ForceFeedbackState.cs # Per-device rumble: change detection, haptic effect lifecycle
GamepadTypes.cs # Gamepad struct (XInput layout), ExtendedRawState, MidiRawState, KbmRawState, Vibration, MotionSnapshot
InputTypes.cs # Enums: DeviceObjectTypeFlags, ObjectAspect, MapType, ObjectGuid, InputDeviceType
VirtualControllerTypes.cs # VirtualControllerType enum, IVirtualController interface
RumbleLogger.cs # Diagnostic logger for rumble/FFB (disabled by default)
InputHookManager.cs # WH_KEYBOARD_LL / WH_MOUSE_LL hooks for mapped input suppression
WebControllerDevice.cs # Virtual input device from browser clients (implements ISdlInputDevice)
Data/
DeadZoneShape.cs # Deadzone shape enum (circular, square, cross)
MappingTranslation.cs # Cross-layout mapping translation (Xbox/DS4/vJoy/MIDI/KBM equivalence)
PadSetting.cs # Per-device mapping config: button/axis descriptors, deadzones, 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/
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)
SDL3 is the sole input abstraction for all physical controllers, including Xbox/XInput gamepads.
| Advantage | Detail |
|---|---|
| Unified API | Gamepad API normalizes button/axis layouts across Xbox, DualSense, Switch Pro, etc.. No per-family code paths |
| Sensor support | Gyro and accelerometer from DualSense, DS4, Switch Pro, and Joy-Con via one API. DirectInput has no gyro/accel |
| Background input |
SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS reads input without window focus |
| Gamepad database | SDL's gamecontrollerdb + PadForge's gamecontrollerdb_padforge.txt auto-map hundreds of controllers |
| HIDAPI backend | Reads exotic controllers (Switch 2 Pro via custom fork) that no Windows API supports natively |
The native XInput P/Invoke (xinput1_4.dll) in Step 5 is used only for detecting which XInput slot (0–3) a new HIDMaestro Xbox 360 virtual controller occupies. It never reads input.
Key SDL3 hints:
SDL_SetHint(SDL_HINT_JOYSTICK_XINPUT, "1"); // Xbox enumeration via XInput backend
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); // Background input without focus
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH2, "1"); // Switch 2 Pro (custom fork)
// NEVER set SDL_HINT_JOYSTICK_RAWINPUT. Conflicts with XInput enumerationVirtual Microsoft and PlayStation controllers use HIDMaestro:
| Advantage | Detail |
|---|---|
| Maintained driver | Signed for Windows 10/11. ScpVBus is abandoned with signing issues |
| DS4 support | Native virtual DS4. ScpVBus only supports Xbox 360 |
| Rumble feedback | Per-controller FeedbackReceived callback delivers game rumble back to PadForge. ScpVBus has no equivalent |
| Slot control | Create controllers at specific XInput slots with proper lifecycle management |
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 |
ViewModels extend ObservableObject (CommunityToolkit.Mvvm), using [ObservableProperty] and [RelayCommand] source generators. ViewModelBase adds OnCultureChanged() for live language switching.
VMs expose commands and raise events but never call services or touch the input pipeline directly:
- VMs raise events (e.g.,
MappingChanged,SlotAdded,SelectedDeviceChanged) -
MainWindow.xaml.csor service classes subscribe - Handlers call
InputService,SettingsService, etc.
This keeps VMs testable and decoupled from the engine thread.
Dependencies are wired manually in App.xaml.cs and MainWindow.xaml.cs:
App.OnStartup()
→ Single-instance mutex check
→ Early language restore (regex parse of PadForge.xml before full load)
→ 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
→ Async Raw Input enumeration (keyboard/mouse on background thread)
The custom title bar was replaced with an app branding bar. A styled bar at the top of the window that uses ExtendViewIntoTitleBar to blend the hamburger menu button and application icon into the window chrome. The branding bar background is pixel-sampled from the current theme to stay consistent across light/dark mode switches.
Two classes with distinct roles:
A static data container shared between engine and UI threads:
-
UserDevices/UserSettingscollections (SyncRootlocking) -
Slot arrays:
SlotCreated[],SlotEnabled[] -
Profile data:
Profiles,ActiveProfileId,EnableAutoProfileSwitching -
Per-type limits:
MaxXbox360Slots,MaxDS4Slots,MaxExtendedSlots, etc. -
Helpers:
CreateDefaultPadSetting(),SwapSlots(),FindSlotForDevice()
Partial class split across:
-
Common/SettingsManager.cs. Profiles, slot arrays, helpers -
InputManager.Step1.UpdateDevices.cs.UserDevices/UserSettingsdeclarations and collection classes
Has no knowledge of XML, ViewModels, or UI.
The persistence and sync layer:
-
Loads
PadForge.xml(orSettings.xmlfallback) viaXmlSerializerintoSettingsManager -
Saves
SettingsManagerstate to XML (manual + auto-save timer) - Syncs SettingsManager data bidirectionally with ViewModels
- Manages profiles: save-as, load, delete, default snapshot
-
Tracks dirty state:
IsDirtyflag,AutoSavedevent
The engine thread reads SettingsManager without referencing the WPF-dependent SettingsService.
InputManager is a partial class split across 8 files for pipeline stage isolation. Each file owns one stage's fields, helpers, and state. This avoids a 5000+ line monolith while keeping stages in a single class (they share per-slot arrays and virtual controller references).
| File | Stage | Responsibility |
|---|---|---|
InputManager.cs |
Core | Fields, constants, Start()/Stop(), PollingLoop(), IDisposable, motion snapshots, DSU broadcast |
InputManager.Step1.UpdateDevices.cs |
Step 1 | SDL device enumeration, open/close, HIDMaestro filtering, UserDevices/UserSettings collection classes |
InputManager.Step2.UpdateInputStates.cs |
Step 2 | Read CustomInputState per device, apply FFB from VibrationStates[] + audio bass |
InputManager.Step3.UpdateOutputStates.cs |
Step 3 | Map 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, submit CombinedOutputStates[] to HIDMaestro/vJoy/MIDI/KBM, XInput slot detection |
InputManager.Step6.RetrieveOutputStates.cs |
Step 6 | Copy CombinedOutputStates[] → RetrievedOutputStates[] for UI |
SettingsManager is also a partial class. Its collection types are declared alongside the Step 1 code that populates them.
Up to seven threads:
_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:
-
Tier 1. HR Waitable Timer:
CreateWaitableTimerExWwithCREATE_WAITABLE_TIMER_HIGH_RESOLUTION(Windows 10 1803+). Sub-ms kernel sleep, near-zero CPU. Sleepsremaining - 0.1ms, leaving the tail for spin-wait. -
Tier 2. Multimedia Timer:
timeSetEvent+ManualResetEvent.WaitOne(x360ce-style fallback). Periodic 1ms callback signals the event. - Tier 3. Thread.Sleep(1): Legacy fallback when remaining > 1.5ms and both timers unavailable.
-
Final spin-wait:
Thread.SpinWait(1)loop againstStopwatch.ElapsedTicksfor sub-ms cycle boundary.
Drift compensation: Each cycle accumulates expectedTicks += targetTicks and compares against wallClock.ElapsedTicks. Late cycles shorten; early cycles lengthen. Drift exceeding 10 cycles (e.g., after system sleep) resets the wall clock.
timeBeginPeriod(1) is set for the polling loop's lifetime. Auto-idle (~20 Hz via Thread.Sleep(50)) activates when no slots are created. Timing target recalculates each cycle from the adjustable PollingIntervalMs (default 1ms, configurable in Settings).
InputService runs a DispatcherTimer at ~33ms interval:
- Reads
RetrievedOutputStates[]/RetrievedKbmRawStates[]for dashboard/schematic display - Reads
UserDevice.InputStatefor Devices page live visualization (only when visible) - Pushes macro snapshots from PadViewModels to
MacroSnapshots[] - Updates
SlotControllerTypes[],SlotExtendedConfigs[],SlotExtendedIsCustom[],_midiConfigs[] - Syncs device list from
UserDevices→DevicesViewModel(viaDeviceService) - 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.
AudioBassDetector uses NAudio's WasapiCapture in loopback mode. NAudio's internal capture thread delivers audio buffers via DataAvailable. The callback runs an 8th-order cascaded IIR low-pass filter (configurable cutoff, default 80 Hz) and updates a volatile _bassEnergy float (0.0–1.0). The engine reads this in Step 2 via AudioBassDetector.BassEnergy and merges it with game rumble via max().
Implements IMMNotificationClient to restart capture on default audio device change.
public static class RawInputListenerA background thread creates a message-only window (HWND_MESSAGE) and runs a GetMessageW pump. Registered for:
-
HID_USAGE_GENERIC_KEYBOARDwithRIDEV_INPUTSINK(background capture) -
HID_USAGE_GENERIC_MOUSEwithRIDEV_INPUTSINK
Per-device state tracked via RAWINPUT.header.hDevice in concurrent dictionaries. The engine reads state in Step 2 via GetKeyboardState() / ConsumeMouseDelta().
Async enumeration: Keyboard and mouse device discovery runs on a Task.Run background thread during startup, preventing slow HID enumeration from blocking the UI thread. Results merge into the device list when the task completes.
DsuMotionServer uses UdpClient.ReceiveAsync() on a background task for DSU client subscriptions (Cemu, Dolphin). Motion data is broadcast by the engine thread after Step 2 (no separate send thread). DSU protocol limited to 4 slots; slots 4–15 skip broadcast.
_hookThread = new Thread(() => HookThreadProc(ready))
{
Name = "InputHookManager",
IsBackground = true
};Created only when "Consume mapped inputs" is enabled. Installs WH_KEYBOARD_LL / WH_MOUSE_LL hooks and runs a GetMessageW pump. Suppression sets update via volatile reference swap from the UI thread. Stopped when the engine stops or hiding is disabled.
_acceptThread = new Thread(AcceptLoop)
{
Name = "WebControllerServer",
IsBackground = true
};Created when the web controller server is enabled. Runs an HttpListener accept loop. WebSocket connections spawn async tasks per client. Each browser client creates a WebControllerDevice (implements ISdlInputDevice) visible in Step 1 enumeration. Also serves static assets and /api/layout JSON.
| Shared State | Writer | Reader | Sync |
|---|---|---|---|
UserDevices.Items |
Engine (Step 1) | UI, Engine |
SyncRoot lock |
UserSettings.Items |
UI thread | Engine (Steps 2–5) |
SyncRoot lock |
UserDevice.InputState |
Engine (Step 2) | UI timer | Atomic ref swap |
CombinedOutputStates[] |
Engine (Step 4) | Engine (Steps 5, 6) | Single-thread write |
RetrievedOutputStates[] |
Engine (Step 6) | UI timer | Value copy (struct) |
VibrationStates[] |
HIDMaestro callback | Engine (Step 2) | Volatile fields |
MacroSnapshots[] |
UI timer | Engine (Step 4b) | Atomic ref swap |
SlotControllerTypes[] |
UI timer | Engine (Step 5) | Volatile read |
MotionSnapshots[] |
Engine (loop) | DSU broadcast | Same thread |
RawInputListener state |
Raw Input thread | Engine (Step 2) | ConcurrentDictionary |
InputHookManager sets |
UI thread | Hook callbacks | Volatile ref swap |
WebControllerDevice.InputState |
WebSocket task | Engine (Step 2) | Atomic ref swap |
AudioBassDetector._bassEnergy |
WASAPI callback | Engine (Step 2) | Volatile float |
Physical controller to game:
Physical Controller (USB/Bluetooth)
│
â–¼
SDL3.dll (HID / XInput / HIDAPI backend)
│
â–¼
Step 1: UpdateDevices()
│ SDL_GetJoysticks() → open new devices → SdlDeviceWrapper
│ Filter HIDMaestro virtual controllers (VID/PID + device path)
│ 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 per-mapping deadzones (activation threshold per mapping row)
│ Apply global deadzones, sensitivity curves, inversion, range clamping
│ CustomInputState → per-device OutputState (Gamepad struct)
│
â–¼
Step 4: CombineOutputStates()
│ For each slot (0–15):
│ Merge all device OutputStates mapped to this slot
│ Axes: max absolute value wins
│ Buttons: OR (any device pressing = pressed)
│ Also produces ExtendedRawState, MidiRawState, KbmRawState
│
â–¼
Step 4b: EvaluateMacros()
│ For each slot with macros:
│ Check triggers against CombinedOutputStates
│ Execute actions: button overrides, axis overrides, volume OSD, toggle
│ Modify CombinedOutputStates in-place
│
â–¼
Step 5: UpdateVirtualDevices()
│ For each created slot:
│ Create/destroy IVirtualController if type changed or slot toggled
│ Submit CombinedOutputStates[i] → virtual controller
│ Xbox 360: HIDMaestro → XInput slot
│ DS4: HIDMaestro → 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 30 Hz)
│
â–¼
Game reads virtual controller via XInput / DirectInput / SDL / raw HID
Game sends rumble via XInput / DirectInput FFB
│
â–¼
HIDMaestro FeedbackReceived callback (HIDMaestro 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
| Package | Version | Purpose |
|---|---|---|
| ModernWpfUI | 0.9.6 | Fluent Design theme (Windows 11-style UI) |
| HelixToolkit.Core.Wpf | 2.27.3 | 3D viewport for controller model visualization |
| CommunityToolkit.Mvvm | 8.2.2 | MVVM: ObservableObject, RelayCommand, source generators |
| Nefarius.HIDMaestro | 1.21.256 | HIDMaestro client for virtual Xbox 360 and DS4 |
| NAudio.Wasapi | 2.2.1 | WASAPI loopback capture for bass-driven rumble |
| Microsoft.Windows.Devices.Midi2 | 1.0.16-rc.3.7 | Windows MIDI Services SDK for virtual MIDI devices |
Native libraries loaded via direct P/Invoke (no NuGet):
| Library | DllImport | Notes |
|---|---|---|
SDL3.dll |
SDL3Minimal.cs |
Custom fork with Switch 2 Pro support, in Resources/SDL3/x64/
|
vJoyInterface.dll |
ExtendedVirtualController.cs |
NativeLibrary.TryLoad fallback from C:\Program Files\vJoy\
|
xinput1_4.dll |
InputManager.Step5 |
XInput slot mask detection only. Never reads input |
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 ReleaseKey publish properties (PadForge.App.csproj):
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>Output: PadForge.App/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/PadForge.exe
SDL3.dll and libusb-1.0.dll are copied to output via <Content> items with CopyToOutputDirectory=PreserveNewest and Link="filename" (flattened to root). They are not embedded in the single-file exe. They must sit adjacent to PadForge.exe.
UseWindowsForms=true is set in the csproj. Required for System.Windows.Forms.NotifyIcon (system tray). WinForms implicit usings are removed to avoid WPF type ambiguities.
Embedded resources (extracted at install/runtime):
| Resource | Content |
|---|---|
HIDMaestro_1.22.0_x64_x86_arm64.exe |
HIDMaestro 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 sprites (included as <Resource>, not <EmbeddedResource>) |
10 languages via .resx files in Resources/Strings/:
| File | Language |
|---|---|
Strings.resx |
English (base/fallback) |
Strings.de.resx |
German |
Strings.es.resx |
Spanish |
Strings.fr.resx |
French |
Strings.it.resx |
Italian |
Strings.ja.resx |
Japanese |
Strings.ko.resx |
Korean |
Strings.nl.resx |
Dutch |
Strings.pt-BR.resx |
Brazilian Portuguese |
Strings.zh-Hans.resx |
Simplified Chinese |
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.Instanceis 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): SetsCurrentUICulture+DefaultThreadCurrentUICulture, raisesPropertyChangedfor all properties (refreshing bindings), then raisesCultureChangedfor ViewModel-side refresh.
{Binding PropName, Source={x:Static strings:Strings.Instance}}WPF updates all bound text when ChangeCulture() fires PropertyChanged.
ViewModelBase subscribes to Strings.CultureChanged and exposes virtual OnCultureChanged(). Derived VMs override it to refresh computed strings. Weak references mean VMs need not unsubscribe.
Strings.ChangeCulture() applies immediately. No restart needed:
-
CurrentUICultureupdated. -
PropertyChangedfires for every resource property, refreshing all XAML bindings. -
CultureChangedfires, letting VMs recompute culture-dependent strings.
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.
| Class | Location | Lifetime | Role |
|---|---|---|---|
SettingsManager |
Common/SettingsManager.cs + partial in Step1 |
Static | Shared state: slot arrays, UserDevices/UserSettings, profiles |
RawInputListener |
Engine/Common/RawInputListener.cs |
Static | Per-device keyboard/mouse input via hidden window |
RumbleLogger |
Engine/Common/RumbleLogger.cs |
Static | Diagnostic logger (disabled by default) |
InputManager |
Common/Input/InputManager.cs |
Singleton | Polling loop, 6-step pipeline, virtual controller lifecycle |
InputService |
Services/InputService.cs |
Singleton | UI–engine bridge, 30Hz timer, macro recording, DSU/web lifecycle |
SettingsService |
Services/SettingsService.cs |
Singleton | XML persistence, auto-save, profile CRUD, ViewModel sync |
DeviceService |
Services/DeviceService.cs |
Singleton | Device list sync, HidHide whitelist management |
Up to 16 virtual controller slots (MaxPads = 16). Per-type limits:
| Type | Limit | Constant |
|---|---|---|
| Xbox 360 | 16 | MaxXbox360Slots = MaxPads |
| DualShock 4 | 16 | MaxDS4Slots = MaxPads |
| vJoy | 16 |
MaxExtendedSlots = 16 (vJoy driver limit) |
| MIDI | 16 | MaxMidiSlots = MaxPads |
| Keyboard+Mouse | 16 | MaxKeyboardMouseSlots = MaxPads |
The "Add Controller" button hides when all 16 slots are in use.
XInput visibility: The XInput API addresses only slots 0–3. Xbox 360 VCs beyond slot 3 still work but may be invisible to XInput-only games. DirectInput, SDL, and raw HID see all 16. DS4 and vJoy are unaffected.
Per-slot state arrays in SettingsManager:
public static bool[] SlotCreated; // Slot exists in config
public static bool[] SlotEnabled; // Slot is active (user toggle)Per-slot state arrays in InputManager:
public Gamepad[] CombinedOutputStates; // Step 4 output
public ExtendedRawState[] CombinedExtendedRawStates; // Step 4 output (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; // HIDMaestro feedback
public MotionSnapshot[] MotionSnapshots; // DSU motion data
public MacroItem[][] MacroSnapshots; // Macro definitions
public VirtualControllerType[] SlotControllerTypes; // Configured type
internal ExtendedVirtualController.ExtendedDeviceConfig[] SlotExtendedConfigs; // vJoy HID config
internal bool[] SlotExtendedIsCustom; // Custom vs gamepad preset
internal MidiSlotConfig[] _midiConfigs; // Per-slot MIDI configuration- 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,SettingsServicedata flow -
Virtual Controllers:
IVirtualControllerimplementations (Xbox 360, DS4, vJoy, MIDI, KBM) - SDL3 Integration: SDL3 P/Invoke, device enumeration, state reading, haptic
- Build and Publish: Build commands, publish configuration, CI/CD