No description
  • Zig 81.5%
  • JavaScript 15.7%
  • Shell 1%
  • TypeScript 0.9%
  • HTML 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Hugo Daniel 44a4b0398d pngine 4.0.0
Release cut of development commit b7654ff: the engine source and
reference docs, without the test suite.

A WGSL document stops being one file, the sugar tier gets an anchor, and the
JavaScript API gets one contract.

Shader text can now be named, imported, held open at a hole and specialised
per module; a document can be another document plus a rebinding; a bind group
can leave its pipeline open; and the graph sugars grow the keys the corpus was
already asking them for. Separately, every key of every lowered form now has a
hand-written twin in `examples/` that renders identically, and a drift gate
that says so. On the host side, every `(buffer :name …)` is writable by name,
and `setFrame` and `setUniform(…, { redraw: false })`, both documented and both
inert through 3.0.1, now do what their documentation says.

Major, because the JavaScript API changed where a host can see it:
`getUniforms` answers in names, `p.audio` is read-only, three instance getters
that always answered `null` or `0` are gone, and calls that used to do nothing
now throw a `TypeError`. A document that compiled under 3.0.1 compiles
unchanged unless it hits one of the refusals listed below, and every one of
those except the symlink rule is a document that failed at runtime or rendered
something it did not ask for.

### Breaking

- **`getUniforms` speaks names.** Each field's `type` is the WGSL spelling
  (`"vec4f"`) where it was the payload's numeric wire tag, and `bufferId`, a
  dispatcher index that no public call accepted, is replaced by `buffer`: the
  `(buffer :name …)` holding the field, the same name `writeBuffer` and
  `getBuffers` take. `buffer` is `null` for a payload compiled before 4.0.0,
  because those payloads carry no buffer names. A tag this runtime does not
  know reads `"unknown"`. The types are published as `UniformField` and
  `UniformTypeName`.
- **`p.audio` is read-only.** It resolves to `duration`, `playing` and
  `currentTime` getters, and assigning through it throws. Through 3.0.1 it
  handed out the whole audio player, whose `play`, `pause`, `stop` and `seek`
  moved the song without moving the animation clock; the instance verbs
  (`play(p)`, `seek(p, t)` and the rest) drive both. The published
  `AudioPlayer` interface is now `AudioInfo`.
- **The instance getters `animation`, `duration` and `currentScene` are
  removed.** No payload has reached the runtime with an animation table since
  the loader rewrite of December 2025, so they answered `null`, `0` and `null`
  on every instance.
- **The instance verbs share one contract.** Arguments are checked first, and
  one of the wrong kind is a `TypeError` before anything is posted, including a
  non-finite time (`seek(p, NaN)`, `draw(p, { time: "1" })`), and including on
  a destroyed instance. A verb on a destroyed instance otherwise returns `p`
  and posts nothing, so `draw(p, …)` no longer throws there, and `draw` returns
  `p` like every other verb. The `pngine()` rejections for a source of the
  wrong kind, a missing `options.canvas` and the viewer's dev-only options are
  `TypeError`s for the same reason.
- **`setUniform(p, name, value, false)` throws.** The trailing boolean never did
  anything in a release (see Fixed). The spelling is
  `setUniform(p, name, value, { redraw: false })`, the trailing options object
  `writeBuffer` also takes, and the `TypeError` names it.
- **Documents the compiler now refuses.** A `:file` path with a symbolic link
  in any component below the document's directory, even a link that points
  back inside it: a downloaded project is a directory, so whoever wrote the
  document also placed its links, and "no links on the path" is a rule an
  author can check by looking. The rest used to compile and then fail or
  misrender: a document over one of the runtime's resource caps (it validated
  clean and died at render with `InvalidResourceId`; it is now refused at its
  source line, naming the cap and the backend that holds it), a
  `write-buffer` whose builtin or `(data …)` source is larger than the buffer
  it writes, a `(bind-group …)` with more than 255 entries, and a `gen` data
  WASM that exports its memory under a name other than `memory`, which no
  runtime reads.

### Added

- **`writeBuffer(p, name, bytes, opts)` and `getBuffers(p)`.** Every
  `(buffer :name …)` is host-writable by the name the author wrote — a bare
  `array<T>` no uniform reflects, a vertex buffer, an indirect-args buffer —
  where before the only route onto the GPU was a reflected struct leaf through
  `setUniform`. The compiler writes the names into the payload as a block at
  the tail of the uniform-table section behind header flag bit 4 (median +36
  bytes over the corpus; a document with no buffers is byte-identical), and
  the runtime resolves them. A write needs `copy-dst` in the buffer's
  `:usage`; a buffer the frame's own queue ops write every draw is refused
  (`getBuffers` reports it as `frameWritten`); a pooled buffer takes
  `poolIndex` (`k`, `"active"`, or absent for every member). Every refusal
  reaches `onError` as a `PngineGPUError` with source `"write-buffer"`, never a
  silent no-op. `draw(p, { buffers })` is the primitive both are sugar over,
  and `--types` writes `Buffers` / `BufferName` into the `.d.ts`.
- **`(wgsl …)` fragments.** A named block of WGSL, `:code` inline or `:file` on
  disk, that shader modules and passes assemble ahead of their own text with
  `:imports [name]`. A fragment is never a GPU object: what reaches wgslender
  and the payload is one assembled text per root, so minification renames
  across fragment boundaries and dead-code elimination drops what a root does
  not reach. `(shader-module … :file …)` reads a whole module the same way.
- **Holes (`:requires`).** A fragment may leave names open for the importing
  module to supply, which is how one library serves several variants with no
  WGSL extension. `validate` checks the list against the fragment's own
  unresolved names, both ways.
- **`(import :file "./base.sjon" …)`.** A document can be another document plus
  a rebinding of its consumed forms: the base compiles ahead of this one with
  the named `(define …)` constants and `(wgsl …)` fragments replaced,
  byte-identical to the base with those edits made by hand.
- **Derived bind groups.** Leave `:layout` out of a `(bind-group …)` and the
  compiler emits one GPU bind group per pipeline that binds it, each holding
  only the entries that pipeline's entry points reach. One resource set, any
  number of pipelines. `(bind-group … :prune-unused true)` is the same pruning
  for a group that does name a `:layout`.
- **`(draw-graph …)`.** One conceptual draw expanded into many concrete ones:
  a table of `(stage …)`s (what to draw into, and the pipeline state a stage
  shares) and `(geometry …)`s (what to draw, and the stages it appears in),
  lowered to one pipeline, one bind group per stage group and one render pass
  per pair. A stage can render into a texture rather than the canvas, and can
  state how its colour targets blend.
- **`(step-group …)` and named graphs.** A graph with a `:name` emits a step
  group instead of a frame, so two graphs — or a graph and hand-written passes
  — compose inside one `(frame …)`.
- **Five keys on `(pass …)`.** `(constant …)` children specialise the
  `override`s of the modules a pass builds; `:format` chooses the format of the
  texture it renders into; `:reads` binds textures the graph did not produce;
  `:scale` renders the intermediate at a fraction of the canvas; `:init` seeds
  that intermediate with a compute shader that runs once.
- **`pngine-frame-inputs`.** A 20-byte `write-buffer` source: `pngine-inputs`
  followed by the frame counter as a `u32`, for shaders that need the pool
  phase rather than the clock.
- **Diagnostics say which file they are in.** A finding inside an imported
  fragment names the FILE and its line inside that file, once per compile
  rather than once per importing module; `--json` entries carry a `"file"` key
  saying which file `line`/`column` belong to. The two reachability advisories
  (`W0001`, `W0002`) are not reported inside an imported fragment — a library
  declaration this document does not reach is a finding about the library —
  and `validate --verbose` prints how many were suppressed, with `--json`
  carrying the count as `suppressed`.
- **The browser compiler takes a file table** and reports what it needs, so a
  host with no filesystem can resolve `:file` and follow an import chain.
- **The sugar tier has a ledger.** `schema/sugar-ledger.mjs` records, for every
  key of every hook-lowered form, what it desugars to and which hand-written
  document proves it is sugar; `zig build drift` derives the key set from the
  schema and fails on a key that is not written down. All 23 are twinned, each
  gated on pixels by a row in the render suite. `zig build test-core` compiles
  every example with an EMPTY lowering registry, so a document either compiles
  byte-identically (it is core) or is refused with `lowering_hook_missing` (it
  is sugar), with nothing in between.
- **`(bytes …)`, verbatim bytes as a `(data …)` source.** `(bytes :file
  "table.bin")` embeds a sidecar file read at compile time and `(bytes :base64
  """…""")` carries the bytes inline (whitespace inside the literal is
  ignored, so a wrapped block diffs by line). The bytes are stored with no
  MIME prefix and sized like an inline array, so a `(buffer … :data …)` can
  size and fill from them, which `(data … :file …)` cannot.
- **`compiler.heapBytes()`** on `pngine/compiler`: the browser compiler's live
  heap, the compile-side counterpart of `getStats(p).wasmBytes`, for a host
  that recompiles on every keystroke and wants to see it stay flat.
- **An `unlocated` array in the `--json` envelopes.** Thirteen of the
  emitter's warnings have no position in the document (a finding about a WGSL
  binding has no SJON node to point at). They were left out of `--json`, so
  `validate --strict` could exit 1 against a report listing no diagnostics.
  They now ride their own array, the same object shape minus `line` and
  `column`, so an editor that turns each `diagnostics` entry into a squiggle
  never meets a line 0.
- Native `--frame` requests `depth32float-stencil8`,
  `rg11b10ufloat-renderable` and `timestamp-query` when the adapter has them,
  as the browser runtime does. `primitive-index`, `texture-formats-tier1` and
  `core-features-and-limits` do not exist in the vendored wgpu-native, so a
  document that needs one renders in the browser only.
- Forty-four examples and seventy-six rejection fixtures. Among the examples:
  `pass_acid_cubes` and `pass_acid_lattice` (a compute.toys cube-space march),
  `pass_game_of_life`, `pass_hdr_tonemap`, `pass_half_res`, `pass_reads_depth`,
  `pass_feedback_compute` (a hand-written compute pass beside a feedback pass
  in one frame), `graph_compose`, three `draw_graph_*` fixtures,
  `frame_index`, `test_data_gen`, ten `_hand.sjon` twins and six
  `_fragments.sjon` pairs that render pixel-identically to the document they
  were split out of. `examples/lib/` holds the 28 WGSL files those documents
  share.

### Fixed

- **`setUniform(…, { redraw: false })` was a no-op.** Its doc promised
  "batch updates without immediate redraw" from the API's first commit; the
  store it wrote was deleted as write-only, and the branch returned `p`. Both
  `setUniform` and `setUniforms` with `redraw: false` now store the values for
  the next draw from anywhere — the play loop's, a seek's, a host `draw()` —
  which carries and clears them. `writeBuffer` follows the same rule, and the
  option is spelled the way `writeBuffer` spells it, as a trailing options
  object (the old positional boolean is under Breaking).
- **`setFrame(p, name)` did nothing.** The draw message has carried the pinned
  frame since the API's first commit and the worker never read it, so a
  document with several `(frame …)` forms always ran its first. The pin now
  reaches the executor through three additive exports (`setFrame`,
  `getFrameCount` and `getFrameNameId`, in `docs/abi.md`); `setFrame(p, null)`
  releases it, `p.currentFrame` reads it back, and an unknown name reaches
  `onError` with source `"set-frame"` while the draw still renders. The
  `ready` message reports the payload's real frame count, where it always said
  1.
- **`(pass … :init …)` did nothing.** The key validated and was documented,
  but the pass lowering never read it, so a pass with it and the same pass
  without it compiled to identical bytes. It now seeds the pass's texture with
  a compute shader that runs once, before the first frame's feedback read. A
  document that carried the key renders the seed now.
- **A `setUniform` that could not land said nothing.** An unknown uniform name,
  or one whose buffer the init stream never created, was a debug-only console
  warning. It reaches `onError` with source `"set-uniform"`, once per distinct
  message, and the call still returns `false`.
- **The core tier could not hear command faults or see the pointer.**
  `pngine/core`'s `createCoreDispatcher` forwarded eight of the twelve
  dispatcher methods the worker tier calls. Without `setOnError`, an unknown
  opcode, an empty descriptor or a failed resource validation was console-only
  there; without the pointer and frame-counter setters, `pointer-inputs` stayed
  zeroed and the frame counter never advanced. All twelve are forwarded now,
  with `setBufferNames`, `writeBuffers` and `getBuffers` beside them.
- **`--html` exported wrong numbers.** Every float needing more than 24
  characters in decimal (`1e30`, `1e-30`, the largest `f32`) reached the page
  as `0`; `inf` and `nan` reached it as bare identifiers the browser rejects
  with a `ReferenceError`; and a very small `:depth-bias-clamp` emitted the key
  with no value. A 12-byte `scene-time-inputs` write overran its buffer, so
  WebGPU refused it. Each builtin source now writes exactly its own size.
- **A diagnostic that did not fit was blanked.** When the diagnostic store
  overflowed, an entry kept an empty message and read as a diagnostic with
  nothing to say. It is dropped and counted in `dropped`, the field that
  already means "this report is a prefix". A line number too large for the
  `--json` writer no longer leaves `"line":` with no value.
- **`miniPngine(canvas, view)` read a `Uint8Array` from byte 0 of its
  buffer**, so a view over part of a larger buffer (a slice of a bundle, a
  fetch body with a prefix) found no pNGf chunk. It reads the view's own bytes.
- **The published `.d.ts` disagreed with the runtime in eight places**, among
  them `destroy` typed as `void` while it returns the instance,
  `DrawOptions.frame` refusing the `null` that `setFrame(p, null)` takes, an
  exported `canvasAlphaMode` with no declaration, `getUniforms` missing
  `elemCount`, and `PngineInstance` hiding `currentFrame` and `audio`. The
  type strings now live in one file that a test derives from the code.
- **`(pass … :file …)` never worked on any shipping backend.**
  `write_buffer_from_wasm`'s first operand is a CALL id and the lowering passed
  a MODULE id, so the browser looked up a call that a `gen`-less data module
  never makes and wrote no bytes: a black canvas where the fixture's own
  comment said orange. A module with no `gen` export is now read at compile
  time and shipped as bytes through an ordinary `write_buffer`, which also
  drops two opcodes the native backend stubs (`test_data_pass` renders on
  native, and its payload is 1,098 bytes smaller).
- **A `gen` data buffer wrote to the wrong place.** The same operand family one
  slot to the left: `write_buffer_from_wasm`'s `buffer_off` is the destination
  in the GPU buffer, and the lowering passed `s`, which addresses the module's
  memory. Correct for every fixture that had `s` = 0 and past the end of an
  `l`-byte buffer for the first that did not. A `gen` module that exports its
  memory under a name other than `memory` is now refused at compile time — the
  runtime reads `exports.memory` or the `env.memory` import, so any other
  spelling generated bytes nobody could read.
- **A pass feature named only in a comment is not a feature.** The `(pass …)`
  lowering detected `post()`, pointer input and sampler use with a substring
  scan over the author's text, so a mention inside a comment enabled a feature
  the shader did not use — three live `wgpu` aborts, and a fourth reader that
  silently produced the wrong module.
- **A root or pass that opens with a directive can import.** `enable` and
  `requires` must precede every declaration in WGSL, so a document whose own
  text began with one could not have a fragment assembled ahead of it. The
  directives are hoisted to the top of the assembled text.
- **The used-but-unbound check asks per PIPELINE, not per document.** A binding
  that some entry point in the document reaches was treated as reached by every
  pipeline, which refused documents that were correct and let the reverse pass.
- A colour target with no authored blend gets no blend state on native, rather
  than a default one the document never asked for.
- The time uniform saturates at the payload's own size rather than at 16 bytes,
  so a 20-byte `pngine-frame-inputs` buffer is filled rather than truncated.
- An imported document's error names the file its line belongs to.

### Changed

- **`pngine/core`: `setFrameCounter`.** The core dispatcher's frame-counter
  setter carries the one name the executor's counter (`getFrameCounter`,
  frames rendered since `init()`) has on every tier. It was briefly
  `setFrameIndex` during development, which beside the main API's `setFrame`
  read as "which `(frame …)`"; no release carried that name.
- A `(pass …)` that draws to the canvas no longer allocates an output texture
  that nothing renders into or reads. Fifteen of the nineteen pass-graph
  examples paid for one: 8.29 MB at 1920×1080, reallocated on every resize.
- `docs/sjon-reference.md` gains sections for fragments and imports, extending
  a document, derived bind groups, pipeline-overridable constants,
  `(draw-graph …)` and `(step-group …)`.
- The corpus stops copying its shared WGSL. Six one-function libraries under
  `examples/lib/` (`perspective`, `look_at`, `inverse_camera`,
  `inv_perspective`, `rotate_vec`, `hash`) are imported by 51 documents
  between them — 37 for `perspective` alone — where each had carried its own
  copy.
2026-09-11 11:42:22 +01:00
docs pngine 4.0.0 2026-09-11 11:42:22 +01:00
examples pngine 4.0.0 2026-09-11 11:42:22 +01:00
npm pngine 4.0.0 2026-09-11 11:42:22 +01:00
schema pngine 4.0.0 2026-09-11 11:42:22 +01:00
scripts pngine 4.0.0 2026-09-11 11:42:22 +01:00
src pngine 4.0.0 2026-09-11 11:42:22 +01:00
tools pngine 4.0.0 2026-09-11 11:42:22 +01:00
.editorconfig pngine 2.2.0 2026-08-16 10:09:26 +01:00
.gitignore pngine 2.2.0 2026-08-16 10:09:26 +01:00
build.zig pngine 4.0.0 2026-09-11 11:42:22 +01:00
build.zig.zon pngine 2.2.0 2026-08-16 10:09:26 +01:00
CHANGELOG.md pngine 4.0.0 2026-09-11 11:42:22 +01:00
LICENSE pngine 2.2.0 2026-08-16 10:09:26 +01:00
package.json pngine 3.0.0 2026-08-20 13:36:26 +01:00
README.md pngine 3.0.0 2026-08-20 13:36:26 +01:00
SECURITY.md pngine 2.2.0 2026-08-16 10:09:26 +01:00
tsconfig.json pngine 2.2.0 2026-08-16 10:09:26 +01:00
vite.config.ts pngine 2.2.0 2026-08-16 10:09:26 +01:00

PNGine

npm license: CC0-1.0

The WebGPU spec mapped 1:1 to S-expressions, packed in a PNG. Self-contained, portable, tiny.

How Small?

Example Bytecode PNG with executor
Triangle 505 B 4.5 KB
Rotating cube 3.9 KB 6.0 KB
Boids simulation 4.9 KB 6.2 KB

The PNG contains everything: image, bytecode, and a WASM executor tailored to what the program uses. No external runtime needed.

Requirements

  • CLI / build from source: Zig 0.16
  • npm package: Node.js 18+
  • Browser runtime: any browser with WebGPU support

Install

npm install pngine

Or build from source. build.zig.zon declares two path dependencies, ../wgslender and ../SJON, so clone all three side by side:

git clone https://github.com/HugoDaniel/wgslender.git
git clone https://github.com/HugoDaniel/SJON.git
git clone https://github.com/HugoDaniel/pngine.git
cd pngine
zig build          # → zig-out/bin/pngine

https://git.hugodaniel.com/releases/pngine.git is the same repository.

Quick Start

Compile a shader to PNG:

pngine examples/simple_triangle.sjon -o triangle.png

Run it in a browser:

import { pngine, play } from 'pngine';

const p = await pngine('triangle.png', {
  canvas: document.getElementById('canvas')
});
play(p);

The PNG is self-contained.

Browser Runtime Profiles

The npm package ships focused runtime profiles:

Profile Import Size (gzip) Usage
Viewer (default) pngine 16.9 KB Production playback — worker + WASM inlined, zero external deps
Mini pngine/mini 3.2 KB Tiny main-thread player for flat (pNGf) payloads — no Worker, no WASM
Mini (no audio) pngine/mini-no-audio 2.9 KB Mini with the audio path dead-code-eliminated
Dev pngine/dev 18.5 KB Full feature browser runtime (selectors, image init, shared fallback)
Core pngine/core 8.9 KB Low-level dispatcher integration
Executor pngine/executor 0.9 KB Payload/executor helper utilities

Viewer inlines the WebWorker as a blob URL at bundle time. The WASM executor is extracted from the PNG itself. Result: a single .mjs file with no external runtime dependencies.

Mini interprets flat command buffers directly on the main thread. No Worker, no OffscreenCanvas, no WASM. Requires --flat compiled PNGs.

Both accept byte buffers (Uint8Array/ArrayBuffer/Blob) as source, so a PNG can be base64-inlined for a fully self-contained .html file.

See npm/pngine/README.md for API details.

How It Works

.sjon source
     |
     v
+----------+     +----------+     +--------------------+
| Compiler | --> | Bytecode | --> | PNG                |
|  (Zig)   |     |  (PNGB)  |     | + image            |
+----------+     +----------+     | + bytecode         |
                                  | + tailored executor|
                                  +--------------------+
                                           |
                                           v
                                  Browser: tiny loader (~2KB)
                                           |
                                           v
                                  Executor (WASM) --> WebGPU

The compiler does the heavy lifting. It validates the .sjon source against the WebGPU schema, picks an executor build with only the plugins the program needs, and bundles everything into the PNG.

The browser loader is minimal: it extracts the bytecode and executor from the PNG, instantiates the WASM, and connects it to WebGPU.

SJON Example

(shader-module :name shader :code """
  @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
    var pos = array<vec2f, 3>(vec2f(0, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));
    return vec4f(pos[i], 0, 1);
  }

  @fragment fn fs() -> @location(0) vec4f {
    return vec4f(1, 0.5, 0, 1);
  }
""")

(render-pipeline :name pipe
  :layout auto
  (vertex :module shader :entry vs)
  (fragment :module shader :entry fs
    (target :format preferred-canvas-format)))

(render-pass :name draw
  (color-attachment :view context-current-texture :load-op clear :store-op store)
  :pipeline pipe
  (draw :vertex-count 3))

(frame :name main :perform [draw])

This compiles to 517 bytes of bytecode. .sjon is validated against the WebGPU schema in schema/pngine.sjon; bare identifiers (shader, pipe) are cross-references resolved at compile time. The full authoring reference is docs/sjon-reference.md; examples/ holds over a hundred working programs.

Writing SJON with an LLM agent? Give it docs/llms.txt: a compact reference with complete programs the test suite validates, at a URL it can fetch whole:

https://raw.githubusercontent.com/HugoDaniel/pngine/main/docs/llms.txt

CLI

Command Description
pngine <input> Compile to PNG with embedded bytecode + executor
pngine compile <input> Compile to .pngb bytecode only
pngine validate <input> Check source: syntax, semantics, WGSL
pngine inspect <input> Inspect bytecode (--deep for runtime analysis)
pngine embed <png> <pngb> Embed bytecode into existing PNG
pngine extract <png> Extract bytecode from PNG
pngine bundle <input> Package a shader and its assets into a ZIP bundle
pngine list <zip|png> List what a bundle or PNG carries
pngine diff <a.png> <b.png> Pixel-compare two PNGs

Options:

Flag Description Default
-o, --output Output path <input>.png
-f, --frame Render actual frame via GPU Off (1x1 transparent)
-s, --size <WxH> Output dimensions 512x512
-t, --time <sec> Animation time 0.0
-m, --minify Minify WGSL shaders (~30% smaller shader text after compression) Off
--flat Compile to flat pNGf format (for mini player, no WASM) Off
--html Emit self-contained HTML with raw WebGPU JS (~1-3 KB) Off
--unpack With --html: skip the deflate pass — bigger, readable Off
--no-executor Don't embed executor (smaller, needs shared runtime) Off

Examples:

# Self-contained PNG
pngine shader.sjon

# Render 512x512 preview
pngine shader.sjon --frame

# Minified shaders for smallest payload
pngine compile shader.sjon -o output.pngb --minify

# Smaller PNG without executor (needs pngine.wasm at runtime)
pngine shader.sjon --no-executor

# Flat PNG for mini player (no WASM executor needed)
pngine shader.sjon --flat

# Self-contained HTML (~1-3KB, raw WebGPU JS, no runtime)
pngine shader.sjon --html -o shader.html

# Check bytecode is valid
pngine inspect output.png

Piping: every command takes - as an input (read stdin) or as -o (write stdout). stdin is identified by its leading bytes — PNG, PNGB, ZIP, else SJON source — so a pipeline needs no filenames. Diagnostics always go to stderr and binary output is refused on a terminal, so | jq and > file stay clean.

pngine extract art.png | pngine inspect -
pngine compile - < shader.sjon > out.pngb     # -o defaults to stdout for `-`
curl -s "$url" | pngine validate - --json | jq

Supported Platforms

The npm package includes native CLI binaries for:

Platform Architecture
macOS Apple Silicon (arm64), Intel (x64)
Linux x64, arm64
Windows x64, arm64

Plus the WASM executor for browser execution: 13,331 bytes for the full build (the build enforces a 13,600-byte cap); smaller variants ship when a payload needs fewer plugins.

Development

zig build              # CLI → zig-out/bin/pngine
zig build web          # WASM + JS for the browser tools (zig-out/playground/)
zig build npm          # cross-compile the npm platform binaries
zig build drift        # verify every generated artifact is current

Development happens in a private repository; the public repositories receive release cuts — engine source, examples and reference docs, without the test suite. Comments in the source cite that repository's development notes by journal section (§N).

Reference docs:

File Formats

Extension Description
.sjon SJON source (schema-driven S-expressions)
.pngb Compiled bytecode
.png PNG with embedded bytecode (and optionally executor)

Features

  • SJON compiler: schema-driven S-expressions validated against schema/pngine.sjon, with cross-references, bounded constant expressions and (pass …) / (init …) sugar
  • PNGB bytecode with DEFLATE compression; PNG embedding and extraction
  • Executor variants tailored to the plugins a payload uses, embedded by default
  • Browser runtime (WebWorker + OffscreenCanvas) in six profiles; flat pNGf payloads for the WASM-free mini player; self-contained --html output
  • Compute shaders with ping-pong buffer pools; WASM-generated data buffers
  • Shape generators (cube, sphere, cone, torus, teapot, dragon)
  • WGSL minification, (constant …) override specialisation, and advisory WGSL lint in validate (--strict turns warnings into exit 1)
  • Native --frame rendering through wgpu-native (Metal); pngine diff for pixel comparison; pngine inspect --deep for WAMR-backed runtime diagnosis
  • - on every command: stdin identified by its bytes, artifacts on stdout, diagnostics on stderr
  • npm package with native CLI binaries for six platforms

License

CC0 1.0 Universal — Public Domain