Skip to content

CLI Reference

pngine is one binary. It compiles a .sjon document (SJON, the S-expression format in which PNGine describes WebGPU work) to PNGB, PNGine’s compact bytecode; wraps that bytecode in a PNG or a standalone HTML file; renders frames on a native GPU; and replays a compiled payload to report what it does.

Terminal window
npm install pngine@^3

The flags and messages on this page are pngine 3.0.0’s. Or build from source:

Terminal window
git clone https://github.com/HugoDaniel/pngine.git
cd pngine
zig build
pngine <command> <input> [options]

With no command, pngine <input.sjon> runs render.

Command Purpose
render Create a PNG with embedded bytecode (the default)
compile Compile source to raw .pngb bytecode
validate Check source: syntax, semantics, WGSL
inspect Replay bytecode against a mock GPU (a call recorder), or under a real WASM runtime for deep analysis
embed Embed bytecode into an existing PNG
extract Extract bytecode from a PNG or ZIP
bundle Package a shader and assets into a ZIP
list List the contents of a ZIP or PNG
diff Pixel-compare two PNGs under a tolerance model
help, version Show help / version

Compile source and write a PNG carrying the bytecode in its pNGb chunk (an ancillary PNG chunk that image tools carry along untouched). By default the image is a 1×1 transparent pixel and the payload is self-contained: the bytecode plus the executor, the small WASM interpreter that plays it, in the smallest variant that covers the document’s features.

Terminal window
pngine <input.sjon> [-o output.png] [options]
pngine render <input.sjon> [-o output.png] [options]
Option Description
-o, --output <path> Output PNG path (default: <input>.png)
-f, --frame Render an actual frame via GPU instead of a 1×1 pixel (needs a GPU-capable build, see below)
-s, --size <WxH> Dimensions when using --frame (default: 512x512)
-t, --time <seconds> Time value for animation (default: 0.0)
-n, --scene <name> Render one named frame (default: all frames)
-e, --embed / --no-embed Embed bytecode in the PNG (default: on)
--no-executor Omit the embedded executor; the page must supply pngine.wasm
-m, --minify Minify WGSL shaders (~30% smaller shader text after compression; no effect on mesh data)
--validate / --no-validate WGSL validation at compile time (default: on)
--html Emit a self-contained HTML file instead (implies --flat; give it -o name.html, since the default output path still ends in .png)
--fullscreen / --no-fullscreen, --fixed HTML canvas sizing (default: fullscreen)
--unpack Emit unpacked HTML (no deflate; larger but debuggable)
--flat Write a pNGf chunk (flat, pre-decoded command buffers for the small main-thread player, pngine/mini) instead of bytecode; no executor
-a, --audio <path> Embed an audio WASM (e.g. a song compiled with the Sointu tracker synthesizer)
--types Also write TypeScript definitions (.d.ts) for the uniforms

--frame is the one option that needs a GPU. It renders through the native backend, wgpu-native, which a CLI carries only when it was built against that vendored library (the default when you zig build on a macOS host, after scripts/download-wgpu-native.sh). The binaries npm ships are GPU-less by design and stop with “needs a native GPU backend that this build lacks”, exit code 7. Everything else, the default 1×1 output included, is pure compilation and runs everywhere.

Compile source to raw bytecode.

Terminal window
pngine compile <input.sjon> [-o output.pngb]
Option Description
-o, --output <path> Output .pngb path (default: <input>.pngb; stdout when the input is -)
-m, --minify Minify WGSL shaders (~30% smaller shader text after compression)
--embed-executor Embed the executor WASM in the payload
--executors-dir <dir> Where to load executor variants from (default: zig-out/executors)
--validate / --no-validate WGSL validation (default: on)

Note the asymmetry with render: compile produces a bare payload and takes --embed-executor to opt in, while render embeds the executor by default and takes --no-executor to opt out.

Source-level validation: the full compiler pipeline with WGSL checking, but no output artifact.

Terminal window
pngine validate <input.sjon> [--json] [--strict] [--verbose]
Option Description
--json Structured JSON report on stdout
--strict Exit 1 on warnings (for CI)
-v, --verbose Show each validation phase

It catches syntax errors, semantic errors (unresolved references, schema violations, cycles) and WGSL errors (type errors, undefined symbols, uniformity).

It also reports advisory WGSL lint findings. Advisory means advisory by default: they print as warnings, ride the --json diagnostics array, and leave the exit code at 0. The one worth acting on is an unused binding: :layout auto strips bindings no entry point uses, which desyncs any (bind-group …) that binds them and otherwise surfaces only as an opaque abort at render time.

--strict turns every warning into exit 1, for CI. It changes the exit code and nothing else: --json still reports "status": "ok" for a valid document, with the findings in diagnostics, so the shape of the report never depends on the flag. The report also carries a dropped count, because the diagnostic sink holds 64 entries; a non-zero dropped means diagnostics is a prefix and “no warnings visible” cannot be read as “no warnings”.

Terminal window
pngine validate shader.sjon
pngine validate shader.sjon --json | jq
pngine validate shader.sjon --strict # fail CI on advisory findings

Bytecode-level inspection. The default mode replays the payload against a mock GPU; --deep runs it under a real WASM runtime for phase-by-phase analysis.

Terminal window
pngine inspect <input> [options]

Accepts .sjon, .pngb, .png and .zip.

Option Description
-v, --verbose Full GPU call trace
--json Structured JSON on stdout (auto-enables --deep)
--strict Exit 1 on warnings (for CI)
-q, --quiet Only output errors
--deep Enable deep runtime analysis (auto-enabled by the options below)
--phase <init|frame|both> Restrict analysis to one phase
--frames <list> Analyse several frames, e.g. 0,30,60
--symptom <name> Focus diagnosis: black, colors, blend, flicker, geometry
-t, --time <seconds> Base time for frame 0
--time-step <seconds> Time between frames (default: 1/60)
-s, --size <WxH> Canvas size (default: 512x512)
--extract-wgsl Include WGSL source in the output
Terminal window
pngine inspect shader.sjon # quick summary
pngine inspect shader.sjon --verbose # full GPU call trace
pngine inspect shader.png --symptom black # diagnose a black canvas
pngine inspect shader.sjon --frames 0,30,60 --json
Terminal window
pngine embed <image.png> <bytecode.pngb> [-o output.png]

Default output is <image>.embedded.png. Either input may be -, but not both.

Terminal window
pngine extract <file.png|file.zip> [-o output.pngb] [--list]

-l, --list reports every PNGine ancillary chunk in the file instead of extracting.

Terminal window
pngine bundle <input.sjon> [-o output.zip] [--assets <dir>] [--no-runtime]
Terminal window
pngine list <file.zip|file.png>

Pixel-compare two PNGs under a shared tolerance model.

Terminal window
pngine diff <a.png> <b.png> [--preset <name>] [--precision <f>] [--max-diff <n>] [--json]
Preset Precision Max per-channel delta
default 0.985 5
high-precision 0.99 2
compute-tolerant 0.95 10

diff has its own exit codes: 0 match, 1 differ, 2 read error, 4 not-a-PNG or size mismatch.

Every command takes - as an input path (read stdin) and as -o (write stdout), so pngine composes:

Terminal window
pngine extract art.png | pngine inspect - # inspect what a PNG carries
pngine compile - -o - < shader.sjon > out.pngb # explicit both ways
pngine compile - < shader.sjon > out.pngb # -o defaults to stdout for `-`
curl -s "$url" | pngine validate - --json | jq
pngine - --frame -s 512x512 -o - < shader.sjon | open -f -a Preview

The rules:

  • stdin is sniffed, not named. The leading bytes decide: a PNG signature, PNGB, PK\x03\x04, else SJON text. Bytes matching none of those are rejected as unrecognized rather than guessed at.
  • A real path still dispatches on its extension. A .png whose contents are not a PNG reports a PNG error rather than being silently reinterpreted.
  • - as input defaults the output to stdout, so -o - is optional.
  • Binary output is refused on a terminal (“pipe or redirect it”); text reports print to a terminal freely.
  • stdout carries only the artifact or the --json report. Every status line, warning and diagnostic goes to stderr, so | jq and > file stay clean.
  • Two-input commands accept - for at most one side; diff - - is an error, not a comparison of stdin with itself.
  • Piped source resolves relative references (a (data … :file "mesh.bin") path, a (wasm-data … :file …) module) against the current directory, since it has no directory of its own.
  • Input is capped at 16 MiB, the same guard that applies to file reads.
Terminal window
# Self-contained PNG with embedded bytecode + executor
pngine shader.sjon
# Custom output path
pngine shader.sjon -o build/shader.png
# Raw bytecode only
pngine compile shader.sjon -o shader.pngb
# Smaller payload: minified WGSL
pngine compile shader.sjon -o shader.pngb --minify
Terminal window
pngine shader.sjon --frame # 512x512 preview
pngine shader.sjon --frame -s 1920x1080 # 1080p
pngine shader.sjon --frame -t 2.5 # frame at t=2.5s
pngine shader.sjon --frame --no-embed # image only, no payload
Terminal window
pngine validate shader.sjon # source errors + advisory lint
pngine inspect shader.png # what the payload does
pngine inspect shader.sjon --verbose # full GPU call trace
Terminal window
pngine embed artwork.png shader.pngb -o final.png
pngine extract final.png -o extracted.pngb
Extension Description
.sjon SJON source (schema-driven S-expressions)
.pngb Compiled bytecode
.png PNG with embedded bytecode
.zip Bundle produced by pngine bundle
Code Description
0 Success
1 Invalid arguments; for validate, a document with errors (or warnings under --strict)
2 File I/O error
3 Compilation error: compile, render and inspect exit 3 on a document that fails validation
4 Format error (PNG/PNGB)
5 Execution error
6 -n, --scene named a frame the payload does not define
7 --frame on a build without the native GPU backend
8 A GPU validation error during --frame

diff overrides these with its own scheme (see above).

The CLI reads no environment variables. All configuration is via command-line arguments.

When installed via npm, use npx:

Terminal window
npx pngine shader.sjon -o shader.png
npx pngine inspect shader.png --verbose

Or add to package.json scripts:

{
"scripts": {
"build:shader": "pngine src/shader.sjon -o dist/shader.png",
"build:preview": "pngine src/shader.sjon --frame -o preview.png"
}
}