Contributing
The codebase is opinionated in a few places where being casual has already been measured to hurt. Read this page before your first PR; it is short and every rule on it has a story behind it (usually in VOXEL.md).
The hard rules
- No ROM-derived byte is ever committed. No cooked pak, no extracted art, no decoded text, no golden PNG. Goldens are frame hashes. If your change wants to commit a picture, capture real hardware output (the XMB art standard) or don't.
- The identity anchor is not yours to move.
*-max.hashesare never re-recorded for a dial edit. The three legitimate re-basing events — a vertex format change, a pack order change, a gameplay change — each pay with a pixel-diff proof: which marks moved, by how many pixels, and why each had to. See the golden ceremony. - No camera-relative representation change inside the visible field. A distance boundary that moves with the player plays as flicker on device. New fidelity work uses uniform dials, or cooks all levels and keeps the boundary out of the frustum. See the no-moving-boundary rule.
- The 24 px underside cull is a pak invariant. Anything that lowers a camera eye below 24 world px — a new rig, a lower rig height, a new pitch rung — invalidates every cooked pak, not just the cooker. Say so in the PR if you touch a rig.
- Gameplay formulas cite their provenance. Every ported rule carries a citation to the Lua it ports. A change to a rules module either matches the reference at its cited lines or declares an adaptation the way
rules/status.tsdoes. - The surface changes by ceremony. Edit
contracts/spec/voxel-spec.ts, regenerate the Rust (gen-voxel-rust.ts), commit both — the byte-compare drift guard fails the build otherwise. - The Pocket3D title card plays first at every launch on the PS Vita, the PSP and the Web Player. It comes from PocketJS (
vendor/pocketjs/engine/pocket3d/crates/pocket3d-title): do not skip, shorten, recolour or redraw it here, and do not draw the mark with the game's own renderer. The call sites arepocket3d_title::vita::play()incrates/pocketvoxel-vita/src/main.rs(beforevita2d_init_advancedstarts GXM),title()incrates/pocketvoxel-psp/src/main.rs(inpsp_main, before the debug screen or the GE draw) andplayTitle()inweb/main.ts(boot, awaited before the game reads input or starts its clock). The one skip is the PSPcaptureandautopilotfeatures, which never ship. - The app icon is the Pocket3D app icon, read from the PocketJS checkout's
vendor/pocketjs/engine/pocket3d/icon/. This repository draws no icon and tracks no copy of one: do not add anICON0.PNG,icon0.png,icon.pngorIcon.png, and do not resize, recolour or crop the PocketJS files. The consumers arexmb_icon_pngincrates/pocketvoxel-psp/Psp.tomland thepack-pbprepack intools/voxel.ts(psp/ICON0.PNG),icononpackageVitaVpkintools/voxel.ts(vita/icon0.png),web/scripts/platform-icons.ts(the web build copies both files to the pathsweb/platform/manifest.jsonpins) andbakeArtworkintools/iphone4s.tsfor the iPod touch 4 (ios/Icon.png,ios/Icon@2x.png). A new console target takes its icon the same way. The rule covers the icon only:PIC1.pngand the LiveAreabg.png/startup.pngare not PocketJS icon files. The procedure isvendor/pocketjs/skills/pocket3d-brand/SKILL.md;tests/pocket3d-icon.test.tsholds the wiring. After a PocketJS pin that changes an icon, updatebytesandsha256of the twoicon0entries inweb/platform/manifest.json: the web build prints the values it needs.
Before you push
bun run tsc # typecheck
bun test # 226 tests; ROM-gated suites skip without inputs
bun tools/voxel.ts check # both tapes, both rungs, against the goldensIf your change legitimately moves the shipped-rung picture (a dial change), run bun tools/voxel.ts record, commit the updated hashes, and put the accounting in the PR: which marks moved and why each one had to. Use bun tools/voxel.ts shots to eyeball, and the PPSSPP e2e to prove the GE agrees.
If check goes red at the -max tier: stop. The fix is in your change, never in the golden file.
Layout
crates/pocketvoxel-core scene core (no_std + alloc, zero deps)
crates/pocketvoxel-sim desktop headless host: rasterizer, PNGs, hashes
crates/pocketvoxel-gu PSP sceGu backend
crates/pocketvoxel-psp the EBOOT shell
crates/pocketvoxel-gxm Vita raw-GXM backend
crates/pocketvoxel-vita the VPK shell
voxelmon/import ROM importer (TS)
voxelmon/cook voxelizer + atlas packer + VXPK writer (TS)
voxelmon/game the gameplay port (TS) — Bun headless and QuickJS
voxelmon/tapes intent tapes
contracts/spec the surface spec + Rust codegen
tools/voxel.ts the pipeline command
tests/ suites + goldens (hashes only)
vendor/pocketjs the engine, pinned as a submoduleThe submodule pin moves deliberately: the PSP host library, the audio module and the toolchain pins all come from one mainline engine commit.
Working on these docs
The site lives in docs/ and builds with VitePress:
bun run docs:dev # local dev server with hot reload
bun run docs:build # production build (also validates links)
bun run docs:preview # serve the built sitedocs/VOXEL.md is the design record — a decision log, not a manual. Keep it append-oriented and let these pages stay the readable layer that links into it; don't duplicate a fact in both places when a link will do.