Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
76 changes: 76 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"//": [
"Project-wide Claude Code permissions, shared with every contributor.",
"Keep entries PORTABLE — relative paths and repo-local scripts only.",
"Machine-specific absolutes (your MSBuild location, your build platform)",
"belong in .claude/settings.local.json, which is gitignored."
],
"permissions": {
"allow": [
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git ls-files:*)",
"Bash(git submodule status:*)",
"PowerShell(git status:*)",
"PowerShell(git diff:*)",
"PowerShell(git log:*)",
"PowerShell(nuget restore:*)",
"PowerShell(.\\\\x64\\\\Debug\\\\ShaderLabTests\\\\ShaderLabTests.exe:*)",
"PowerShell(.\\\\x64\\\\Release\\\\ShaderLabTests\\\\ShaderLabTests.exe:*)",
"PowerShell(.\\\\ARM64\\\\Debug\\\\ShaderLabTests\\\\ShaderLabTests.exe:*)",
"PowerShell(.\\\\ARM64\\\\Release\\\\ShaderLabTests\\\\ShaderLabTests.exe:*)",
"PowerShell(.\\\\x64\\\\Debug\\\\ShaderLabHeadless\\\\ShaderLabHeadless.exe:*)",
"PowerShell(.\\\\x64\\\\Release\\\\ShaderLabHeadless\\\\ShaderLabHeadless.exe:*)",
"PowerShell(.\\\\ARM64\\\\Debug\\\\ShaderLabHeadless\\\\ShaderLabHeadless.exe:*)",
"PowerShell(.\\\\ARM64\\\\Release\\\\ShaderLabHeadless\\\\ShaderLabHeadless.exe:*)",
"PowerShell(pwsh -NoProfile -File .\\\\Tests\\\\RunTests.ps1:*)",
"PowerShell(pwsh -NoProfile -File .\\\\Tests\\\\RunHeadlessSmoke.ps1:*)",
"PowerShell(pwsh -NoProfile -File .\\\\Tests\\\\RunBrokerSmoke.ps1:*)",
"PowerShell(pwsh -NoProfile -File .\\\\Tests\\\\RunMathTests.ps1:*)",
"PowerShell(pwsh -NoProfile -File .\\\\Tests\\\\RunCliTests.ps1:*)",
"PowerShell(.\\\\Tests\\\\RunHeadlessSmoke.ps1:*)",
"PowerShell(.\\\\Tests\\\\RunBrokerSmoke.ps1:*)",
"PowerShell(Get-Process:*)",
"PowerShell(Get-AppxPackage:*)",
"PowerShell(Get-Content:*)",
"PowerShell(Select-String:*)",
"PowerShell(Select-Object:*)",
"PowerShell(ConvertFrom-Json:*)",
"PowerShell(Test-Path:*)",
"mcp__shaderlab__list_sessions",
"mcp__shaderlab__use_session",
"mcp__shaderlab__graph_overview",
"mcp__shaderlab__graph_get_node",
"mcp__shaderlab__graph_snapshot",
"mcp__shaderlab__graph_save_json",
"mcp__shaderlab__graph_get_view",
"mcp__shaderlab__list_effects",
"mcp__shaderlab__list_gpus",
"mcp__shaderlab__list_display_profiles",
"mcp__shaderlab__registry_get_effect",
"mcp__shaderlab__effect_get_hlsl",
"mcp__shaderlab__get_display_info",
"mcp__shaderlab__read_analysis_output",
"mcp__shaderlab__read_pixel_region",
"mcp__shaderlab__read_pixel_trace",
"mcp__shaderlab__preview_get_view",
"mcp__shaderlab__perf_timings",
"mcp__shaderlab__node_logs"
],
"ask": [
"PowerShell(Add-AppxPackage:*)",
"PowerShell(Remove-AppxPackage:*)",
"PowerShell(Stop-Process:*)",
"PowerShell(signtool:*)"
],
"deny": [
"Read(./**/*.pfx)",
"Read(./**/*.snk)",
"Read(./**/*.p12)"
]
}
}
165 changes: 165 additions & 0 deletions .claude/skills/shaderlab-build/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
name: shaderlab-build
description: Build and test ShaderLab (x64 or ARM64) — correct MSBuild selection, per-project builds, the test runner, headless probes, and release/MSIX packaging with its signing gotchas. Use when asked to build, compile, run tests, package a release, or when a build fails with PCH/toolset/packaging errors.
---

# Building & testing ShaderLab

Windows only. **x64 and ARM64** both build; outputs land in
`<Platform>\<Config>\<Project>\`. Needs VS 2022 17.8+ (2026 / v18 works), the Windows
App SDK 1.8 workload, Windows SDK 10.0.26100+, and `nuget.exe` on PATH.

Resolve MSBuild rather than hardcoding a path — edition and version differ per machine:

```pwsh
$vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
-latest -products * -requires Microsoft.Component.MSBuild -property installationPath
$msb = "$vs\MSBuild\Current\Bin\MSBuild.exe"
# ARM64 HOST ONLY -- the default binary above will fail, see next section:
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $msb = "$vs\MSBuild\Current\Bin\arm64\MSBuild.exe" }
```

## On an ARM64 host, use the ARM64 MSBuild

`MSBuild\Current\Bin\MSBuild.exe` is 32-bit and reports `PROCESSOR_ARCHITECTURE=x86`
under emulation, so toolset selection in `Microsoft.Cpp.ToolsetLocation.props` never
matches its ARM64 branch and falls through to the **32-bit** `bin\HostX86\arm64\cl.exe`.
That compiler exhausts its ~3 GB address space on the large generated translation
units and fails with:

```
C3859: Failed to create virtual memory for PCH
C1076: compiler limit: internal heap limit reached
```

`/p:PreferredToolArchitecture=x64` does **not** help — the props file declares
`TreatAsLocalProperty` and demotes it straight back to `x86`. The `arm64\` MSBuild
yields `VCToolArchitecture=NativeARM64` and builds clean.

Also required: the `Microsoft.VisualStudio.Component.UWP.VC.ARM64` component. Without
it the packaged WinUI app has no ARM64 platform and fails with *"The
BaseOutputPath/OutputPath property is not set for project 'ShaderLab.vcxproj'"* —
while the plain desktop projects build fine, so the failure looks partial and
unrelated.

The x64 CI matrix cross-compiles ARM64 from an x64 runner, so neither trap surfaces
there. The `native-arm64` job (`runs-on: windows-11-arm`) exists to catch the first
one, and fails loudly if the arm64 MSBuild is missing rather than falling through.

## Build targets

Build the narrowest project that answers the question — the full solution includes
the packaged app and is much slower.

```pwsh
$plat = 'x64' # or 'ARM64'

# Tests only (pulls in the engine). The usual inner loop.
& $msb ShaderLabTests.vcxproj /p:Configuration=Debug /p:Platform=$plat /m /v:m /nologo

# Headless host (engine + console host, no WinUI).
& $msb ShaderLabHeadless.vcxproj /p:Configuration=Debug /p:Platform=$plat /m /v:m /nologo

# Everything, including the MSIX-packaged app.
& $msb ShaderLab.slnx /p:Configuration=Debug /p:Platform=$plat /m /v:m /nologo
```

First build after a fresh clone: `nuget restore ShaderLab.slnx -SolutionDirectory .`
(packages.config style, not PackageReference). Submodules `exprtk` and `miniz` must be
present — `git submodule update --init --recursive`.

The GUI locks `ShaderLab.exe` / `ShaderLabEngine.dll` in the layout and a lingering
hub locks the broker copy. Kill them before rebuilding:

```pwsh
Get-Process ShaderLab, ShaderLabMcpBroker -ErrorAction SilentlyContinue | Stop-Process -Force
```

## Tests

```pwsh
& ".\$plat\Debug\ShaderLabTests\ShaderLabTests.exe" --adapter warp
```

Ends with `ALL <n> TESTS PASSED`; exit code is the failure count. `--adapter warp`
uses the software rasterizer — no GPU dependency, and what CI runs. Covers the graph
model, evaluator, bindings, bytecode cache, `GraphUiSnapshot`, dispatcher, MCP router
+ JSON-RPC contracts, broker frame codec / crypto / peer identity, GPU-binding
matrices, and the HLSL math bench.

Other suites: `Tests\RunHeadlessSmoke.ps1`, `Tests\RunBrokerSmoke.ps1`,
`Tests\RunCliTests.ps1`, `Tests\RunMathTests.ps1`, and `Tests\RunTests.ps1` (MCP —
see the **shaderlab-run** skill).

## Headless probes

Cheapest way to answer a numeric question — no deploy, no GUI:

```pwsh
$h = ".\$plat\Debug\ShaderLabHeadless\ShaderLabHeadless.exe"

# Render one node. The --output extension picks the encoder: .jxr/.wdp is
# 64bpp half, HDR preserved (and implies --no-tonemap); anything else is
# 8-bit sRGB PNG. --graph takes a .effectgraph ZIP or a bare graph JSON.
& $h --graph Tests\fixtures\test_cli_basic.json --node 3 --output out.png --adapter warp
& $h --graph Tests\fixtures\test_cli_basic.json --node 3 --output out.jxr --adapter warp

# FP32 pixel readback
& $h --graph <graph> --node <id> --pixels --adapter warp

# Batch parameter sweep
& $h --graph <graph> --script sweep.json --script-output result.json --adapter warp
```

Flags: `--width/--height` (default 1024), `--adapter warp|default`,
`--input-peak-nits` / `--output-peak-nits`, `--no-tonemap`,
`--enable/--disable-gpu-bindings`, `--reap-shader-cache` / `--clear-shader-cache`,
and `--mcp-session --pipe <name>`.

## Release / MSIX packaging

```pwsh
& $msb ShaderLab.slnx /p:Configuration=Release /p:Platform=$plat `
/p:AppxBundle=Never /p:UapAppxPackageBuildMode=SideloadOnly /p:GenerateAppxPackageOnBuild=true
```

Output: `AppPackages\ShaderLab\ShaderLab_<ver>_<Platform>_Test\` (msix + `Dependencies\<arch>\`).

**Unsigned** (what `release.yml` ships): inject the unsigned-namespace OID into
`Package.appxmanifest` (`Publisher="CN=ShaderLab"` →
`…, OID.2.25.311729368913984317654407730594956997722=1"`) and add
`/p:AppxPackageSigningEnabled=false`. Restore the plain manifest afterward **from a
byte-for-byte backup, not `git checkout`** — the working tree may hold uncommitted
manifest edits.

**Signed for local install** (plain manifest, no OID): MSBuild's own signing fails
here (APPX0105/APPX0107 importing `ShaderLab_TemporaryKey.pfx`). Sign manually:

```pwsh
# <thumbprint> = your CN=ShaderLab dev cert in CurrentUser\My. Find it with:
# Get-ChildItem Cert:\CurrentUser\My | Where-Object Subject -eq 'CN=ShaderLab'
signtool sign /fd SHA256 /sha1 <thumbprint> <msix>
```

`signtool` lives under `packages\Microsoft.Windows.SDK.BuildTools.*\bin\...\{arm64,x64}\`.

Signing gotchas, each of which has cost real time:

- **`0x8007000b` ("SignerSign() failed / unexpected internal error") means Publisher ≠
cert subject** — not an ARM64 signtool bug. Usually the msix still carries the OID
Publisher while you sign with the plain `CN=ShaderLab` cert. Check the msix's
internal `AppxManifest.xml` Publisher first.
- **Incremental packaging can leave a stale msix.** After swapping the manifest
(OID ↔ plain) MSBuild may not re-pack, so you sign yesterday's package. Force a
re-pack by moving `<Platform>\Release\ShaderLab\AppxManifest.xml` *and* the stale
`...Test\*.msix` aside, then rebuild.
- **A signed msix install needs the cert in `LocalMachine\TrustedPeople`** (admin,
one-time `Import-Certificate`). The dev cert is only in `CurrentUser\TrustedPeople`,
which suffices for `-Register` under Dev Mode but not for a signed install.
- **Unsigned + non-admin `Install.ps1` fails `0x80073D2B`.** ShaderLab is full-trust
(both `App` and `Hub` are `Windows.FullTrustApplication`), and an unsigned package
with executable activations requires an **elevated all-users** install — or a signed
release. This is a known release-process gap, recorded in `README.md`.

Fastest path into the app to test something: `Add-AppxPackage -Register` (dev-mode, no
signature check) — see the **shaderlab-run** skill.
123 changes: 123 additions & 0 deletions .claude/skills/shaderlab-run/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: shaderlab-run
description: Deploy, launch, and drive the packaged ShaderLab app — MSIX registration, shell activation, enabling the MCP session, and running the MCP test suite against a live GUI or a headless session. Use when asked to run/launch/deploy ShaderLab, to see a change in the real app, to connect over MCP, or when an MCP session is missing or stale.
---

# Running ShaderLab

Packaged WinUI 3 app. Deploy has several traps that fail in ways that look like app
bugs. Follow the order below.

Prefer **headless** when the question is numeric — it skips all of this. See
`docs/hosts/headless.md`, and `CLAUDE.md` for the inner-loop table.

## 0. Build first

Use the **shaderlab-build** skill. The GUI locks `ShaderLab.exe` and
`ShaderLabEngine.dll` in the layout, and a lingering `ShaderLabMcpBroker` hub locks
the layout's broker copy — **kill both before rebuilding** or the copy step fails.

```pwsh
Get-Process ShaderLab, ShaderLabMcpBroker -ErrorAction SilentlyContinue | Stop-Process -Force
```

A titleless lingering `ShaderLab.exe` is a hung shutdown — safe to kill.

## 1. Register the package — from the layout root, never `AppX\`

```pwsh
Add-AppxPackage -Register <Platform>\Debug\ShaderLab\AppxManifest.xml # x64 or ARM64
```

**Why the layout root matters:** an incremental build refreshes the layout root but
**not** `AppX\`. A registration pointing at `...\ShaderLab\AppX` runs stale binaries,
and new-exe/old-resources mixes abort at startup — which reads as an app crash, not a
deploy problem.

Binary-only rebuilds need no re-register. When registration fails:

| HRESULT | Meaning | Fix |
|---|---|---|
| `0x80073D02` | Package in use | Kill ShaderLab + broker processes, retry |
| `0x80073CFB` | Manifest content changed, version didn't | `Get-AppxPackage -Name ShaderLab \| Remove-AppxPackage`, then re-register. Config in `%LOCALAPPDATA%\ShaderLab` survives |
| `0x80070490` | Stale registration ("indexed state handler") | `Remove-AppxPackage` then re-register |
| — | Registration silently points at the old path | Re-registering the *same version* is a no-op that keeps the old path. `Remove-AppxPackage` first |

## 2. Launch by shell activation — never the exe directly

```pwsh
explorer.exe "shell:AppsFolder\ShaderLab_9v3yd384n9j18!App"
```

`Start-Process ShaderLab.exe` crashes with a `Debug Error! abort() has been called`
CRT dialog (packaged-app dependency resolution). The AUMIDs are
`ShaderLab_9v3yd384n9j18!App` and `…!Hub`.

**Cold Debug start can take >45 s.** There is no HTTP port to poll — readiness means
the session appears in `list_sessions`.

## 3. MCP

MCP is enabled by `%LOCALAPPDATA%\ShaderLab\config.json` containing `{"mcp": true}`;
alternatives are the `--mcp` arg or the toolbar toggle. On launch the GUI copies the
shim to
`%LOCALAPPDATA%\ShaderLab\bin\ShaderLabMcpBroker.exe` and registers a session with the
hub, which the shim activates on demand.

Transport is **shim → hub → session** over named pipes, bodies sealed end-to-end
(P-256 / HKDF / AES-GCM). There is no HTTP listener — it was deleted in stdio-migration
Step 9 (engine ABI 3).

To use it from this session: `list_sessions`, then `use_session <id>` to pin a window.

> **Note:** repo `.mcp.json` points the ShaderLab MCP server at
> `x64/Debug/ShaderLabMcpBroker/ShaderLabMcpBroker.exe` — the right default, since x64
> is what CI and most contributors build. **On an ARM64 host that path is wrong**: it
> either does not exist or is a stale x64 build running under emulation, while your
> real shim is in `ARM64\Debug\`. `.mcp.json` is committed and has no per-platform
> form, so override it locally rather than editing it — and if MCP behaves oddly,
> check which binary is actually running before debugging the protocol.

## 4. MCP test suite

Against the running GUI (the shim activates the packaged hub):

```pwsh
pwsh -NoProfile -File .\Tests\RunTests.ps1 -HubAumid 'ShaderLab_9v3yd384n9j18!Hub'
```

No-GUI, the way CI does it (GUI-only tests self-skip):

```pwsh
$env:SHADERLAB_MCP_ALLOW_UNPACKAGED = '1'
$bin = '.\x64\Debug' # or .\ARM64\Debug
$pipe = "ShaderLab.mcp.dev.$([guid]::NewGuid().ToString('N'))"
$hub = Start-Process $bin\ShaderLabMcpBroker\ShaderLabMcpBroker.exe `
-ArgumentList '--hub','--pipe',$pipe,'--idle-exit-sec','600' -PassThru
$sess = Start-Process $bin\ShaderLabHeadless\ShaderLabHeadless.exe `
-ArgumentList '--graph','Tests\fixtures\test_cli_basic.json','--mcp-session',
'--pipe',$pipe,'--session-label','dev','--adapter','warp' -PassThru
try { pwsh -NoProfile -File .\Tests\RunTests.ps1 -Pipe $pipe -Adapter warp }
finally { Stop-Process -Id $sess.Id, $hub.Id -Force -ErrorAction SilentlyContinue }
```

Verifying **hub activation** specifically: the acceptance check is a real MCP client
connecting — its shim runs the shipped `ActivateHub()` path. Close and reopen the
client; it should reconnect with no cold ShaderLab restart (the hub outlives the
client's job object, which is why `IApplicationActivationManager` is used over
`CreateProcess`). A PowerShell/C# re-implementation of `ActivateHub` tests the OS API
rather than the shim and **can false-pass** — don't use it as the gate.

## Screenshots and visual confirmation

Capture through the graph rather than the screen: `render_capture_node` over MCP, or
headless `--output`. That captures the actual scRGB FP16 pipeline output; an OS
screenshot of an HDR window does not.

**But a captured PNG is 8-bit SDR**, so it has clipped everything above scRGB 1.0
(80 nits) and lost wide-gamut negatives — it cannot settle an HDR or gamut question.
Prefer numbers (`read_pixel_region`, `read_analysis_output`) and, when you need to
*look*, capture a diagnostic node whose output is SDR by construction — `Nit Map`,
`Luminance Heatmap`, `Gamut Highlight`, `CIE Chromaticity Plot`, `Delta E Comparator`
in Heatmap mode. Say which you used, and flag when a verdict is passing through a tone
map. Full rationale in `CLAUDE.md` §*Looking at HDR output*.
Loading
Loading