Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,17 @@ hlslkit-generate-defines --log CommunityShaders.log

# Scan for buffer conflicts across features
hlslkit-buffer-scan --features-dir features/

# Prove a shader refactor changed no behavior (compiles base ref vs working tree,
# compares DXBC across VR x HDR_OUTPUT permutations; exit 0 identical / 2 differs)
pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # bash: tools/verify-shader-refactor.sh
```

When refactoring an existing shader (especially the decompile-transcription shaders like
`ISTemporalAA.hlsl`), use `tools/verify-shader-refactor.ps1` to prove the change is
behavior-preserving: identical compiled bytecode means a provable no-op. See
`docs/development/shader-workflow.md` for details.
Comment thread
alandtse marked this conversation as resolved.

### Custom CMake Targets

**Package and Deployment Targets**:
Expand Down
19 changes: 19 additions & 0 deletions docs/development/shader-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,27 @@ cmake --build build/ALL --target COPY_SHADERS

# Full deployment (DLL + tests + shaders)
cmake --build build/ALL --target DEPLOY_ALL

# Prove an HLSL refactor changed no behavior (compares compiled DXBC vs a git ref)
pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # or tools/verify-shader-refactor.sh
```

## Verifying refactors

`tools/verify-shader-refactor.ps1` (bash wrapper: `tools/verify-shader-refactor.sh`)
compiles a shader from a base git ref and from the working tree across the
`VR` × `HDR_OUTPUT` permutations, then compares the compiled bytecode:

- **IDENTICAL** SHA-256 of the `.cso` ⇒ the refactor is a provable no-op (fxc emits
Comment thread
alandtse marked this conversation as resolved.
no timestamps without `/Zi`, so identical source ⇒ identical bytes).
- **DIFFERS** ⇒ it dumps and diffs the `/Fc` assembly so a legitimate-but-non-identical
change can be reviewed.

Exit codes: `0` all identical, `2` some differ, `1` compile error. Defaults to comparing
the working tree against `merge-base(HEAD, origin/dev)`; pass `-BaseRef <ref>` to override.
Requires `fxc.exe` from the Windows SDK. The permutation sweep is strong evidence, not the
full `shader-validation.yaml` matrix — pass `-Permutations` for exotic define combos.

## Overview

Two deployment targets for different workflows:
Expand Down
196 changes: 196 additions & 0 deletions tools/verify-shader-refactor.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<#
.SYNOPSIS
Prove an HLSL refactor is behavior-preserving by comparing compiled bytecode.

.DESCRIPTION
Compiles a shader from a base git revision and from the current working tree
across a set of preprocessor permutations, then compares the resulting DXBC.

Tier 1 (this script): identical SHA-256 of the compiled .cso == provably identical
GPU program. fxc emits no timestamps without /Zi, so same source -> same bytes.

Tier 2 (this script, on mismatch): dumps /Fc assembly for both revisions and lists
the differing lines (base/work markers), so a legitimate-but-non-identical refactor
(e.g. register reorder) can be eyeballed.

A refactor that is Tier-1 IDENTICAL on the swept permutations needs no further proof.
Note: the default sweep (VR x HDR_OUTPUT) is strong evidence, not the full build
matrix from shader-validation.yaml. Pass -Permutations for exotic define combos.

.PARAMETER Shader
Path to the .hlsl file (repo-relative or absolute).

.PARAMETER BaseRef
Git ref to treat as "before". Default: merge-base of HEAD and origin/dev.

.PARAMETER IncludeDir
Shader include root passed to fxc /I. Default: package/Shaders.

.PARAMETER Permutations
Optional explicit permutation list; each entry is a space-separated define set,
e.g. -Permutations "PSHADER","PSHADER VR". Overrides the auto sweep.

.PARAMETER Entry
Shader entry point. Default: main.

.PARAMETER Profile
fxc target profile. Default: auto (cs_5_0 for *CS.hlsl, else ps_5_0).

.EXAMPLE
pwsh tools/verify-shader-refactor.ps1 package/Shaders/ISTemporalAA.hlsl

.EXAMPLE
pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl -BaseRef HEAD~1
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Shader,
[string]$BaseRef,
[string]$IncludeDir = "package/Shaders",
[string[]]$Permutations,
[string]$Entry = "main",
[string]$Profile,
[string]$Fxc
)

# Continue (not Stop): native git calls write warnings to stderr that would otherwise
# abort the run; control flow keys off explicit $LASTEXITCODE checks and `throw`.
$ErrorActionPreference = "Continue"

function Resolve-Fxc {
if ($Fxc -and (Test-Path $Fxc)) { return $Fxc }
$cmd = Get-Command fxc.exe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$roots = @("${env:ProgramFiles(x86)}\Windows Kits\10\bin", "${env:ProgramFiles}\Windows Kits\10\bin")
$found = foreach ($r in $roots) {
if (Test-Path $r) {
Get-ChildItem -Path $r -Recurse -Filter fxc.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "x64" }
}
}
$pick = $found | Sort-Object FullName -Descending | Select-Object -First 1
if (-not $pick) { throw "fxc.exe not found. Install the Windows 10/11 SDK or pass -Fxc." }
return $pick.FullName
}

# Resolve repo root so the script works from any cwd.
$repoRoot = (git rev-parse --show-toplevel 2>$null)
if (-not $repoRoot) { throw "Not inside a git repository." }
Push-Location $repoRoot
$work = $null
try {
$fxcPath = Resolve-Fxc

# Normalize the shader path to repo-relative (forward slashes) for `git show`.
# (Path.GetRelativePath is unavailable in Windows PowerShell 5.1 / .NET Framework.)
$shaderFull = (Resolve-Path $Shader).Path
$rootFull = (Resolve-Path $repoRoot).Path
$relPath = $shaderFull.Substring($rootFull.Length).TrimStart('\', '/').Replace('\', '/')

Comment thread
alandtse marked this conversation as resolved.
if (-not $BaseRef) {
$BaseRef = (git merge-base HEAD origin/dev 2>$null)
if (-not $BaseRef) { $BaseRef = "HEAD" }
}

Comment thread
alandtse marked this conversation as resolved.
if (-not $Profile) {
$Profile = if ($relPath -match 'CS\.hlsl$') { "cs_5_0" } else { "ps_5_0" }
}
$stageDefine = switch -Wildcard ($Profile) {
"cs_*" { "CSHADER" }
"vs_*" { "VSHADER" }
default { "PSHADER" }
}

if (-not $Permutations -or $Permutations.Count -eq 0) {
$Permutations = @(
"$stageDefine",
"$stageDefine VR",
"$stageDefine HDR_OUTPUT",
"$stageDefine VR HDR_OUTPUT"
)
}

# Materialize the base revision of the file.
$work = Join-Path ([IO.Path]::GetTempPath()) ("shaderverify_" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force $work | Out-Null
$baseFile = Join-Path $work "base.hlsl"
$baseBlob = "${BaseRef}:${relPath}"
# Capture and write as UTF-8 (no BOM); PS 5.1 `>` redirection emits UTF-16, which fxc misreads.
$baseContent = git show $baseBlob 2>$null
if ($LASTEXITCODE -ne 0 -or -not $baseContent) {
throw "Could not read '$relPath' at '$BaseRef' (git show $baseBlob)."
}
[IO.File]::WriteAllLines($baseFile, $baseContent)

function Compile([string]$src, [string]$defs, [string]$outFile, [switch]$Asm) {
# Preserve explicit-valued defines (e.g. SHADOWFILTER=0); only bare names get =1.
$defArgs = @()
foreach ($d in ($defs -split '\s+' | Where-Object { $_ })) {
$defArgs += "/D"
$defArgs += $(if ($d -like '*=*') { $d } else { "$d=1" })
}
$fmt = if ($Asm) { "/Fc" } else { "/Fo" }
$out = & $fxcPath /nologo /T $Profile /E $Entry @defArgs /I $IncludeDir $src $fmt $outFile 2>&1
return @{ Code = $LASTEXITCODE; Out = $out }
}

Write-Host "Shader : $relPath"
Write-Host "Base ref : $BaseRef"
Write-Host "Profile : $Profile (entry $Entry)"
Write-Host "Include : $IncludeDir"
Write-Host ("-" * 60)

$allIdentical = $true
$anyError = $false

foreach ($perm in $Permutations) {
$tag = $perm
$baseCso = Join-Path $work "base.cso"
$workCso = Join-Path $work "work.cso"
$rb = Compile $baseFile $perm $baseCso
$rw = Compile $shaderFull $perm $workCso

if ($rb.Code -ne 0 -or $rw.Code -ne 0) {
$anyError = $true
$which = if ($rb.Code -ne 0) { "BASE" } else { "WORK" }
Write-Host "[$tag] COMPILE-ERROR ($which)" -ForegroundColor Red
($(if ($rb.Code -ne 0) { $rb.Out } else { $rw.Out }) | Where-Object { $_ -match 'error|warning' } | Select-Object -First 6) |
ForEach-Object { Write-Host " $_" }
continue
}

$hb = (Get-FileHash $baseCso -Algorithm SHA256).Hash
$hw = (Get-FileHash $workCso -Algorithm SHA256).Hash
if ($hb -eq $hw) {
Write-Host "[$tag] IDENTICAL" -ForegroundColor Green
} else {
$allIdentical = $false
Write-Host "[$tag] DIFFERS base=$($hb.Substring(0,12)) work=$($hw.Substring(0,12))" -ForegroundColor Yellow
# Tier 2: assembly diff for inspection (Compare-Object avoids git's CRLF/exit noise).
$baseAsm = Join-Path $work "base.asm"; $workAsm = Join-Path $work "work.asm"
Compile $baseFile $perm $baseAsm -Asm | Out-Null
Compile $shaderFull $perm $workAsm -Asm | Out-Null
$d = Compare-Object (Get-Content $baseAsm) (Get-Content $workAsm)
if ($d) {
$d | Select-Object -First 40 | ForEach-Object {
$mark = if ($_.SideIndicator -eq '=>') { 'work' } else { 'base' }
Write-Host (" [{0}] {1}" -f $mark, $_.InputObject)
}
if (@($d).Count -gt 40) { Write-Host (" ... (+{0} more asm lines)" -f (@($d).Count - 40)) }
}
}
}

Write-Host ("-" * 60)
if ($anyError) { Write-Host "RESULT: compile error" -ForegroundColor Red; $exit = 1 }
elseif ($allIdentical) { Write-Host "RESULT: behavior-preserving (all permutations identical)" -ForegroundColor Green; $exit = 0 }
else { Write-Host "RESULT: bytecode differs - inspect asm diff above" -ForegroundColor Yellow; $exit = 2 }

exit $exit
Comment thread
alandtse marked this conversation as resolved.
}
finally {
# Runs on normal exit and on throw, so the temp dir never leaks.
if ($work -and (Test-Path $work)) { Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue }
Pop-Location
}
Comment thread
alandtse marked this conversation as resolved.
19 changes: 19 additions & 0 deletions tools/verify-shader-refactor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Thin wrapper around verify-shader-refactor.ps1 for bash/WSL/git-bash users.
# fxc.exe is Windows-only and MSYS mangles its /switches, so the real work lives
# in PowerShell. This just forwards arguments verbatim.
#
# Usage: tools/verify-shader-refactor.sh package/Shaders/Foo.hlsl [-BaseRef HEAD~1] ...
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ps1="$here/verify-shader-refactor.ps1"

if command -v pwsh >/dev/null 2>&1; then
exec pwsh -NoProfile -ExecutionPolicy Bypass -File "$ps1" "$@"
elif command -v powershell.exe >/dev/null 2>&1; then
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1" "$@"
else
echo "Need PowerShell (pwsh or powershell.exe) on PATH." >&2
exit 1
fi
Loading