feat(package): .pocket v1 — one app, every target, one file#139
Merged
Conversation
One EBOOT now embeds every PSP-admissible app plus a Cover Flow deck (LAUNCHER.md). SELECT inside any app summons the deck over a frozen shot of the interrupted app; picking a card swaps the whole guest (fresh QuickJS realm + Ui core per app). Resume = relaunch — no suspend anywhere in the protocol. - spec ops 39..41 (appTable/appLaunch/appShot, append-only) + @pocketjs/framework/launcher wrappers with degraded no-op mode - native: build.rs multi-bundle embed (registry TSV -> generated APPS table), switch.rs (pending switch, SELECT edge latch, framebuffer capture + bilinear 256x128 shot with forced alpha), run_guest loop with full teardown (JS runtime free, Ui swap, vid/svc/font resets); broken guests fall back to the launcher instead of halting - host-sim: launcher.ts drives the same protocol over bootWorld for deterministic switch-flow tests; shot.ts is the downscale twin - demos/launcher: perspective-pipeline Cover Flow (spring-driven translateX/translateZ/rotateY cards, sim-rendered covers, frozen-shot scrim overlay, INTERRUPTED badge, browse-only fallback) - scripts/launcher.ts: computed admission registry (15 in, ipod-nano + note out), deterministic sim covers, one-command EBOOT build - tests: launcher-sim (protocol, latching, determinism-by-hash, registry freshness), e2e-launcher-ppsspp (3-swap journey, exact 217/220 capture signature); no launcher pixel goldens by design - single-app EBOOTs byte-identical: full PPSSPP e2e suite passes byte-exact after the main loop restructure Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rowsing First run on a real PSP over PSPLINK surfaced three issues: - mashing the d-pad outran the deck: springs let the selection race ahead of the animation, so CROSS launched a card the eye had not reached (picking "motions" landed on zoomlab). Browse transforms are now 140 ms ease-out tweens retargeted per press — visual center lags the selection by at most one hop. Reproduced + verified in sim. - heavy aliasing on tilted cards: the IMG flags byte existed in the pak layout but no cook path ever set it. encodeImageEntry gains a flags param, scripts/build.ts reads an optional per-app images.json, and every cover bakes IMG_FLAG_LINEAR; a 2 px alpha fade ring on each cover lets bilinear soften the polygon edges too. The frozen shot uploads linear as well (it stretches 256x128 -> 480x272). - everything ran at PSPLINK's 222 MHz default: the host never set the clock. run() now pins 333/166 MHz at boot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eats Hold LTRIGGER/RTRIGGER to flow through cards at 10/s (first step on the press frame), hold a d-pad direction to key-repeat after 250 ms. Every step retargets the same 140 ms tweens, so the deck glides continuously instead of stepping — and selection still moves one hop per step, so the visual center never trails what CROSS launches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Held-flow no longer steps card-to-card with decelerating tweens (it visibly paused at each card). The deck position is now a fractional scrub advanced every frame while the input is held, cards are placed with the new jump() primitive (direct set — the core kills any running tween on the prop), and release tweens home from the exact fraction. Deck geometry became one continuous function whose integer samples are the unchanged discrete targets. jump() is exported from @pocketjs/framework/animation as the general scrub primitive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CIRCLE launches the centered card (console convention); CROSS and SELECT back out to the interrupted app. This also closes the real-hardware report of every pick landing in the last app: users confirmed with O, which was bound to resume. A regression test now pins summon -> browse -> CIRCLE to the browsed card. - The deck stands on a baked stage: scripts/launcher.ts renders a 256x128 Aqua-era gradient (black floor, cool center glow, faint card sheen) stretched full-screen with bilinear, like the frozen shot. - Trigger flow 10 -> 14 cards/s, held d-pad 7.5 -> 9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A single-frame tap advances the scrub by only a fraction of a card; Math.round on release sucked it back to the origin. A flow that ends displaced from where it began now settles at least one card in the displacement's direction (the deck wall is the only exception). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- All four browse inputs are one flow mechanism now: d-pad LEFT/RIGHT scrubs exactly like the L/R triggers (18 cards/s held, tap = one card via the no-round-back rule). The discrete step handlers and the key-repeat delay are gone. - The multi-app EBOOT gets an XMB identity: demos/launcher/psp/Psp.toml with a committed ICON0 (a settled deck render resized to 144x80) and PIC1 (the stage gradient at 480x272), regenerated by the build's new art step via the sim host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Card textures double to 256x256: cover on top, a vertically mirrored copy below with a baked alpha falloff (~30% at the seam, gone 60% down). One 192x192 quad per card carries both, so the reflection rides the card's rotation for free and the fade lives in texture alpha — the 3D pipeline has no per-quad gradient, the stage simply shows through. (The first attempt used a second flipped Image per card and was dropped for clutter; baking is both cheaper and correct.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two real-hardware finds from the reflection feature: - The single 256x256 card quad put the cover/reflection seam mid-quad, where the GE's screen-space affine sampling bends texture lines at the triangle diagonal on tilted cards (a two-segment polyline where the card's bottom edge should be). Cover and reflection are now TWO quads stacked in the rotating container — the seam is a shared geometric edge and projects straight; interior affine warp stays, as the era intended. - Full-res reflection textures tipped the PSP arena over (~2 MB extra texture heap) and boot parked on the OOM handler. Reflections now ship quarter-res PSM_4444 (16 KB a card) — invisible at 30% alpha, and the sim never noticed either issue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PSM_4444 gave the 0.3->0 reflection ramp only ~5 alpha steps: visible horizontal bands every few rows on hardware. Quarter-res stays (32 KB a card keeps the arena comfortable); the 8-bit alpha ramp is what the fade actually needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PLATFORM.md lands the platform roadmap (layers, .pocket package draft, dynamic install admission, bytecode plan, memory governance, the sim -> PPSSPP -> hardware DX pipeline), and the veil is its first piece: - Every guest swap now plays ~24 host-drawn vblanks before the blocking eval: the outgoing frame's shot dims (every switch captures one now; guests still only see it on summon), the Pocket mark fades in centered, and an additive highlight sweeps across it — vertex-alpha strips over the mark's own texture, no second asset. The settled frame holds through the eval; the incoming guest's first present replaces it. - The mark rasterizes at build time from the brand SVG's exact geometry (SDF + supersample — the PNGs carry an opaque plate and the SVG's stroked shapes are outside bake-svg's subset), 128x128 RGBA embedded. - Real-GE find: the ui path leaves blending disabled between frames — the veil must sceGuEnable(Blend) itself or texture alpha is ignored. - Contract-invisible by construction: no op, no capability, frames present outside the input-indexed loop. The e2e switch signature is byte-for-byte unchanged (217/220, same gaps); capture builds dump veil frames separately as vNNNN.raw for inspection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three real-hardware reports, one root cause plus one GE truth: - The shot's 2:1 center crop deformed everything downstream: the veil's "dimming screen" stretched 13% vertically, covers lost their top and bottom 16 px. The downscale now takes the FULL 480x272 frame into the pow2 256x128 texture (stored slightly squeezed); every consumer draws at screen aspect — fullscreen 480x272, cards at 192x109 — which undoes the squeeze exactly. No crop, no net deformation. - The veil's left-to-right sweep read as a marching band of seams: GE sprites do not interpolate color across a quad, so the strip-built gradient was 12 flat steps. Replaced with a uniform-per-frame brightness swell on the mark — spatially constant by construction, nothing to tear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher's tilted cards still showed interior texture lines kinking at the quad diagonal — TEX_TRI sampling is affine per triangle, and the earlier cover/reflection split only straightened the SEAM, not content lines inside each quad. collect_3d now emits foreshortened image quads as a grid of cells sized by the perspective variation per axis (projectively correct UVs at every cell corner; residual per-cell affine error shrinks quadratically). Flat or affine mappings keep the single-quad path — all 49 pixel goldens pass byte-exact, and the e2e switch signature is unchanged. Also drops the launcher's corner texts (POCKET LAUNCHER / 15 APPS) — the deck speaks for itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ched The pocketjs.dev hero's 3D PSP now runs the SAME multi-app deck the EBOOT ships, on the wasm core: - PocketHost gains enableAppSwitching() — the browser twin of native/src/switch.rs + host-sim/launcher.ts: the three app* ops overlay every guest's namespace, SELECT is stripped from non-launcher guests with a host-tracked summon edge, and a switch is the same whole-guest swap runIIFE always was (frozen shot downscaled via the shared host-sim/shot.ts filter and re-uploaded into the next world). A broken guest falls back into the launcher, like the native rule. - The site build packages /stage/apps/ — launcher + all 15 admitted bundles + apps.json from the registry — replacing the single settings-main screen app; bundles prefetch idle-priority after the hero is up so launches swap instantly. - The stage model's SELECT button (and Shift on the keyboard) summons, exactly like the hardware chord. - site/verify.ts: dedicated Chrome profile dir (new-Chrome CDP requirement) + SwiftShader flags; the switch protocol is verified headlessly by driving PocketHost directly (WebGL-independent probe): boot -> launch Café -> SELECT summon with shot + resume, all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/site-build.ts (the deploy workflow's single entry) now runs the launcher artifact chain — registry scan, per-app bundles, deterministic covers, launcher compile — before site/build.ts, which requires them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The distribution container from PLATFORM.md, built to model the whole target spread (desktop widget / vita / psp), not a psp-web pair: - spec/pocket-package.ts: manifest × TARGET VARIANTS. Dist bundles are target-flavored (density-split rasters), so each variant carries its own resolved plan, js (NUL-included for zero-copy device eval), pak and cover behind a typed APPEND-ONLY section table (unknown kinds skip — the op-table discipline as a file format). Per-variant FNV-1a64 makes `thin` (universal -> device subset) identity-stable; a footer hash makes torn copies self-announcing. - core/src/package.rs: no_std zero-copy reader. Both implementations pin to ONE committed synthetic fixture — two readers, one contract. - scripts/pocket-pack.ts: build (per-target outdirs — the stale-dist lesson institutionalized) / inspect / thin / verify (footer + re-admission of the embedded manifest per variant). Proven with a universal hero.pocket: psp (density-1 pak 464KB) + vita (density-2 pak 1.8MB) side by side. - The launcher chain packs psp packages; the EBOOT now embeds .pocket files VERBATIM and boots guests straight out of them (build identity = hash of the package files); the site serves the same files to the wasm host, which decodes the psp variant in the browser. - e2e switch signature byte-identical (217/220), full test chain green (two stale site assertions from the stage copy change fixed along the way), core tests green, real-PSP reload + ms0 install verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardware report after the .pocket switch (log stream fighting the veil, hang on summon) traced to a deployment slip, not the format: the shipped release EBOOT had POCKETJS_TRACE baked in — debug-screen prints interleaving with the veil's presents, on the XMB where host0: does not even exist. PPSSPP's package-mode e2e runs the identical summon path byte-exact, and the traceless rebuild is the fix. Hardening from the same look: - scripts/psp.ts now warns LOUDLY when trace is baked into a release build — this exact slip shipped once. - Embedded .pocket files are anchored behind psp::Align16: their payloads are 16-aligned relative to the file start, include_bytes! guarantees nothing, and MIPS faults on unaligned word loads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
marked this pull request as ready for review
July 22, 2026 06:18
…port work) Conflicts were squash-merge echoes of the launcher PR plus the ESP32 viewport refactor: kept the .pocket package side everywhere it owned the hunk, took main's join()-style paths and SimViewportOptions, and merged the test chain as the union of both additions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
.pocketdistribution container from PLATFORM.md L2 — designed from the start to model Pocket's whole target spread (desktop widget / vita / psp and whatever comes next), not a psp+web pair.Format (spec/pocket-package.ts + core/src/package.rs)
manifest × target variants. Dist bundles are target-flavored (PSP bakes density-1 rasters, Vita density-2, a desktop widget resolves a live viewport), so "the app" is one target-neutral
pocket.jsonplus one variant per target: its resolved plan (admission evidence), js (NUL-included so devices eval zero-copy from .rodata), pak, and cover — behind a typed, append-only section table (unknown kinds are skipped: the op-table discipline as a file format).thinextracts a device subset from a universal file without changing any variant's identity — store holds the universal file, the memory stick carries the thinned one, same format.no_stdzero-copy Rust reader are both pinned to one committed synthetic fixture — they cannot drift apart silently.Proven with a universal
hero.pocket: psp variant (density-1 pak 464 KB) and vita variant (density-2 pak 1.8 MB) side by side in one file;thin --target psp→ 579 KB device file,verifyre-admits the embedded manifest per variant.Consumers wired in this PR
.pocketfiles verbatim; the core reader extracts js/pak zero-copy at boot. Build identity = hash of the package files (bundle-hash twin + devtools bridge updated). Single-app EBOOTs keep the classic inline embed, byte-identical./stage/apps/*.pocketserved as-is; the wasm host decodes the psp variant in the browser.pocket:pack build|inspect|thin|verify(each target compiles into its own outdir — the stale-dist lesson, institutionalized),launcher.ts packfor the EBOOT/site chain.Verification
bun run testfull chain green (+ new test/pocket-package.test.ts; two stale site assertions from the previous stage-copy change fixed).cargo test packagegreen (Rust reader vs the shared fixture).bun run e2e:launcher: byte-identical switch signature (217/220, same gaps) with the EBOOT now booting guests out of packages..pocketfiles, green.🤖 Generated with Claude Code