-
Notifications
You must be signed in to change notification settings - Fork 6
Driver Installation Internals
PadForge manages installation, detection, and uninstallation of three drivers and one service: ViGEmBus (virtual Xbox 360/DS4 controllers), HidHide (device hiding), vJoy (virtual joystick), and Windows MIDI Services (virtual MIDI output). All installation logic lives in a single static class with embedded installer resources.
File: PadForge.App/Common/DriverInstaller.cs
Namespace: PadForge.Common
public static class DriverInstaller| Resource Name | Type | Size | Purpose |
|---|---|---|---|
ViGEmBus_1.22.0_x64_x86_arm64.exe |
EXE (WiX bootstrapper) | ~5 MB | ViGEmBus installer with bundled MSI |
HidHide_1.5.230_x64.exe |
EXE (WiX bootstrapper) | ~3 MB | HidHide installer with bundled MSI |
vJoyDriver.zip |
ZIP | ~200 KB | 5 files: vjoy.sys, hidkmdf.sys, vjoy.inf, vjoy.cat, vJoyInterface.dll |
Declared in PadForge.App.csproj:
<EmbeddedResource Include="Resources\ViGEmBus_1.22.0_x64_x86_arm64.exe" />
<EmbeddedResource Include="Resources\HidHide_1.5.230_x64.exe" />
<EmbeddedResource Include="Resources\vJoyDriver.zip" />public static void InstallViGEmBus()- Extracts embedded
ViGEmBus_1.22.0_x64_x86_arm64.exeto%TEMP%\PadForge_ViGEmBus\. - Runs the bootstrapper with
/extractto unpack the MSI and supporting files. - Finds the MSI:
ViGEmBus.x64.msion 64-bit OS,ViGEmBus.msifallback, thenViGEmBus*.msiglob. - Runs
msiexec /i "{msiPath}" /qb /norestartwith UAC elevation (Verb = "runas"). - Cleans up temp directory.
public static void UninstallViGEmBus()Same extraction flow, then runs msiexec /x "{msiPath}" /qb /norestart with elevation.
public static string GetViGEmVersion()Scans HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall (both Registry64 and Registry32 views) for entries containing "ViGEm" in DisplayName. Returns DisplayVersion or null.
public static void InstallHidHide()Same pattern as ViGEmBus: extract bootstrapper, extract MSI (HidHide.msi or HidHide*.msi), run msiexec /i with elevation.
public static void UninstallHidHide()Extract and run msiexec /x with elevation.
public static bool IsHidHideInstalled()
public static string GetHidHideVersion()Scans registry Uninstall keys for entries containing "HidHide" or "HID Hide" in DisplayName. TryGetHidHideMsiInfo() also extracts the product code GUID from the subkey name for uninstall operations.
private static bool TryGetHidHideMsiInfo(out string displayVersion, out string productCode)vJoy installation is significantly more complex than ViGEmBus/HidHide because it bypasses the Inno Setup installer entirely and uses direct driver store + SetupAPI device creation.
public static void InstallVJoy()Everything runs in a single elevated PowerShell script (one UAC prompt):
Critical: Device nodes must be removed FIRST so the driver can unload. Reversing this order causes the service to get stuck in STOP_PENDING (irrecoverable without reboot).
1. pnputil /remove-device "ROOT\HIDCLASS\NNNN" /subtree (for N = 0000..0015)
2. Sleep 2 seconds
3. sc.exe stop vjoy
4. Sleep 2 seconds
5. pnputil /delete-driver {oemInf} /uninstall /force (for each stale OEM inf)
6. sc.exe delete vjoy
7. Remove service registry keys from ALL ControlSets
8. Delete install directory and stale driver binary
Extracts vJoyDriver.zip to C:\Program Files\vJoy\.
pnputil /add-driver vjoy.inf /install
The PowerShell script uses Add-Type to define inline C# P/Invoke declarations for SetupAPI:
// SetupAPI P/Invoke declarations (embedded in PowerShell script)
public static class PF_SetupApi
{
public const int DIF_REGISTERDEVICE = 0x19;
public const int SPDRP_HARDWAREID = 0x01;
public const int DICD_GENERATE_ID = 0x01;
[StructLayout(LayoutKind.Sequential)]
public struct SP_DEVINFO_DATA
{
public int cbSize;
public Guid ClassGuid;
public int DevInst;
public IntPtr Reserved;
}
// P/Invoke: setupapi.dll
SetupDiCreateDeviceInfoList(ref Guid ClassGuid, IntPtr hwndParent) -> IntPtr
SetupDiCreateDeviceInfoW(IntPtr DeviceInfoSet, string DeviceName, ...) -> bool
SetupDiSetDeviceRegistryPropertyW(IntPtr DeviceInfoSet, ...) -> bool
SetupDiCallClassInstaller(int InstallFunction, ...) -> bool
SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet) -> bool
// P/Invoke: newdev.dll
UpdateDriverForPlugAndPlayDevicesW(IntPtr hwndParent, string HardwareId,
string FullInfPath, int InstallFlags, out bool bRebootRequired) -> bool
}Device creation sequence:
-
SetupDiCreateDeviceInfoList— Create device info set for HIDClass GUID ({745a17a0-74d3-11d0-b6fe-00a0c90f57da}). -
SetupDiCreateDeviceInfoW("HIDClass", ...)— Critical:DeviceNameparameter must be the class name"HIDClass", NOT the hardware ID. Backslashes inDeviceNamecauseERROR_INVALID_DEVINST_NAME(0xE0000205). -
SetupDiSetDeviceRegistryPropertyW(SPDRP_HARDWAREID, ...)— Set hardware ID:root\VID_1234&PID_BEAD&REV_0222(Unicode double-null terminated). -
SetupDiCallClassInstaller(DIF_REGISTERDEVICE)— Register device. -
UpdateDriverForPlugAndPlayDevicesW— Install driver on the new device node.
This creates a ROOT\HIDCLASS\0000 device node. PnP matches the OEM .inf and loads vjoy.sys.
public static void UninstallVJoy()Runs an elevated batch script (.cmd):
-
Remove device nodes FIRST (for N = 0000..0015):
pnputil /remove-device "ROOT\HIDCLASS\NNNN" /subtree - Wait 2 seconds for driver to unload.
- Stop service:
sc stop vjoy - Wait 2 seconds.
- Delete service:
sc delete vjoy -
Fallback: If
sc deletefailed (e.g.,STOP_PENDING), directly remove service registry keys from ALL ControlSets viareg delete. - Remove driver from store:
pnputil /delete-driver {oemInf} /uninstall /force - Delete install directory (
C:\Program Files\vJoy\) and stale driver binary (%SystemRoot%\System32\drivers\vjoy.sys). - Remove legacy Inno Setup uninstall registry entries via PowerShell one-liner.
- Call
CleanVJoyRegistryArtifacts()for final registry cleanup.
private static void CleanVJoyRegistryArtifacts()Removes registry keys that can cause the vJoy installer to hang on reinstall:
-
HKLM\SYSTEM\{ControlSet}\Services\vjoy(CurrentControlSet, ControlSet001, ControlSet002, ControlSet003) HKLM\SYSTEM\CurrentControlSet\Control\MediaProperties\...\OEM\VID_1234&PID_BEADHKLM\SYSTEM\ControlSet001\Services\EventLog\System\vjoyHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\...\hidkmdf.sysHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\...\vjoy.sys- Same paths under
WOW6432Node HKCU\...\OEM\VID_1234&PID_BEAD
private static void CleanVJoyDeviceClassEntries()Removes vJoy device entries from the shared HID device class key:
HKLM\SYSTEM\ControlSet001\Control\Class\{781ef630-72b2-11d2-b852-00c04fad5101}
Only deletes subkeys where the "Class" registry value equals "vjoy" (case-insensitive).
public static bool IsVJoyInstalled()
public static string GetVJoyVersion()Two detection paths:
-
Primary: Check for
vjoy.sysinC:\Program Files\vJoy\. If present, readFileVersionInfo.GetVersionInfo(). -
Fallback: Scan registry Uninstall keys for "vJoy" in
DisplayName(detects legacy Inno Setup installs).
private static string GetVJoyVersionFromRegistry()Scans both Registry64 and Registry32 views of HKLM\...\Uninstall.
private static string[] FindVJoyOemInfs()Runs pnputil /enum-drivers and parses the output to find OEM .inf files belonging to vJoy. Identifies vJoy entries by checking for "shaul" or "vjoy" in the driver block text. Handles varying output formats across Windows versions. Returns array of oem*.inf names.
Unlike the other drivers, Windows MIDI Services is NOT embedded — the installer is ~210 MB and must be downloaded from GitHub at install time.
public static async Task InstallMidiServicesAsync()- Creates temp directory
%TEMP%\PadForge_MidiServices\. - Queries the GitHub API (
https://api.github.com/repos/microsoft/MIDI/releases) for the latest release. Note:/releases/latestreturns 404 becausemicrosoft/MIDIonly publishes pre-releases — uses the full/releasesendpoint and parses the first entry. - Finds the SDK Runtime x64 installer asset (pattern:
Windows.MIDI.Services.SDK.Runtime.and.Tools.*-x64.exe). - Downloads the installer (~210 MB) with a 10-minute timeout.
- Runs the WiX Burn bootstrapper with
/install /quiet /norestart. Since PadForge is already elevated, runs directly (norunas) to avoidWin32Exceptionon some systems. Waits up to 5 minutes. - Calls
MidiVirtualController.ResetAvailability()so the cached availability check re-evaluates. - Cleans up temp directory.
private static async Task<string> FindMidiServicesDownloadUrl(HttpClient http)Parses the GitHub releases JSON to find the browser_download_url for the SDK Runtime x64 .exe asset. Uses simple string search (no JSON library dependency). Throws InvalidOperationException if no matching asset is found.
public static void UninstallMidiServices()Finds the MIDI Services uninstall string from the registry (FindMidiServicesUninstallString()), parses the quoted executable path and any existing arguments, appends /quiet /norestart, and launches the uninstaller. The uninstaller is launched fire-and-forget (waits up to 5 minutes) because the MIDI Services SDK DLLs are loaded in-process -- waiting for the uninstaller to finish would cause a native crash when the backing service is removed mid-session.
public static bool IsMidiServicesInstalled()Delegates to MidiVirtualController.IsAvailable(), which initializes the Windows MIDI Services SDK and checks if the service is running. Result is cached after first check.
The Settings page UI disables uninstall buttons when the corresponding driver/service is actively in use. This prevents removing a driver while virtual controllers depend on it.
| Driver | Guard Condition | Delegate |
|---|---|---|
| ViGEmBus | Any created slot uses Xbox 360 or DS4 | HasAnyViGEmSlots |
| vJoy | Any created slot uses vJoy | HasAnyVJoySlots |
| MIDI Services | Any created slot uses MIDI | HasAnyMidiSlots |
| HidHide | Any device has HidHide hiding enabled | HasAnyHidHideDevices |
Guards are implemented as Func<bool> delegates on SettingsViewModel, injected by MainWindow.xaml.cs. RefreshDriverGuards() re-evaluates all uninstall command CanExecute states after slot creation, deletion, or type changes.
private static string ExtractEmbeddedResource(string resourceFileName, string tempDir)Extracts an embedded resource to a temp directory by searching manifest resource names with IndexOf (case-insensitive). Returns the full path to the extracted file. Throws FileNotFoundException if the resource is not found.
private static string ExtractInstallerBundle(string exePath, string tempDir)Runs the WiX bootstrapper with /extract "{extractDir}" to unpack the MSI and supporting files. Uses UseShellExecute = false and CreateNoWindow = true. Waits up to 60 seconds.
private static string FindMsi(string extractDir, string primaryName, string fallbackPattern)Searches the extracted bundle for an MSI file. Tries exact name first (Directory.GetFiles(extractDir, primaryName, SearchOption.AllDirectories)), then glob pattern. Throws FileNotFoundException if not found.
private static void RunElevated(string fileName, string arguments)Starts a process with Verb = "runas" and UseShellExecute = true (triggers UAC prompt). WindowStyle = Hidden. Waits up to 180 seconds (3 minutes).
private static void RunMsiElevated(string arguments)Convenience wrapper that calls RunElevated("msiexec.exe", arguments).
private static void CleanupTempDir(string tempDir)Deletes temp directory recursively, ignoring all exceptions.
Critical design constraint: vjoy.sys reads ALL DeviceNN registry keys from EVERY device node and concatenates them into one HID report descriptor. With N device nodes and N registry keys, each node creates N HID collections, resulting in N^2 total controllers visible in joy.cpl.
Example: 2 device nodes + 2 registry keys = each node creates 2 collections = 4 controllers (but only 2 are real). 1 device node + 2 registry keys = 1 node creates 2 collections = 2 controllers (correct).
Solution: Always exactly 1 ROOT\HIDCLASS device node. The number of virtual joysticks is controlled by DeviceNN registry keys (Device01..Device{N}), not by the number of device nodes. When scaling up/down: update registry keys, then restart the node via pnputil disable/enable.
This architecture is enforced by VJoyVirtualController.EnsureDevicesAvailable() (in PadForge.App/Common/Input/VJoyVirtualController.cs), not by DriverInstaller itself.
While DriverInstaller handles the initial driver install/uninstall, runtime device management is handled by VJoyVirtualController:
-
EnsureDevicesAvailable()— Ensures exactly 1 device node exists. Writes N HID report descriptors to registry. If descriptor count changes, restarts node so driver re-reads descriptors. -
CreateVJoyDevice()— Creates device node via SetupAPI if none exists. -
DeleteVJoyDevice()— Removes device node viapnputil /remove-device. -
WriteDeviceDescriptors()— WritesHidReportDescriptor(REG_BINARY) andHidReportDescriptorSize(REG_DWORD) toHKLM\..\vjoy\Parameters\Device{NN}. Only writes when descriptors differ from existing (avoids disturbing live devices). -
CountExistingDevices()— Usespnputil /enum-devices /class HIDClass(notGetVJDStatuswhich returns stale data).
File: PadForge.App/Common/HidHideController.cs
Namespace: PadForge.Common
While DriverInstaller handles HidHide install/uninstall, runtime device management (blacklisting, whitelisting, cloaking) is handled by HidHideController. This static class communicates directly with the HidHide control device (\\.\HidHide) via P/Invoke IOCTLs.
| IOCTL | Code | Direction | Purpose |
|---|---|---|---|
GET_WHITELIST |
0x80016000 |
Read | Get whitelisted application paths |
SET_WHITELIST |
0x80016004 |
Write | Replace whitelisted application paths |
GET_BLACKLIST |
0x80016008 |
Read | Get blacklisted device instance IDs |
SET_BLACKLIST |
0x8001600C |
Write | Replace blacklisted device instance IDs |
GET_ACTIVE |
0x80016010 |
Read | Get cloaking active state (1 byte) |
SET_ACTIVE |
0x80016014 |
Write | Enable/disable cloaking (1 byte) |
GET/SET list operations use the Multi-SZ format: null-separated UTF-16 strings with a double-null terminator. SET operations replace the entire list (not append).
static bool IsAvailable() // Can open \\.\HidHide
static List<string> GetBlacklist() // Device instance IDs
static void SetBlacklist(List<string> ids) // Replace entire blacklist
static void AddToBlacklist(string instanceId) // Add if not present
static void RemoveFromBlacklist(string id) // Remove if present
static List<string> GetWhitelist() // DOS device paths
static void SetWhitelist(List<string> paths) // Replace entire whitelist
static void EnsureWhitelisted(string appPath) // Add PadForge if not present
static bool GetActive() // Cloaking enabled?
static void SetActive(bool active) // Enable/disable cloaking
static void RemoveManagedDevices() // Remove only PadForge's entries
static string DevicePathToInstanceId(string p) // \\?\HID#... -> HID\VID_...\..._managedDeviceIds (internal HashSet<string>) tracks which device instance IDs PadForge has added to the blacklist. RemoveManagedDevices() removes only these entries, leaving entries added by other tools (e.g., the HidHide Configuration Client) untouched.
The whitelist requires paths in DOS device format (\Device\HarddiskVolumeN\path\to\app.exe), not regular paths (C:\path\to\app.exe). ToDosDevicePath() uses QueryDosDeviceW to convert drive letters to volume device names.
DevicePathToInstanceId() converts SDL/Windows device paths (\\?\HID#VID_054C&PID_0CE6#...{guid}) to PnP device instance IDs (HID\VID_054C&PID_0CE6\...) for the blacklist:
- Strip
\\?\prefix - Remove trailing GUID suffix (
{...}) - Replace
#with\
PadForge auto-elevates when vJoy is installed (handled in App.xaml.cs). This means:
-
ViGEmBus/HidHide install/uninstall: UAC prompt per operation (launched via
msiexecwithrunas). - vJoy install: Single elevated PowerShell script (one UAC prompt for the entire flow).
- vJoy runtime: No additional UAC prompts needed since the app is already elevated.
| Driver | Temp Directory |
|---|---|
| ViGEmBus | %TEMP%\PadForge_ViGEmBus\ |
| HidHide | %TEMP%\PadForge_HidHide\ |
| vJoy | %TEMP%\PadForge_vJoy\ |
| vJoy install script | %TEMP%\PadForge_vjoy_install.ps1 |
| vJoy install log | %TEMP%\PadForge_vjoy_install.log |
| vJoy uninstall script | %TEMP%\PadForge_vjoy_uninstall.cmd |
| vJoy uninstall log | %TEMP%\PadForge_vjoy_uninstall.log |
| MIDI Services | %TEMP%\PadForge_MidiServices\ |
All temp directories are cleaned up after each operation.