Pocket Figma: Figma at 333 MHz · PocketJS
← BlogThe Sony PSP is twenty-two years old: 333 MHz, 32 MB of RAM, a GPU with no shaders. For the past weeks we have been teaching it modern UI — our runtime, PocketJS, runs real Solid and Vue JSX components on it at a locked 60 FPS, and it has already shipped a 3D shooter. But every screen so far was ours: our components, our layouts, sized to the machine. The honest test of a 2D UI runtime is a document drawn by someone who never once thought about your hardware — and the most demanding documents in the world come out of Figma .
So that became the challenge: open a real Figma Community file, as published. Not a cooked-up demo document — the Paper Wireframe Kit: 14,430 nodes, 2,293 component instances, a canvas 26,000 pixels wide, hand-drawn Patrick Hand lettering, photos, masks, the works. If the runtime can hold that, there is not much 2D left to be afraid of. It worked out better than we expected: you pan with the analog nub and zoom with the shoulder triggers, at 60 FPS, and it ships as one EBOOT.PBP you drop on a Memory Stick. We call it Pocket Figma .
Native 480×272 output of the shipping build at 59% zoom, mid-pan across the kit's cover. This frame — like every PSP screenshot in this post — is the executable's own framebuffer, captured in the deterministic emulator our byte-exact tests run on.<br>Pocket Figma is open source at pocket-stack/pocket-figma. And like OpenStrike before it, it exists to make a point about architecture: the device never parses, it only consumes. A design file is just the most literal possible test of that law, because a design file is nothing but things to parse.
This post is the full story: what is actually inside a .fig file — down to individual bytes — how you turn a 26,000-pixel-wide canvas into something a fixed-function GPU can stream, and why the whole thing is a pure function of the button mask.
A .fig file is a database, not a picture
Export any Figma document and you get a .fig — a ZIP with a thumbnail, the referenced images, and one opaque blob called canvas.fig. That blob opens with the magic bytes fig-kiwi, and the name is the first delight of this project: kiwi is Evan Wallace's schema format — Figma's co-founder wrote the serialization layer, published it as open source, and the file format carries his name in its header. More on him at the end.
The blob is a sequence of length-prefixed compressed chunks — and the compression is sniffed per chunk, not per file: in our kit, chunk 0 is raw deflate while chunk 1 opens with the zstd frame magic. Chunk 0 is a kiwi binary schema ; chunk 1 is the document, encoded in that schema. Read that again: the file tells you how to read itself. You do not chase a version-specific spec — you decode the embedded schema with the kiwi library and get back a typed tree of everything Figma knows about the document. Here is the whole layout, from the ZIP down to a single shape's bytes — every value below is read from the real file:
Paper Wireframe Kit (Community).fig — a 22 MB ZIP
canvas.fig<br>3.1 MB · fig-kiwi
images/ ×44<br>19.6 MB — the kit's photos
thumbnail.png<br>14 KB
meta.json<br>244 B
canvas.fig — 8-byte magic · u32 version · length-prefixed chunks
66 69 67 2d 6b 69 77 69<br>= "fig-kiwi"
65 00 00 00<br>u32 version = 101
30 6f 00 00<br>= 28,464 bytes →
chunk 0 · raw deflate<br>→ 69 KB kiwi schema, the decoder ring
b3 be 2e 00<br>= 3,063,219 bytes →
chunk 1 · zstd 28 b5 2f fd<br>→ the 9.8 MB document<br>compression is sniffed per chunk, not per file — this very file mixes raw deflate (chunk 0) and zstd (chunk 1)
chunk 1, decoded with the schema from chunk 0<br>{ type, sessionID, ackID, nodeChangeOrder,<br>nodeChanges: [ ×14,430 ],<br>ELLIPSE 3537 · VECTOR 2410 · INSTANCE 2293 · SYMBOL 1937 · TEXT 1522 …<br>blobs: [ ×4,066 ] }<br>binary geometry + glyph outlines, referenced by index<br>type · sessionID · ackID — the shape of a live multiplayer update, saved to disk
blobs[1] — one shape's fill geometry, all 46 bytes<br>01<br>00 00 00 00 00 00 00 00<br>moveTo 0, 0<br>02<br>00 00 c8 42 00 00 00 00<br>lineTo 100, 0<br>02<br>00 00 c8 42 00 00 c8 42<br>lineTo 100, 100<br>02<br>00 00 00 00 00 00 c8 42<br>lineTo 0, 100<br>02<br>00 00 00 00 00 00 00 00<br>lineTo 0, 0<br>00<br>closePath
100 × 100<br>u8 command (0 Z · 1 M · 2 L · 3 Q · 4 C) followed by f32 LE coordinates — 00 00 c8 42 is 100.0<br>One field cluster in that middle box deserves a pause. The decoded document's top level is not the shape of a document format — type, sessionID, ackID is the shape of a multiplayer update . A .fig file reads as a saved sync message: "here is everything that changed," where everything happens to be the entire document. The same protocol that lets two designers edit one canvas is what you replay when you open the file.
Here is the discovery that made this project feasible in a weekend rather than a quarter: Figma bakes its own render into the file. Every shape node carries fillGeometry and strokeGeometry — flattened path blobs where strokes have already been outlined into fills,...