-
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 8 solution:
| Project | Target | Role |
|---|---|---|
| PadForge.App |
net8.0-windows (WPF, WinExe) |
Main application: UI, input pipeline, virtual controllers, services |
| PadForge.Engine |
net8.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
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)
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/
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 hybrid sleep/spin-wait loop:
-
>1.5ms remaining:
Thread.Sleep(1)— real sleep, near-zero CPU -
<=1.5ms remaining:
Thread.SpinWait(1)— precise busy-wait via CPU PAUSE instructions
timeBeginPeriod(1) is set for the lifetime of the polling loop to support multimedia timer resolution across SDL, ViGEm, and the UI dispatcher.
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/net8.0-windows/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 three output types share this global limit (MaxXbox360Slots = MaxDS4Slots = MaxVJoySlots = 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 Gamepad[] RetrievedOutputStates; // Step 6 output (UI display)
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