-
Notifications
You must be signed in to change notification settings - Fork 6
Driver Installation Internals
PadForge installs, detects, and uninstalls four drivers/services: ViGEmBus (virtual Xbox 360/DS4), HidHide (device hiding), vJoy (virtual joystick), and Windows MIDI Services (virtual MIDI output). All logic lives in one static class with embedded installer resources.
File: PadForge.App/Common/DriverInstaller.cs
Namespace: PadForge.Common
- Architecture Overview
- Embedded Resources
- Shared Helpers
- ViGEmBus
- HidHide
- vJoy
- Windows MIDI Services
- Uninstall Guards
- vJoy Single-Node Architecture
- vJoy Device Lifecycle (Runtime)
- HidHide Runtime API (HidHideController)
- Elevation Strategy
- Temp Directories
- Error Handling and Rollback
graph TD
subgraph DriverInstaller["DriverInstaller (static class)"]
direction TB
VG["ViGEmBus<br/>Embedded EXE + MSI"]
HH["HidHide<br/>Embedded EXE + MSI"]
VJ["vJoy<br/>Embedded ZIP + SetupAPI"]
MS["MIDI Services<br/>GitHub API download"]
end
VG -->|"/extract -> msiexec /i"| VIGEM_DRV["ViGEmBus Driver"]
HH -->|"/extract -> msiexec /i"| HH_DRV["HidHide Driver"]
VJ -->|"ZIP extract -> pnputil -> SetupAPI"| VJ_DRV["vjoy.sys"]
MS -->|"GitHub releases API -> download -> /install"| MS_SVC["MIDI Services"]
VG -.->|"runas UAC"| UAC1["UAC Prompt"]
HH -.->|"runas UAC"| UAC2["UAC Prompt"]
VJ -.->|"Single elevated PS1"| UAC3["UAC Prompt"]
MS -.->|"Already elevated"| NOUAC["No UAC Needed"]
| 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" />Private methods reused across all install/uninstall flows.
| Method | Signature | Behavior |
|---|---|---|
ExtractEmbeddedResource |
(string resourceFileName, string tempDir) |
Finds resource via case-insensitive IndexOf on GetManifestResourceNames(), streams to {tempDir}\{resourceFileName}. Throws FileNotFoundException (listing all resource names) if not found. |
ExtractInstallerBundle |
(string exePath, string tempDir) |
Runs WiX bootstrapper with /extract to unpack MSI into {tempDir}\Extracted\. Recreates directory if it exists. 60s timeout. |
FindMsi |
(string extractDir, string primaryName, string fallbackPattern) |
Searches recursively for MSI: exact name first, then glob fallback. Throws FileNotFoundException if neither matches. |
RunElevated |
(string fileName, string arguments) |
Launches process with Verb = "runas" (UAC prompt), hidden window, 180s timeout. Used by ViGEmBus, HidHide, and vJoy install. |
RunMsiElevated |
(string arguments) |
Wrapper: RunElevated("msiexec.exe", arguments). |
CleanupTempDir |
(string tempDir) |
Recursive delete, swallows all exceptions. Called in finally blocks. |
public static void InstallViGEmBus()flowchart TD
A[Extract embedded<br/>ViGEmBus_1.22.0_x64_x86_arm64.exe<br/>to %TEMP%\PadForge_ViGEmBus\] --> B
B[Run bootstrapper with /extract<br/>to unpack MSI] --> C
C{Is 64-bit OS?}
C -->|Yes| D1[Find ViGEmBus.x64.msi]
C -->|No| D2[Find ViGEmBus.msi]
D1 --> D3[Fallback: ViGEmBus*.msi glob]
D2 --> D3
D3 --> E["msiexec /i {msiPath} /qb /norestart<br/>(UAC elevation via runas)"]
E --> F[Cleanup temp directory]
Flow: Extract embedded bootstrapper to %TEMP%\PadForge_ViGEmBus\, run /extract to unpack MSI, find MSI (ViGEmBus.x64.msi on 64-bit, ViGEmBus.msi fallback, ViGEmBus*.msi glob), run msiexec /i with UAC elevation, clean up temp directory in finally.
public static void UninstallViGEmBus()Same extraction flow as install, then runs msiexec /x with elevation. Temp cleanup in finally.
public static string GetViGEmVersion()Scans HKLM\...\Uninstall (both Registry64 and Registry32 views) for "ViGEm" in DisplayName (case-insensitive). Returns DisplayVersion, or null if not found.
public static void InstallHidHide()Same pattern as ViGEmBus: extract embedded bootstrapper, extract MSI (HidHide.msi primary, HidHide*.msi fallback), run msiexec /i with elevation.
public static void UninstallHidHide()Same extraction flow, then msiexec /x with elevation.
public static bool IsHidHideInstalled()
public static string GetHidHideVersion()Both delegate to TryGetHidHideMsiInfo(), which scans Uninstall keys (both Registry64 and Registry32) for "HidHide" or "HID Hide" in DisplayName (case-insensitive). Returns DisplayVersion and subkey name as productCode (GUID). GetHidHideVersion() falls back to "Installed" when DisplayVersion is null/empty.
vJoy installation bypasses the Inno Setup installer entirely, using direct driver store + SetupAPI device creation.
public static void InstallVJoy()Runs everything in a single elevated PowerShell script (one UAC prompt).
flowchart TD
A[Extract vJoyDriver.zip<br/>to %TEMP%\PadForge_vJoy\] --> B
B[FindVJoyOemInfs<br/>detect stale OEM .inf files] --> C
C[Generate PadForge_vjoy_install.ps1] --> D
D["RunElevated powershell.exe<br/>-NoProfile -ExecutionPolicy Bypass<br/>(UAC prompt)"]
subgraph PS1["PowerShell Script Execution"]
direction TB
S1["Step 1: Pre-install Cleanup<br/>Remove device nodes 0000-0015<br/>Sleep 2s<br/>Stop vjoy service<br/>Sleep 2s<br/>Delete stale OEM infs<br/>Delete vjoy service<br/>Remove service registry keys<br/>Delete install dir + stale driver"] --> S2
S2["Step 2: Extract Driver Files<br/>Expand-Archive to<br/>C:\Program Files\vJoy\"] --> S3
S3["Step 3: Add Driver to Store<br/>pnputil /add-driver vjoy.inf /install"] --> S4
S4["Step 4: Create Device Node<br/>SetupAPI P/Invoke via Add-Type<br/>(see below)"]
end
D --> PS1
PS1 --> E[Delete script file]
E --> F[Cleanup temp directory]
Critical ordering: Device nodes MUST be removed first so the driver can unload. Stopping the service before removing nodes causes STOP_PENDING (irrecoverable without reboot).
The generated PowerShell script:
# 1. Remove all device nodes (0000..0015)
for ($i = 0; $i -le 15; $i++) {
pnputil /remove-device "ROOT\HIDCLASS\$($i.ToString('D4'))" /subtree 2>&1 | Out-Null
}
Start-Sleep -Seconds 2
# 2. Stop the vjoy service
sc.exe stop vjoy 2>&1 | Out-Null
Start-Sleep -Seconds 2
# 3. Delete stale OEM inf files (pre-enumerated by FindVJoyOemInfs)
pnputil /delete-driver oem42.inf /uninstall /force 2>&1 | Out-Null
# ... one line per stale OEM inf
# 4. Delete the vjoy service
sc.exe delete vjoy 2>&1 | Out-Null
# 5. Remove service registry keys from ALL ControlSets
foreach ($cs in @('CurrentControlSet','ControlSet001','ControlSet002','ControlSet003')) {
Remove-Item "HKLM:\SYSTEM\$cs\Services\vjoy" -Recurse -Force -ErrorAction SilentlyContinue
}
# 6. Delete install directory and stale driver binary
Remove-Item 'C:\Program Files\vJoy' -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$env:SystemRoot\System32\drivers\vjoy.sys" -Force -ErrorAction SilentlyContinueNew-Item -Path 'C:\Program Files\vJoy' -ItemType Directory -Force | Out-Null
Expand-Archive -Path '{zipPath}' -DestinationPath 'C:\Program Files\vJoy' -Forcepnputil /add-driver 'C:\Program Files\vJoy\vjoy.inf' /installThe PowerShell script uses Add-Type to define inline C# P/Invoke for SetupAPI and newdev.dll:
public static class PF_SetupApi
{
// Constants
public const int DIF_REGISTERDEVICE = 0x19;
public const int SPDRP_HARDWAREID = 0x01;
public const int DICD_GENERATE_ID = 0x01;
// Structure
[StructLayout(LayoutKind.Sequential)]
public struct SP_DEVINFO_DATA
{
public int cbSize;
public Guid ClassGuid;
public int DevInst;
public IntPtr Reserved;
}
// setupapi.dll P/Invoke
[DllImport("setupapi.dll", SetLastError = true)]
public static extern IntPtr SetupDiCreateDeviceInfoList(
ref Guid ClassGuid, IntPtr hwndParent);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool SetupDiCreateDeviceInfoW(
IntPtr DeviceInfoSet, string DeviceName, ref Guid ClassGuid,
string DeviceDescription, IntPtr hwndParent, int CreationFlags,
ref SP_DEVINFO_DATA DeviceInfoData);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool SetupDiSetDeviceRegistryPropertyW(
IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData,
int Property, byte[] PropertyBuffer, int PropertyBufferSize);
[DllImport("setupapi.dll", SetLastError = true)]
public static extern bool SetupDiCallClassInstaller(
int InstallFunction, IntPtr DeviceInfoSet,
ref SP_DEVINFO_DATA DeviceInfoData);
[DllImport("setupapi.dll", SetLastError = true)]
public static extern bool SetupDiDestroyDeviceInfoList(
IntPtr DeviceInfoSet);
// newdev.dll P/Invoke
[DllImport("newdev.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UpdateDriverForPlugAndPlayDevicesW(
IntPtr hwndParent, string HardwareId, string FullInfPath,
int InstallFlags, out bool bRebootRequired);
}Device creation sequence:
| Step | API Call | Parameters | Error Handling |
|---|---|---|---|
| 1 | SetupDiCreateDeviceInfoList |
ClassGuid = {745a17a0-74d3-11d0-b6fe-00a0c90f57da} (HIDClass), hwndParent = Zero
|
Log + exit 1 if returns -1
|
| 2 | SetupDiCreateDeviceInfoW |
DeviceName = "HIDClass" (not hardware ID), Description = "vJoy Device", Flags = DICD_GENERATE_ID
|
Log + cleanup + exit 1
|
| 3 | SetupDiSetDeviceRegistryPropertyW |
SPDRP_HARDWAREID, Buffer = root\VID_1234&PID_BEAD&REV_0222 (Unicode double-null terminated) |
Log + cleanup + exit 1
|
| 4 | SetupDiCallClassInstaller |
DIF_REGISTERDEVICE |
Log + cleanup + exit 1
|
| 5 | SetupDiDestroyDeviceInfoList |
Frees device info set | Always called |
| 6 | UpdateDriverForPlugAndPlayDevicesW |
HardwareId = root\VID_1234&PID_BEAD&REV_0222, FullInfPath = C:\Program Files\vJoy\vjoy.inf, InstallFlags = 1
|
Log + exit 1
|
Critical: DeviceName in step 2 must be the class name "HIDClass", not the hardware ID. Backslashes cause ERROR_INVALID_DEVINST_NAME (0xE0000205).
This creates a ROOT\HIDCLASS\0000 device node. PnP matches the OEM .inf and loads vjoy.sys. Install uses InstallFlags = 1 (INSTALLFLAG_FORCE); runtime CreateVJoyDevices uses 0 to skip re-binding.
Logging: Timestamped entries to %TEMP%\PadForge_vjoy_install.log. Final line: "OK", "FAIL: ...", or "EXCEPTION: ...".
public static void UninstallVJoy()Runs an elevated batch script:
flowchart TD
A[FindVJoyOemInfs<br/>detect OEM .inf files] --> B
B[Generate PadForge_vjoy_uninstall.cmd] --> C
C["RunElevated cmd.exe /c script<br/>(UAC prompt)"]
subgraph CMD["Batch Script Execution"]
direction TB
U1["Remove device nodes 0000-0015<br/>pnputil /remove-device /subtree"] --> U2
U2["timeout /t 2"] --> U3
U3["sc stop vjoy"] --> U4
U4["timeout /t 2"] --> U5
U5["sc delete vjoy"] --> U6
U6["Fallback: reg delete vjoy service<br/>from ALL ControlSets"] --> U7
U7["pnputil /delete-driver oem*.inf<br/>/uninstall /force"] --> U8
U8["rmdir /s /q C:\Program Files\vJoy<br/>del vjoy.sys from drivers\"] --> U9
U9["PowerShell one-liner:<br/>Remove legacy Inno Setup<br/>uninstall registry entries"]
end
C --> CMD
CMD --> D[CleanVJoyRegistryArtifacts<br/>C# method, no elevation needed]
D --> E[Delete script file]
Uninstall steps:
| Step | Action | Detail |
|---|---|---|
| 1 | Remove device nodes (0000–0015) | pnputil /remove-device /subtree |
| 2 | Wait 2s | Driver unload |
| 3 | Stop service | sc stop vjoy |
| 4 | Wait 2s | |
| 5 | Delete service | sc delete vjoy |
| 6 | Fallback | If sc delete fails (e.g. STOP_PENDING), delete service registry keys from all ControlSets via reg delete
|
| 7 | Remove driver from store |
pnputil /delete-driver {oemInf} /uninstall /force per stale OEM inf |
| 8 | Delete files |
C:\Program Files\vJoy\ and %SystemRoot%\System32\drivers\vjoy.sys
|
| 9 | Remove legacy entries | PowerShell one-liner scanning Uninstall + WOW6432Node for *vJoy*
|
| 10 | Final registry cleanup |
CleanVJoyRegistryArtifacts() (in-process, no extra elevation) |
private static void CleanVJoyRegistryArtifacts()Removes registry keys that can cause the vJoy installer to hang on reinstall. All deletions are best-effort (throwOnMissingSubKey: false, per-key exception handling).
HKLM paths deleted:
| Registry Path | Purpose |
|---|---|
SYSTEM\CurrentControlSet\Services\vjoy |
Service entry (current) |
SYSTEM\ControlSet001\Services\vjoy |
Service entry (ControlSet001) |
SYSTEM\ControlSet002\Services\vjoy |
Service entry (ControlSet002) |
SYSTEM\ControlSet003\Services\vjoy |
Service entry (ControlSet003) |
SYSTEM\CurrentControlSet\Control\MediaProperties\PrivateProperties\Joystick\OEM\VID_1234&PID_BEAD |
Joystick OEM properties |
SYSTEM\ControlSet001\Services\EventLog\System\vjoy |
Event log registration |
SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\%SystemRoot%/System32/drivers/hidkmdf.sys |
PnP lockdown entry |
SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\%SystemRoot%/System32/drivers/vjoy.sys |
PnP lockdown entry |
SOFTWARE\WOW6432Node\...\PnpLockdownFiles\...\hidkmdf.sys |
Same under WOW6432Node |
SOFTWARE\WOW6432Node\...\PnpLockdownFiles\...\vjoy.sys |
Same under WOW6432Node |
HKCU path deleted:
| Registry Path | Purpose |
|---|---|
System\CurrentControlSet\Control\MediaProperties\PrivateProperties\Joystick\OEM\VID_1234&PID_BEAD |
User-level joystick OEM properties |
Then calls CleanVJoyDeviceClassEntries().
private static void CleanVJoyDeviceClassEntries()Removes vJoy entries from the shared HID device class key (HKLM\SYSTEM\ControlSet001\Control\Class\{781ef630-72b2-11d2-b852-00c04fad5101}). Enumerates subkeys (0000, 0001, ...), deletes any where Class equals "vjoy" (case-insensitive). Best-effort; exceptions caught silently.
private static string[] FindVJoyOemInfs()Called before install/uninstall to find stale driver store entries. Runs pnputil /enum-drivers (30s timeout), parses output with a state machine that tracks "Published Name : oemXX.inf" lines and scans each block for "shaul" or "vjoy" (case-insensitive). Returns matching oem*.inf names, or empty array on error.
public static bool IsVJoyInstalled()
public static string GetVJoyVersion()Two detection paths:
| Path | Method | Detail |
|---|---|---|
| Primary | Check C:\Program Files\vJoy\vjoy.sys
|
FileVersionInfo.GetVersionInfo(). Falls back to "Installed" if version info empty. |
| Fallback | GetVJoyVersionFromRegistry() |
Scans Uninstall keys (Registry64 + Registry32) for "vJoy" in DisplayName. Detects legacy Inno Setup installs. |
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()flowchart TD
A[Create %TEMP%\PadForge_MidiServices\] --> B
B["HttpClient with UserAgent='PadForge'<br/>Timeout = 10 minutes"] --> C
C["GET https://api.github.com/repos/<br/>microsoft/MIDI/releases<br/>(full releases list, not /latest)"] --> D
D["FindMidiServicesDownloadUrl<br/>Parse JSON for SDK.Runtime x64 .exe"] --> E
E["Download installer (~210 MB)<br/>Stream to MidiServicesSdkRuntime.exe"] --> F
F["Run installer directly (no runas)<br/>/install /quiet /norestart<br/>Wait up to 5 minutes"] --> G
G["MidiVirtualController.ResetAvailability()<br/>Clear cached SDK check"] --> H
H[Cleanup temp directory]
Why /releases not /releases/latest: The microsoft/MIDI repo only publishes pre-releases. /releases/latest returns 404 without a stable release; /releases returns all releases (first = most recent).
Why no runas: PadForge is already elevated (auto-elevation in App.xaml.cs). Using Verb = "runas" when already elevated throws Win32Exception on some systems.
Post-install: Calls MidiVirtualController.ResetAvailability() so the cached SDK check re-evaluates.
private static async Task<string> FindMidiServicesDownloadUrl(HttpClient http)Parses the GitHub releases JSON to find the SDK Runtime x64 installer URL. Uses simple string search (no JSON library): finds "browser_download_url" occurrences, extracts URLs, matches on "SDK.Runtime" + "x64" + .exe (case-insensitive). Returns first match; throws InvalidOperationException if none found.
Asset pattern: Windows.MIDI.Services.SDK.Runtime.and.Tools.*-x64.exe
public static void UninstallMidiServices()Gets UninstallString from registry via FindMidiServicesUninstallString(), parses quoted/unquoted paths, appends /quiet /norestart, launches hidden. Waits up to 5 minutes. Not fire-and-forget.waits even though MIDI SDK DLLs may be loaded in-process (removing the backing service mid-session can cause issues).
private static string FindMidiServicesUninstallString()Scans Uninstall keys (Registry64 + Registry32) for DisplayName exactly matching "Windows MIDI Services Runtime and Tools" (case-insensitive). Returns UninstallString, or null if not found.
public static bool IsMidiServicesInstalled()Returns true if FindMidiServicesUninstallString() is non-null. Checks registry for the WiX Burn bootstrapper entry, not SDK runtime availability (that is MidiVirtualController.IsAvailable()).
The Settings page disables uninstall buttons when a driver/service is in use, preventing removal 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 Func<bool> delegates on SettingsViewModel, injected by MainWindow.xaml.cs. RefreshDriverGuards() re-evaluates CanExecute after slot creation, deletion, or type changes.
Critical constraint: vjoy.sys reads ALL DeviceNN registry keys from EVERY device node, concatenating them into one HID report descriptor. N nodes with N keys each = N^2 controllers in joy.cpl.
| Nodes | Registry Keys | Collections per Node | Total Controllers | Correct? |
|---|---|---|---|---|
| 1 | 2 | 2 | 2 | Yes |
| 2 | 2 | 2 | 4 (2 phantom) | No |
| 3 | 3 | 3 | 9 (6 phantom) | No |
Solution: Always exactly 1 ROOT\HIDCLASS device node. Joystick count is controlled by DeviceNN registry keys (Device01–Device{N}), not device nodes. Scaling: update registry keys, then restart the node via remove+create. Enforced by VJoyVirtualController.EnsureDevicesAvailable(), not by DriverInstaller.
Runtime device management is handled by VJoyVirtualController, not DriverInstaller.
public static bool EnsureDevicesAvailable(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)
public static bool EnsureDevicesAvailable(int requiredCount = 1)Both delegate to EnsureDevicesAvailableCore(). Called at ~1000Hz from the polling thread, so fast-path optimization is critical.
flowchart TD
A["EnsureDevicesAvailableCore(requiredCount, configs)"] --> B{First call?}
B -->|Yes| B1["EnsureDriverInStore()<br/>EnsureFfbRegistryKeys()"]
B1 --> C
B -->|No| C
C{Fast path?<br/>count unchanged,<br/>configs match,<br/>DLL loaded}
C -->|Yes| DONE[return true]
C -->|No| D
D["EnsureDllLoaded()<br/>CountExistingDevices()"] --> E
E["WriteDeviceDescriptors()<br/>Updates registry keys"] --> F
F{requiredCount == 0?}
F -->|Yes| G1{descriptors changed<br/>AND nodes exist?}
G1 -->|Yes| G2["DisableDeviceNode()<br/>(full remove)"]
G1 -->|No| DONE
G2 --> DONE
F -->|No| H{Existing nodes?}
H -->|0 nodes| I["CreateVJoyDevices(1)<br/>Wait up to 5s for PnP bind"]
H -->|1 node| J{Descriptors changed?}
H -->|>1 nodes| K["Remove excess nodes<br/>Keep first node"]
J -->|Yes| L["RestartDeviceNode(countChanged: true)<br/>Full remove + create cycle"]
J -->|No| M{DLL loaded?}
I --> DONE
K --> L
L --> DONE
M -->|Yes| DONE
M -->|No| N[return false]
Fast path: When count unchanged, configs match, and DLL loaded, returns immediately.no pnputil or registry access.
Config change detection: Compares per-device configs (Axes, Buttons, Povs) element-by-element against _lastDeviceConfigs. Clones after each call to prevent aliasing.
private static bool WriteDeviceDescriptors(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)Writes HID report descriptors to HKLM\SYSTEM\CurrentControlSet\services\vjoy\Parameters\.
- Deletes excess
DeviceNNsubkeys whereNN > requiredCount. - For each device 1–
requiredCount: uses per-device config if available, otherwise defaults to Xbox 360 layout (6 axes, 11 buttons, 1 POV). Builds binary descriptor viaBuildHidDescriptor(). -
Change detection: Compares existing
HidReportDescriptorbyte-by-byte. Only writes if different (avoids disturbing live devices). - Writes
HidReportDescriptor(REG_BINARY) +HidReportDescriptorSize(REG_DWORD). - Returns
trueif any keys were modified.
internal static bool CreateVJoyDevices(int count = 1)Creates device nodes via PowerShell SetupAPI P/Invoke (same pattern as install). Key differences from InstallVJoy():
| Aspect | InstallVJoy | CreateVJoyDevices |
|---|---|---|
| UAC |
RunElevated with Verb = "runas"
|
No runas (app already elevated) |
UpdateDriverForPlugAndPlayDevicesW flags |
InstallFlags = 1 (INSTALLFLAG_FORCE) |
InstallFlags = 0 (no force) |
| Service registration | Handled by pnputil /add-driver
|
Manually creates service registry key if missing |
| Driver file copy | Extracted from ZIP | Copies vjoy.sys to System32\drivers\ if missing |
| Pre-cleanup | Full cleanup of existing install | None |
Also ensures the vjoy service registry key exists (creates if missing: Type=1, Start=3, ErrorControl=0, ImagePath=System32\DRIVERS\vjoy.sys).
private static void RestartDeviceNode(bool countChanged = true)Restarts the vJoy device node so the driver re-reads registry descriptors. Strategy depends on change type:
| Change Type | countChanged |
Strategy |
|---|---|---|
| Content only | false |
DICS_PROPCHANGE (in-place restart, re-reads descriptors). Falls through to full restart on failure. |
| Count change | true |
Full remove + recreate. HIDCLASS only creates child PDOs during EvtDeviceAdd, so DICS_PROPCHANGE cannot add/remove PDOs. |
Full restart cascade (tried in order):
| Priority | Method | Note |
|---|---|---|
| 1 | SetupApiRestart.DisableDevice() |
DICS_DISABLE |
| 2 | CfgMgr32.CM_Disable_DevNode() |
Fallback for Win11 26200+ |
| 3 | SetupApiRestart.RemoveDevice() |
After disable |
| 4 | pnputil /remove-device |
Fallback |
| 5 | SetupApiRestart.RemoveDevice() |
Without prior disable (last resort) |
| 6 | DICS_PROPCHANGE |
Won't change PDO count but re-reads descriptors |
| 7 | DICS_ENABLE |
Re-enable if disabled but couldn't remove |
After successful remove, creates a fresh node via CreateVJoyDevices(1) and waits up to 5s for PnP to bind.
Generation tracking: _generation increments on every restart/removal. Controllers use ReAcquireIfNeeded() to detect stale handles and re-acquire device IDs.
internal static void DisableDeviceNode()Fully removes the vJoy device node (not just disable). Called when requiredCount == 0.
Pre-requisites:
| Step | Method | Why |
|---|---|---|
| 1 | RelinquishAllDevices() |
Releases device IDs 1–16. Without this, CM_Disable_DevNode returns CR_REMOVE_VETOED (23). |
| 2 | RefreshVJoyDllHandles() |
Sends WM_DEVICECHANGE / DBT_DEVICEQUERYREMOVE to vJoyInterface.dll's hidden window to close stale handles. Without this, DICS_DISABLE takes ~5s. |
After removal, runs pnputil /scan-devices synchronously to clean up ghost child PDOs (must be synchronous to prevent racing with ViGEm VC creation).
public static int CountExistingDevices()Delegates to EnumerateVJoyInstanceIds(): runs pnputil /enum-devices /class HIDClass, parses for ROOT\HIDCLASS\* entries containing "vJoy". Does not use GetVJDStatus (returns stale data). Caches results in _cachedInstanceIds for fast shutdown access.
File: PadForge.App/Common/HidHideController.cs
Namespace: PadForge.Common
Runtime device management (blacklisting, whitelisting, cloaking) 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 Multi-SZ format: null-separated UTF-16 strings with double-null terminator. SET replaces 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 List<string> GetWhitelist() // DOS device paths
static void SetWhitelist(List<string> paths) // Replace entire whitelist
static bool GetActive() // Cloaking enabled?
static void SetActive(bool active) // Enable/disable cloaking
static void RemoveManagedDevices() // Remove only PadForge's entries
static void SyncManagedDevices(HashSet<string> desiredIds) // Atomic diff-based blacklist sync
static void ClearAll() // Clear blacklist + disable cloaking
static List<string> FindInstanceIdsByVidPid(ushort, ushort) // Enumerate HID devices by VID/PID (USB + BLE formats)
static string DevicePathToInstanceId(string p) // \\?\HID#... -> HID\VID_...\...
static string ToDosDevicePathPublic(string filePath) // C:\... -> \Device\HarddiskVolumeN\..._managedDeviceIds (HashSet<string>) tracks device IDs PadForge added to the blacklist. RemoveManagedDevices() removes only these entries, leaving entries from other tools untouched.
Removed methods: AddToBlacklist(string) and RemoveFromBlacklist(string) were removed. All blacklist management now goes through SyncManagedDevices(HashSet<string>), which performs an atomic diff-based sync (add missing, remove excess) in a single operation.
Merge-based cache: ApplyDeviceHiding uses a merge-based approach for its resolved instance ID cache. New IDs are added but previously cached IDs are never discarded. This ensures offline devices that were resolved in a prior cycle remain in the blacklist even if they are not currently enumerable.
FindInstanceIdsByVidPid() matches device instance IDs in two formats:
-
USB HID:
HID\VID_045E&PID_0B13\.... StandardVID_XXXX&PID_XXXXpattern -
Bluetooth LE (HID-over-GATT):
HID\VID&0202D0&PID&0101\.... UsesVID&02XXXX(USB-assigned) orVID&01XXXX(Bluetooth SIG-assigned) paired withPID&XXXX
This dual matching ensures Bluetooth LE controllers (e.g., Xbox Series X via Bluetooth) are fully hidden. Both the HID child and BTHLEDEVICE parent nodes.
Whitelist requires DOS device paths (\Device\HarddiskVolumeN\...), not regular paths (C:\...). ToDosDevicePath() converts via QueryDosDeviceW.
DevicePathToInstanceId() converts device paths (\\?\HID#VID_054C&PID_0CE6#...{guid}) to PnP instance IDs (HID\VID_054C&PID_0CE6\...): strip \\?\ prefix, remove trailing GUID {...}, replace # with \.
PadForge auto-elevates when vJoy is installed (App.xaml.cs):
| Scenario | Elevation Method | UAC Prompts |
|---|---|---|
| ViGEmBus/HidHide install/uninstall |
msiexec via RunElevated with Verb = "runas"
|
1 per operation |
| vJoy install | Single elevated PowerShell script via RunElevated
|
1 |
| vJoy runtime (CreateVJoyDevices, etc.) | App already elevated | 0 |
| MIDI Services install | App already elevated; direct Process.Start
|
0 |
| 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 |
| vJoy device create script | %TEMP%\PadForge_vjoy_create_device.ps1 |
| vJoy device create log | %TEMP%\PadForge_vjoy_create_device.log |
| MIDI Services | %TEMP%\PadForge_MidiServices\ |
All cleaned up after each operation via CleanupTempDir() in finally blocks.
All methods use try/finally for cleanup: temp directories always deleted, script files deleted after execution, log files deleted after reading.
vJoy scripts use $ErrorActionPreference = 'Continue' so individual failures do not abort the script.
| Script | Error Strategy |
|---|---|
| Install | Every SetupAPI call checks return value, logs "FAIL: ..." with Win32 error, exits code 1 |
| Uninstall | Redirects stderr to null, continues regardless (best-effort) |
CleanVJoyRegistryArtifacts() |
Per-key exception handling; never aborts on a missing key |
No explicit rollback. On partial failure:
| Driver | Behavior |
|---|---|
| ViGEmBus / HidHide | MSI installer handles rollback |
| vJoy | Pre-install cleanup removes previous install first, so failure leaves "not installed" state. Re-running starts fresh. |
| MIDI Services | WiX Burn bootstrapper handles rollback |
-
Virtual Controllers:
Xbox360VirtualController,DS4VirtualController,VJoyVirtualController,MidiVirtualControllerconsuming installed drivers -
vJoy Deep Dive: VJoy device node lifecycle, HID descriptors, runtime
EnsureDevicesAvailable() -
Architecture Overview: Elevation strategy, auto-elevation in
App.xaml.cs - Build and Publish: Embedded driver installer resources (ViGEmBus, HidHide, vJoyDriver.zip)
-
Settings and Serialization: Driver status display in
SettingsViewModel -
XAML Views:
SettingsPagedriver install/uninstall buttons and guards