Limits & Roadmap
What the engine refuses, what it clamps, and what I have not built yet. Recall the triangle from Getting Started: one shader module, one pipeline, one pass, one frame, 631 bytes of bytecode. Nothing on this page will ever bother it. But documents grow, and this page lists every wall a growing document can hit. The values matter less than the enforcement points: a compile-time rejection is a diagnostic you read, a runtime clamp is a failure you reconstruct. I’ll walk you through which caps are which, and one place where I made the executor refuse outright because clamping there would have been worse than either.
Hard caps
Section titled “Hard caps”| Limit | Value | Enforced |
|---|---|---|
| Passes per document | 32 | Compile time; the emitter rejects the document |
| Frames per document | 64 | Compile time |
Bundles per :execute-bundles |
16 | Compile time |
Arguments per (wasm-call …) |
32 | Compile time |
| Colour 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, from the inside
Section titled “The pass cap, from the inside”Why 32? The embedded executor (the small WASM interpreter that plays the bytecode inside each payload) stays between 9 and 14 KB partly by allocating nothing: its tables are fixed arrays, sized when the WASM is built. Passes live in one of those:
the executor's pass table, sized at build time:
slot 0 1 2 31 +-------+-------+-------+ ... +-------+ | range | range | range | | range | +-------+-------+-------+ ... +-------+
range: where that pass's opcodes begin and end in the bytecodeA 33rd pass has no slot. And here is the trap the compile-time check exists
to close: the executor stores a range only for pass_id < 32 and drops the
rest at load, while the native reference dispatcher (the one behind --frame
and the test suite) keeps an unbounded pass map. Without the check, an
over-cap document renders perfectly in a native preview and loses passes in
every browser. Works-on-my-machine, made literal. So the emitter refuses the
document instead, and its diagnostic tells the whole story:
pass 'pass33' is pass #33, but the browser executor stores at most 32passes — passes beyond that are silently dropped at load (native`--frame` has no such cap, so this renders in a preview but not in abrowser)The 64-frame cap is the same asymmetry with a different fixed table, refused the same way.
The silent clamps
Section titled “The silent clamps”The executor’s bytecode and data buffer clamps are ABI behaviour (lengths are clamped, never rejected) and matter only to hosts driving the WASM API directly.
The counter-example: the command buffer
Section titled “The counter-example: the command buffer”That pays the promise from the top of the page. Why does the command buffer refuse (a status code) when the executor’s other buffers clamp? Because those clamps are harmless: a truncated source or bytecode buffer fails validation a moment later. A clamped command stream would not fail, it would tear:
what clamping would hand the player: 64 KiB cap v | begin-pass | set-pipeline | draw | writ▓▓▓▓▓ ^ the cut lands mid-command, and every byte past it decodes as noise played into a real GPU queue
what the executor does instead:
no partial buffer is emitted; `init` returns 6, `frame` returns 3, and the host must skip that buffer rather than play itThe triangle’s frame is a handful of commands; a document has to work hard to fill 64 KiB. But where a clamp stays harmless, the ABI clamps, and where it would play garbage, the executor refuses. That is the whole enforcement policy in one sentence.
Deliberate omissions
Section titled “Deliberate omissions”Decisions I made, not gaps:
- No resource destruction. I kept the command stream create-only; the
triangle makes a module, a pipeline and a pass, and frees none of them.
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.compileandrenderdon’t pay for it, and I left the rule engine out of the browser compiler entirely (88 KB).
Not built yet
Section titled “Not built yet”- 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:setFrameover multiple(frame …)definitions. - Shader composition. One
(shader-module …)is one WGSL compilation unit; the triangle’svsandfssharing a module is the only composition there is. A concatenation opcode exists in the format but is reserved: never emitted, rejected by the dispatcher.
Checking a document against the caps
Section titled “Checking a document against the caps”pngine validate shader.sjon --json # schema + WGSL + cap diagnosticspngine inspect shader.sjon # what the compiled payload actually doesAn exercise: make a cap fire once, so you know its voice. Duplicate the
triangle’s (render-pass …) form until there are 33 (let a shell loop write
the boilerplate; each copy needs a distinct :name, all can share the one
pipeline), list them in the frame, and run pngine validate. The diagnostic
quoted above arrives at pass #33, names the offending pass, and exits
non-zero. That one loud refusal is the contrast worth holding on to: it is
exactly what the silent clamps never give you.
Related
Section titled “Related”- CLI Reference -
validate,inspect, exit codes - WASM API - The frozen ABI behind the freeze rules
- SJON Reference - Every form that exists today