msi-mode-utility.ps1

<#PSScriptInfo

.VERSION 1.2.0

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

[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 (release.yml stamps the tag into it). 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 ""

# Out-GridView exists only on Windows editions with a desktop - Server Core has
# none; fail up front with instructions instead of a raw CommandNotFound mid-run.
# -Status prints to the console and does not need it.
if (-not $Status -and -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 = @{}
foreach ($d in $present) { $names[$d.InstanceId] = $d.FriendlyName }

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

    # MSI-capable devices expose "Device Parameters\Interrupt Management".
    $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
    }

    # Absent key/value = no explicit override: the driver default applies,
    # and MSI-X-capable devices may already run in MSI-X mode regardless.
    $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 MSI-capable device(s) (storage controllers, bridges, ...) are hidden by the default filter. Use -ShowAll to include them." -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
}

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