msi-mode-utility.ps1

<#PSScriptInfo

.VERSION 1.1.3

.GUID 01a3b6f9-f755-49e4-8cff-86c95dc4cb83

.AUTHOR vadyaravadim

.COMPANYNAME

.COPYRIGHT

.TAGS Windows Windows10 Windows11 Gaming MSI MessageSignaledInterrupts DPC Latency PCI GPU Performance Tweak

.LICENSEURI https://github.com/vadyaravadim/msi-mode-utility/blob/main/LICENSE

.PROJECTURI https://github.com/vadyaravadim/msi-mode-utility

.ICONURI

.EXTERNALMODULEDEPENDENCIES

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES

.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 MSI-capable PCI device, including bridges and abstract
    controllers (hidden by default).
.PARAMETER Disable
    Set the selected devices to MSI OFF instead of ON.
.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.)
#>

[CmdletBinding()]
param(
    [switch]$ShowAll,
    [switch]$Disable,
    [switch]$Elevated   # internal: set by the self-elevation relaunch
)

$ErrorActionPreference = 'Stop'

# 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' }
    $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://raw.githubusercontent.com/vadyaravadim/msi-mode-utility/main/msi-mode-utility.ps1' -TimeoutSec 30
    } 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
        Write-Host "Existing $saved differs - previous copy kept as $saved.bak" -ForegroundColor Yellow
    }
    [IO.File]::WriteAllText($saved, $body, [Text.Encoding]::UTF8)
    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
}

$principal = New-Object Security.Principal.WindowsPrincipal(
    [Security.Principal.WindowsIdentity]::GetCurrent())
if (-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
}

# PowerShell 7 ships without Out-GridView (Server Core has none at all);
# fail up front with instructions instead of a raw CommandNotFound mid-run.
if (-not (Get-Command Out-GridView -ErrorAction SilentlyContinue)) {
    Write-Host "Out-GridView is not available in this PowerShell. Run the script with Windows PowerShell (powershell.exe), or install the Microsoft.PowerShell.GraphicalTools module." -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
)

function Get-DeviceName {
    param([Microsoft.Win32.RegistryKey]$Key)
    $fn = $Key.GetValue('FriendlyName')
    if ([string]::IsNullOrWhiteSpace($fn)) { $fn = $Key.GetValue('DeviceDesc') }
    if ($fn -and $fn -match ';') {
        # Strip the @res;Text prefix; keep the raw string if nothing follows ';'
        # (a malformed indirect string) so the device stays visible.
        $text = $fn.Split(';')[-1]
        if (-not [string]::IsNullOrWhiteSpace($text)) { $fn = $text }
    }
    return $fn
}

Write-Host "Scanning PCI devices..." -ForegroundColor Cyan
$pciRoot = 'HKLM:\SYSTEM\CurrentControlSet\Enum\PCI'
$rows = New-Object System.Collections.Generic.List[object]

$hidden = 0
foreach ($devClass in Get-ChildItem $pciRoot -ErrorAction SilentlyContinue) {
    foreach ($inst in Get-ChildItem $devClass.PSPath -ErrorAction SilentlyContinue) {
        $name = Get-DeviceName -Key $inst
        if ([string]::IsNullOrWhiteSpace($name)) { continue }

        # MSI-capable devices expose "Device Parameters\Interrupt Management".
        $imPath = Join-Path $inst.PSPath 'Device Parameters\Interrupt Management'
        if (-not (Test-Path $imPath)) { continue }

        if (-not $ShowAll) {
            # HD Audio controllers register under the System class, not Media;
            # their locale-invariant marker is the HDAudBus service.
            $classGuid = $inst.GetValue('ClassGUID')
            if ($classGuid -notin $IncludeClassGuids -and
                $inst.GetValue('Service') -ne 'HDAudBus') { $hidden++; continue }
        }

        # Absent key/value = no explicit override: the driver default applies,
        # and MSI-X-capable devices may already run in MSI-X mode regardless.
        $msiPath = Join-Path $imPath 'MessageSignaledInterruptProperties'
        $v = (Get-ItemProperty -Path $msiPath -Name 'MSISupported' -ErrorAction SilentlyContinue).MSISupported
        $status = 'Default'
        if     ($v -eq 1) { $status = 'Enabled' }
        elseif ($v -eq 0) { $status = 'Disabled' }

        $rows.Add([PSCustomObject]@{
            Name       = $name
            MSI        = $status
            DeviceID   = $inst.PSChildName
            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 MSI-capable device(s) (storage controllers, bridges, ...) are hidden by the default filter. Use -ShowAll to include them." -ForegroundColor DarkGray
}

if ($Disable) { $action = 'DISABLE'; $target = 0; $label = 'OFF' }
else          { $action = 'enable';  $target = 1; $label = 'ON ' }
$selected = $rows |
    Sort-Object MSI, Name |
    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
}

# 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 $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 -Path $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
}
Write-Host "REBOOT REQUIRED for changes to take effect." -ForegroundColor Green
Wait-IfElevatedWindow