Skip to content

Driver Installation Internals

hifihedgehog edited this page Mar 19, 2026 · 30 revisions

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

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

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.

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

UninstallViGEmBus()

public static void UninstallViGEmBus()

Same extraction flow as install, then runs msiexec /x with elevation. Temp cleanup in finally.

GetViGEmVersion()

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.


HidHide

InstallHidHide()

public static void InstallHidHide()

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

UninstallHidHide()

public static void UninstallHidHide()

Same extraction flow, then msiexec /x with elevation.

Detection: IsHidHideInstalled() / GetHidHideVersion()

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

vJoy installation bypasses the Inno Setup installer entirely, using direct driver store + SetupAPI device creation.

InstallVJoy()

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

Step 1: Pre-install Cleanup

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 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 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: ...".

UninstallVJoy()

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

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)

Registry Cleanup

CleanVJoyRegistryArtifacts()

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

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.

FindVJoyOemInfs()

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.

Detection: IsVJoyInstalled() / GetVJoyVersion()

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.

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

FindMidiServicesDownloadUrl()

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

UninstallMidiServices()

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

FindMidiServicesUninstallString()

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.

IsMidiServicesInstalled()

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


Uninstall Guards

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.


vJoy Single-Node Architecture

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 (Device01Device{N}), not device nodes. Scaling: update registry keys, then restart the node via remove+create. Enforced by VJoyVirtualController.EnsureDevicesAvailable(), not by DriverInstaller.


vJoy Device Lifecycle (Runtime)

Runtime device management is handled by VJoyVirtualController, not DriverInstaller.

EnsureDevicesAvailable()

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

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.

WriteDeviceDescriptors()

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

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

  1. Deletes excess DeviceNN subkeys where NN > requiredCount.
  2. 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 via BuildHidDescriptor().
  3. Change detection: Compares existing HidReportDescriptor byte-by-byte. Only writes if different (avoids disturbing live devices).
  4. Writes HidReportDescriptor (REG_BINARY) + HidReportDescriptorSize (REG_DWORD).
  5. Returns true if any keys were modified.

CreateVJoyDevices()

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

RestartDeviceNode()

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.

DisableDeviceNode()

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

CountExistingDevices()

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.


HidHide Runtime API (HidHideController)

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 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 Multi-SZ format: null-separated UTF-16 strings with double-null terminator. SET replaces 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 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
static string DevicePathToInstanceId(string p)              // \\?\HID#... -> HID\VID_...\...
static string ToDosDevicePathPublic(string filePath)        // C:\... -> \Device\HarddiskVolumeN\...

Managed Device Tracking

_managedDeviceIds (HashSet<string>) tracks device IDs PadForge added to the blacklist. RemoveManagedDevices() removes only these entries, leaving entries from other tools untouched.

DOS Device Path Conversion

Whitelist requires DOS device paths (\Device\HarddiskVolumeN\...), not regular paths (C:\...). ToDosDevicePath() converts via QueryDosDeviceW.

Device Instance ID Conversion

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


Elevation Strategy

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

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


Error Handling and Rollback

General Strategy

All methods use try/finally for cleanup: temp directories always deleted, script files deleted after execution, log files deleted after reading.

vJoy-Specific

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 Rollback

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

Clone this wiki locally