LAUNCHER.md specifies what ships today: one PSP EBOOT or Vita VPK, every target-admissible app embedded, whole-guest switching behind three append-only surface ops. This document is the forward design: what turns that launcher into a platform — a mini-app runtime with an install story, a system transition, and a DX that stays instant — without breaking the contracts that got us here.
The one-line thesis: Pocket is already most of a mini-app runtime, and its unfair advantage is determinism. Same bundle + same inputs = same bytes on screen, on every host. That means third-party packages can be admission-checked, golden-verified and byte-audited by CI — platform review as a pure function, not a queue of humans.
| layer | contents | status |
|---|---|---|
| L0 contract | spec.ts ISA, append-only ops, capability registry, manifest admission | SHIPPED — the platform's constitution |
| L1 hosts | psp / vita / sim / web against one HostOps contract; guest lifecycle (boot/teardown/switch) | switching shipped on psp + vita + sim + web |
| L2 distribution | .pocket package, on-device install, host-relay push |
format SHIPPED (v1, below); install + push next |
| L3 experience | launcher-as-Home, switch veil, DevTools time travel | launcher shipped; veil below |
Rule 5 of RUNTIMES.md stays the platform's first law: every capability a guest can observe is a surface op behind a capability id — never a host branch. The launcher followed it (ops 39..41) and stayed testable on every host; distribution must follow it too.
Everything in this document is the guest execution class — one portable
bundle, runtime admission against the target registry. The AOT class
(Pocket Vapor/Static: recompiled per device, compile-time admission against
board data) scales by a different rule set on purpose; pocket.json's
execution.classes names the split and vapor/BOARDS.md
is its constitution.
A guest swap has a dead window: teardown is instant but the incoming
bundle's JS_Eval blocks the worker for 0.3–0.5 s (dev-trace builds print
boot stages there, which is the "QuickJS log stream" seen on hardware;
production builds hold the last frame). The veil turns that window into a
designed moment:
- After the outgoing frame presents (and the summon shot is captured), the HOST owns the GE. It plays a short direct-drawn animation — ~24 vblanks: frozen shot (or stage black) dimmed underneath, the baked Pocket mark fading in centered, a highlight sweeping across it — then starts the eval. The display holds the settled last veil frame through the eval, and the incoming guest's first present replaces it.
- Everything is host-side and asset-baked (the mark rasterizes from
assets/brand/at build time into an embedded RGBA texture; the sweep is vertex-alpha strips over the same texture, additive-blended — the PSP-era way to mask a glow to a glyph without a second texture). - The guest never knows. No op, no capability, no sim/golden impact: veil frames present outside the input-indexed frame loop, so the capture identity (input at frame N ↔ file fN) and the e2e switch signature are untouched.
- Budget: one 128×128 RGBA texture (64 KB rodata) + ~100 lines of GE code.
Two refinements stack on top later, both cheap: the launcher can play a
200 ms card-zoom before calling appLaunch (pure JS, no host change), and
qjsc bytecode (below) shrinks the hold the veil is papering over.
Spec: contracts/spec/pocket-package.ts (TS encoder/decoder) + engine/core/src/package.rs
(no_std zero-copy reader), pinned to one committed fixture so the two
implementations cannot drift. One file per app:
header "PCKT" v1, manifest length, variant count
manifest pocket.json verbatim (the SAME file the build resolver admitted)
variants TARGET VARIANTS — dist bundles are target-flavored (psp density 1,
vita density 2, a desktop widget's live viewport…), so the app is
manifest × variants. Each variant: target id + hostAbi + a typed,
APPEND-ONLY section table (1 identity, 2 plan, 3 js — NUL-included
for zero-copy device eval, 4 pak, 5 cover; unknown kinds skip).
Per-variant FNV-1a64: `thin` extracts a device subset from a
universal file without changing any variant's identity.
footer FNV-1a64 over the whole file (the stale-embed tripwire, now a
file format)
Tools: bun run pocket:pack build --manifest … --target psp --target vita
(each target compiles into its own outdir — the stale-dist lesson,
institutionalized), plus inspect, thin, verify (footer + per-variant
re-admission of the embedded manifest). The launcher chain
(tools/launcher.ts pack --target psp|vita) emits target-thinned packages
into separate trees. Both native binaries embed the .pocket files verbatim
and boot guests zero-copy out of them; the site serves the PSP packages to the
wasm host.
Design rules:
- The manifest travels INSIDE the package and is re-admitted on device (below) — a package is self-describing, never trusted by filename.
- The hash footer makes torn copies and stale syncs self-announcing — the same tripwire philosophy as the embedded-bundle hash (a stale embed once burned two rounds of hardware verification; distribution inherits the lesson for free).
- Corrupt or inadmissible packages fail into the launcher's existing broken-guest path (log + return to deck), never a halt.
- Loading: the eval path already treats bundles as data — the PSP host
reads
.pocketfiles fromms0:/POCKET/apps/into the arena and evals exactly like an embedded entry (svc/video proved runtime file IO). The appTable becomes the union of embedded entries and scanned packages; covers come from the package. Memory model is unchanged except the current app's js+pak live in the heap instead of rodata — the budget table gains one line. - Admission on device: build-time admission is
validateAndResolveBuildPlan, a pure function. The device re-runs the decision — either a minimal Rust port of the capability-superset + viewport check, or (simpler first step) the pack tool embeds the resolved plan and the device verifies plan.target/abi/viewport against its own profile. Either way: no capability, no boot — same rule as the build. - Channels, honestly ordered: (1) copy to the memory stick — works
today, zero code; (2)
pocket pushover the existing USB svc relay — the Mac downloads, verifies, writes to ms0; (3) wireless on-device fetch — LAST, because the PSP has no TLS and an 802.11b radio; the trusted downloader stays on the tethered host. Every step e2e-testable before the next.
QuickJS evals ~100 KB of source in ~0.3–0.5 s at 333 MHz; that is the
whole switch latency. qjsc bytecode loads 5–10× faster and shrinks
bundles. Plan: pocket pack --bytecode emits qjsc output alongside source
(sim keeps evaling source — the wasm core has no qjsc), the PSP host
prefers bytecode when the package carries it, and the bundle hash covers
whichever form is embedded. Risk to manage: bytecode pins the exact
QuickJS version — the package header gains an engine-abi field, and a
mismatch falls back to source.
The PSP-1000's 24 MB is the floor and texture heap is the cliff (the
reflection feature OOM'd it once — the handler parks on vblank, which in
PPSSPPHeadless looks like a silent timeout). Platform rule: pocket pack
computes a per-app memory line (js + pak + expected texture heap) and the
launcher displays it; admission warns when a package cannot fit the
device's arena. Budgets become data, not folklore.
The launcher's PSP hardware round found four bugs invisible to the sim (nearest-sampling shimmer, the affine seam kink, the texture-heap OOM, 4-bit-alpha banding) — PPSSPP's software GE caught three, real hardware the rest. Vita adds a native-density Vita3K leg with current-guest sidecars and repeated resource-reuse swaps before its real-device pass. That gradient is the platform's QA design:
- sim — second-scale iteration, deterministic traces, tree asserts;
- console emulator — PPSSPPHeadless exercises GE semantics (affine sampling, texture-cache quirks, RAM ceiling); Vita3K exercises the real Vita host lifecycle, 960×544 CPU-oracle renders, LiveArea VPK, and GXM resource boundaries under baked input scripts;
- hardware — PSPLINK hot-reload (
reset→ldstart) andpspsh cpon PSP; install the validateddist/vita/launcher-main.vpkfor the Vita pass.
Single-command chains (launcher.ts scan|covers|build), append-only
contracts, and goldens as gates keep third-party DX at "write TSX → see it
in a second → one command to the handheld."
- No suspend/resume state for guests — resume is relaunch, the frozen
shot is an affordance. A KV-style
app.stateop can come later as its own capability; nothing in the switch protocol presumes it. - No on-device code signing theater — the hash footer is integrity, not security; the PSP's threat model is a memory stick.
- No host branches for "special" apps — the launcher itself is an ordinary manifest app and must stay one.