GPU Rendering Validation Checklist¶
Status of GPU rendering verification across platforms and configurations.
Start With the Bounded Health Probe¶
Run the installed diagnostic before inspecting build files or opening a live window:
The command performs a known offscreen draw plus pixel readback and a known
compute submission plus mapped-result verification. Use
pulp doctor gpu --no-render --json only for inventory/preflight when device
acquisition itself must be avoided; it performs no active render or compute
work and returns unverified. Both commands work outside a source checkout.
Agents can request the same typed evidence with the
pulp_gpu_doctor MCP tool.
Interpret the exit status with the JSON evidence:
0means all required real-work proofs passed and at least one required probe reported authentic adapter identity.1means the work completed but a measured assertion failed.2means a requested stage was unavailable or could not be verified.
Do not turn exit 2 into a skipped pass. Likewise, do not infer a discrete or hardware adapter from a Metal, D3D12, Vulkan, or other backend label. Trust only the adapter identity returned by Dawn; a null/software adapter is useful for negative coverage but is never hardware proof. Identities are per probe and do not claim Renderer3D, HeadlessSurface, and GpuCompute acquired the same device.
If the diagnostic reports unavailable or unverified, follow the remediation in the result, then use the Skia GPU build skill to inspect bundle discovery and ABI details. If it reports a completed failure, preserve the JSON result and reproduce with the focused render/compute tests before moving to a live application.
Authenticate a published hardware run¶
A checked-in pulp doctor gpu --json file proves measured GPU behavior, but
its JSON alone does not authenticate the machine that produced it. For a run
that will support a protected hardware-coverage claim, publish the result and
the canonical
pulp.gpu-health-run-attestation.v1
schema in one commit, then produce the signed sibling attestation and publish
that file in a later commit. The separation is required because a file cannot
contain the SHA of the commit that contains that same file without a circular
hash.
The producer reads the health result and schema from the exact evidence commit, selects one required passing probe with authentic hardware name/backend/device, hashes the exact producer binary, and signs the canonical statement with a pre-provisioned Ed25519 SSH host key. It never creates a key or changes host configuration:
private-host-inventory read-stable-machine-id | \
python3 tools/scripts/gpu_health_run_attestation.py \
--repository "$PWD" \
--health-result docs/validation/gpu-health/a1/m5/pulp-doctor-gpu.json \
--output docs/validation/gpu-health/a1/m5/run-attestation.json \
--signing-key /path/to/pre-provisioned/m5-ed25519 \
--host-id m5 --stable-machine-id-stdin \
--configuration 'power=low;fallback=false' \
--probe-id gpu-compute-magnitude \
--implementation-revision '<40-character implementation SHA>' \
--evidence-publication-revision '<40-character result/schema commit SHA>' \
--producer-binary /absolute/path/to/pulp-cpp
The private inventory tool must write exactly one non-empty LF-terminated UTF-8
identifier of at most 1024 bytes. The producer refuses an interactive terminal,
NUL, multiple lines, trailing data, invalid UTF-8, and oversized input. No
machine identifier is accepted through argv or environment variables, and
stdin has no competing JSON or evidence role. The raw value must not be copied
into the evidence tree or logs. The producer hashes it immediately and
publishes only
stable_machine_id_sha256, computed as SHA-256 over the ASCII domain
pulp.gpu-health.machine.v1, one NUL byte, and the exact UTF-8 bytes of the raw
identifier. This non-reversible pseudonym deliberately makes the same machine
linkable across Pulp GPU-health evidence, but it is domain-separated from raw
identifier hashes and other application namespaces. host_id remains a
non-sensitive logical alias such as m5, never a serial number or user name.
Verification is independent of the producer. Its local trusted-host registry
maps one logical host_id to the expected machine pseudonym and SSH public key;
the registry contains neither the raw stable identifier nor a private key. The
verifier requires the implementation,
evidence, and containing attestation revisions to form the expected ancestry,
requires the latter two to be ancestors of the named protected ref, re-reads
the schema and health result from Git, re-hashes the live producer binary,
requires the caller's exact implementation revision and selected probe as well
as the remaining selection policy, verifies the host signature, and
applies an explicit freshness ceiling to the GPU result's machine-produced
measured_at_utc. Re-signing an old result cannot refresh that timestamp:
python3 tools/scripts/verify_gpu_health_run_attestation.py \
--repository "$PWD" \
--attestation-revision '<40-character commit containing run-attestation.json>' \
--attestation-path docs/validation/gpu-health/a1/m5/run-attestation.json \
--protected-ref origin/main \
--trusted-hosts /path/to/local/trusted-gpu-hosts.json \
--producer-binary /private/read-only/snapshot/pulp-cpp \
--expected-producer-binary-path /absolute/signed/path/to/pulp-cpp \
--expected-implementation-revision '<40-character implementation SHA>' \
--expected-host-id m5 \
--expected-stable-machine-id-sha256 '<64 lowercase hex pseudonym>' \
--expected-configuration 'power=low;fallback=false' \
--expected-probe-id gpu-compute-magnitude \
--expected-adapter-name '<exact Dawn adapter name>' \
--expected-backend Metal --expected-device '<exact Dawn device identity>' \
--max-age-seconds 1800
--expected-implementation-revision is policy data, not a revision expression:
it must be the exact lowercase 40-hex commit ID. The attestation revision and
protected ref are resolved once to immutable commits; the verifier's freshness
clock is always the current system UTC time and has no command-line override.
--expected-producer-binary-path is the exact logical path signed by the
attestation. --producer-binary is only the local regular-file byte source: the
verifier opens it without symlink traversal, hashes that one descriptor, and
fails if its inode, path, size, or timestamps change during the read. It may
therefore be a private read-only snapshot at a different path. The snapshot
path is never emitted; the verification record retains the signed logical path
and the verified digest.
Version 1 does not authenticate a platform build ID, code-signature identity, team ID, CDHash, or package-signing metadata. Caller-supplied strings are not evidence and the producer and verifier reject the former build/signature CLI options. Consumers, including A3 policy, must authorize only the signed logical path, the descriptor-read SHA-256, and the protected Git publication bindings. A future platform-specific metadata extractor requires a new versioned contract before such metadata can carry authority.
On success, stdout is exactly one canonical JSON record conforming to
pulp.gpu-health-run-attestation-verification.v1.
The closed record retains the resolved protected, evidence, and attestation
commits; Git blob IDs and SHA-256 digests for the attestation, v2 health result,
and all three canonical schemas; every selection/host/producer policy value;
the trusted-registry digest and matched Ed25519 fingerprint; signature result;
and the verified chronology. It also identifies verification contract version
1 and hashes the verifier entrypoint plus its two adjacent source-owned Python
dependencies. Consumers must compare those three artifact digests to their own
approved evidence before execution; the verifier cannot establish trust in
itself merely by reporting its own digest. Failure is nonzero, writes the
diagnostic to stderr, and emits no partial or pass-shaped stdout.
The SDK installs the schema under share/pulp/contracts and the complete
verifier bundle under share/pulp/gpu-health-run-attestation-verifier. The
canonical verification schema is read from the evidence-publication commit,
so that schema must already exist at the recorded evidence revision and pass
the same protected-ancestry publication proof. The trusted registry stays
local and private-policy-controlled; the record contains only its path and
digest, never its public-key text or a raw machine identifier.
The trusted-host registry is a closed JSON object with
schema: pulp.gpu-health-trusted-hosts.v1, version: 1, and a hosts array.
Each host has exactly host_id, stable_machine_id_sha256, and public_key
(the single-line ssh-ed25519 ... public key). A missing trust entry, stale run,
unprotected commit, changed binary/result/schema, older-but-ancestral
implementation, alternate passing probe, or cross-host/configuration/adapter
reuse is a verification failure, never unavailable-as-pass.
DPR experiment evidence (A4 v2)¶
The committed A4 corpus defines an evidence-only experiment; it does not change
Pulp's scale policy. The canonical result is deliberately inconclusive with
zero v2 cells until the protected A3 product policy supplies every scenario's
frame budget, timer-noise bound, memory-sampler resolution, and required M5
coverage. Validate that truthful boundary before scheduling hardware work:
python3 tools/scripts/gpu_dpr_experiment.py validate-manifest
python3 tools/scripts/gpu_dpr_experiment.py validate-result \
docs/validation/gpu-dpr/terminal-result.json
emit-plan is a structural nonterminal fixture surface, not collection
authority. The terminal runner derives revisions, policy, and manifest from the
fixed receipts described below; a caller-generated plan cannot replace them.
The v2 plan covers exactly 84 original and 84 same-machine repeat cells: seven scenarios, DPR 1/1.5/2/3, and exact/configured-max/nonshipping-adaptive modes. Every cell has five reset warm-ups, 30 aligned measured triplets of at least 240 frames, and 20 reset fresh-process first-nonblank trials. Mode order and all bootstrap seeds use canonical UTF-8 length-prefixed fields and SHA-256 counter-mode; candidate intervals use exactly 10,000 aligned-trial percentile resamples. The Forge DAW aggregate additionally requires AUv2 in Logic plus VST3 and CLAP in REAPER for every cell. Metrics remain independently retained; the three subreceipts must agree on terminal verdict, gates, and bound identity.
A2T Perfetto coverage and the protected A3 policy/campaign are hard
prerequisites. Missing categories, a missing/changed pair or repeat, substituted
host/provider/format, stale dimensions, unavailable metric, exact-baseline or
fidelity failure, SKIP, or INCONCLUSIVE leaves v2 inconclusive. Historical v1
receipts are retained only as historical-v1-nonterminal; they count as zero v2
cells and cannot select B5.
Execute and resume the matrix after A3 authority¶
Do not initialize collection while v2_protocol.status is
blocked-product-policy. Do not ask a caller to update or supply the manifest.
After the fixed A2T, A3 DPR product-policy, and A3 runtime terminal receipts are
ordinary blobs at live protected Pulp main, init-v2 validates those exact
bytes and derives the authorized manifest itself. The run directory must be an
absolute absent path created by the runner. Each v2 adapter is an absolute
executable path and receives --request <json> --output <directory>; its
producer receipt must name eight real artifacts that the runner can snapshot
and revalidate.
python3 tools/scripts/gpu_dpr_runner.py init-v2 \
--run-dir /absolute/owned/pulp-dpr-run \
--experiment-id <campaign-id> \
--trace-analyzer /absolute/path/to/the/a2t-authorized-analyzer
python3 tools/scripts/gpu_dpr_runner.py run-v2 \
--run-dir /absolute/owned/pulp-dpr-run \
--cell 'original__dense-text-thin-strokes__exact__dpr-1' \
--adapter /absolute/path/to/a/v2-measurement-adapter
python3 tools/scripts/gpu_dpr_runner.py status-v2 \
--run-dir /absolute/owned/pulp-dpr-run
The existing init/run/issue/ingest protocol and checked-in native/web
adapters remain historical v1 collection machinery. They are nonterminal until
a real product-specific v2 adapter emits the closed producer receipt and eight
artifact kinds; do not pass a v1 adapter to run-v2 or wrap missing product
legs with invented JSON. A v2 timeout, per-stream output overflow, bad exit,
missing receipt, or rejected bytes closes that nonce as inconclusive and allows
a fresh attempt. It never completes the cell.
The checked-in Pulp-native v1 adapter supports both a capture-only preflight and a v1 measurement producer for the three frozen native fixtures. Build the producer with benchmark counters and tracing enabled, then point the adapter at that exact executable:
The native DPR measurement producer and its process/IPC tests are currently POSIX-desktop-only (macOS and Linux). Windows and Emscripten builds retain GPU recipe discovery and the portable probe model, but do not configure the native process/session producer until it has an equivalent platform implementation.
cmake -S . -B build-dpr -DCMAKE_BUILD_TYPE=Release \
-DPULP_BENCHMARK=ON -DPULP_TRACING=ON
tools/ci/governed-build.sh cmake --build build-dpr --config Release \
--target pulp-gpu-dpr-native-measurement
PULP_DPR_NATIVE_MEASUREMENT_BIN="$PWD/build-dpr/tools/cli/gpu_probe/pulp-gpu-dpr-native-measurement" \
python3 tools/scripts/gpu_dpr_runner.py run \
--run-dir /tmp/pulp-dpr-run \
--adapter dense-text-thin-strokes="$PWD/tools/scripts/gpu_dpr_pulp_native_adapter.py" \
--adapter shader-heavy-controls="$PWD/tools/scripts/gpu_dpr_pulp_native_adapter.py" \
--adapter meters-waveforms="$PWD/tools/scripts/gpu_dpr_pulp_native_adapter.py"
The producer uses one public editor-surface/WidgetBridge tree for capture,
logical input, 30 steady frame/counter samples, authentic Dawn identity, and
the nonce-correlated Perfetto trace. First-frame timing comes from 20 sequential
fresh child processes. Each child ledger row binds the attempt number/nonce,
unique PID, producer and content digests, Pulp build identity, exact adapter,
and its sample; the producer, adapter, and runner all verify that provenance.
Without PULP_DPR_NATIVE_MEASUREMENT_BIN, the adapter retains its safe
capture-only preflight and deliberately returns INCONCLUSIVE.
Each raw metric carries an explicit measured, derived, or unavailable
provenance plus its definition. unavailable has no samples or invented
percentiles and cannot make a cell policy-eligible. GPU timing also carries an
empirical resolution estimate and five baseline/five known-extra-work trials;
the extra-work control must be independently distinguishable before any timer
sample is accepted. The input oracle is frozen in the scenario manifest. The
producer reports the actual physical pointer event, logical point recovered
from that event, and hit target rather than supplying both sides of its own
expectation.
Fidelity compares two independently hashed PNGs of the same content/state and
reports numeric pixel similarity, small-text luminance variation, and
thin-stroke coverage. Text/stroke statistics are computed only inside the
scenario's frozen logical ROIs; whole-frame content variance cannot satisfy
those feature oracles. Adaptive trials retain the measured over/under-budget
samples and every scale transition; copied mode metadata is not adaptive
evidence. The browser producer requires exactly playwright-core@1.61.1 and
refuses another installed version. Old receipts invalidated by these instrument
rules remain preserved, but
docs/validation/gpu-dpr/instrument-validity-state.json marks them
SUPERSEDED/NONCOUNTED; never delete or silently promote them. Install the
browser dependency from its exact lock with npm ci --ignore-scripts in
examples/web-demos/super-convolver-ui/browser-test; do not use an unpinned
global Playwright package.
Historical v1 ingest accepts an independently produced cell receipt and
applies the v1 fidelity, logical-input, artifact-hash, identity, trace-category,
and raw-sample checks. Every closed trace question must return exactly the cell's issued
32-hex attempt nonce as its sole GPU evidence ID; IDs from another cell or a
capture containing ambiguous cohorts fail closed rather than being unioned.
For v2, finalize-v2 --run-dir /absolute/owned/pulp-dpr-run accepts no
manifest, draft, result, or disposition argument. It freshly revalidates the
terminal dependencies and rederives all 168 nonce-bound runner receipts and
1,344 artifact files before it recomputes
all candidate-vs-exact and adaptive-vs-configured intervals, regression gates,
same-unit repeat tolerances, class support, and the simplest-policy tie-break.
A candidate needs repeated material affected DPR-3 evidence in Pulp-native,
Forge-native/DAW, and web classes. no-change cancels B5; either candidate
leaves B5 waiting-trigger on B0-adopted-vellum-api-refresh. Every B5 receipt
keeps authorizes_policy_change=false.
A complete measurement JSON is still only a publication candidate. Publish the
runner-derived bytes only at the fixed manifest/result repository paths. From a
clean checkout at the exact fresh live protected Pulp main head, run
verify-live-v2 --evidence-root /absolute/owned/pulp-dpr-run; it emits a durable
receipt binding those ordinary Git blobs and bytes. validate-result requires
and freshly recomputes that receipt, unions
classic branch-protection and repository-ruleset required checks (including app
IDs), exhausts bounded pagination, and requires the unique latest matching
result to be successful. An open PR, wrong-app same-name check, truncated API
response, dirty/symlinked/outside/substituted candidate, wrong blob/type/head,
local commit, or caller-written true remains nonterminal. The complete
operator contract is in docs/validation/gpu-dpr/README.md.
Verified (Real Hardware)¶
| Platform | Backend | Surface | Rendering | Status |
|---|---|---|---|---|
| macOS (Apple Silicon) | Metal | CAMetalLayer (NSView) | Skia Graphite | Verified — GPU demo runs at 60fps |
| macOS (Apple Silicon) | Metal | CAMetalLayer (PluginViewHost) | Skia Graphite | Verified — DAW-embedded rendering |
Implemented (Awaiting Hardware Validation)¶
| Platform | Backend | Surface | Rendering | Status |
|---|---|---|---|---|
| iOS | Metal | CAMetalLayer (UIView) | Skia Graphite | Implemented — IOSGpuWindowHost + IOSGpuPluginViewHost |
| Windows | D3D12 | HWND (SDL3) | Skia Graphite | Implemented — SDL3 HWND extraction + Dawn D3D12 |
| Linux/X11 | Vulkan | X11 Window (SDL3) | Skia Graphite | Implemented — SDL3 X11 extraction + Dawn Vulkan |
| Android | Vulkan | ANativeWindow (SurfaceView) | Skia Graphite | Implemented — ANativeWindow extraction + Dawn Vulkan |
Architecture¶
Platform Window (NSView/UIView/HWND/SurfaceView/SDL3)
│
▼
Native Surface Handle (CAMetalLayer*/HWND/ANativeWindow/X11 Window)
│
▼
GpuSurface (Dawn/WebGPU)
├── Metal backend (macOS/iOS)
├── D3D12 backend (Windows)
└── Vulkan backend (Linux)
│
▼
SkiaSurface (Skia Graphite)
│
▼
SkCanvas → View tree painting
Render Loop¶
| Platform | Mechanism | Target FPS |
|---|---|---|
| macOS | CVDisplayLink → main queue dispatch | Display refresh (60-120Hz) |
| iOS | CADisplayLink | Display refresh (60-120Hz) |
| Android | AChoreographer | Display refresh |
| Windows | DwmFlush, with timer fallback if DWM is unavailable | Display refresh or 60Hz fallback |
| Linux | Timer fallback until native present-sync is wired | 60Hz |
| WASM | requestAnimationFrame | Display refresh |
Known Limitations¶
- Windows GPU:
DwmFlushgives compositor-paced frames when DWM is available; headless or remote sessions degrade to the 60Hz timer fallback - Linux GPU: X11 surface creation is wired, but frame pacing is still the 60Hz timer fallback
- iOS GPU: CADisplayLink frame pacing exists, but device runtime validation is still pending
- Linux Wayland: SDL3 can extract Wayland handles, but
GpuSurfacepresentation consumes X11 handles only today - WASM: WebGPU support depends on browser (Chrome 113+, Firefox 120+)
Test Coverage¶
Start a clean-agent investigation with catalog discovery, not a remembered recipe ID:
pulp gpu recipes list --json
pulp gpu recipes list --symptom compute-readback-mismatch --json
mkdir -p "$PWD/artifacts/gpu"
pulp gpu recipes scaffold gpu-compute.magnitude.v1 \
--output "$PWD/artifacts/gpu/magnitude-workspace"
The scaffold destination itself must not exist; its parent must already exist.
Run the scaffolded baseline twice, then its negative control. Baseline exit 0
means verified pass; the deliberate mutation must produce exit 1; exit 2 means
the requested evidence was unavailable or unverified and must not be counted as
a pass. Correlate the emitted gpu_evidence_id with the gpu-probe Perfetto
question when the wrong value is downstream of scheduling or frame work.
The A5 clean-agent harness exercises that flow rather than merely checking a
catalog lookup. Starting only from compute-readback-mismatch, it selects the
live CLI's unique callable recipe, scaffolds a workspace, executes the real
seeded failure, diagnoses its completed typed pass, removes only the documented
--negative-control seed, and proves the repaired rerun with stable
input/oracle hashes. See
docs/validation/gpu-clean-agent/README.md
for the standalone invocation and structural v4 evidence contract. Its
verification receipt is nonterminal; protected planning acceptance remains a
separate human-reviewed artifact after the exact Pulp head lands.
For a live product, query the exact instance separately with
dev.pulp.gpu/health.read@1 under inspect-readonly. That cheap snapshot is a
first-frame/control-plane signal, not a replacement for an offline oracle or a
Perfetto capture. The catalog's callable field is compile/runtime capability;
it does not claim that a host published the live operation.
Treat trace localization, platform-race proof, and product acceptance as three separate gates. For example, a trace can show paint averaging about 1 ms while resize spans remain near one 60 Hz frame (roughly 15.7 ms median and 18.7 ms at p99), narrowing the investigation to acquire, present, or compositor ordering. It cannot by itself prove the root cause. Use an AppKit/GPU event-order harness with a planted-old-behavior negative control to prove a redundant same-size resize callback or retained-cover lifetime race, then close the loop with a 60 fps recording and human interaction/feel validation. Trace evidence narrows the stage; the deterministic harness proves the platform race; product proof shows the fix solved the user-visible problem.
- 13 cross-platform render tests (GpuSurface + SkiaSurface)
- GPU demo validates continuous animation, vector drawing, resize
- Headless tests verify surface creation and texture lifecycle
pulp doctor gpuverifies bounded render/readback and compute/map work using the same public rendering primitives an installed consumer usespulp gpu probe --recipe <id> --artifacts <dir>localizes wrong values and pixels with deterministic inputs, independent CPU/content oracles, authentic adapter identity, and bounded hash-declared artifacts. Run the matching--negative-controlpath to prove the oracle detects a real seeded mutation; its expected exit status is 1 with typed failure evidence.- In builds that advertise it,
threejs.multi-pass.v1uses the hash-verified installed/sourcethree.webgpu.jsruntime through V8 and Dawn. Its three bounded RGBA readbacks show where background, intermediate, and final content first diverge; the C++ color-region oracle does not consume Three.js's own expected values. Run the baseline twice before the seeded mutation when diagnosing nondeterminism.