msi-mode-utility.ps1
|
<#PSScriptInfo .VERSION 1.4.0 .GUID 01a3b6f9-f755-49e4-8cff-86c95dc4cb83 .AUTHOR vadyaravadim .COMPANYNAME .COPYRIGHT Copyright (c) 2026 vadyaravadim .TAGS Windows Windows10 Windows11 Gaming MSI MessageSignaledInterrupts DPC Latency PCI GPU Performance Tweak PSEdition_Desktop PSEdition_Core .LICENSEURI https://github.com/vadyaravadim/msi-mode-utility/blob/main/LICENSE .PROJECTURI https://github.com/vadyaravadim/msi-mode-utility .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES https://github.com/vadyaravadim/msi-mode-utility/blob/main/CHANGELOG.md .PRIVATEDATA #> <# .SYNOPSIS Native MSI (Message Signaled Interrupts) mode manager for PCI devices. .DESCRIPTION Lists PCI devices, shows their MSI status in Out-GridView, and enables MSI for the devices you select. Zero external dependencies. Windows PowerShell 5.1+. .PARAMETER ShowAll Show every PCI device with an Interrupt Management key, including storage controllers and bridges (hidden by default). Picking a storage controller asks first: if it runs your boot drive and fails in the new mode, Windows may not start, Safe Mode included. .PARAMETER Disable Set the selected devices to MSI OFF instead of ON. .PARAMETER Status Print the device list with the interrupt mode each device actually runs in, change nothing. Does not need Administrator rights. .NOTES A reboot is required for changes to take effect. Revert: apply the msi_undo_*.reg file written before each change. Each undo file is a per-run snapshot: after several runs touching the same device, apply them newest-to-oldest - only the oldest file holds the original state. (-Disable writes an explicit 0; it does not restore the original "value absent" state - only the undo file does.) .EXAMPLE .\msi-mode-utility.ps1 Double-click Run.bat, or right-click this file > Run with PowerShell. No parameters needed - it elevates itself. .EXAMPLE .\msi-mode-utility.ps1 -Status .EXAMPLE .\msi-mode-utility.ps1 -ShowAll .EXAMPLE .\msi-mode-utility.ps1 -Disable .LINK https://github.com/vadyaravadim/msi-mode-utility #> [CmdletBinding()] param( [switch]$ShowAll, [switch]$Disable, [switch]$Status, [switch]$Elevated # internal: set by the self-elevation relaunch ) # Keep the self-elevated window open so the user can read the output. function Wait-IfElevatedWindow { if ($Elevated) { Read-Host "Press Enter to close" | Out-Null } } # Without this, an unhandled error closes the self-elevated window before # the user can read the message. trap { Write-Host "ERROR: $_" -ForegroundColor Red Wait-IfElevatedWindow # Under `irm | iex` this runs inside the user's own session, where `exit` # would close their console - rethrow so only the piped script stops. if ($PSCommandPath) { exit 1 } break } # Mode switches forwarded on every relaunch (the irm|iex bootstrap rerun below # and the self-elevation later) - one list so neither path can silently drop one. function Get-ForwardedSwitchList { $a = @() if ($ShowAll) { $a += '-ShowAll' } if ($Disable) { $a += '-Disable' } if ($Status) { $a += '-Status' } $a } # Launched via `irm <url> | iex` - no file on disk. The undo .reg is written # next to the script, so a stable path is required: save the script to the # user profile (not TEMP - the undo file must survive automatic temp cleanup) # and rerun it from there (the rerun handles elevation). if (-not $PSCommandPath) { # The piped text is not recoverable from inside iex ($MyInvocation there # holds the caller's command line, not the script body) - download the # script. try { $body = Invoke-RestMethod 'https://github.com/vadyaravadim/msi-mode-utility/releases/latest/download/msi-mode-utility.ps1' -TimeoutSec 30 -ErrorAction Stop } catch { Write-Host "ERROR: could not download the script ($($_.Exception.Message)). Check your internet connection, or save the script to a file and run it from there." -ForegroundColor Red return } $saved = Join-Path $env:USERPROFILE 'msi-mode-utility.ps1' if ((Test-Path $saved) -and ([IO.File]::ReadAllText($saved) -cne $body)) { Copy-Item $saved "$saved.bak" -Force -ErrorAction Stop Write-Host "Existing $saved differs - previous copy kept as $saved.bak" -ForegroundColor Yellow } # UTF8Encoding($false) = no BOM: a BOM would break a later `irm | iex` of # the saved copy and violates the ASCII/no-BOM invariant the repo enforces. [IO.File]::WriteAllText($saved, $body, [Text.UTF8Encoding]::new($false)) Write-Host "Script saved to: $saved (the undo file will be written next to it)" -ForegroundColor Cyan # @(): a single forwarded switch unrolls to a scalar, and splatting a # scalar string breaks powershell.exe -File switch binding on PS 5.1. $fwd = @(Get-ForwardedSwitchList) powershell -NoProfile -ExecutionPolicy Bypass -File $saved @fwd # The rerun's exit code stays in $LASTEXITCODE for scripted callers. return } # Only now: under `irm | iex` the block above runs in the caller's own session, # where Stop would stay behind in their console after the script is done. $ErrorActionPreference = 'Stop' # ---- Everything below -Status writes the registry: Administrator required ---- $principal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $Status -and -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "Not running as Administrator. Requesting elevation..." -ForegroundColor Yellow try { # Always powershell.exe (not pwsh) so Out-GridView is guaranteed. $argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-Elevated') + (Get-ForwardedSwitchList) Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs } catch { # Not always a refusal (UAC service disabled, ...) - show the real cause. Write-Host "ERROR: elevation failed ($($_.Exception.Message)). Run this script as Administrator." -ForegroundColor Red Read-Host "Press Enter to close" | Out-Null } return } # Read from this file's own PSScriptInfo block - the one place the version # lives. 0.0.0 is the committed placeholder: a clone or ZIP of main, not a # release. $version = [regex]::Match((Get-Content -LiteralPath $PSCommandPath -Raw), '(?m)^\.VERSION\s+(\S+)').Groups[1].Value $version = if ($version -eq '0.0.0') { 'dev build' } else { "v$version" } Write-Host "" Write-Host "===================================" -ForegroundColor Cyan Write-Host " MSI MODE UTILITY $version" -ForegroundColor Cyan Write-Host "===================================" -ForegroundColor Cyan Write-Host "" $serverLevelsKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels' $serverLevels = Get-ItemProperty -LiteralPath $serverLevelsKey -ErrorAction SilentlyContinue $serverCore = $serverLevels -and $serverLevels.'Server-Gui-Shell' -ne 1 if (-not $Status -and ($serverCore -or -not (Get-Command Out-GridView -ErrorAction SilentlyContinue))) { Write-Host "Out-GridView is not available in this PowerShell. It needs a Windows edition with a desktop (not Server Core); on a desktop edition, run the script with Windows PowerShell (powershell.exe)." -ForegroundColor Red Wait-IfElevatedWindow return } # Latency-critical classes kept when -ShowAll is NOT set. Matched by ClassGUID, # not display name: names are localized and OEM-specific (a real xHCI controller # can be named "(Intel(R),3.20,1.20)"), so keyword matching silently misses devices. $IncludeClassGuids = @( '{4d36e968-e325-11ce-bfc1-08002be10318}', # Display (GPU) '{4d36e972-e325-11ce-bfc1-08002be10318}', # Net (NIC) '{4d36e96c-e325-11ce-bfc1-08002be10318}', # Media (sound cards) '{36fc9e60-c465-11cf-8056-444553540000}' # USB host controllers ) Write-Host "Scanning PCI devices..." -ForegroundColor Cyan # Present devices only: Enum\PCI also keeps the keys of removed hardware (an # old GPU), and writing those changes nothing. Windows resolves the names. $present = @(Get-PnpDevice -PresentOnly) $names = @{} $classOf = @{} foreach ($d in $present) { $names[$d.InstanceId] = $d.FriendlyName $classOf[$d.InstanceId] = $d.ClassGuid } # The interrupts Windows actually assigned this boot - what the device runs # in, whatever MSISupported says: a negative IRQ number is a message-signaled # vector, a non-negative one a legacy line that other devices may share. $irqs = @{} $lineUsers = @{} foreach ($r in Get-CimInstance Win32_PNPAllocatedResource) { if ($r.Antecedent.CimClass.CimClassName -ne 'Win32_IRQResource') { continue } $n = [int64]$r.Antecedent.IRQNumber if ($n -gt 2147483647) { $n -= 4294967296 } # WMI reports it unsigned $irqs[$r.Dependent.DeviceID] += @($n) if ($n -ge 0) { $lineUsers[$n] += @($r.Dependent.DeviceID) } } $rows = New-Object System.Collections.Generic.List[object] $hidden = 0 foreach ($dev in $present) { if ($dev.InstanceId -notlike 'PCI\*' -or -not $dev.FriendlyName) { continue } # Only devices with "Device Parameters\Interrupt Management" are listed: # MSISupported lives under it. The key does not prove MSI support. $imPath = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\$($dev.InstanceId)\Device Parameters\Interrupt Management" if (-not (Test-Path $imPath)) { continue } # HD Audio controllers register under the System class, not Media; their # locale-invariant marker is the HDAudBus service. if (-not $ShowAll -and $dev.ClassGuid -notin $IncludeClassGuids -and $dev.Service -ne 'HDAudBus') { $hidden++; continue } $msiPath = "$imPath\MessageSignaledInterruptProperties" $v = (Get-ItemProperty -Path $msiPath -Name 'MSISupported' -ErrorAction SilentlyContinue).MSISupported $setting = 'Default' if ($v -eq 1) { $setting = 'Enabled' } elseif ($v -eq 0) { $setting = 'Disabled' } $vectors = $irqs[$dev.InstanceId] $lines = @($vectors | Where-Object { $_ -ge 0 }) if ($lines) { $shared = @() foreach ($n in $lines) { foreach ($other in $lineUsers[$n]) { if ($other -ne $dev.InstanceId -and $names[$other]) { $shared += $names[$other] } } } $mode = "Line IRQ $($lines -join ',')" if ($shared) { $mode += ", shared with: $($shared -join '; ')" } } elseif ($vectors) { $mode = 'MSI' } else { $mode = '-' } $rows.Add([PSCustomObject]@{ Name = $dev.FriendlyName MSI = $setting Mode = $mode DeviceID = $dev.InstanceId RegPath = $msiPath # target key we will write }) } if ($rows.Count -eq 0) { Write-Host "No matching devices found. Try -ShowAll." -ForegroundColor Yellow Wait-IfElevatedWindow return } if ($hidden) { Write-Host "$hidden more device(s) with an Interrupt Management key (storage controllers, bridges, ...) are hidden by the default filter. Use -ShowAll to include them, and leave the controller of your boot drive alone." -ForegroundColor DarkGray } # Line-IRQ devices sort first: they are the ones MSI can change. $rows = $rows | Sort-Object Mode, Name if ($Status) { $rows | Format-Table Name, MSI, Mode -AutoSize -Wrap | Out-Host Wait-IfElevatedWindow; return } if ($Disable) { $action = 'DISABLE'; $target = 0; $label = 'OFF'; $targetState = 'Disabled' } else { $action = 'enable'; $target = 1; $label = 'ON '; $targetState = 'Enabled' } $selected = $rows | Out-GridView -Title "Select devices to $action MSI Mode (Ctrl-click for multiple)" -PassThru if (-not $selected) { Write-Host "No devices selected. No changes made." -ForegroundColor Yellow Wait-IfElevatedWindow return } # A device already at the target would still get an undo file whose # "previous" value is the tweak itself, and be reported as updated. $selected = @($selected | Where-Object MSI -ne $targetState) if (-not $selected) { Write-Host "Every selected device is already $($targetState.ToLower()) - nothing to change, no undo file written." -ForegroundColor Green Wait-IfElevatedWindow return } # A storage controller that fails in the new mode can take the boot drive with # it, and Safe Mode loads the same controller driver, so the undo file may be # out of reach. Only -ShowAll lists these classes. $storageClassGuids = @( '{4d36e96a-e325-11ce-bfc1-08002be10318}', # HDC (SATA/AHCI controllers) '{4d36e97b-e325-11ce-bfc1-08002be10318}' # SCSIAdapter (NVMe, RAID, VMD) ) $storage = @($selected | Where-Object { $classOf[$_.DeviceID] -in $storageClassGuids }) if ($storage) { Write-Host "You picked a storage controller: $(($storage | ForEach-Object Name) -join '; ')" -ForegroundColor Yellow Write-Host "If it runs your boot drive and fails in the new mode, Windows may not start, Safe Mode included. The README's Disclaimer says how to recover." -ForegroundColor Yellow $answer = Read-Host "Continue anyway? [y/N]" if ($answer -notmatch '^y') { Write-Host "Nothing changed." -ForegroundColor Green Wait-IfElevatedWindow; return } Write-Host "" } # Lightweight rollback: record the CURRENT state of every selected key into a # .reg file BEFORE changing anything. Double-clicking it reverts everything. # Undo is value-level on purpose: a "[-key]" stanza would also wipe values the # tool never wrote (e.g. MessageNumberLimit added later by a driver or tweak); # deleting just MSISupported may leave an empty key behind, which is harmless. # The suffix loop keeps two runs within the same second from clobbering # each other's undo file. $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $undoFile = Join-Path $PSScriptRoot "msi_undo_$stamp.reg" $n = 1 while (Test-Path -LiteralPath $undoFile) { $undoFile = Join-Path $PSScriptRoot ("msi_undo_{0}_{1}.reg" -f $stamp, $n++) } $undo = New-Object System.Text.StringBuilder [void]$undo.AppendLine('Windows Registry Editor Version 5.00') [void]$undo.AppendLine('') foreach ($d in $selected) { # Provider path -> raw path for the .reg format $raw = $d.RegPath -replace '^.*Registry::', '' $old = (Get-ItemProperty -Path $d.RegPath -Name 'MSISupported' -ErrorAction SilentlyContinue).MSISupported [void]$undo.AppendLine("[$raw]") if ($null -eq $old) { [void]$undo.AppendLine('"MSISupported"=-') # value was absent -> delete it } else { [void]$undo.AppendLine(('"MSISupported"=dword:{0:x8}' -f [int]$old)) } [void]$undo.AppendLine('') } Set-Content -LiteralPath $undoFile -Value $undo.ToString() -Encoding Unicode Write-Host "Undo file saved: $undoFile (double-click it to revert, then reboot)" -ForegroundColor Cyan $updated = 0 $failed = 0 foreach ($d in $selected) { try { if (-not (Test-Path $d.RegPath)) { New-Item -Path $d.RegPath -Force | Out-Null # create subkey if absent } New-ItemProperty -Path $d.RegPath -Name 'MSISupported' ` -Value $target -PropertyType DWord -Force | Out-Null Write-Host (" [{0}] {1}" -f $label, $d.Name) -ForegroundColor Green $updated++ } catch { Write-Host (" [ERR] {0}: {1}" -f $d.Name, $_) -ForegroundColor Red $failed++ } } Write-Host "" Write-Host "Done. $updated of $(@($selected).Count) device(s) updated." -ForegroundColor Green if ($failed) { # The undo file lists the failed devices too; reverting an unchanged value is a no-op. Write-Host "$failed device(s) failed - see errors above." -ForegroundColor Yellow } if ($updated) { Write-Host "REBOOT REQUIRED for changes to take effect." -ForegroundColor Green } if ($updated -and -not $Disable) { Write-Host "" Write-Host "Useful? A star on GitHub helps others find it: https://github.com/vadyaravadim/msi-mode-utility" } Wait-IfElevatedWindow |