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


Architecture Overview

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"]
Loading

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

Shared Helpers

These private methods are reused across all driver install/uninstall flows.

ExtractEmbeddedResource()

private static string ExtractEmbeddedResource(string resourceFileName, string tempDir)
  1. Calls Assembly.GetExecutingAssembly() to access embedded resources.
  2. Searches GetManifestResourceNames() for a match using IndexOf (case-insensitive substring match).
  3. Creates tempDir if it does not exist.
  4. Streams the resource to {tempDir}\{resourceFileName} via GetManifestResourceStream.
  5. Returns the full path to the extracted file.
  6. Throws FileNotFoundException if the resource name is not found (error message lists all available resource names for debugging).

ExtractInstallerBundle()

private static string ExtractInstallerBundle(string exePath, string tempDir)

Runs the WiX bootstrapper with /extract "{extractDir}" to unpack the MSI and supporting files into {tempDir}\Extracted\. Deletes and recreates the extraction directory if it already exists. Uses UseShellExecute = false and CreateNoWindow = true. Waits up to 60 seconds. Returns the extraction directory path.

FindMsi()

private static string FindMsi(string extractDir, string primaryName, string fallbackPattern)

Searches the extracted bundle for an MSI file using Directory.GetFiles with SearchOption.AllDirectories:

  1. Tries exact name first (e.g., ViGEmBus.x64.msi).
  2. Falls back to glob pattern (e.g., ViGEmBus*.msi).
  3. Throws FileNotFoundException if neither match yields results.

RunElevated()

private static void RunElevated(string fileName, string arguments)

Starts a process with Verb = "runas" and UseShellExecute = true (triggers UAC prompt). WindowStyle = ProcessWindowStyle.Hidden. Waits up to 180 seconds (3 minutes). Used by ViGEmBus, HidHide, and vJoy install (PowerShell script).

RunMsiElevated()

private static void RunMsiElevated(string arguments)

Convenience wrapper: RunElevated("msiexec.exe", arguments).

CleanupTempDir()

private static void CleanupTempDir(string tempDir)

Deletes temp directory recursively via Directory.Delete(tempDir, true), ignoring all exceptions. Called in finally blocks to guarantee cleanup.


ViGEmBus

InstallViGEmBus()

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]
Loading

Flow:

  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 in finally block.

UninstallViGEmBus()

public static void UninstallViGEmBus()

Same extraction flow as install, then runs msiexec /x "{msiPath}" /qb /norestart with elevation. Temp directory cleaned up in finally block.

GetViGEmVersion()

public static string GetViGEmVersion()

Scans HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall (both RegistryView.Registry64 and RegistryView.Registry32 views) for entries containing "ViGEm" in DisplayName (case-insensitive IndexOf). Returns DisplayVersion value, or null if not found.


HidHide

InstallHidHide()

public static void InstallHidHide()

Same pattern as ViGEmBus: extract embedded HidHide_1.5.230_x64.exe bootstrapper, extract MSI (HidHide.msi primary, HidHide*.msi fallback), run msiexec /i with elevation.

UninstallHidHide()

public static void UninstallHidHide()

Extract and run msiexec /x with elevation. Same extraction flow as install.

Detection: IsHidHideInstalled() / GetHidHideVersion()

public static bool IsHidHideInstalled()
public static string GetHidHideVersion()

Both delegate to TryGetHidHideMsiInfo():

private static bool TryGetHidHideMsiInfo(out string displayVersion, out string productCode)

Scans registry Uninstall keys (both Registry64 and Registry32 views) for entries where DisplayName contains "HidHide" or "HID Hide" (case-insensitive). Extracts:

  • DisplayVersion for version display.
  • The subkey name as productCode (GUID format {...}) for uninstall operations.

GetHidHideVersion() returns "Installed" as a fallback when DisplayVersion is null/empty.


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

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]
Loading

Step 1: Pre-install Cleanup

Critical ordering: Device nodes MUST be removed FIRST so the driver can unload. Reversing this order (stopping service before removing nodes) causes the service to get stuck in STOP_PENDING (irrecoverable without reboot).

The generated PowerShell script executes:

# 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 SilentlyContinue

Step 2: Extract Driver Files

New-Item -Path 'C:\Program Files\vJoy' -ItemType Directory -Force | Out-Null
Expand-Archive -Path '{zipPath}' -DestinationPath 'C:\Program Files\vJoy' -Force

Step 3: Add Driver to Store

pnputil /add-driver 'C:\Program Files\vJoy\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 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 in the PowerShell script:

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 parameter in step 2 must be the class name "HIDClass", NOT the hardware ID. Backslashes in DeviceName cause ERROR_INVALID_DEVINST_NAME (0xE0000205).

This creates a ROOT\HIDCLASS\0000 device node. PnP matches the OEM .inf and loads vjoy.sys.

The install script uses InstallFlags = 1 (INSTALLFLAG_FORCE) to bind the driver, while the runtime CreateVJoyDevices uses InstallFlags = 0 to avoid re-binding already-bound devices.

Logging: Every step writes timestamped entries to %TEMP%\PadForge_vjoy_install.log. The final line is "OK" on success or "FAIL: ..." / "EXCEPTION: ..." on failure.

UninstallVJoy()

public static void UninstallVJoy()

Runs an elevated batch script (.cmd):

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]
Loading

Detailed uninstall steps:

  1. Remove device nodes FIRST (for i = 0000..0015): pnputil /remove-device "ROOT\HIDCLASS\{i:D4}" /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:
    • HKLM\SYSTEM\CurrentControlSet\Services\vjoy
    • HKLM\SYSTEM\ControlSet001\Services\vjoy
    • HKLM\SYSTEM\ControlSet002\Services\vjoy
    • HKLM\SYSTEM\ControlSet003\Services\vjoy
  7. Remove driver from store: pnputil /delete-driver {oemInf} /uninstall /force (one per stale OEM inf).
  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 that scans both HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall and WOW6432Node for entries with DisplayName -like '*vJoy*'.
  10. Call CleanVJoyRegistryArtifacts() for final registry cleanup (runs in-process, no elevation needed since method uses Registry.LocalMachine APIs which succeed under elevation).

Registry Cleanup

CleanVJoyRegistryArtifacts()

private static void CleanVJoyRegistryArtifacts()

Removes registry keys that can cause the vJoy installer to hang on reinstall. All deletions use throwOnMissingSubKey: false and catch exceptions per-key (best-effort cleanup).

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

CleanVJoyDeviceClassEntries()

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}

Algorithm:

  1. Open the class key read-only.
  2. Enumerate all subkeys (e.g., 0000, 0001, ...).
  3. For each subkey, read the "Class" registry value.
  4. If Class equals "vjoy" (case-insensitive via StringComparison.OrdinalIgnoreCase), delete that subkey tree.
  5. All operations are best-effort (exceptions caught silently).

FindVJoyOemInfs()

private static string[] FindVJoyOemInfs()

Called before install/uninstall to find stale driver store entries.

  1. Runs pnputil.exe /enum-drivers with RedirectStandardOutput = true, CreateNoWindow = true.
  2. Reads all output, waits up to 30 seconds.
  3. Parses line-by-line using a state machine:
    • Detects "Published Name : oemXX.inf" lines (prefix match "Published" + contains ":").
    • Saves the OEM inf name as currentOem.
    • Scans subsequent lines in the same block for "shaul" or "vjoy" (case-insensitive).
    • If found, marks the block as a vJoy entry (isVJoyBlock = true).
    • On encountering the next "Published Name" line, saves the previous match (if any) and starts a new block.
    • Handles the last block after the loop ends.
  4. Returns array of oem*.inf names. Returns empty array on any exception.

Detection: 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() for the version. Returns "Installed" if version info is empty.
  2. Fallback: Scan registry Uninstall keys for "vJoy" in DisplayName via GetVJoyVersionFromRegistry() (detects legacy Inno Setup installs).
private static string GetVJoyVersionFromRegistry()

Scans both RegistryView.Registry64 and RegistryView.Registry32 views of HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. Returns DisplayVersion or "Installed" (fallback), or null if not found.


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()
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]
Loading

Why /releases instead of /releases/latest: The microsoft/MIDI repository only publishes pre-releases. The /releases/latest endpoint returns 404 for repos without a non-pre-release. The full /releases endpoint returns all releases as an array; the first entry is the most recent.

Why no runas: PadForge is already elevated when vJoy is installed (auto-elevation in App.xaml.cs). Launching the installer with Verb = "runas" causes a Win32Exception on some systems because the process is already elevated. Instead, it runs directly with UseShellExecute = false and CreateNoWindow = true.

Post-install: Calls MidiVirtualController.ResetAvailability() so the cached availability check re-evaluates (the MIDI SDK DLLs are now present).

FindMidiServicesDownloadUrl()

private static async Task<string> FindMidiServicesDownloadUrl(HttpClient http)

Parses the GitHub releases JSON response to find the download URL for the SDK Runtime x64 installer.

Algorithm (simple string search, no JSON library dependency):

  1. Search for "browser_download_url" occurrences in the JSON string.
  2. For each occurrence, extract the URL between "http and the closing ".
  3. Check if the URL contains "SDK.Runtime" AND "x64" AND ends with ".exe" (all case-insensitive).
  4. Return the first matching URL.
  5. Throw InvalidOperationException if no matching asset is found.

Asset name pattern: Windows.MIDI.Services.SDK.Runtime.and.Tools.*-x64.exe

UninstallMidiServices()

public static void UninstallMidiServices()
  1. Calls FindMidiServicesUninstallString() to get the registry UninstallString.
  2. Parses the uninstall command:
    • If quoted: extracts path between first two " characters, remainder is existing args.
    • If unquoted: splits at first space.
  3. Appends /quiet /norestart to existing arguments.
  4. Launches the uninstaller with UseShellExecute = false, CreateNoWindow = true.
  5. Waits up to 5 minutes (300_000 ms).

No fire-and-forget: Despite the MIDI Services SDK DLLs being loaded in-process, the method waits for the uninstaller to finish (up to 5 minutes). The comment in the source notes this can cause issues if the backing service is removed mid-session.

FindMidiServicesUninstallString()

private static string FindMidiServicesUninstallString()

Scans both Registry64 and Registry32 views of HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall for an entry where DisplayName equals "Windows MIDI Services Runtime and Tools" (exact match, case-insensitive via StringComparison.OrdinalIgnoreCase). Returns the UninstallString value, or null if not found.

IsMidiServicesInstalled()

public static bool IsMidiServicesInstalled()

Returns true if FindMidiServicesUninstallString() returns a non-null value. This checks the registry for the WiX Burn bootstrapper bundle entry, NOT the SDK runtime availability (that is MidiVirtualController.IsAvailable()).


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.


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.

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. 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 remove+create.

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. The following methods manage the device node lifecycle during normal operation:

EnsureDevicesAvailable()

public static bool EnsureDevicesAvailable(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)
public static bool EnsureDevicesAvailable(int requiredCount = 1)

Both delegate to EnsureDevicesAvailableCore(). Called from the polling thread at ~1000Hz, so includes a fast path to avoid expensive operations on every call.

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]
Loading

Fast path: When _currentDescriptorCount == requiredCount and configs haven't changed and the DLL is loaded, returns immediately without any pnputil enumeration or registry writes. This is critical because the method is called at 1000Hz from the polling thread.

Config change detection: Compares per-device configs (Axes, Buttons, Povs) element-by-element against _lastDeviceConfigs. Clones the config array after each call to prevent aliasing.

WriteDeviceDescriptors()

private static bool WriteDeviceDescriptors(int requiredCount, VJoyDeviceConfig[] perDeviceConfigs)

Writes HID report descriptors to registry at HKLM\SYSTEM\CurrentControlSet\services\vjoy\Parameters\.

  1. Opens the Parameters key for writing.
  2. Deletes excess keys: Enumerates existing DeviceNN subkeys, deletes any where NN > requiredCount.
  3. Writes descriptors: For each device i (1..requiredCount):
    • Uses per-device config if available (perDeviceConfigs[i-1]), otherwise defaults to Xbox 360 layout (6 axes, 11 buttons, 1 POV).
    • Calls BuildHidDescriptor(reportId, axes, buttons, povs) to generate the binary descriptor.
    • Change detection: Reads existing HidReportDescriptor (REG_BINARY) and compares byte-by-byte. Only writes if the descriptor differs (avoids disturbing live devices whose driver has already read the registry).
    • Writes both HidReportDescriptor (REG_BINARY) and HidReportDescriptorSize (REG_DWORD).
  4. Returns true if any registry keys were actually modified (written or deleted).

CreateVJoyDevices()

internal static bool CreateVJoyDevices(int count = 1)

Creates device nodes via a PowerShell script using the same SetupAPI P/Invoke pattern as the install flow. 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

The script also ensures the vjoy service registry key exists (creates it if missing with Type=1, Start=3, ErrorControl=0, ImagePath=System32\DRIVERS\vjoy.sys).

RestartDeviceNode()

private static void RestartDeviceNode(bool countChanged = true)

Restarts the single vJoy device node so the driver re-reads registry descriptors. Strategy depends on whether the descriptor count changed:

  • Content-only change (countChanged = false): Uses DICS_PROPCHANGE to restart the driver stack in-place. This re-reads the HID descriptor from registry without recreating child PDOs. Falls through to full restart if DICS_PROPCHANGE fails.
  • Count change (countChanged = true): Must fully remove + recreate the device node because HIDCLASS only creates child PDOs during EvtDeviceAdd (device node creation). DICS_PROPCHANGE (stop/start) re-reads descriptors but does NOT create or destroy child PDOs.

Full restart cascade (tried in order until one succeeds):

  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 fallback (won't change PDO count but re-reads descriptors)
  7. DICS_ENABLE re-enable if we disabled but couldn't remove

After successful remove, creates a fresh device node via CreateVJoyDevices(1) and waits up to 5 seconds for PnP to bind.

Generation tracking: _generation is incremented on every restart or removal. Connected controllers use ReAcquireIfNeeded() to detect stale handles (generation mismatch) and re-acquire their device IDs.

DisableDeviceNode()

internal static void DisableDeviceNode()

Fully removes the vJoy device node (not just disable). Called when requiredCount == 0 (all vJoy slots inactive).

Pre-requisites before any node operation:

  1. RelinquishAllDevices() -- releases all vJoy device IDs (1-16) so the driver releases its handles. Without this, CM_Disable_DevNode returns CR_REMOVE_VETOED (23).
  2. RefreshVJoyDllHandles() -- sends WM_DEVICECHANGE / DBT_DEVICEQUERYREMOVE to vJoyInterface.dll's hidden window ("win32app_vJoyInterface_DLL" class) to close its stale internal control device handle (h0) and clear its cached device namespace. Without this, DICS_DISABLE takes ~5 seconds waiting for the handle to time out.

After successful removal, runs pnputil /scan-devices synchronously to clean up ghost child PDOs. Must be synchronous to prevent racing with ViGEm VC creation on the next polling cycle.

CountExistingDevices()

public static int CountExistingDevices()

Delegates to EnumerateVJoyInstanceIds(), which runs pnputil /enum-devices /class HIDClass and parses the output for ROOT\HIDCLASS\* entries whose description contains "vJoy". Does NOT use GetVJDStatus (which returns stale data from the DLL's cached state). Caches results in _cachedInstanceIds for fast access during shutdown.


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:

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

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
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 temp directories are cleaned up after each operation via CleanupTempDir() in finally blocks.


Error Handling and Rollback

General Strategy

All install/uninstall methods use try/finally blocks for cleanup:

  • Temp directories are always deleted, even on failure.
  • Script files are deleted after execution (try { File.Delete(scriptPath); } catch { }).
  • Log files are deleted after reading results.

vJoy-Specific Error Handling

The vJoy install/uninstall scripts use $ErrorActionPreference = 'Continue' so individual step failures do not abort the entire script. Each critical step logs its result:

  • Install script: Every SetupAPI call checks the return value, logs "FAIL: ..." with the Win32 error code, and exits with code 1.
  • Uninstall script: Every command redirects stderr to null (>nul 2>&1) and continues regardless of failure (best-effort cleanup).
  • CleanVJoyRegistryArtifacts(): Every registry deletion catches exceptions individually (never aborts on a single missing key).

No Rollback

There is no explicit rollback mechanism. If a driver install fails partway through:

  • ViGEmBus/HidHide: MSI installer handles its own rollback.
  • vJoy: Pre-install cleanup removes the previous installation first, so a failed install leaves the system in a "not installed" state rather than a corrupted state. Re-running install will start fresh.
  • MIDI Services: WiX Burn bootstrapper handles its own rollback.

Clone this wiki locally