Skip to content

Limits & Roadmap

What the engine refuses, what it clamps, and what is not built yet. Every cap below is enforced; the interesting part is where: a compile-time rejection is a diagnostic you see, a runtime clamp is one you don’t.

Limit Value Enforced
Passes per document 32 Compile time; the emitter rejects the document
Bundles per :execute-bundles 16 Compile time
Arguments per (wasm-call …) 32 Compile time
Color attachments per multiple-render-target (MRT) pass 8 Structural; the emitter cannot emit more
CLI input size (file or stdin) 16 MiB Read time
Browser compiler source 256 KiB Clamped silently (see below)
Browser compiler output 256 KiB Errors
Executor bytecode buffer 256 KiB Clamped silently (per-binary default)
Executor data buffer 512 KiB Clamped silently (per-binary default)
Executor command buffer 64 KiB Reported: init returns 6, frame returns 3
Diagnostics in one report 64 Reported: --json carries a dropped count

The pass cap exists because the embedded executor (the small WASM interpreter that plays the bytecode inside each payload) stores pass ranges in a fixed 32-slot table; the compiler rejecting a 33rd pass up front is what keeps an over-cap document from rendering under native tests while losing passes in browsers.

The silent clamps are the ones to know about. The in-browser compiler’s setSourceLen truncates a document past 256 KiB instead of erroring, so an oversized source fails validation with a diagnostic that looks unrelated. The tell: native pngine validate accepts the same bytes. The executor’s buffer clamps are ABI behavior (lengths are clamped, never rejected) and matter only to hosts driving the WASM API directly.

The command-buffer cap is the counter-example, and deliberately so: a buffer that overflowed would tear the command stream mid-command, so the executor returns a status code instead of emitting a prefix, and the host must skip that buffer rather than play it.

Decisions, not gaps:

  • No resource destruction. The command stream is create-only; resource lifetime is scoped to the GPU device, released by whole-instance teardown (destroy(p)). A payload is data: it describes what exists, not an allocation sequence to balance.
  • PNGB, the bytecode format, stays at version 0. Payloads evolve by shipping a newer embedded executor, never by changing the header. The executor↔JS interface is frozen as ABI v1, append-only.
  • WGSL lint is validate-only. compile and render don’t pay for it, and the browser compiler omits the rule engine entirely (88 KB).
  • Multi-file documents. No import form; keep each document single-file. Compile-time reuse across files means generating the SJON source (the S-expression language PNGine reads) with your own tooling.
  • Timeline authoring. The payload format has an animation table and the runtime reads it (p.animation, p.duration), but no SJON form emits one. Sequencing is a JavaScript concern today: setFrame over multiple (frame …) definitions.
  • Shader composition. One (shader-module …) is one WGSL compilation unit. A concatenation opcode exists in the format but is reserved: never emitted, rejected by the dispatcher.
Terminal window
pngine validate shader.sjon --json # schema + WGSL + cap diagnostics
pngine inspect shader.sjon # what the compiled payload actually does