#
.SYNOPSIS
RigCheck - read-only gaming PC performance audit for NVIDIA / AMD / Intel systems.
.DESCRIPTION
Changes nothing and needs no admin rights. Inspects Windows settings, CPU platform,
RAM (XMP/EXPO, channels), GPU (driver, PCIe link, Resizable BAR), displays (refresh
rate, which port), storage, network and background apps, then writes an HTML and a
plain-text report to the Desktop. Then it asks whether to upload the report to
rigcheck.narrowlake.dev for a shareable link, FPS estimates and before/after tracking
(user and PC names are removed first), and opens the web or local report.
Run it with games closed so the background-load check means something.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\rigcheck.ps1
#>
[CmdletBinding()]
param(
[string]$OutDir = [Environment]::GetFolderPath('Desktop'),
[switch]$NoOpen,
[switch]$Upload, # upload without asking
[switch]$NoUpload, # never upload, never ask
[string]$Server = 'https://rigcheck.narrowlake.dev'
)
# This file must stay pure ASCII: Windows PowerShell 5.1 reads BOM-less files as ANSI.
Set-StrictMode -Off
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$Version = '0.2.0'
$Findings = New-Object System.Collections.Generic.List[object]
$Specs = New-Object System.Collections.Specialized.OrderedDictionary
# Status meanings:
# FIX - misconfigured, costs real performance; fix it
# CHECK - probably worth changing, depends on the user
# VERIFY - this script cannot see it; look yourself (instructions given)
# OK - configured well
# INFO - context only
function Add-Finding([string]$Category, [string]$Check, [string]$Status, [string]$Value, [string]$Advice) {
$Findings.Add([pscustomobject]@{ Category = $Category; Check = $Check; Status = $Status; Value = $Value; Advice = $Advice })
}
function Step([string]$Name, [scriptblock]$Block) {
Write-Host (" checking {0}..." -f $Name) -ForegroundColor DarkGray
try { & $Block } catch { Add-Finding 'RigCheck' $Name 'INFO' 'This check could not run on this PC' $_.Exception.Message }
}
function Try-Get([scriptblock]$Block) { try { & $Block } catch { $null } }
function Get-Reg([string]$Path, [string]$Name) {
try { (Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction Stop).$Name } catch { $null }
}
function Get-PciLink([string]$InstanceId) {
try {
$keys = 'DEVPKEY_PciDevice_CurrentLinkSpeed', 'DEVPKEY_PciDevice_MaxLinkSpeed', 'DEVPKEY_PciDevice_CurrentLinkWidth', 'DEVPKEY_PciDevice_MaxLinkWidth'
$h = @{}
foreach ($p in (Get-PnpDeviceProperty -InstanceId $InstanceId -KeyName $keys -ErrorAction Stop)) { $h[$p.KeyName] = $p.Data }
if ($null -eq $h['DEVPKEY_PciDevice_CurrentLinkWidth']) { return $null }
[pscustomobject]@{
CurGen = [int]$h['DEVPKEY_PciDevice_CurrentLinkSpeed']; MaxGen = [int]$h['DEVPKEY_PciDevice_MaxLinkSpeed']
CurWidth = [int]$h['DEVPKEY_PciDevice_CurrentLinkWidth']; MaxWidth = [int]$h['DEVPKEY_PciDevice_MaxLinkWidth']
}
} catch { $null }
}
function Get-AgeDays($Date) { if ($Date) { [int]((Get-Date) - [datetime]$Date).TotalDays } else { $null } }
# ------------------------------------------------------------------ display enumeration
Add-Type -ErrorAction SilentlyContinue -TypeDefinition @"
using System; using System.Runtime.InteropServices;
public class RcDisp {
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct DEVMODE {
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmDeviceName;
public short dmSpecVersion, dmDriverVersion, dmSize, dmDriverExtra; public int dmFields;
public int dmPositionX, dmPositionY, dmDisplayOrientation, dmDisplayFixedOutput;
public short dmColor, dmDuplex, dmYResolution, dmTTOption, dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string dmFormName;
public short dmLogPixels; public int dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, dmDisplayFrequency;
public int dmICMMethod, dmICMIntent, dmMediaType, dmDitherType, dmReserved1, dmReserved2, dmPanningWidth, dmPanningHeight;
}
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct DISPLAY_DEVICE {
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceID;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public string DeviceKey;
}
[DllImport("user32.dll", CharSet=CharSet.Ansi)] public static extern bool EnumDisplaySettings(string dev, int mode, ref DEVMODE dm);
[DllImport("user32.dll", CharSet=CharSet.Ansi)] public static extern bool EnumDisplayDevices(string dev, int i, ref DISPLAY_DEVICE dd, int flags);
}
"@
function Get-ActiveDisplays {
$out = @()
for ($i = 0; $i -lt 16; $i++) {
$dd = New-Object RcDisp+DISPLAY_DEVICE
$dd.cb = [Runtime.InteropServices.Marshal]::SizeOf($dd)
# [NullString]::Value, not $null: PowerShell turns $null into "" for string parameters.
if (-not [RcDisp]::EnumDisplayDevices([NullString]::Value, $i, [ref]$dd, 0)) { break }
if (($dd.StateFlags -band 1) -eq 0) { continue } # not attached to desktop
$mon = New-Object RcDisp+DISPLAY_DEVICE
$mon.cb = $dd.cb
[void][RcDisp]::EnumDisplayDevices($dd.DeviceName, 0, [ref]$mon, 0)
$cur = New-Object RcDisp+DEVMODE
$cur.dmSize = [int16][Runtime.InteropServices.Marshal]::SizeOf($cur)
if (-not [RcDisp]::EnumDisplaySettings($dd.DeviceName, -1, [ref]$cur)) { continue }
$maxHzAtRes = 0; $maxW = 0; $maxH = 0; $m = 0
$dm = New-Object RcDisp+DEVMODE
$dm.dmSize = $cur.dmSize
while ([RcDisp]::EnumDisplaySettings($dd.DeviceName, $m, [ref]$dm)) {
if ($dm.dmPelsWidth -eq $cur.dmPelsWidth -and $dm.dmPelsHeight -eq $cur.dmPelsHeight -and $dm.dmDisplayFrequency -gt $maxHzAtRes) { $maxHzAtRes = $dm.dmDisplayFrequency }
if (($dm.dmPelsWidth * $dm.dmPelsHeight) -gt ($maxW * $maxH)) { $maxW = $dm.dmPelsWidth; $maxH = $dm.dmPelsHeight }
$m++
}
$out += [pscustomobject]@{
Device = $dd.DeviceName; Adapter = $dd.DeviceString; Monitor = $mon.DeviceString
Width = $cur.dmPelsWidth; Height = $cur.dmPelsHeight; Hz = $cur.dmDisplayFrequency; MaxHzAtRes = $maxHzAtRes
MaxWidth = $maxW; MaxHeight = $maxH
}
}
$out
}
# ------------------------------------------------------------------ RAM helpers
function Get-RatedSpeed([string]$Part) {
# Best-effort decode of common gaming-kit part numbers into rated MT/s.
if (-not $Part) { return $null }
$p = $Part.Trim().ToUpper()
if ($p -match '^KF[45](\d{2})C\d') { return [int]$matches[1] * 100 } # Kingston Fury: KF560C36 = 6000, KF436C17 = 3600
if ($p -match '^F[45]-(\d{4})') { return [int]$matches[1] } # G.Skill: F5-6000J3038F16G
if ($p -match '^CM\w*?M\d[A-Z](\d{4})[A-Z]\d') { return [int]$matches[1] } # Corsair: CMK32GX5M2B6000C36
if ($p -match '(\d{4})HC\d{2}') { return [int]$matches[1] } # TeamGroup: FF3D532G6000HC38A
if ($p -match '^AX[45]U(\d{4})') { return [int]$matches[1] } # ADATA XPG: AX5U6000C3016G
if ($p -match '^PV\w*?G(\d{3})C\d') { return [int]$matches[1] * 10 } # Patriot Viper: PVV532G600C36K
if ($p -match '^(BL|CP)\w*?G(\d{2})C\d{2}') { return [int]$matches[2] * 100 } # Crucial Ballistix / Pro: BL2K16G36C16, CP16G60C36
if ($p -match '(?$null | Select-Object -First 1) -split ',\s*'
$q = @(& $smi -q 2>$null)
$bar1 = $null; $vram = $null
for ($i = 0; $i -lt $q.Count; $i++) {
if ($q[$i] -match 'BAR1 Memory Usage' -and $q[$i + 1] -match ':\s*(\d+)\s*MiB') { $bar1 = [int]$matches[1] }
if ($q[$i] -match 'FB Memory Usage' -and $q[$i + 1] -match ':\s*(\d+)\s*MiB') { $vram = [int]$matches[1] }
}
[pscustomobject]@{ Name = $csv[0]; Driver = $csv[1]; PowerLimit = [double]$csv[2]; PowerDefault = [double]$csv[3]; Temp = $csv[4]; Util = [int]$csv[5]; Bar1MiB = $bar1; VramMiB = $vram }
}
}
function Get-LargestBarMiB([string]$PnpId) {
# Largest memory window assigned to a PCI device. Over 256 MiB means Resizable BAR / SAM is active.
$refs = @(Get-CimInstance Win32_PNPAllocatedResource | Where-Object { $_.Dependent.DeviceID -eq $PnpId -and $_.Antecedent.CimClass.CimClassName -eq 'Win32_DeviceMemoryAddress' })
if (-not $refs.Count) { $refs = @(Get-CimInstance Win32_PNPAllocatedResource | Where-Object { $_.Dependent.DeviceID -eq $PnpId -and $null -ne $_.Antecedent.StartingAddress }) }
if (-not $refs.Count) { return $null }
$starts = @($refs | ForEach-Object { [uint64]$_.Antecedent.StartingAddress })
$max = 0
foreach ($r in (Get-CimInstance Win32_DeviceMemoryAddress)) {
if ($starts -contains [uint64]$r.StartingAddress) {
$size = ([uint64]$r.EndingAddress - [uint64]$r.StartingAddress + 1) / 1MB
if ($size -gt $max) { $max = $size }
}
}
[int]$max
}
# ------------------------------------------------------------------ specs summary
$Specs['OS'] = if ($os) { '{0} (build {1})' -f $os.Caption, $os.BuildNumber } else { '?' }
$Specs['CPU'] = $cpuName
$Specs['Motherboard'] = if ($board) { '{0} {1}' -f $board.Manufacturer, $board.Product } else { '?' }
$Specs['BIOS'] = if ($bios) { '{0} ({1:yyyy-MM-dd})' -f $bios.SMBIOSBIOSVersion, $bios.ReleaseDate } else { '?' }
if ($dimms.Count) {
$totalGB = [math]::Round((($dimms | Measure-Object Capacity -Sum).Sum) / 1GB)
$Specs['RAM'] = '{0} GB, {1} stick(s), {2} MT/s, {3}' -f $totalGB, $dimms.Count, ($dimms | Measure-Object ConfiguredClockSpeed -Minimum).Minimum, (($dimms | ForEach-Object { $_.PartNumber.Trim() } | Select-Object -Unique) -join ' + ')
}
$Specs['GPU'] = (@($gpus | Where-Object { $_.Kind -ne 'Virtual' } | ForEach-Object {
$v = if ($_.Kind -eq 'NVIDIA' -and $nv) { $nv.Driver } else { $_.DriverVersion }
'{0} (driver {1})' -f $_.Name, $v
}) -join '; ')
$Specs['Displays'] = (@($displays | ForEach-Object { '{0}x{1} @ {2} Hz on {3}' -f $_.Width, $_.Height, $_.Hz, $_.Adapter }) -join '; ')
$Specs['Type'] = if ($isLaptop) { 'Laptop' } else { 'Desktop' }
# ================================================================== CHECKS
Step 'CPU platform' {
if ($bios -and $bios.ReleaseDate) {
$age = Get-AgeDays $bios.ReleaseDate
$val = '{0}, released {1:yyyy-MM-dd} ({2} days ago)' -f $bios.SMBIOSBIOSVersion, $bios.ReleaseDate, $age
if ($age -gt 730) { Add-Finding 'CPU' 'BIOS age' 'CHECK' $val 'BIOS is over 2 years old. Updates usually improve memory compatibility, boost behaviour and security. Download from the motherboard maker''s support page; expect XMP/EXPO to reset to off afterwards.' }
else { Add-Finding 'CPU' 'BIOS age' 'OK' $val '' }
}
# Intel 13th/14th gen Raptor Lake instability microcode (0x12B or newer)
if ($isIntel -and $cpuName -match 'i[579]-1[34]\d{3}') {
$rev = $null
$raw = Get-Reg 'HKLM:\HARDWARE\DESCRIPTION\System\CentralProcessor\0' 'Update Revision'
if ($raw -and $raw.Length -ge 8 -and [BitConverter]::ToUInt32($raw, 4) -ne 0) { $rev = [BitConverter]::ToUInt32($raw, 4) }
elseif ($raw -and $raw.Length -ge 4) { $rev = [BitConverter]::ToUInt32($raw, 0) }
if ($rev) {
$hex = '0x{0:X}' -f $rev
if ($rev -lt 0x12B) { Add-Finding 'CPU' 'Intel 13th/14th gen microcode' 'FIX' "Microcode $hex (needs 0x12B or newer)" 'Raptor Lake CPUs on old microcode can degrade permanently from high voltage. Update the BIOS to a version with microcode 0x12B or later as soon as possible.' }
else { Add-Finding 'CPU' 'Intel 13th/14th gen microcode' 'OK' "Microcode $hex" '' }
}
}
if ($isAMD -and $cpuName -match 'Ryzen') {
$chipsetSw = $uninstall | Where-Object { $_.DisplayName -match 'AMD Chipset Software' } | Select-Object -First 1
$ppm = $signed | Where-Object { $_.DeviceName -match 'AMD PPM Provisioning|AMD PSP|AMD Provisioning' } | Select-Object -First 1
if ($chipsetSw) { Add-Finding 'CPU' 'AMD chipset driver' 'OK' "Installed ($($chipsetSw.DisplayVersion))" '' }
elseif ($ppm) { Add-Finding 'CPU' 'AMD chipset driver' 'OK' "AMD platform drivers present ($($ppm.DeviceName))" '' }
else { Add-Finding 'CPU' 'AMD chipset driver' 'FIX' 'Not installed (only Windows built-in drivers found)' 'Install "AMD Chipset Drivers" for your motherboard''s chipset from amd.com/support, then reboot. It provides Ryzen power management and core scheduling.' }
if ($cpuName -match '(7900|7950|9900|9950)X3D') {
$vc = $signed | Where-Object { $_.DeviceName -match '3D V-Cache' } | Select-Object -First 1
if ($vc) { Add-Finding 'CPU' 'X3D V-Cache optimizer' 'OK' 'AMD 3D V-Cache Performance Optimizer driver present' 'Keep Xbox Game Bar installed and Game Mode on; the driver uses them to park the non-cache CCD in games.' }
else { Add-Finding 'CPU' 'X3D V-Cache optimizer' 'FIX' 'V-Cache optimizer driver missing' 'Dual-CCD X3D chips need the AMD chipset driver (3D V-Cache Performance Optimizer) plus Game Bar so games run on the cache cores. Install the chipset driver and reboot.' }
}
Add-Finding 'CPU' 'Precision Boost Overdrive / Curve Optimizer' 'VERIFY' 'Not visible from Windows' 'Optional: enabling PBO with a negative Curve Optimizer in the BIOS often gives 3-8% in CPU-bound games while running cooler. Test stability (OCCT, CoreCycler) after changing.'
}
if ($isIntel -and $cpuName -match 'i[3579]-1[2-4]\d{3}|Core\(TM\) Ultra|Core Ultra' -and $build -lt 22000) {
Add-Finding 'CPU' 'Hybrid CPU on Windows 10' 'CHECK' "Windows build $build" 'Intel P-core/E-core CPUs schedule games better on Windows 11 (Thread Director).'
}
}
Step 'power plan' {
$active = (powercfg /getactivescheme) -join ' '
$guid = if ($active -match '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})') { $matches[1] } else { '' }
$name = if ($active -match '\(([^)]+)\)\s*$') { $matches[1] } else { $guid }
$overlay = Get-Reg 'HKLM:\SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes' 'ActiveOverlayAcPowerScheme'
$overlayName = switch ($overlay) {
'961cc777-2547-4f9d-8174-7d86181b8a7a' { 'Best power efficiency' }
'ded574b5-45a0-4f42-8737-46345c09c238' { 'Best performance' }
default { 'Balanced' }
}
if ($guid -eq 'a1841308-3541-4fab-bc81-f71556f20b4a') {
Add-Finding 'Windows' 'Power plan' 'FIX' $name 'Power saver caps CPU clocks. Switch to Balanced (Control Panel > Power Options).'
} elseif ($guid -eq '381b4222-f694-41f0-9685-ff5bb260df2e') {
if ($overlay -eq '961cc777-2547-4f9d-8174-7d86181b8a7a') { Add-Finding 'Windows' 'Power mode' 'FIX' "Balanced plan, power mode: $overlayName" 'Settings > System > Power > Power mode: set to Best performance (or Balanced).' }
else { Add-Finding 'Windows' 'Power plan' 'OK' "Balanced, power mode: $overlayName" '' }
} else {
$adv = ''
if ($isAMD -and $cpuName -match 'Ryzen') { $adv = 'AMD recommends the Balanced plan for Ryzen once the chipset driver is installed; High/Ultimate can block idle states and slightly reduce boost headroom. Fine either way.' }
Add-Finding 'Windows' 'Power plan' 'INFO' $name $adv
}
# Max processor state below 100% throttles the CPU. Output is localised, so read the hex values only:
# the last two are "Current AC" and "Current DC".
$hexes = [regex]::Matches(((powercfg /q SCHEME_CURRENT SUB_PROCESSOR PROCTHROTTLEMAX) -join ' '), '0x([0-9a-fA-F]{8})')
if ($hexes.Count -ge 2) {
$ac = [Convert]::ToInt32($hexes[$hexes.Count - 2].Groups[1].Value, 16)
if ($ac -lt 100) { Add-Finding 'Windows' 'Maximum processor state' 'FIX' "$ac% when plugged in" 'The power plan limits the CPU. Power Options > Change plan settings > Advanced > Processor power management > Maximum processor state = 100%.' }
}
if ($isLaptop -and $battery.Count) {
$onBattery = @($battery | Where-Object { $_.BatteryStatus -eq 1 }).Count -gt 0
if ($onBattery) { Add-Finding 'Windows' 'Laptop power' 'CHECK' 'Running on battery' 'Laptops cut CPU and GPU power on battery. Plug in for gaming.' }
}
}
Step 'memory' {
if (-not $dimms.Count) { return }
$type = [int]$dimms[0].SMBIOSMemoryType
$ddr = switch ($type) { 26 { 'DDR4' } 34 { 'DDR5' } 24 { 'DDR3' } 35 { 'LPDDR5' } 30 { 'LPDDR4' } default { 'DDR?' } }
$cfg = [int](($dimms | Measure-Object ConfiguredClockSpeed -Minimum).Minimum)
if ($ddr -eq 'DDR5' -and $cfg -gt 0 -and $cfg -lt 3200) { $cfg *= 2 } # some firmware reports MHz, not MT/s
if ($ddr -eq 'DDR4' -and $cfg -gt 0 -and $cfg -lt 1600) { $cfg *= 2 }
$parts = @($dimms | ForEach-Object { ([string]$_.PartNumber).Trim() } | Select-Object -Unique)
$rated = Get-RatedSpeed $parts[0]
$locs = ($dimms | ForEach-Object { '{0}/{1}' -f $_.BankLabel, $_.DeviceLocator }) -join ', '
$speedVal = "$ddr running $cfg MT/s" + $(if ($rated) { " (kit rated $rated MT/s, part $($parts[0]))" } else { " (part $($parts[0]))" })
if ($isLaptop -or $ddr -like 'LP*') {
Add-Finding 'Memory' 'Memory speed' 'INFO' $speedVal 'Laptop memory speed is usually fixed by the manufacturer.'
} elseif ($rated -and $cfg -lt ($rated - 100)) {
Add-Finding 'Memory' 'XMP / EXPO' 'FIX' $speedVal 'RAM is running below its rated speed. Enable the XMP / EXPO / DOCP profile in the BIOS. On AMD Ryzen this is often the single biggest free gain.'
} elseif (-not $rated -and (($ddr -eq 'DDR4' -and $cfg -le 2666) -or ($ddr -eq 'DDR5' -and $cfg -le 5600))) {
Add-Finding 'Memory' 'XMP / EXPO' 'CHECK' $speedVal "$cfg MT/s is a default (JEDEC) speed. If your kit is rated faster (check the sticker or the product page), enable XMP / EXPO / DOCP in the BIOS."
} else {
Add-Finding 'Memory' 'XMP / EXPO' 'OK' $speedVal ''
}
if ($dimms.Count -eq 1) {
Add-Finding 'Memory' 'Dual channel' $(if ($isLaptop) { 'CHECK' } else { 'FIX' }) "1 stick ($locs)" 'A single stick runs in single-channel mode, which can cost 10-30% in CPU-bound games. Add a matching second stick.'
} elseif ($dimms.Count -eq 2) {
$ch = @($dimms | ForEach-Object { Get-RamChannel $_ })
if ($ch[0] -and $ch[1] -and $ch[0] -eq $ch[1]) { Add-Finding 'Memory' 'Dual channel' 'FIX' "Both sticks report channel $($ch[0]) ($locs)" 'Both sticks appear to be in the same channel. Move them to the slots the motherboard manual recommends for two sticks (usually A2 + B2, the 2nd and 4th slots from the CPU).' }
else { Add-Finding 'Memory' 'Dual channel' 'OK' "2 sticks ($locs)" '' }
} elseif ($dimms.Count -eq 3) {
Add-Finding 'Memory' 'Dual channel' 'CHECK' "3 sticks ($locs)" 'An odd stick count runs partly single-channel. Use 2 or 4 matching sticks.'
} elseif ($dimms.Count -ge 4 -and $ddr -eq 'DDR5') {
Add-Finding 'Memory' 'Stick count' 'INFO' "4 sticks of DDR5 ($locs)" 'Four DDR5 sticks often cannot run their rated XMP/EXPO speed. Two larger sticks are faster than four smaller ones.'
}
if ($parts.Count -gt 1) { Add-Finding 'Memory' 'Matched kit' 'CHECK' ($parts -join ' + ') 'Mixed RAM models often will not run XMP/EXPO stably, or force the slowest timings. A single matched kit is best.' }
$totalGB = [math]::Round((($dimms | Measure-Object Capacity -Sum).Sum) / 1GB)
if ($totalGB -lt 16) { Add-Finding 'Memory' 'Capacity' 'CHECK' "$totalGB GB" 'Modern games want 16 GB; 32 GB if you stream or keep a browser and Discord open.' }
if ($os) {
$usedPct = [int](100 - ($os.FreePhysicalMemory / $os.TotalVisibleMemorySize * 100))
if ($usedPct -ge 85) { Add-Finding 'Memory' 'Memory in use now' 'CHECK' "$usedPct% used" 'RAM is nearly full, so Windows will page to disk and games stutter. Close heavy apps, or this was measured while a game ran.' }
$reservedMB = [int](($dimms | Measure-Object Capacity -Sum).Sum / 1MB - $os.TotalVisibleMemorySize / 1KB)
if ($dgpus.Count -and $igpus.Count -and -not $isLaptop -and $reservedMB -gt 400) {
Add-Finding 'Memory' 'Integrated GPU reservation' 'INFO' "$reservedMB MB of RAM reserved for hardware/iGPU" 'The CPU''s integrated graphics is enabled and reserves system RAM. If no monitor uses the motherboard ports, you can disable the iGPU in the BIOS.'
}
}
if ($isAMD -and $cpuName -match 'Ryzen' -and -not $isLaptop) {
if ($ddr -eq 'DDR5') {
Add-Finding 'Memory' 'Memory controller ratio (UCLK)' 'VERIFY' 'Not visible from Windows' 'Run ZenTimings (free, zentimings.protonrom.com). UCLK must equal MCLK (e.g. 3000 / 3000 at 6000 MT/s). If UCLK is half of MCLK, set "UCLK DIV1 MODE" to "UCLK=MEMCLK" in the BIOS. Above 6400 MT/s, half is expected.'
} elseif ($ddr -eq 'DDR4') {
Add-Finding 'Memory' 'Infinity Fabric ratio (FCLK)' 'VERIFY' 'Not visible from Windows' 'Run ZenTimings (free, zentimings.protonrom.com). FCLK, UCLK and MCLK should all match (e.g. 1800 / 1800 / 1800 at 3600 MT/s).'
}
}
}
Step 'graphics' {
if (-not $dgpus.Count -and -not $igpus.Count) { Add-Finding 'GPU' 'Graphics card' 'INFO' 'No GPU detected' ''; return }
if ($gpus | Where-Object Kind -eq 'Basic') {
Add-Finding 'GPU' 'Graphics driver' 'FIX' 'Microsoft Basic Display Adapter in use' 'A GPU has no real driver. Install the driver from nvidia.com, amd.com or intel.com.'
}
foreach ($g in $dgpus) {
$age = Get-AgeDays $g.DriverDate
$ver = if ($g.Kind -eq 'NVIDIA' -and $nv) { $nv.Driver } else { $g.DriverVersion }
$val = '{0}: driver {1}, dated {2:yyyy-MM-dd} ({3} days ago)' -f $g.Name, $ver, $g.DriverDate, $age
if ($age -gt 270) { Add-Finding 'GPU' 'Driver age' 'CHECK' $val 'GPU driver is over 9 months old. New drivers often add game-specific fixes and performance. Update via NVIDIA App, AMD Software or Intel Arc Control.' }
else { Add-Finding 'GPU' 'Driver age' 'OK' $val '' }
$link = Get-PciLink $g.PnpId
if ($link) {
$lv = 'PCIe Gen{0} x{1} (card supports Gen{2} x{3})' -f $link.CurGen, $link.CurWidth, $link.MaxGen, $link.MaxWidth
if ($link.CurWidth -lt $link.MaxWidth -and -not $isLaptop) {
Add-Finding 'GPU' 'PCIe link width' 'CHECK' $lv 'The card is running fewer lanes than it supports. Make sure it is in the top full-length slot. Some boards drop it to x8 when certain M.2 slots are used (usually a small loss on Gen4/Gen5).'
} elseif ($link.CurGen -lt $link.MaxGen) {
Add-Finding 'GPU' 'PCIe link' 'INFO' $lv 'Lower generation than the card supports. GPUs drop link speed at idle to save power, which is normal. If it stays low while gaming, the slot or the BIOS PCIe setting is limiting it.'
} else {
Add-Finding 'GPU' 'PCIe link' 'OK' $lv ''
}
}
$barMiB = $null
if ($g.Kind -eq 'NVIDIA' -and $nv -and $nv.Bar1MiB) { $barMiB = $nv.Bar1MiB }
else { $barMiB = Try-Get { Get-LargestBarMiB $g.PnpId } }
if ($barMiB) {
$label = if ($g.Kind -eq 'AMD') { 'Resizable BAR / Smart Access Memory' } else { 'Resizable BAR' }
if ($barMiB -gt 256) { Add-Finding 'GPU' $label 'OK' "Enabled (BAR window $barMiB MiB)" '' }
else {
$sev = if ($g.Kind -eq 'Arc') { 'FIX' } else { 'CHECK' }
$why = if ($g.Kind -eq 'Arc') { 'Intel Arc loses 20-40% performance without it.' } else { 'Typically worth 0-10% depending on the game.' }
Add-Finding 'GPU' $label $sev "Off (BAR window $barMiB MiB)" "Enable 'Above 4G Decoding' and 'Re-Size BAR Support' in the BIOS (needs UEFI boot with CSM off). $why"
}
}
if ($g.Kind -eq 'NVIDIA' -and $nv -and $nv.PowerDefault -gt 0 -and $nv.PowerLimit -lt ($nv.PowerDefault * 0.95)) {
Add-Finding 'GPU' 'Power limit' 'CHECK' ('{0:N0} W of default {1:N0} W' -f $nv.PowerLimit, $nv.PowerDefault) 'The GPU power limit has been lowered (MSI Afterburner or similar). Fine if you undervolted on purpose; otherwise reset it.'
}
if ($g.Kind -eq 'AMD' -and -not ($uninstall | Where-Object { $_.DisplayName -match '^AMD Software' })) {
Add-Finding 'GPU' 'AMD Software' 'INFO' 'Driver-only install' 'AMD Software: Adrenalin Edition adds Anti-Lag, Radeon Chill and driver updates. Optional.'
}
}
# Monitor plugged into the motherboard instead of the graphics card
if ($dgpus.Count -and -not $isLaptop) {
foreach ($d in $displays) {
$onIgpu = $igpus | Where-Object { $_.Name -eq $d.Adapter }
if ($onIgpu) { Add-Finding 'Display' 'Monitor cable' 'FIX' "$($d.Device) is driven by $($d.Adapter)" 'This monitor is plugged into the motherboard. Plug it into the graphics card''s ports instead, or games run through the much slower integrated graphics.' }
}
}
if ($isLaptop -and $dgpus.Count -and $igpus.Count) {
Add-Finding 'GPU' 'Laptop GPU switching' 'INFO' 'Laptop with integrated + dedicated GPU' 'Make sure games use the dedicated GPU: Settings > System > Display > Graphics > pick the game > High performance. A MUX / "dGPU only" mode in the laptop''s control app helps further.'
}
$hags = Get-Reg 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers' 'HwSchMode'
if ($hags -eq 2) { Add-Finding 'GPU' 'Hardware-accelerated GPU scheduling' 'OK' 'On' '' }
elseif ($hags -eq 1) { Add-Finding 'GPU' 'Hardware-accelerated GPU scheduling' 'CHECK' 'Off' 'Required for DLSS Frame Generation and AMD Fluid Motion Frames. Settings > System > Display > Graphics > Advanced graphics settings, then reboot.' }
else { Add-Finding 'GPU' 'Hardware-accelerated GPU scheduling' 'VERIFY' 'Windows default (not set explicitly)' 'Check Settings > System > Display > Graphics > Advanced graphics settings. Turn it on if you use DLSS Frame Generation or AMD Fluid Motion Frames.' }
}
Step 'displays' {
foreach ($d in $displays) {
$name = '{0} ({1}x{2})' -f $d.Device, $d.Width, $d.Height
if ($d.MaxHzAtRes -ge 75 -and $d.Hz -lt ($d.MaxHzAtRes - 2)) {
Add-Finding 'Display' 'Refresh rate' 'FIX' "$name running $($d.Hz) Hz, supports $($d.MaxHzAtRes) Hz" 'Settings > System > Display > Advanced display > Choose a refresh rate. If the higher rate is missing, use a DisplayPort cable or a certified HDMI 2.1 cable.'
} else {
Add-Finding 'Display' 'Refresh rate' 'OK' "$name at $($d.Hz) Hz (max $($d.MaxHzAtRes) Hz)" ''
}
if ($d.MaxWidth -and ($d.Width * $d.Height) -lt ($d.MaxWidth * $d.MaxHeight) -and -not $isLaptop) {
Add-Finding 'Display' 'Desktop resolution' 'CHECK' "$name, monitor supports $($d.MaxWidth)x$($d.MaxHeight)" 'The desktop runs below the monitor''s native resolution, which looks blurry. Set the recommended resolution in Display settings.'
}
}
}
Step 'Windows gaming settings' {
$gm = Get-Reg 'HKCU:\Software\Microsoft\GameBar' 'AutoGameModeEnabled'
if ($gm -eq 0) { Add-Finding 'Windows' 'Game Mode' 'CHECK' 'Off' 'Settings > Gaming > Game Mode: On. It deprioritises background work while a game runs.' }
else { Add-Finding 'Windows' 'Game Mode' 'OK' 'On' '' }
$cap = Get-Reg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\GameDVR' 'AppCaptureEnabled'
$hist = Get-Reg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\GameDVR' 'HistoricalCaptureEnabled'
if ($cap -eq 1 -and $hist -eq 1) { Add-Finding 'Windows' 'Xbox background recording' 'CHECK' 'Recording the last 30 seconds continuously' 'Settings > Gaming > Captures > Record what happened: Off, unless you use it. It costs a few percent of GPU.' }
else { Add-Finding 'Windows' 'Xbox background recording' 'OK' 'Off' '' }
$dx = [string](Get-Reg 'HKCU:\Software\Microsoft\DirectX\UserGpuPreferences' 'DirectXUserGlobalSettings')
if ($dx -match 'SwapEffectUpgradeEnable=0') { Add-Finding 'Windows' 'Optimizations for windowed games' 'CHECK' 'Off' 'Settings > System > Display > Graphics > Change default graphics settings: turn on "Optimizations for windowed games". Lowers latency in borderless-windowed games.' }
$dg = Try-Get { Get-CimInstance -Namespace root\Microsoft\Windows\DeviceGuard -ClassName Win32_DeviceGuard }
if ($dg) {
if (@($dg.SecurityServicesRunning) -contains 2) {
Add-Finding 'Windows' 'Memory Integrity (HVCI)' 'CHECK' 'On' 'Core isolation > Memory integrity costs about 0-10% in some games. Turning it off is a security trade-off; it is your call. Windows Security > Device security > Core isolation.'
} else {
Add-Finding 'Windows' 'Memory Integrity (HVCI)' 'OK' 'Off' ''
}
}
$ms = Get-Reg 'HKCU:\Control Panel\Mouse' 'MouseSpeed'
if ($ms -eq '1') { Add-Finding 'Windows' 'Enhance pointer precision' 'INFO' 'On (mouse acceleration)' 'Most FPS players turn this off for consistent aim: Control Panel > Mouse > Pointer Options.' }
if ($build -gt 0 -and $build -lt 22000) { Add-Finding 'Windows' 'Windows version' 'CHECK' "Windows 10 (build $build)" 'Windows 10 support ended October 2025. Windows 11 also schedules modern CPUs better.' }
}
Step 'storage' {
$pd = @(Get-PhysicalDisk)
foreach ($d in $pd) {
if ($d.HealthStatus -ne 'Healthy') { Add-Finding 'Storage' 'Drive health' 'FIX' "$($d.FriendlyName): $($d.HealthStatus)" 'Windows reports this drive as unhealthy. Back up now and check it with CrystalDiskInfo.' }
}
function Get-DriveMedia([string]$Letter) {
$part = Get-Partition -DriveLetter $Letter -ErrorAction Stop
$disk = $pd | Where-Object { $_.DeviceId -eq [string]$part.DiskNumber } | Select-Object -First 1
if ($disk) { [pscustomobject]@{ Media = [string]$disk.MediaType; Bus = [string]$disk.BusType; Name = $disk.FriendlyName } }
}
$sys = $env:SystemDrive.TrimEnd(':')
$vol = Get-Volume -DriveLetter $sys
$freePct = [int]($vol.SizeRemaining / $vol.Size * 100)
$sysMedia = Try-Get { Get-DriveMedia $sys }
$val = '{0}: {1} GB free of {2} GB ({3}%) on {4} {5}' -f $sys, [int]($vol.SizeRemaining / 1GB), [int]($vol.Size / 1GB), $freePct, $sysMedia.Bus, $sysMedia.Media
if ($sysMedia -and $sysMedia.Media -eq 'HDD') { Add-Finding 'Storage' 'Windows drive' 'FIX' $val 'Windows is on a hard drive. Moving to any SSD is the biggest responsiveness upgrade a PC can get.' }
if ($freePct -lt 10) { Add-Finding 'Storage' 'Free space' 'FIX' $val 'The Windows drive is almost full. Leave at least 10-15% free for updates, the pagefile and shader caches.' }
elseif ($freePct -lt 15) { Add-Finding 'Storage' 'Free space' 'CHECK' $val 'Getting full. Aim for 15% or more free.' }
else { Add-Finding 'Storage' 'Free space' 'OK' $val '' }
# Steam libraries on a hard drive
$steam = Get-Reg 'HKCU:\Software\Valve\Steam' 'SteamPath'
if ($steam) {
$vdf = Join-Path ($steam -replace '/', '\') 'steamapps\libraryfolders.vdf'
if (Test-Path $vdf) {
$libs = [regex]::Matches((Get-Content $vdf -Raw), '"path"\s+"([^"]+)"') | ForEach-Object { $_.Groups[1].Value -replace '\\\\', '\' }
foreach ($l in $libs) {
$letter = $l.Substring(0, 1)
$m = Try-Get { Get-DriveMedia $letter }
if ($m -and $m.Media -eq 'HDD') { Add-Finding 'Storage' 'Game library on hard drive' 'CHECK' "Steam library $l is on $($m.Name) (HDD)" 'Games on a hard drive load slowly and can stutter while streaming assets. Move the games you play to an SSD (Steam > Storage > Move).' }
}
}
}
$trim = (fsutil behavior query DisableDeleteNotify 2>$null) -join ' '
if ($trim -match 'NTFS DisableDeleteNotify\s*=\s*1') { Add-Finding 'Storage' 'TRIM' 'CHECK' 'Disabled for NTFS' 'SSD TRIM is off, so SSDs slow down over time. Run as admin: fsutil behavior set DisableDeleteNotify 0' }
foreach ($c in @(Get-PnpDevice -Class SCSIAdapter -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match 'NVM|NVMe' })) {
$link = Get-PciLink $c.InstanceId
if ($link -and $link.CurWidth -lt $link.MaxWidth) {
Add-Finding 'Storage' 'NVMe link' 'CHECK' ('{0}: Gen{1} x{2} (drive supports Gen{3} x{4})' -f $c.FriendlyName, $link.CurGen, $link.CurWidth, $link.MaxGen, $link.MaxWidth) 'The SSD has fewer PCIe lanes than it supports. Try the M.2 slot nearest the CPU.'
}
}
}
Step 'network' {
foreach ($a in @(Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object Status -eq 'Up')) {
$isWifi = $a.PhysicalMediaType -match '802\.11|Wireless' -or $a.InterfaceDescription -match 'Wi-?Fi|Wireless|802\.11'
if ($isWifi) {
$w = (netsh wlan show interfaces) -join "`n"
$sig = if ($w -match '(\d{1,3})\s*%') { [int]$matches[1] } else { $null }
$radio = if ($w -match '(802\.11\w+)') { $matches[1] } else { '' }
$val = "$($a.InterfaceDescription), $($a.LinkSpeed), signal $sig% $radio"
if ($sig -and $sig -lt 60) { Add-Finding 'Network' 'Wi-Fi signal' 'CHECK' $val 'Weak Wi-Fi causes lag spikes and packet loss. Move closer to the router, use 5/6 GHz, or use Ethernet.' }
else { Add-Finding 'Network' 'Connection' 'INFO' $val 'Wired Ethernet gives steadier ping than Wi-Fi if it is an option.' }
} else {
$val = "$($a.InterfaceDescription) at $($a.LinkSpeed)"
if ($a.LinkSpeed -match '^(10|100) Mbps') { Add-Finding 'Network' 'Ethernet link speed' 'FIX' $val 'Linked at 100 Mbps or less, usually a damaged cable or a bad port. Replace the cable (Cat5e or better) or try another router port.' }
else { Add-Finding 'Network' 'Ethernet link speed' 'OK' $val '' }
}
}
}
Step 'startup apps' {
# Run keys plus the StartupApproved flags that Task Manager / Settings toggle.
# Byte 0 odd = disabled. No flag = enabled.
$sets = @(
@{ Run = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Flag = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run' },
@{ Run = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'; Flag = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run' },
@{ Run = 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'; Flag = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run32' }
)
$enabled = New-Object System.Collections.Generic.List[string]
foreach ($s in $sets) {
$key = Try-Get { Get-Item -LiteralPath $s.Run }
if (-not $key) { continue }
foreach ($n in $key.GetValueNames()) {
if (-not $n -or $n -eq 'SecurityHealth') { continue }
$flag = Get-Reg $s.Flag $n
if (-not $flag -or ($flag[0] -band 1) -eq 0) { $enabled.Add(($n -replace '^electron\.app\.', '' -replace '^GoogleChromeAutoLaunch_.*', 'Google Chrome').Trim()) }
}
}
foreach ($dir in @([Environment]::GetFolderPath('Startup'), [Environment]::GetFolderPath('CommonStartup'))) {
foreach ($f in @(Get-ChildItem -LiteralPath $dir -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'desktop.ini' })) {
$flag = Get-Reg 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder' $f.Name
if (-not $flag) { $flag = Get-Reg 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder' $f.Name }
if (-not $flag -or ($flag[0] -band 1) -eq 0) { $enabled.Add([IO.Path]::GetFileNameWithoutExtension($f.Name)) }
}
}
# Store-app startup tasks: State 2 or 4 = enabled
$sad = 'HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\SystemAppData'
foreach ($pkg in @(Get-ChildItem -LiteralPath $sad -ErrorAction SilentlyContinue)) {
foreach ($task in @(Get-ChildItem -LiteralPath $pkg.PSPath -ErrorAction SilentlyContinue)) {
$st = Get-Reg $task.PSPath 'State'
if ($st -eq 2 -or $st -eq 4) { $enabled.Add(($pkg.PSChildName -replace '_\w+$', '' -replace '^[\w\d]+\.', '')) }
}
}
$list = ($enabled | Select-Object -Unique) -join ', '
$n = @($enabled | Select-Object -Unique).Count
if ($n -gt 12) { Add-Finding 'Background' 'Startup apps' 'CHECK' "$n enabled: $list" 'Lots of apps start with Windows. Each adds background CPU wake-ups that hurt 1% lows. Task Manager > Startup apps: disable anything you do not need running all the time.' }
else { Add-Finding 'Background' 'Startup apps' 'OK' "$n enabled: $list" '' }
}
Step 'background apps' {
$names = @($procs | ForEach-Object { $_.ProcessName.ToLower() } | Select-Object -Unique)
$recorders = [ordered]@{
'obs64' = 'OBS'; 'medal' = 'Medal'; 'overwolf' = 'Overwolf'; 'outplayed' = 'Outplayed'; 'segra' = 'Segra / Autoclipper'
'screenrec' = 'ScreenRec'; 'bdcam' = 'Bandicam'; 'action' = 'Mirillis Action'; 'xsplit.core' = 'XSplit'
'steelseriesgg' = 'SteelSeries GG (Moments)'; 'insights' = 'Insights Capture'
'amdrsserv' = 'AMD ReLive'; 'streamlabs obs' = 'Streamlabs'
}
$running = @(foreach ($k in $recorders.Keys) { if ($names | Where-Object { $_ -eq $k -or $_ -like "$k*" }) { $recorders[$k] } })
if ($running.Count -ge 2) { Add-Finding 'Background' 'Screen recorders' 'CHECK' ($running -join ', ') 'More than one recording / clipping app is running. Each one that buffers replays costs GPU time and frame-time consistency. Keep one.' }
elseif ($running.Count -eq 1) { Add-Finding 'Background' 'Screen recorders' 'INFO' $running[0] 'One clipping app running. If NVIDIA Instant Replay or AMD ReLive is also on, turn one off.' }
$heavy = [ordered]@{
'armourycrate' = 'ASUS Armoury Crate'; 'lightingservice' = 'ASUS Aura'; 'icue' = 'Corsair iCUE'; 'nzxt cam' = 'NZXT CAM'
'signalrgb' = 'SignalRGB'; 'msi.centralserver' = 'MSI Center'; 'razer synapse' = 'Razer Synapse'; 'razercentralservice' = 'Razer Synapse'
'wallpaper64' = 'Wallpaper Engine'; 'wallpaper32' = 'Wallpaper Engine'; 'lively' = 'Lively Wallpaper'
'nvidia broadcast' = 'NVIDIA Broadcast'; 'bluestacksservices' = 'BlueStacks Services'
}
$hr = @(foreach ($k in $heavy.Keys) { if ($names | Where-Object { $_ -eq $k -or $_ -like "$k*" }) { $heavy[$k] } }) | Select-Object -Unique
if (@($hr).Count) { Add-Finding 'Background' 'Known resource-heavy apps' 'INFO' (@($hr) -join ', ') 'These are known to use noticeable CPU, GPU or DPC time. RGB suites can be replaced by OpenRGB. NVIDIA Broadcast uses the GPU whenever an effect is on. Wallpaper Engine should be set to pause when a game is focused.' }
# CPU use over a 3-second sample
$a = @{}; foreach ($p in (Get-Process)) { try { $a[$p.Id] = $p.TotalProcessorTime.TotalSeconds } catch {} }
Start-Sleep -Seconds 3
$rows = foreach ($p in (Get-Process)) {
try { if ($a.ContainsKey($p.Id)) { [pscustomobject]@{ Name = $p.ProcessName; Pct = ($p.TotalProcessorTime.TotalSeconds - $a[$p.Id]) / 3 / $logical * 100 } } } catch {}
}
$top = @($rows | Where-Object { $_.Name -ne 'Idle' -and $_.Pct -ge 2 } | Group-Object Name | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Pct = ($_.Group | Measure-Object Pct -Sum).Sum } } | Sort-Object Pct -Descending | Select-Object -First 5)
$total = ($rows | Where-Object { $_.Name -ne 'Idle' } | Measure-Object Pct -Sum).Sum
$val = 'Total {0:N0}% now. Top: {1}' -f $total, $(if ($top.Count) { ($top | ForEach-Object { '{0} {1:N0}%' -f $_.Name, $_.Pct }) -join ', ' } else { 'nothing above 2%' })
if ($total -ge 15) { Add-Finding 'Background' 'CPU use at idle' 'CHECK' $val 'Something is using the CPU while the PC should be idle. If a game or update was running, ignore this; otherwise look at the top entries in Task Manager.' }
else { Add-Finding 'Background' 'CPU use at idle' 'OK' $val '' }
}
# ================================================================== REPORT
$order = @{ 'FIX' = 0; 'CHECK' = 1; 'VERIFY' = 2; 'OK' = 3; 'INFO' = 4 }
$sorted = @($Findings | Sort-Object @{ Expression = { $order[$_.Status] } }, Category, Check)
$counts = @{}; foreach ($k in $order.Keys) { $counts[$k] = @($sorted | Where-Object Status -eq $k).Count }
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm'
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("RigCheck $Version report - $stamp")
foreach ($k in $Specs.Keys) { [void]$sb.AppendLine(('{0,-12} {1}' -f ($k + ':'), $Specs[$k])) }
[void]$sb.AppendLine('')
[void]$sb.AppendLine(('FIX {0} | CHECK {1} | VERIFY {2} | OK {3} | INFO {4}' -f $counts['FIX'], $counts['CHECK'], $counts['VERIFY'], $counts['OK'], $counts['INFO']))
foreach ($f in $sorted) {
[void]$sb.AppendLine(('[{0}] {1} - {2}: {3}' -f $f.Status, $f.Category, $f.Check, $f.Value))
if ($f.Advice -and $f.Status -ne 'OK') { [void]$sb.AppendLine(" -> $($f.Advice)") }
}
$text = $sb.ToString()
function HE([string]$s) { [System.Net.WebUtility]::HtmlEncode($s) }
$colors = @{ 'FIX' = '#ef4444'; 'CHECK' = '#f59e0b'; 'VERIFY' = '#3b82f6'; 'OK' = '#22c55e'; 'INFO' = '#94a3b8' }
$labels = @{ 'FIX' = 'Fix this'; 'CHECK' = 'Worth checking'; 'VERIFY' = 'Check manually'; 'OK' = 'Looks good'; 'INFO' = 'For your information' }
$h = New-Object System.Text.StringBuilder
[void]$h.Append(@"
RigCheck report
RigCheck report
$(HE $stamp) · read-only scan, nothing was changed · v$Version
"@)
foreach ($k in $Specs.Keys) { [void]$h.Append("| $(HE $k) | $(HE $Specs[$k]) |
") }
[void]$h.Append('
')
foreach ($k in 'FIX', 'CHECK', 'VERIFY', 'OK', 'INFO') { [void]$h.Append("
$($counts[$k])$(HE $labels[$k])
") }
[void]$h.Append('
')
foreach ($k in 'FIX', 'CHECK', 'VERIFY', 'OK', 'INFO') {
$group = @($sorted | Where-Object Status -eq $k)
if (-not $group.Count) { continue }
[void]$h.Append("$(HE $labels[$k]) ($($group.Count))
")
foreach ($f in $group) {
[void]$h.Append("$(HE $f.Category)$(HE $f.Check)
$(HE $f.Value)
")
if ($f.Advice -and $k -ne 'OK') { [void]$h.Append("
$(HE $f.Advice)
") }
[void]$h.Append('
')
}
}
[void]$h.Append(@"
"@)
if (-not (Test-Path $OutDir)) { $OutDir = $env:TEMP }
$base = Join-Path $OutDir ('RigCheck_{0}' -f (Get-Date -Format 'yyyyMMdd-HHmm'))
$utf8 = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllText("$base.html", $h.ToString(), $utf8)
[IO.File]::WriteAllText("$base.txt", $text, $utf8)
Write-Host ''
foreach ($f in $sorted) {
if ($f.Status -eq 'OK' -or $f.Status -eq 'INFO') { continue }
$c = switch ($f.Status) { 'FIX' { 'Red' } 'CHECK' { 'Yellow' } default { 'Cyan' } }
Write-Host ('[{0}] ' -f $f.Status) -ForegroundColor $c -NoNewline
Write-Host ('{0}: {1}' -f $f.Check, $f.Value)
}
Write-Host ''
Write-Host ('FIX {0} CHECK {1} VERIFY {2} OK {3} INFO {4}' -f $counts['FIX'], $counts['CHECK'], $counts['VERIFY'], $counts['OK'], $counts['INFO']) -ForegroundColor Cyan
Write-Host "Report: $base.html" -ForegroundColor Cyan
# ================================================================== OPTIONAL UPLOAD
function Get-MachineId {
# One-way hash of hardware IDs, only used to match scans of the same PC for before/after.
$junk = '^(0+|F+|[0-9A-F]{8}-0{4}-0{4}-0{4}-0{12}|To be filled.*|Default string|None|N/A|System Serial Number|Base Board Serial Number)$'
$parts = @(
(Try-Get { (Get-CimInstance Win32_ComputerSystemProduct).UUID }),
$(if ($board) { [string]$board.SerialNumber }),
$(if ($bios) { [string]$bios.SerialNumber })
) | Where-Object { $_ -and $_.Trim() -notmatch $junk -and $_ -ne 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF' }
if (-not $parts) { return $null }
$sha = [Security.Cryptography.SHA256]::Create()
$hash = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes('rigcheck-v1|' + ($parts -join '|')))
(($hash | ForEach-Object { $_.ToString('x2') }) -join '').Substring(0, 32)
}
function Hide-Names([string]$s) {
if (-not $s) { return $s }
foreach ($w in @($env:USERNAME, $env:COMPUTERNAME, $env:USERDOMAIN) | Where-Object { $_ -and $_.Length -ge 3 } | Sort-Object Length -Descending) {
$s = [regex]::Replace($s, [regex]::Escape($w), '[hidden]', 'IgnoreCase')
}
$s
}
$webUrl = $null
$doUpload = [bool]$Upload
if (-not $Upload -and -not $NoUpload) {
Write-Host ''
Write-Host "Upload this report to $($Server -replace '^https?://', '')?" -ForegroundColor Cyan
Write-Host ' You get a link to share, estimated FPS gains for each fix, and a before/after' -ForegroundColor DarkGray
Write-Host ' comparison when you run RigCheck again. Sent: the hardware summary and findings' -ForegroundColor DarkGray
Write-Host ' above plus an anonymous machine ID. Your user name and PC name are removed.' -ForegroundColor DarkGray
$ans = Read-Host ' Upload? [Y/n]'
$doUpload = ($null -ne $ans) -and ($ans.Trim() -eq '' -or $ans.Trim() -match '^(y|yes)$')
}
if ($doUpload) {
try {
try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}
$specsOut = [ordered]@{}
foreach ($k in $Specs.Keys) { $specsOut[$k] = Hide-Names ([string]$Specs[$k]) }
$payload = [ordered]@{
schema = 1
version = $Version
machine = (Get-MachineId)
specs = $specsOut
findings = @($sorted | ForEach-Object {
[ordered]@{ category = $_.Category; check = $_.Check; status = $_.Status; value = (Hide-Names $_.Value); advice = (Hide-Names $_.Advice) }
})
}
$json = $payload | ConvertTo-Json -Depth 6 -Compress
$resp = Invoke-RestMethod -Uri "$Server/api/reports" -Method Post -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($json)) -TimeoutSec 30 -UseBasicParsing
$webUrl = [string]$resp.url
Write-Host "Online report: $webUrl" -ForegroundColor Green
} catch {
Write-Host "Upload failed: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host 'The local report on your Desktop has everything except the FPS estimates.' -ForegroundColor Yellow
}
}
if (-not $NoOpen) { if ($webUrl) { Start-Process $webUrl } else { Start-Process "$base.html" } }