-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture Overview
This page describes the high-level architecture of the PadForge codebase: solution structure, project layout, design philosophy, threading model, dependencies, and build system.
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.
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
Properties/
AssemblyInfo.cs # Version, metadata (GenerateAssemblyInfo=false)
Common/
ControllerIcons.cs # SVG path data for controller type icons (Xbox, DS4, vJoy)
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 # XML serialization, slot management, PadSetting defaults, profile CRUD
StartupHelper.cs # Run-at-startup registry helper (HKCU\...\Run)
VirtualKey.cs # Windows VK code → display name lookup table
Input/
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 → Gamepad)
InputManager.Step4.CombineOutputStates.cs # Multi-device merge per slot
InputManager.Step4b.EvaluateMacros.cs # Macro trigger/action state machine
InputManager.Step5.VirtualDevices.cs # ViGEm + vJoy 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)
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 with INotifyPropertyChanged (CommunityToolkit.Mvvm)
MainViewModel.cs # Shell VM: navigation, sidebar items, profile list
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
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)
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/
RangeSlider.cs # Custom dual-thumb range slider control (dead zone min/max)
Converter/
AxisToPercentConverter.cs # short → "N%" display string
BoolToColorConverter.cs # bool → SolidColorBrush
BoolToInstallTextConverter.cs # bool → "Install" / "Uninstall" text
BoolToOpacityConverter.cs # bool → 1.0 / 0.35 opacity
BoolToVisibilityConverter.cs # bool → Visible / Collapsed
NormToCanvasConverter.cs # Normalized float → Canvas pixel coordinate
NormToTriggerHeightConverter.cs # Trigger value → bar height
NormToTriggerSlideConverter.cs # Trigger value → slide offset
NullToCollapsedConverter.cs # null → Collapsed, non-null → Visible
PercentToSizeConverter.cs # Percentage → pixel size
PovToAngleConverter.cs # POV centidegrees → rotation angle
StatusToColorConverter.cs # Online/offline status → color
StringToGeometryConverter.cs # SVG path string → WPF Geometry
StringToVisibilityConverter.cs # Non-empty string → Visible, empty → Collapsed
Resources/
ControllerIcons.xaml # XAML resource dictionary with controller icon geometries
PadForge.ico # Application icon
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/
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, 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/
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/
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
capture_screenshots.ps1 # Automated screenshot capture script
cleanup_vjoy.ps1 # vJoy device node cleanup utility
dump_ui_tree.ps1 # WPF visual tree dump for debugging
overlay_positions.py # 2D controller overlay coordinate generator
All physical controller input (including Xbox/XInput controllers) flows through SDL3. There is no separate XInput code path for reading controller state. The native XInput P/Invoke in Step 5 (xinput1_4.dll) is used exclusively for detecting which XInput slot a newly created ViGEm virtual controller occupies — it never reads input.
Key SDL3 configuration:
SDL_SetHint(SDL_HINT_JOYSTICK_XINPUT, "1"); // Enables Xbox enumeration
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); // Background input
// NEVER set SDL_HINT_JOYSTICK_RAWINPUT -- conflicts with XInput enumerationViewModels use ObservableObject from CommunityToolkit.Mvvm as their base class, providing [ObservableProperty] and [RelayCommand] source generators. The ViewModelBase class in the project extends this base.
ViewModels expose commands and raise events. They do not directly call services or touch the input pipeline. Instead:
- VMs raise events (e.g.,
MappingChanged,SlotAdded) -
MainWindow.xaml.csor service classes subscribe to these events - Event handlers call into
InputService,SettingsService, etc.
This keeps VMs testable and decoupled from the engine thread.
Dependencies are wired manually as singletons in App.xaml.cs and MainWindow.xaml.cs. The typical lifecycle:
App.OnStartup()
-> new MainWindow()
-> Creates InputService, SettingsService, DeviceService
-> Creates ViewModels with injected services
-> Starts InputManager polling loop
PadForge uses up to five distinct 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:
-
Tier 1: HR Waitable Timer (
CreateWaitableTimerExW, Windows 10 1803+) — sub-ms kernel sleep, near-zero CPU -
Tier 2: Multimedia Timer (
timeSetEvent+ManualResetEvent.WaitOne) — x360ce-style fallback - Tier 3: Thread.Sleep(1) — legacy fallback when both timers fail
-
Final spin-wait:
Thread.SpinWait(1)for precise sub-ms cycle boundary
Wall-clock drift compensation ensures the long-term average matches the target Hz exactly. timeBeginPeriod(1) is set for the lifetime of the polling loop. Auto-idle mode (~20 Hz) activates when no virtual controller slots are created.
Timing target is recalculated each cycle from the runtime-adjustable PollingIntervalMs property (default: 1ms).
InputService creates a DispatcherTimer at ~33ms interval that:
- Reads
RetrievedOutputStates[]from the engine for dashboard/schematic display - Reads
UserDevice.InputStatefor the Devices page live visualization - Pushes macro snapshots to
MacroSnapshots[]for engine consumption - Updates
SlotControllerTypes[],SlotVJoyConfigs[],SlotVJoyIsCustom[]
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.
public static class RawInputListenerA dedicated background thread creates a message-only window (CreateWindowExW with HWND_MESSAGE parent) and runs a GetMessageW pump. Registered for:
-
HID_USAGE_GENERIC_KEYBOARDwithRIDEV_INPUTSINK(background capture) -
HID_USAGE_GENERIC_MOUSEwithRIDEV_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().
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.
_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.
_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.
| 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 |
| 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 |
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\.
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 ReleaseKey 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. They are not embedded in the single-file exe — they must be adjacent to PadForge.exe at runtime.
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 -
2DModels/**/*.png— 2D controller sprite assets (included as<Resource>, not<EmbeddedResource>)
| Class | Location | Lifetime | Role |
|---|---|---|---|
SettingsManager |
Common/SettingsManager.cs + partial in Step1 |
Static | XML persistence, slot arrays, UserDevices/UserSettings collections |
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 snapshot push |
SettingsService |
Services/SettingsService.cs |
Singleton instance | Settings load/save, PadSetting application |
PadForge supports up to 16 virtual controller slots (MaxPads = 16). All four output types share this global limit (MaxXbox360Slots = MaxDS4Slots = MaxVJoySlots = MaxMidiSlots = 16). There is no per-type cap.
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