Bytecode Format
PNGB is PNGine’s bytecode, the compiled form of a .sjon document (SJON, the
S-expression source language): a 40-byte header, an optional embedded executor
(the small WASM interpreter that plays the bytecode), an opcode stream, and a
set of trailing tables. All integers are little-endian.
The format is locked at version 0 by design. A self-contained payload carries its own interpreter (the embedded executor), so payload evolution rides on the executor rather than on the header: the WASM↔JS interface is what is versioned, and it is frozen as ABI v1 (see WASM API).
File Structure
Section titled “File Structure”+--------------------------------------+| Header (40 bytes) |+--------------------------------------+| Executor WASM (if embedded) |+--------------------------------------+| Bytecode (opcode stream) |+--------------------------------------+| String Table |+--------------------------------------+| Data Section |+--------------------------------------+| WGSL Table |+--------------------------------------+| Uniform Table |+--------------------------------------+| Animation Table |+--------------------------------------+| Device-Limits Table (if flag set) |+--------------------------------------+Header (v0)
Section titled “Header (v0)”| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | magic | PNGB (0x50 0x4E 0x47 0x42) |
| 4 | 2 | version | Format version (0) |
| 6 | 2 | flags | Bitfield (below) |
| 8 | 1 | plugins | Plugin set bitfield (below) |
| 9 | 3 | reserved | u24 LE offset of the device-limits table when flags bit 2 is set; {0,0,0} otherwise |
| 12 | 4 | executor_offset | Byte offset of the embedded executor (0 if none) |
| 16 | 4 | executor_length | Executor WASM size in bytes |
| 20 | 4 | string_table_offset | Byte offset of the string table |
| 24 | 4 | data_section_offset | Byte offset of the data section |
| 28 | 4 | wgsl_table_offset | Byte offset of the WGSL table |
| 32 | 4 | uniform_table_offset | Byte offset of the uniform table |
| 36 | 4 | animation_table_offset | Byte offset of the animation table |
The header is frozen at 40 bytes. New optional tables use spare capacity (a
free flags bit plus the reserved u24) and serialize to zero bytes when unused,
so payloads that don’t use a feature stay byte-identical to older ones. The
device-limits table (emitted by (limits …))
is the worked example.
| Bit | Name | Description |
|---|---|---|
| 0 | has_embedded_executor | Executor WASM is embedded in the payload |
| 1 | has_animation_table | Animation table is present |
| 2 | has_device_limits | Device-limits table is present; its offset is in reserved |
| 3 | canvas_alpha_premultiplied | The document asked for (canvas :alpha-mode premultiplied); clear means WebGPU’s default, opaque |
| 4–15 | reserved | 0 |
Plugin Set
Section titled “Plugin Set”One byte naming the runtime features the payload needs. The compiler detects them from the document and picks the smallest executor variant that covers them.
| Bit | Plugin | Covers |
|---|---|---|
| 0 | core | Bytecode parsing, buffers, command emission (always set) |
| 1 | render | Render pipelines, render passes, draw commands |
| 2 | compute | Compute pipelines, dispatch |
| 3 | wasm | Nested WASM execution ((wasm-call …), (wasm-data …)) |
| 4 | animation | Scene timeline tables |
| 5 | texture | Image/texture loading |
| 6–7 | reserved | 0 |
Executor variants as built today (uncompressed, measured 2026-08-18; the whole payload is DEFLATE-compressed inside the PNG, so the 11.8 KB render executor plus the triangle’s 631 bytes of bytecode land at 4.6 KB on disk):
| Variant | Size |
|---|---|
pngine-core.wasm |
9.1 KB |
pngine-compute.wasm |
9.4 KB |
pngine-render.wasm |
11.8 KB |
pngine-render-anim.wasm |
11.8 KB |
pngine-render-compute.wasm |
12.1 KB |
pngine-render-compute-anim.wasm |
12.1 KB |
pngine-render-wasm.wasm |
12.7 KB |
pngine-full.wasm |
13.6 KB |
Bytecode Section
Section titled “Bytecode Section”The opcode stream starts after the header (or after the executor, if embedded)
and runs to string_table_offset. Each instruction is a 1-byte opcode followed
by its operands.
Operand layouts are not listed here: they are defined in one place in the
engine, src/bytecode/wire_schema.zig,
which the emitter, the scanner and the dispatcher all read, so the three
cannot drift. The opcode values themselves are append-only: never renumbered,
never reused.
Resource creation (0x00–0x0E)
| Code | Name |
|---|---|
| 0x00 | nop |
| 0x01 | create_buffer |
| 0x02 | create_texture |
| 0x03 | create_sampler |
| 0x04 | create_shader_module |
| 0x05 | create_shader_concat (reserved: never emitted, dispatcher rejects it) |
| 0x06 | create_bind_group_layout |
| 0x07 | create_pipeline_layout |
| 0x08 | create_render_pipeline |
| 0x09 | create_compute_pipeline |
| 0x0A | create_bind_group |
| 0x0B | create_image_bitmap |
| 0x0C | create_texture_view |
| 0x0D | create_query_set |
| 0x0E | create_render_bundle |
Pass operations (0x10–0x1F)
| Code | Name |
|---|---|
| 0x10 | begin_render_pass |
| 0x11 | begin_compute_pass |
| 0x12 | set_pipeline |
| 0x13 | set_bind_group |
| 0x14 | set_vertex_buffer |
| 0x15 | set_index_buffer |
| 0x16 | draw |
| 0x17 | draw_indexed |
| 0x18 | dispatch |
| 0x19 | end_pass |
| 0x1A | execute_bundles |
| 0x1B | begin_render_pass_mrt |
| 0x1C | draw_indirect |
| 0x1D | draw_indexed_indirect |
| 0x1E | dispatch_indirect |
| 0x1F | set_viewport |
Queue operations (0x20–0x2C)
| Code | Name |
|---|---|
| 0x20 | write_buffer |
| 0x21 | write_uniform |
| 0x22 | copy_buffer_to_buffer |
| 0x23 | copy_texture_to_texture |
| 0x24 | submit |
| 0x25 | copy_external_image_to_texture |
| 0x26 | init_wasm_module |
| 0x27 | call_wasm_func |
| 0x28 | write_buffer_from_wasm |
| 0x29 | resolve_query_set |
| 0x2A | write_time_uniform |
| 0x2B | write_pointer_uniform |
| 0x2C | write_audio_data |
Frame control (0x30–0x35)
| Code | Name |
|---|---|
| 0x30 | define_frame |
| 0x31 | end_frame |
| 0x32 | exec_pass |
| 0x33 | define_pass |
| 0x34 | end_pass_def |
| 0x35 | exec_pass_once |
Pool operations (0x40–0x43): ping-pong resource selection for
:pool 2 buffers
| Code | Name |
|---|---|
| 0x40 | select_from_pool |
| 0x41 | set_vertex_buffer_pool |
| 0x42 | set_bind_group_pool |
| 0x43 | begin_render_pass_pool |
Extended pass state (0x4A–0x52)
| Code | Name |
|---|---|
| 0x4A | set_pass_timestamp_writes |
| 0x4B | set_pass_occlusion_query_set |
| 0x4C | end_occlusion_query |
| 0x4D | begin_occlusion_query |
| 0x4E | set_stencil_reference |
| 0x4F | set_scissor_rect |
| 0x50 | set_pass_depth_stencil_ops |
| 0x51 | set_blend_constant |
| 0x52 | set_pass_clear_values |
Varint Encoding
Section titled “Varint Encoding”Numeric operands use a 1/2/4-byte prefix code (payload bits big-endian within the value):
| Value range | Encoding | Bytes |
|---|---|---|
| 0–127 | 0xxxxxxx |
1 |
| 128–16383 | 10xxxxxx xxxxxxxx |
2 |
| 16384+ | 11xxxxxx + 3 bytes |
4 |
String Table
Section titled “String Table”Interned strings: entry-point names, uniform field names, frame names.
count: u16offsets: u16 × count (from the start of the string data)lengths: u16 × countdata: UTF-8 bytes, not null-terminatedData Section
Section titled “Data Section”Binary blobs referenced by opcodes: shader code, vertex data, descriptors, embedded WASM modules.
count: u16entries: (offset: u32, length: u32) × countdata: raw bytesRemaining Tables
Section titled “Remaining Tables”Three tables follow the data section, and a fourth is appended when its flag is set. Their byte layouts live with their reader/writer pairs in the engine source; the payload offsets above locate them.
| Table | Purpose | Source of truth |
|---|---|---|
| WGSL | Maps shader modules to data-section entries | src/bytecode/format.zig |
| Uniform | Per-binding uniform field metadata (name, offset, type); what setUniform resolves names against |
src/bytecode/uniform_table.zig |
| Animation | Scene timeline (flags, duration, scene list). The runtime reads it, but no SJON form emits one today; see JavaScript API | src/bytecode/animation_table.zig |
| Device limits | requiredLimits for device acquisition, from (limits …) |
src/bytecode/format.zig |
The uniform and device-limits tables are frontend-only: the JS runtime and the native renderer parse them, the embedded executor never does.
PNG Embedding
Section titled “PNG Embedding”PNGine uses five ancillary chunks, all public and safe-to-copy, so image tools that don’t know them preserve them:
| Chunk | Contents |
|---|---|
pNGb |
PNGB bytecode (executor embedded by default) |
pNGm |
Animation metadata as JSON, for the JS runtime |
pNGa |
Audio WASM module (e.g. a song compiled with the Sointu tracker synthesizer) |
pNGf |
Flat pre-decoded command buffers for the small main-thread mini player (--flat) |
pNGw |
DEFLATE-compressed WGSL source, used by --html output |
pNGb Chunk Layout
Section titled “pNGb Chunk Layout”version: u8 (0x01)flags: u8 (bit 0: payload is DEFLATE-compressed)payload: bytes (PNGB, optionally compressed)Compression is raw DEFLATE with no zlib header, so browsers decompress it with
DecompressionStream("deflate-raw"). The chunk sits immediately before IEND;
the image pixels are untouched. pNGa and pNGf use the same two-byte
wrapper; pNGm uses the version byte alone and stores its JSON uncompressed.
pNGf Chunk Layout
Section titled “pNGf Chunk Layout”Inside that wrapper, a flat payload carries a header of its own:
version: u8 (2)flags: u8 (bit 0: premultiplied canvas)init_len: u32 LE Length of the init command bufferframe_len: u32 LE Length of the per-frame command bufferdata_len: u32 LE Length of the inline data sectioninit_cmds, frame_cmds, dataThe two buffers hold the executor’s command-buffer opcodes, decoded at compile
time by replaying the bytecode against the mock GPU, which is why a pNGf
payload carries no executor. The version is 2 because clear values became one
f32 per channel and the pass commands changed shape with them; a player that
reads any other value must refuse the payload rather than walk it.
Chunk Naming
Section titled “Chunk Naming”pNGb follows PNG chunk-naming rules:
- p lowercase: ancillary, so decoders that don’t know it still display the image
- N uppercase: public namespace
- G uppercase: reserved bit, must be uppercase
- b lowercase: safe to copy through image editors
Measured Sizes
Section titled “Measured Sizes”Compiled from the engine’s examples/ corpus (2026-08-18). .pngb is bare
bytecode; .png is the self-contained artifact: 1×1 image, bytecode and
DEFLATE-compressed executor:
| Example | .pngb |
Self-contained .png |
|---|---|---|
simple_triangle |
631 B | 4,716 B |
rotating_cube |
3,901 B | 6,082 B |
boids (compute) |
4,902 B | 6,343 B |
Related
Section titled “Related”- WASM API - The executor that interprets this format
- CLI Reference -
compile,inspect,embed,extract - Shipping a Player - What reads each chunk