-
Notifications
You must be signed in to change notification settings - Fork 6
Driver Installation Internals
PadForge v3 deals with three drivers/services and a legacy v2 cleanup path:
-
HIDMaestro is the user-mode UMDF2 driver that publishes every virtual gamepad except Keyboard+Mouse. It is not installed by
DriverInstaller. The driver binaries, INF, profiles, and signing tools all ship insideHIDMaestro.Core.dll;HMContext.InstallDriver()(called from the engine on startup) registers them with Windows. -
HidHide is the kernel-mode driver that hides physical controllers from games. Embedded as a WiX Burn bootstrapper EXE, install/uninstall via
msiexec. -
Windows MIDI Services is downloaded on demand from GitHub releases (the installer is ~210 MB, too large to embed) and run with
/install /quiet /norestart. - Legacy v2 driver cleanup offers to uninstall ViGEmBus and vJoy on first v3 launch when either is detected. v2 used those two drivers as PadForge's virtual-controller backends. v3 replaces both with HIDMaestro.
Driver-side code lives in two files:
-
PadForge.App/Common/DriverInstaller.cs(PadForge.Common) handles HidHide + Windows MIDI Services install/uninstall, plus the legacy v2 ViGEmBus and vJoy uninstall paths. -
PadForge.App/Common/Input/InputManager.Step5.VirtualDevices.csownsEnsureHMaestroContext(), which calls into the HM SDK to register the HIDMaestro driver with Windows.
- Architecture Overview
- Embedded Resources
- Shared Helpers
- HIDMaestro
- HidHide
- Windows MIDI Services
- Legacy v2 driver cleanup (ViGEmBus, vJoy)
- HidHide Runtime API (HidHideController)
- Uninstall Guards
- Elevation Strategy
- Temp Directories
- Error Handling and Rollback
graph TD
subgraph Engine["InputManager.Step5"]
HM["EnsureHMaestroContext()<br/>HMContext.InstallDriver()"]
end
subgraph DriverInstaller["DriverInstaller (static class)"]
direction TB
HH["HidHide<br/>Embedded EXE bootstrapper"]
MS["Windows MIDI Services<br/>GitHub releases API download"]
LC["Legacy v2 cleanup<br/>(detect + uninstall ViGEmBus, vJoy if present)"]
end
HM -->|"InstallDriver() inside HIDMaestro.Core.dll"| HM_DRV["HIDMaestro UMDF2 driver<br/>(225+ profiles bundled in the SDK)"]
HH -->|"embedded HidHide_1.5.230_x64.exe<br/>/extract -> msiexec /i HidHide.msi"| HH_DRV["HidHide kernel driver"]
MS -->|"GitHub /releases -> SDK Runtime x64 EXE -> /install"| MS_SVC["Windows MIDI Services<br/>(Win11 24H2+)"]
LC -.->|"msiexec /x ViGEm,<br/>cmd script for vJoy<br/>(pnputil /remove-device, sc, reg, rmdir)"| OLD["ViGEmBus / vJoy<br/>(only if detected)"]
HM -.-> ELEV["PadForge process<br/>(elevated via app.manifest)"]
HH -.-> ELEV
MS -.-> ELEV
LC -.-> ELEV
app.manifest declares <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>, so Windows shows the UAC shield on the icon and prompts once when the process starts. Every install/uninstall path runs inside the already-elevated process. There are no further UAC prompts mid-session.
The OpenXInput shim (xinput1_4.dll under Resources/OpenXInput/x64/) is not managed by DriverInstaller. It ships embedded inside PadForge.exe as a <Content> item bundled by IncludeNativeLibrariesForSelfExtract. App.xaml.cs calls SetDllDirectory on the single-file extract directory so the loader resolves PadForge's copy ahead of C:\Windows\System32\xinput1_4.dll. Nothing to install or uninstall, the shim is removed when you delete the PadForge folder.
devobj.dll is deliberately not shipped with the EXE. OpenXInput's source tree contains a stub devobj.dll (every export returns 0xCDCDCDCD) only to satisfy xinput1_4.dll's static-link import at compile time. Shipping it would let SetDllDirectory pre-empt C:\Windows\System32\devobj.dll for the entire process, including setupapi.dll's own DevObj* imports. setupapi then crashes during HID class enumeration. See PadForge #69. The system devobj.dll resolves from System32 unaided.
| Resource | Type | Approximate size | Purpose |
|---|---|---|---|
HIDMaestro.Core.dll (referenced via HintPath, not embedded) |
Managed assembly | varies by version | HIDMaestro SDK and bundled UMDF2 driver. Loaded by the CLR; HMContext.InstallDriver() registers the driver with Windows on first engine start. |
Resources\HidHide_1.5.230_x64.exe |
EXE (WiX Burn bootstrapper) | ~3 MB | HidHide kernel driver. Bundled MSI extracted and run silently. |
Resources\OpenXInput\x64\xinput1_4.dll |
DLL (Content) | ~150 KB | OpenXInput shim. Not an installer. Bundled into the single-file EXE via IncludeNativeLibrariesForSelfExtract and loaded via SetDllDirectory on the extract directory at runtime. |
Windows MIDI Services is not embedded. It is downloaded on demand from api.github.com/repos/microsoft/MIDI/releases (~210 MB). The download path is ephemeral. Nothing is bundled with PadForge.
Declared in PadForge.App.csproj:
<Reference Include="HIDMaestro.Core">
<HintPath>Resources\HIDMaestro\HIDMaestro.Core.dll</HintPath>
</Reference>
<EmbeddedResource Include="Resources\HidHide_1.5.230_x64.exe" />
<Content Include="Resources\OpenXInput\x64\xinput1_4.dll" Link="xinput1_4.dll" />HIDMaestro.Core.dll is a <Reference>, not a <ProjectReference>. Using a project reference would build from source and pull in unstable in-progress work from the HIDMaestro repo. Updates happen by copying the Release build of HIDMaestro.Core.dll from the HIDMaestro repo into Resources\HIDMaestro\ after a tag is cut there. PadForge currently ships HIDMaestro 1.3.12.
Private methods reused across HidHide and the MIDI Services 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 the WiX bootstrapper with /extract to unpack its MSI into {tempDir}\Extracted\. Recreates the directory if it exists. 60s timeout. |
FindMsi |
(string extractDir, string primaryName, string fallbackPattern) |
Searches recursively for an MSI: exact name first, then glob fallback. Throws FileNotFoundException if neither matches. |
RunElevated |
(string fileName, string arguments) |
Launches a child process with Verb = "runas", hidden window, 180s timeout. PadForge is already elevated via app.manifest, so Windows does not show a UAC prompt when launching the child. Used by HidHide install/uninstall and the legacy vJoy uninstall script. |
RunMsiElevated |
(string arguments) |
Wrapper: RunElevated("msiexec.exe", arguments). |
CleanupTempDir |
(string tempDir) |
Recursive delete, swallows all exceptions. Called in finally blocks. |
FindUninstallProductCode |
(string displayNameSubstring) |
Scans HKLM\...\Uninstall (Registry64 + Registry32) for a DisplayName containing the substring. Returns the subkey name (the MSI ProductCode GUID {XXXXXXXX-...}) or null. Used for ViGEmBus uninstall, where PadForge does not embed the MSI. |
PadForge does not ship a separate HIDMaestro installer EXE or MSI, and DriverInstaller has no InstallHIDMaestro / UninstallHIDMaestro methods. The HM SDK assembly (HIDMaestro.Core.dll) bundles the UMDF2 driver binaries, INF, signing tools, and 225+ device profiles. Driver registration happens at engine-start time via the SDK.
InputManager.Step5.VirtualDevices.cs owns the HM lifecycle:
private void EnsureHMaestroContext()
{
if (_hmaestroContext != null || _hmaestroContextFailed) return;
lock (_hmaestroContextLock)
{
if (_hmaestroContext != null || _hmaestroContextFailed) return;
try
{
// Preflight: sweep leftover HM virtuals from prior sessions
// (crash / forced kill / ungraceful exit). Without this,
// InstallDriver's RemoveOldDriverPackages step fails with
// "device using INF" because stale device nodes still
// reference the old driver package.
try { HMContext.RemoveAllVirtualControllers(); } catch { }
var ctx = new HMContext();
int n = ctx.LoadDefaultProfiles();
ctx.InstallDriver();
_hmaestroContext = ctx;
// ... ProcessExit hook to purge VCs on ungraceful shutdown
}
catch (Exception ex)
{
_hmaestroContextFailed = true;
RaiseError("Failed to initialize HIDMaestro.", ex);
}
}
}InstallDriver() is idempotent and safe to call every Start(). Elevation is required, supplied by app.manifest.
SettingsPage.xaml shows a fixed green "Installed" dot for HIDMaestro and the SDK assembly version (read once at startup):
private static string GetEmbeddedHidMaestroVersion()
{
var asm = typeof(HIDMaestro.HMContext).Assembly;
var v = asm.GetName().Version;
return v != null ? $"v{v.Major}.{v.Minor}.{v.Build}" : string.Empty;
}There are no Install or Uninstall buttons. The card is informational only because the SDK assembly is always present in the publish output. Comment in MainWindow.xaml.cs (line 1593) documents this:
// HIDMaestro is always available (embedded in HIDMaestro.Core.dll), so the
// Xbox / PlayStation / Extended categories are always enabled. MIDI still
// depends on Windows MIDI Services.HMContext.RemoveAllVirtualControllers() is called in three places:
| Site | When | Purpose |
|---|---|---|
EnsureHMaestroContext preflight |
Before each InstallDriver()
|
Purge stragglers from a prior crash so the install can succeed. |
ProcessExit hook |
Process teardown | Safety net for ungraceful exits where HMController.Destroy() did not run. Skipped when _cleanShutdownPerformed is set by the normal Stop path. |
App.xaml.cs final shutdown |
App close | Mirrors the ProcessExit hook for graceful exits. |
PadForge does not expose a "remove HIDMaestro driver" path, and HIDMaestro is not designed to be removed like an ordinary user-mode driver. Removing it through Device Manager or pnputil /delete-driver is not supported and may leave the system in an inconsistent state. Deleting the PadForge folder leaves HM registered but inert. It only services PadForge's virtual-device creation requests, so an unused driver is effectively dormant.
public static void InstallHidHide()Extract embedded HidHide_1.5.230_x64.exe to %TEMP%\PadForge_HidHide\, run /extract to unpack the MSI, locate HidHide.msi (with HidHide*.msi glob fallback), then run msiexec /i "{msiPath}" /qb /norestart via RunMsiElevated. Temp cleanup in finally.
public static void UninstallHidHide()Same extraction flow as install, then msiexec /x "{msiPath}" /qb /norestart via RunMsiElevated. Temp cleanup in finally.
public static bool IsHidHideInstalled()
public static string GetHidHideVersion()Both delegate to TryGetHidHideMsiInfo(out displayVersion, out productCode), which scans Uninstall keys (Registry64 + Registry32) for "HidHide" or "HID Hide" in DisplayName (case-insensitive). Returns DisplayVersion and the subkey name (the MSI ProductCode GUID) when matched. GetHidHideVersion() falls back to "Installed" when DisplayVersion is null/empty.
The installer is ~210 MB so it is not embedded. It is downloaded from the GitHub API 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 via app.manifest. Using Verb = "runas" when already elevated throws Win32Exception on some systems, so MIDI uses a direct Process.Start.
Post-install: calls MidiVirtualController.ResetAvailability() so the cached SDK availability 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 the first match. Throws InvalidOperationException if none found.
Asset pattern: Windows.MIDI.Services.SDK.Runtime.and.Tools.*-x64.exe
public static void UninstallMidiServices()Calls FindMidiServicesUninstallString() to retrieve the registry UninstallString. Parses quoted/unquoted exe paths and any preserved arguments, appends /quiet /norestart, launches hidden via Process.Start, waits up to 5 minutes. Throws InvalidOperationException if no uninstall entry is found.
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 the registry for the WiX Burn bootstrapper entry, not SDK runtime availability (that lives in MidiVirtualController.IsAvailable()).
PadForge v2 used ViGEmBus (Xbox / DS4 virtuals) and vJoy (everything else) as separate drivers. v3 replaces both with HIDMaestro. To keep upgrading users from carrying around dead drivers, MainWindow.xaml.cs::MaybeOfferLegacyDriverCleanup() runs once on first v3 launch.
The dialog only fires when both:
-
_viewModel.Settings.LegacyDriverCleanupOfferedisfalse(per-user once-only flag persisted inPadForge.xml), and - At least one of
DriverInstaller.IsExtendedInstalled()(vJoy) orDriverInstaller.GetViGEmVersion() != null(ViGEmBus) returns truthy.
If neither legacy driver is detected, the flag is flipped to true and the offer is silently skipped. Otherwise PadForge raises a Wpf.Ui.Controls.MessageBox titled "Legacy Driver Cleanup", listing the detected legacy drivers, and offers Uninstall / Keep buttons. On Uninstall, both UninstallViGEmBus() and UninstallVJoy() run inline (try/catch wraps the pair so a failure in one still attempts the other, and any exception surfaces a follow-up "encountered an error" dialog). The flag is then flipped to true regardless of outcome to avoid re-prompting on every launch.
The whole entry point is wrapped in a top-level try/catch that swallows everything because it runs as async void from the dispatcher. An unhandled exception there would surface as a generic "unexpected error" dialog at startup. On detection failure, the flag is not flipped, so the next launch retries.
public static bool IsExtendedInstalled()Two-path detection:
| Path | Method | Detail |
|---|---|---|
| Primary | Check C:\Program Files\vJoy\vjoy.sys
|
File existence test. Catches v2 PadForge's minimal SetupAPI install. |
| Fallback | GetVJoyVersionFromRegistry() |
Scans Uninstall keys (Registry64 + Registry32) for "vJoy" in DisplayName. Catches legacy Inno Setup installs. |
public static string GetViGEmVersion()Scans Uninstall keys (Registry64 + Registry32) for "ViGEm" in DisplayName (case-insensitive). Returns DisplayVersion, or null if not found.
public static void UninstallViGEmBus()Calls FindUninstallProductCode("ViGEm") to look up the MSI ProductCode GUID. If found, runs msiexec /x {ProductCode} /qb /norestart via RunMsiElevated. No-op if no ProductCode is found.
Registry-based lookup means PadForge does not have to embed the 6 MB ViGEmBus installer. The MSI is already on the user's machine via Windows Installer cache.
public static void UninstallVJoy()Builds a .cmd script at %TEMP%\PadForge_vjoy_uninstall.cmd and runs it via RunElevated("cmd.exe", "/c \"{scriptPath}\""), then calls CleanExtendedRegistryArtifacts() in-process for the registry mop-up. Script deletion in finally.
The script is generated dynamically because the OEM .inf names are determined at runtime by FindExtendedOemInfs().
flowchart TD
A[FindExtendedOemInfs<br/>detect OEM .inf files] --> B
B[Generate PadForge_vjoy_uninstall.cmd] --> C
C["RunElevated cmd.exe /c script<br/>(elevation inherited from PadForge)"]
subgraph CMD["Batch Script Execution"]
direction TB
U1["pnputil /remove-device ROOT\\HIDCLASS\\NNNN /subtree<br/>for NNNN in 0000..0015"] --> U2
U2["timeout /t 2"] --> U3
U3["sc stop vjoy"] --> U4
U4["timeout /t 2"] --> U5
U5["sc delete vjoy"] --> U6
U6["reg delete HKLM\\SYSTEM\\{CurrentControlSet,ControlSet001..003}\\Services\\vjoy /f<br/>(belt-and-braces if sc delete returned STOP_PENDING)"] --> U7
U7["pnputil /delete-driver oemNN.inf /uninstall /force<br/>per stale OEM inf"] --> U8
U8["rmdir /s /q C:\\Program Files\\vJoy<br/>del System32\\drivers\\vjoy.sys"] --> U9
U9["powershell one-liner: scan HKLM Uninstall + WOW6432Node Uninstall<br/>for DisplayName -like '*vJoy*' and Remove-Item -Recurse -Force"]
end
C --> CMD
CMD --> D[CleanExtendedRegistryArtifacts<br/>C# method, no further elevation needed]
D --> E[Delete .cmd file]
Step ordering matters: device nodes are removed before sc stop so the driver can fully unload from the kernel. Stopping the service while devices are still attached leaves it in STOP_PENDING, which is irrecoverable without a reboot.
private static void CleanExtendedRegistryArtifacts()Removes registry keys that can cause a reinstall of vJoy to hang. Best-effort (throwOnMissingSubKey: false, per-key try/catch).
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\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\%SystemRoot%/System32/drivers/hidkmdf.sys |
Same under WOW6432Node |
SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Setup\PnpLockdownFiles\%SystemRoot%/System32/drivers/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 |
After the path list, calls CleanExtendedDeviceClassEntries().
private static void CleanExtendedDeviceClassEntries()Enumerates subkeys under HKLM\SYSTEM\ControlSet001\Control\Class\{781ef630-72b2-11d2-b852-00c04fad5101} (the shared HID device class key) and deletes any whose Class value equals "vjoy" (case-insensitive). Best-effort. Exceptions caught silently.
private static string[] FindExtendedOemInfs()Runs pnputil.exe /enum-drivers (30s timeout, output captured), then walks the output line-by-line tracking the most recent Published Name : oemXX.inf and watching each block for "shaul" or "vjoy" (case-insensitive). Returns matching oem*.inf names; returns an empty array on any error. Used by UninstallVJoy() to know which pnputil /delete-driver lines to emit.
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 \.
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 |
|---|---|---|
| HidHide | Any device has HidHide hiding enabled | HasAnyHidHideDevices |
| MIDI Services | Any created slot uses MIDI | HasAnyMidiSlots |
Guards are Func<bool> delegates on SettingsViewModel, injected by MainWindow.xaml.cs. RefreshDriverGuards() re-evaluates CanExecute on the uninstall commands after slot creation, deletion, or type changes.
HIDMaestro has no Install or Uninstall command in v3 (the SDK assembly is always available), so there is no guard to enforce. The legacy v2 cleanup dialog is single-shot and does not appear on the Settings page, so it has no guard either.
PadForge requires elevation unconditionally. app.manifest declares:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>Windows shows the UAC shield on the icon and prompts once when the process starts. The previous v2 approach of relaunching mid-session via Verb = "runas" (and the conditional auto-elevation tied to vJoy presence) is gone. Every code path that needs admin runs inside the already-elevated process.
| Scenario | Elevation Method | UAC Prompts |
|---|---|---|
| PadForge launch |
app.manifest requireAdministrator
|
1 (per launch, if UAC is enabled) |
HMContext.InstallDriver() (HM driver register) |
App already elevated | 0 |
| HidHide install/uninstall |
msiexec via RunElevated (child inherits PadForge's elevation) |
0 |
| MIDI Services install | Direct Process.Start (no runas to avoid Win32Exception on already-elevated processes) |
0 |
| Legacy ViGEmBus uninstall |
msiexec /x {ProductCode} via RunElevated
|
0 |
| Legacy vJoy uninstall |
cmd.exe /c {script} via RunElevated
|
0 |
| Driver runtime operations (HM device lifecycle, HidHide whitelist edits, etc.) | App already elevated | 0 |
| Driver | Temp Directory |
|---|---|
| HidHide | %TEMP%\PadForge_HidHide\ |
| MIDI Services | %TEMP%\PadForge_MidiServices\ |
| Legacy vJoy uninstall script | %TEMP%\PadForge_vjoy_uninstall.cmd |
All cleaned up after each operation via CleanupTempDir() or direct File.Delete() in finally blocks.
HIDMaestro has no temp directory because PadForge does not unpack any installer for it.
All install/uninstall methods use try/finally for cleanup: temp directories always deleted, script files deleted after execution.
| Path | Error Strategy |
|---|---|
HIDMaestro InstallDriver()
|
Caught in EnsureHMaestroContext. On failure, sets _hmaestroContextFailed = true (sticky for the session) and calls RaiseError("Failed to initialize HIDMaestro.", ex). The engine continues running for KB+M and (if installed) MIDI categories; HM-backed slot creation is gated on the context being non-null. |
| HidHide install/uninstall | MSI installer handles its own rollback. PadForge surfaces no specific error UI; failures bubble up as exceptions. |
| MIDI Services | WiX Burn bootstrapper handles rollback. PadForge surfaces no specific error UI; HTTP and process timeouts both throw. |
| Legacy ViGEmBus uninstall | No-op when no ProductCode found. Otherwise relies on MSI rollback. |
| Legacy vJoy uninstall | The .cmd script redirects every step's stderr to nul and continues regardless (>nul 2>&1). This is best-effort: a failed pnputil line should not prevent the next sc delete. The follow-up CleanExtendedRegistryArtifacts() is also per-key best-effort. |
There is no explicit rollback machinery in DriverInstaller. On partial failure of a legacy vJoy uninstall, surviving residue (a stuck service, a stale OEM .inf) is harmless to v3 because v3 does not use vJoy at all. The next launch re-runs the legacy cleanup offer (the persisted flag only flips on successful dialog completion, not on failed Uninstall choice).
-
Virtual Controllers:
HMaestroVirtualController(Xbox / PlayStation / Extended),MidiVirtualController,KeyboardMouseVirtualControllerconsuming installed drivers -
HIDMaestro Deep Dive: HM SDK surface (
HMContext,HMProfile,HMController), thread-pool lifecycle, OpenXInput shim, bubble-up cascade, inactivity timeout -
Architecture Overview: Elevation strategy (
requireAdministratorinapp.manifest) - Build and Publish: Embedded driver resources (HidHide installer; HIDMaestro is referenced as a managed assembly)
-
Settings and Serialization: Driver status display in
SettingsViewModel -
XAML Views:
SettingsPagedriver install/uninstall buttons and guards