Skip to content

Driver Installation Internals

hifihedgehog edited this page Mar 19, 2026 · 30 revisions

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


DriverInstaller

public static class DriverInstaller

Embedded Resources

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" />

ViGEmBus

InstallViGEmBus()

public static void InstallViGEmBus()
  1. Extracts embedded ViGEmBus_1.22.0_x64_x86_arm64.exe to %TEMP%\PadForge_ViGEmBus\.
  2. Runs the bootstrapper with /extract to unpack the MSI and supporting files.
  3. Finds the MSI: ViGEmBus.x64.msi on 64-bit OS, ViGEmBus.msi fallback, then ViGEmBus*.msi glob.
  4. Runs msiexec /i "{msiPath}" /qb /norestart with UAC elevation (Verb = "runas").
  5. Cleans up temp directory.

UninstallViGEmBus()

public static void UninstallViGEmBus()

Same extraction flow, then runs msiexec /x "{msiPath}" /qb /norestart with elevation.

GetViGEmVersion()

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.


HidHide

InstallHidHide()

public static void InstallHidHide()

Same pattern as ViGEmBus: extract bootstrapper, extract MSI (HidHide.msi or HidHide*.msi), run msiexec /i with elevation.

UninstallHidHide()

public static void UninstallHidHide()

Extract and run msiexec /x with elevation.

IsHidHideInstalled() / GetHidHideVersion()

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

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.

InstallVJoy()

public static void InstallVJoy()

Everything runs in a single elevated PowerShell script (one UAC prompt):

Step 1: Pre-install Cleanup

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

Step 2: Extract Driver Files

Extracts vJoyDriver.zip to C:\Program Files\vJoy\.

Step 3: Add Driver to Store

pnputil /add-driver vjoy.inf /install

Step 4: Create Device Node via SetupAPI

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:

  1. SetupDiCreateDeviceInfoList — Create device info set for HIDClass GUID ({745a17a0-74d3-11d0-b6fe-00a0c90f57da}).
  2. SetupDiCreateDeviceInfoW("HIDClass", ...)Critical: DeviceName parameter must be the class name "HIDClass", NOT the hardware ID. Backslashes in DeviceName cause ERROR_INVALID_DEVINST_NAME (0xE0000205).
  3. SetupDiSetDeviceRegistryPropertyW(SPDRP_HARDWAREID, ...) — Set hardware ID: root\VID_1234&PID_BEAD&REV_0222 (Unicode double-null terminated).
  4. SetupDiCallClassInstaller(DIF_REGISTERDEVICE) — Register device.
  5. 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.

UninstallVJoy()

public static void UninstallVJoy()

Runs an elevated batch script (.cmd):

  1. Remove device nodes FIRST (for N = 0000..0015): pnputil /remove-device "ROOT\HIDCLASS\NNNN" /subtree
  2. Wait 2 seconds for driver to unload.
  3. Stop service: sc stop vjoy
  4. Wait 2 seconds.
  5. Delete service: sc delete vjoy
  6. Fallback: If sc delete failed (e.g., STOP_PENDING), directly remove service registry keys from ALL ControlSets via reg delete.
  7. Remove driver from store: pnputil /delete-driver {oemInf} /uninstall /force
  8. Delete install directory (C:\Program Files\vJoy\) and stale driver binary (%SystemRoot%\System32\drivers\vjoy.sys).
  9. Remove legacy Inno Setup uninstall registry entries via PowerShell one-liner.
  10. Call CleanVJoyRegistryArtifacts() for final registry cleanup.

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_BEAD
  • HKLM\SYSTEM\ControlSet001\Services\EventLog\System\vjoy
  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\...\hidkmdf.sys
  • HKLM\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).

IsVJoyInstalled() / GetVJoyVersion()

public static bool IsVJoyInstalled()
public static string GetVJoyVersion()

Two detection paths:

  1. Primary: Check for vjoy.sys in C:\Program Files\vJoy\. If present, read FileVersionInfo.GetVersionInfo().
  2. 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.

FindVJoyOemInfs()

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.


Windows MIDI Services

Unlike the other drivers, Windows MIDI Services is NOT embedded — the installer is ~210 MB and must be downloaded from GitHub at install time.

InstallMidiServicesAsync()

public static async Task InstallMidiServicesAsync()
  1. Creates temp directory %TEMP%\PadForge_MidiServices\.
  2. Queries the GitHub API (https://api.github.com/repos/microsoft/MIDI/releases) for the latest release. Note: /releases/latest returns 404 because microsoft/MIDI only publishes pre-releases — uses the full /releases endpoint and parses the first entry.
  3. Finds the SDK Runtime x64 installer asset (pattern: Windows.MIDI.Services.SDK.Runtime.and.Tools.*-x64.exe).
  4. Downloads the installer (~210 MB) with a 10-minute timeout.
  5. Runs the WiX Burn bootstrapper with /install /quiet /norestart. Since PadForge is already elevated, runs directly (no runas) to avoid Win32Exception on some systems. Waits up to 5 minutes.
  6. Calls MidiVirtualController.ResetAvailability() so the cached availability check re-evaluates.
  7. Cleans up temp directory.

FindMidiServicesDownloadUrl()

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.

UninstallMidiServices()

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.

IsMidiServicesInstalled()

public static bool IsMidiServicesInstalled()

Checks the registry for a Windows MIDI Services uninstall entry via FindMidiServicesUninstallString(). Returns true if the uninstall string is found, indicating MIDI Services is installed. This is separate from MidiVirtualController.IsAvailable(), which checks SDK runtime availability.


Uninstall Guards

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.


Shared Helpers

ExtractEmbeddedResource()

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.

ExtractInstallerBundle()

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.

FindMsi()

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.

RunElevated()

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).

RunMsiElevated()

private static void RunMsiElevated(string arguments)

Convenience wrapper that calls RunElevated("msiexec.exe", arguments).

CleanupTempDir()

private static void CleanupTempDir(string tempDir)

Deletes temp directory recursively, ignoring all exceptions.


vJoy Single-Node Architecture

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.


vJoy Device Lifecycle (Runtime)

While DriverInstaller handles the initial driver install/uninstall, runtime device management is handled by VJoyVirtualController:

  1. 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.
  2. CreateVJoyDevice() — Creates device node via SetupAPI if none exists.
  3. DeleteVJoyDevice() — Removes device node via pnputil /remove-device.
  4. WriteDeviceDescriptors() — Writes HidReportDescriptor (REG_BINARY) and HidReportDescriptorSize (REG_DWORD) to HKLM\..\vjoy\Parameters\Device{NN}. Only writes when descriptors differ from existing (avoids disturbing live devices).
  5. CountExistingDevices() — Uses pnputil /enum-devices /class HIDClass (not GetVJDStatus which returns stale data).

HidHide Runtime API (HidHideController)

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 Codes

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)

Buffer Format

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).

Public API

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_...\...

Managed Device Tracking

_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.

DOS Device Path Conversion

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.

Device Instance ID Conversion

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:

  1. Strip \\?\ prefix
  2. Remove trailing GUID suffix ({...})
  3. Replace # with \

Elevation Strategy

PadForge auto-elevates when vJoy is installed (handled in App.xaml.cs). This means:

  • ViGEmBus/HidHide install/uninstall: UAC prompt per operation (launched via msiexec with runas).
  • 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.

Temp Directories

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.

Clone this wiki locally