cpu-parking-disabler.ps1

<#PSScriptInfo

.VERSION 1.2.0

.GUID 3c68d1e3-ff2b-4cc2-b7c1-6c384c72cd0d

.AUTHOR vadyaravadim

.COMPANYNAME

.COPYRIGHT

.TAGS Windows Windows10 Windows11 Gaming CPU CoreParking PowerPlan Performance Latency Stutter Tweak

.LICENSEURI https://github.com/vadyaravadim/cpu-parking-disabler/blob/main/LICENSE

.PROJECTURI https://github.com/vadyaravadim/cpu-parking-disabler

.ICONURI

.EXTERNALMODULEDEPENDENCIES

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES

.PRIVATEDATA

#>


<#
.SYNOPSIS
    Disables CPU core parking and sets Energy Performance Preference to maximum performance.

.DESCRIPTION
    Shows how many cores Windows has parked right now and the four power
    settings behind it, then sets Core Parking Min Cores (CPMINCORES /
    CPMINCORES1) to 100 and Energy Performance Preference (PERFEPP / PERFEPP1)
    to 0 on the active power scheme, for AC and DC, and shows the parked-core
    count again so the effect is visible on screen.

    Supports Intel 12th gen+ hybrid CPUs (separate P-core/E-core settings) and
    non-hybrid CPUs (AMD Ryzen, older Intel), where the Class 1 (P-core)
    settings do not exist and are skipped. Dual-CCD X3D Ryzens (7900X3D,
    7950X3D, 9900X3D, 9950X3D) get a warning first: AMD parks the non-V-Cache
    CCD during games THROUGH core parking, and this tweak defeats that.

    The previous values are written to parking_undo_<stamp>.json next to the
    script before anything changes; -Undo puts them back. The script relaunches
    itself as Administrator when needed.

.PARAMETER Status
    Show the parked-core count and the current settings, change nothing.
    Does not need Administrator rights.

.PARAMETER Undo
    Restore the values recorded by the newest parking_undo_*.json next to the
    script. Files are per-run snapshots: after several runs, -Undo once per
    run, newest to oldest - only the oldest holds the original state.

.EXAMPLE
    .\cpu-parking-disabler.ps1

    Double-click Run.bat, or right-click this file > Run with PowerShell.
    No parameters needed - it elevates itself.

.EXAMPLE
    .\cpu-parking-disabler.ps1 -Status

.EXAMPLE
    .\cpu-parking-disabler.ps1 -Undo

.LINK
    https://github.com/vadyaravadim/cpu-parking-disabler
#>

[CmdletBinding()]
param(
    [switch]$Status,
    [switch]$Undo,
    [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 ($Status) { $a += '-Status' }
    if ($Undo)   { $a += '-Undo' }
    $a
}

# Launched via `irm <url> | iex` - no file on disk. The undo file 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/cpu-parking-disabler/main/cpu-parking-disabler.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 'cpu-parking-disabler.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
    }
    # 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
}

# ---- Everything below -Status writes power settings: 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 {
        $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
}

# ---- Helpers ----

# powercfg reports failure by exit code and writes the reason to stderr. Under
# Windows PowerShell 5.1 a stderr line captured with 2>&1 becomes a terminating
# error while $ErrorActionPreference is Stop, so the call runs with Continue
# and the caller decides what a failure means (absent setting vs. real error).
function Invoke-Powercfg([string[]]$ArgumentList) {
    $ErrorActionPreference = 'Continue'
    $out = & powercfg @ArgumentList 2>&1
    [pscustomobject]@{ Ok = ($LASTEXITCODE -eq 0); Text = (@($out | ForEach-Object { "$_" }) -join "`n") }
}

# $null when the setting does not exist on this CPU (Class 1 on non-hybrid).
# /qh includes hidden settings, so this works before the registry unhide too.
# Labels are localized on non-English Windows, so they are not matched: the
# last two hex values of the block are AC then DC on every locale.
function Get-PowerSettingValue([string]$Setting, [string]$Scheme = 'SCHEME_CURRENT') {
    $r = Invoke-Powercfg @('/qh', $Scheme, 'SUB_PROCESSOR', $Setting)
    if (-not $r.Ok) { return $null }
    $hex = @([regex]::Matches($r.Text, '0x([0-9a-fA-F]{8})') | ForEach-Object { [Convert]::ToInt32($_.Groups[1].Value, 16) })
    if ($hex.Count -lt 2) { throw "unexpected powercfg output for $Setting`n$($r.Text)" }
    [pscustomobject]@{ AC = $hex[-2]; DC = $hex[-1] }
}

function Set-PowerSettingValue([string]$Setting, [int]$AC, [int]$DC, [string]$Scheme = 'SCHEME_CURRENT') {
    foreach ($call in @(@('/setacvalueindex', $AC), @('/setdcvalueindex', $DC))) {
        $r = Invoke-Powercfg @($call[0], $Scheme, 'SUB_PROCESSOR', $Setting, $call[1])
        if (-not $r.Ok) { throw "powercfg $($call[0]) $Setting failed: $($r.Text)" }
    }
}

# A hidden processor setting can be ignored by the OS even after powercfg
# writes it, so Attributes = 0 comes BEFORE any write. Idempotent; the
# settings stay visible in Power Options afterwards, which is harmless.
$PowerSettingsKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\54533251-82be-4824-96c1-47b60b740d00'
function Show-PowerSetting([string]$Guid) {
    Set-ItemProperty -Path (Join-Path $PowerSettingsKey $Guid) -Name Attributes -Value 0 -Type DWord
}

# Language-independent, unlike the '\Processor Information(*)\Parking Status'
# counter path, which is localized on non-English Windows. Instance names are
# "group,index"; the _Total rows are skipped. $null when the performance
# counters are disabled or corrupt on this system.
function Get-ParkedCoreCount {
    $rows = @(Get-CimInstance Win32_PerfFormattedData_Counters_ProcessorInformation -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match '^\d+,\d+$' })
    if (-not $rows.Count) { return $null }
    [pscustomobject]@{ Parked = @($rows | Where-Object { $_.ParkingStatus -eq 1 }).Count; Total = $rows.Count }
}

function Format-ParkedCount($Count) {
    if ($null -eq $Count) { return 'unavailable (performance counters are disabled on this system)' }
    '{0} of {1}' -f $Count.Parked, $Count.Total
}

function Get-ActiveScheme {
    $r = Invoke-Powercfg @('/getactivescheme')
    if (-not $r.Ok) { throw "powercfg /getactivescheme failed: $($r.Text)" }
    [pscustomobject]@{
        Guid = [regex]::Match($r.Text, '[0-9a-fA-F-]{36}').Value
        Name = [regex]::Match($r.Text, '\(([^)]+)\)\s*$').Groups[1].Value
    }
}

# Rows are skipped when the setting is absent (Class 1 on non-hybrid CPUs).
$settingDefs = @(
    [pscustomobject]@{ Name = 'CPMINCORES';  Guid = '0cc5b647-c1df-4637-891a-dec35c318583'; Target = 100; Label = 'core parking min cores, E-cores / all cores' }
    [pscustomobject]@{ Name = 'CPMINCORES1'; Guid = '0cc5b647-c1df-4637-891a-dec35c318584'; Target = 100; Label = 'core parking min cores, P-cores' }
    [pscustomobject]@{ Name = 'PERFEPP';     Guid = '36687f9e-e3a5-4dbf-b1dc-15eb381c6863'; Target = 0;   Label = 'energy performance preference, E-cores / all cores' }
    [pscustomobject]@{ Name = 'PERFEPP1';    Guid = '36687f9e-e3a5-4dbf-b1dc-15eb381c6864'; Target = 0;   Label = 'energy performance preference, P-cores' }
)

Write-Host ""
Write-Host "===================================" -ForegroundColor Cyan
Write-Host " CPU PARKING DISABLER" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host ""

# ---- Undo mode ----
if ($Undo) {
    # Sort by the name stamp - LastWriteTime survives renames and can mislead.
    $undoFile = Get-ChildItem -Path $PSScriptRoot -Filter 'parking_undo_*.json' -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -notmatch '\.applied\.json$' } |
        Sort-Object Name | Select-Object -Last 1
    if (-not $undoFile) {
        Write-Host "No parking_undo_*.json found next to the script - nothing to undo." -ForegroundColor Yellow
        Wait-IfElevatedWindow; return
    }
    $snap = Get-Content $undoFile.FullName -Raw | ConvertFrom-Json
    Write-Host "Reverting: $($undoFile.Name)" -ForegroundColor Cyan
    Write-Host "Power scheme : $($snap.SchemeName) ($($snap.Scheme))"
    # Written to the scheme that was changed, not whatever is active now - the
    # user may have switched schemes since. Unhide first, as on apply: a major
    # Windows update can hide the settings again.
    foreach ($v in $snap.Values) {
        Show-PowerSetting ($settingDefs | Where-Object Name -eq $v.Setting).Guid
        Set-PowerSettingValue $v.Setting $v.AC $v.DC $snap.Scheme
        Write-Host (" [OK ] {0,-12} AC {1,3} DC {2,3}" -f $v.Setting, $v.AC, $v.DC) -ForegroundColor Green
    }
    # Re-reads the active scheme so the restored values take effect right away.
    $r = Invoke-Powercfg @('/setactive', 'SCHEME_CURRENT')
    if (-not $r.Ok) { throw "powercfg /setactive failed: $($r.Text)" }
    Rename-Item $undoFile.FullName ($undoFile.FullName -replace '\.json$', '.applied.json')
    if ((Get-ActiveScheme).Guid -ne $snap.Scheme) {
        Write-Host "That scheme is not the active one - the restored values take effect when you switch back to it." -ForegroundColor Yellow
    }
    $remaining = @(Get-ChildItem -Path $PSScriptRoot -Filter 'parking_undo_*.json' -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -notmatch '\.applied\.json$' })
    if ($remaining.Count) {
        Write-Host "$($remaining.Count) older undo file(s) remain - run -Undo again to revert earlier runs." -ForegroundColor Yellow
    }
    # The kernel re-evaluates parking within a few hundred ms of the scheme change.
    Start-Sleep -Milliseconds 1000
    Write-Host ""
    Write-Host "Parked cores : $(Format-ParkedCount (Get-ParkedCoreCount))"
    Write-Host "Done." -ForegroundColor Green
    Wait-IfElevatedWindow; return
}

# ---- Current state ----
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$scheme = Get-ActiveScheme
$parkedBefore = Get-ParkedCoreCount
$settings = @(foreach ($d in $settingDefs) {
    $v = Get-PowerSettingValue $d.Name
    if ($null -ne $v) {
        $d | Add-Member NoteProperty AC $v.AC -PassThru | Add-Member NoteProperty DC $v.DC -PassThru |
            Add-Member NoteProperty Ok (($v.AC -eq $d.Target) -and ($v.DC -eq $d.Target)) -PassThru
    }
})
$hybrid = [bool]($settings | Where-Object Name -eq 'CPMINCORES1')
# Dual-CCD X3D parts: AMD's 3D V-Cache Performance Optimizer parks the
# non-V-Cache CCD during games THROUGH core parking. Single-CCD X3D parts
# (7800X3D, 9800X3D) have nothing to park and are not affected.
$dualCcdX3D = ($cpu.Name -match 'X3D') -and ($cpu.NumberOfCores -ge 12)

$parkedColor = if ($null -eq $parkedBefore) { 'Yellow' } elseif ($parkedBefore.Parked) { 'Red' } else { 'Green' }
Write-Host ("CPU : {0} ({1} cores / {2} threads{3})" -f $cpu.Name.Trim(), $cpu.NumberOfCores, $cpu.NumberOfLogicalProcessors, $(if ($hybrid) { ', hybrid P+E' } else { '' }))
Write-Host "Power scheme : $($scheme.Name)"
Write-Host "Parked cores : " -NoNewline
Write-Host (Format-ParkedCount $parkedBefore) -ForegroundColor $parkedColor
Write-Host ""
Write-Host (" {0,-12} {1,4} {2,4} {3,6}" -f 'Setting', 'AC', 'DC', 'Target') -ForegroundColor DarkGray
foreach ($s in $settings) {
    $mark = if ($s.Ok) { 'ok' } else { '->' }
    Write-Host (" {0,-12} {1,4} {2,4} {3,6} {4} ({5})" -f $s.Name, $s.AC, $s.DC, $s.Target, $mark, $s.Label) -ForegroundColor $(if ($s.Ok) { 'DarkGray' } else { 'Yellow' })
}
Write-Host ""
if ($dualCcdX3D) {
    Write-Host "NOTE: $($cpu.Name.Trim()) is a dual-CCD X3D part. AMD parks the non-V-Cache CCD during games through core parking on purpose - keeping every core awake defeats that, and games can run worse, not better." -ForegroundColor Yellow
    Write-Host ""
}

if ($Status) { Wait-IfElevatedWindow; return }

if ($dualCcdX3D) {
    $answer = Read-Host "Continue anyway? [y/N]"
    if ($answer -notmatch '^y') {
        Write-Host "Nothing changed." -ForegroundColor Green
        Wait-IfElevatedWindow; return
    }
    Write-Host ""
}

# Nothing to change -> no undo file: a repeat-run snapshot would record the
# already-tweaked state, and the next -Undo would "revert" to it instead of
# the original.
if (-not ($settings | Where-Object { -not $_.Ok })) {
    Write-Host "All values already at target - nothing to do, no undo file written." -ForegroundColor Green
    Wait-IfElevatedWindow
    return
}

# ---- Undo file: the CURRENT values of every setting, BEFORE changing anything ----
# The suffix loop keeps two runs within the same second from clobbering
# each other's undo file.
$base = Get-Date -Format 'yyyyMMdd_HHmmss'
$stamp = $base
$n = 1
while (Test-Path (Join-Path $PSScriptRoot "parking_undo_$stamp.json")) { $stamp = '{0}_{1}' -f $base, $n++ }
$undoPath = Join-Path $PSScriptRoot "parking_undo_$stamp.json"
$snapshot = [ordered]@{
    Scheme     = $scheme.Guid
    SchemeName = $scheme.Name
    Values     = @(foreach ($s in $settings) { [ordered]@{ Setting = $s.Name; AC = $s.AC; DC = $s.DC } })
}
ConvertTo-Json $snapshot -Depth 4 | Set-Content -Path $undoPath -Encoding UTF8
Write-Host "Undo file saved: $undoPath (revert with -Undo)" -ForegroundColor Cyan
Write-Host ""

# ---- Apply ----
Write-Host "Applying..."
foreach ($s in $settings) {
    Show-PowerSetting $s.Guid
    Set-PowerSettingValue $s.Name $s.Target $s.Target
    Write-Host (" [OK ] {0,-12} AC {1,3} -> {2,-3} DC {3,3} -> {4}" -f $s.Name, $s.AC, $s.Target, $s.DC, $s.Target) -ForegroundColor Green
}
$r = Invoke-Powercfg @('/setactive', 'SCHEME_CURRENT')
if (-not $r.Ok) { throw "powercfg /setactive failed: $($r.Text)" }

# The kernel re-evaluates parking within a few hundred ms of the scheme change.
Start-Sleep -Milliseconds 1000
$parkedAfter = Get-ParkedCoreCount

Write-Host ""
Write-Host "===================================" -ForegroundColor Cyan
Write-Host " DONE" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Parked cores : " -NoNewline
Write-Host (Format-ParkedCount $parkedBefore) -ForegroundColor $parkedColor -NoNewline
Write-Host " -> " -NoNewline
Write-Host (Format-ParkedCount $parkedAfter) -ForegroundColor $(if ($null -eq $parkedAfter) { 'Yellow' } elseif ($parkedAfter.Parked) { 'Red' } else { 'Green' })
Write-Host ""
Write-Host "Applied to power scheme '$($scheme.Name)':"
Write-Host " - CPU parking: DISABLED (all cores always active)"
Write-Host " - EPP: 0 (max performance)"
Write-Host ""
Write-Host "Revert any time with: .\cpu-parking-disabler.ps1 -Undo" -ForegroundColor DarkGray
Wait-IfElevatedWindow