A world map of places people remember. A place is a small, self-contained 3D scene of one real spot — a street corner, a stairway, a café — pinned to its location on a shared globe. People will publish their own places (publicly or privately) and download other people's places to visit them.
This repository holds the first-party places, the pipeline that turns a place into a pack for a handheld GPU, native PS Vita, Nintendo 3DS, PSP, iPod touch 4 and Android renderers, and the one interface they all draw. Publishing and downloading are not built yet. Vita and 3DS target 30 fps; PSP supports night streets and daytime slopes/streets through its fixed-function GE pipeline; the iPod touch draws all seven from the 3DS's kind of pack; the Android app, designed for and measured on a Redmi 1S, draws all seven from it at up to 1280 × 720.
It plays in a browser at atlas.studio.pocket.nexus. Its page on Pocket Studio, studio.pocket.nexus/games/atlas, has recordings and the packages for each device, which members download.
Places share their assets across the reference and handheld renderers:
web/is the reference renderer: a standalone three.js + Vite app with no PocketJS dependency, with a night-side globe to pick a place. Every asset is generated at load time.vita/renders the same place on a PS Vita with its own GXM pipeline: Cg programs compiled on the device by SceShaccCg, 4× MSAA HDR targets and the effect set the place needs.n3ds/renders the globe and all seven live places on an Old 3DS, using a PICA200 cook of the same assets, native 400 × 240 output and a 30fps quality budget. See the 3DS build and debug workflow.psp/renders the globe and the places its GE pipeline supports on a PSP.ipod/renders the globe and the seven places on an iPod touch 4 (iOS 6, SGX535, OpenGL ES 2) at 480 × 320, from the 3DS lowering with GLES texels and Griffith's lights and haze cooked for it. See how it draws, builds and is measured.android/renders the globe and all seven places on a Redmi 1S (Android 4.3, Adreno 305, OpenGL ES 3.0) at up to 1280 × 720 and 30 fps, from the 3DS lowering with ETC2 texels at 1024, relief cooked into the textures and the sun's shadows read per pixel from a depth map. Its one APK holds an ARMv7 and a 64-bit library and targets API 34: a current phone installs it and shows the 16:9 picture in the middle of a window of another shape. See how it draws, builds and is measured.wgpu/draws the atlas screen in a browser tab with wgpu over WebGPU: the iPod touch's globe in WGSL, with the interface over it and the page as any of the five devices the game has a package for (PS Vita, PSP, Nintendo 3DS, iPod touch, Android phone). A place is the PS Vita's renderer on wgpu, from the PS Vita's pack. See Browser tab.ui/is what all of them put on the screen in two dimensions: one PocketJS app with a presentation for each form of device. See The interface.
The web app exports glTF 2.0 with extras.pocketAtlas. The cooker seals a lossless PlaceIR, then independently lowers it into Vita, PICA, GE or GLES assets. See the compiler boundaries, commands and migration plan.
| Place | Id | Where | Rendering it drives |
|---|---|---|---|
| Rainy Night Konbini | tokyo-konbini |
Tokyo backstreet | wet ground with a planar reflection, rain, lit haze, interior-mapped windows, baked vertex lighting, moving lights |
| Suga Shrine Stairs | suga-shrine-stairs |
Yotsuya, Tokyo (the 男坂 stairs) | sun with a shadow map, sky occlusion baked into the vertices, alpha-tested foliage, daytime sky with a cloud panorama, ACES grade |
| Radio Kaikan at Blue Hour | akihabara-radio-kaikan |
Akihabara, Tokyo (秋葉原ラジオ会館, the 2014 building) | twilight sky (sun below the horizon), animated LED signage (flipbooks and scrolling strips), backlit window artwork, panel lights and lamps baked with sky occlusion, pedestrians and a passing train |
| Kamakura-Kōkōmae Crossing | kamakura-koko-mae-crossing |
Shichirigahama, Kamakura (鎌倉高校前1号踏切 on the Enoden) | open water (wave layers, Fresnel sky reflection, glitter path) to a 16 km horizon in FogExp2 haze, scrolling surf strips, flashing crossing lamps and gates driven by material and node tracks, a train, Route 134 traffic |
| Sangubashi in Bloom | sangubashi-crossing |
Yoyogi, Tokyo (参宮橋3号踏切) | spring foliage, animated petals, an eight-car commuter train, synchronised barriers and moving sunlight shadows; Vita picture quality accepted; recorded frame-rate limits documented |
| Griffith Observatory at Blue Hour | griffith-observatory |
Mount Hollywood, Los Angeles, over the basin (September 2015) | light fields of GXM point sprites (52k city lights, 5k moving), height haze with an inversion layer to a 71 km horizon, floodlit masonry baked into vertices, parallax windows, a resolution boost to 640×362 |
| Lombard Street in Bloom | sf-lombard-street |
Hyde to Leavenworth, Russian Hill, San Francisco | eight surveyed switchbacks, stepped footways, red brick paving, hydrangeas and bougainvillea, bay-window houses, a clear daytime sky, a Waymo I-PACE, a Tesla Cybercab and three visitors in a 120 s loop; shared daylight adaptation for PSP |
Real places fall into a finite set of kinds; the registry names them (PlaceKind in web/src/core/types.ts): night-street, daytime-slope, dusk-street, daytime-coast, daytime-street, dusk-vista for the places built so far, and night-slope, dusk-coast, night-coast, interior and rooftop for the places still to come. Each first-party place brings its kind's rendering to the best quality the handheld holds, and the work goes into the shared renderer and cooker so later places of the same kind reuse it. Glass (places/shared/glass.ts) blends premultiplied on the web as on the device. The workflow and quality bar for making a place are in the pocket-atlas-place skill (.claude/skills/pocket-atlas-place/).
| Path | Contents |
|---|---|
web/ |
three.js reference places (src/places/<id>, shared code in src/places/shared), globe, scripts: export-place.ts, export-atlas.ts, preview-place.ts |
crates/pocket3d-place |
pack formats: .place (META JSON + texture, geometry and animation blobs) and the Vita's atlas.pack (the globe and where its pins go); sRGB helpers |
crates/pocket3d-place-cook |
glTF → pack: BC1/BC3/BC5 textures with mips, quantized vertices, baked vertex lighting and sky occlusion, low-poly shelf stock, octahedral environment, effect textures; the atlas pack (atlas.rs); annotation readers (extras.rs) |
ui/, tools/atlas-ui.ts |
the interface: one PocketJS app (ui/app), its presentations (ui/app/presentations), the protocol it speaks with a renderer (ui/app/protocol.ts), host tests (ui/test); the tool compiles it for a device |
crates/pocket-atlas-interface |
the renderer's side of that protocol for the Rust renderers (the C ones use n3ds/src/interface.c) |
vendor/pocketjs/devices/vita/pocket-vita-gxm |
Shared GXM memory/program/target/texture mechanisms, optional runtime SceShaccCg; no scene or material policy |
vita/ |
Vita app: place loader (scene.rs), frame renderer (frame.rs), atlas globe (atlas.rs), the interface's guest (interface.rs), what a visitor can set (settings.rs), file locations (paths.rs), Cg programs (vita/shaders), LiveArea art |
psp/ |
PSP app: GE rendering of the places and the globe (globe.rs), animated nodes and skinning, the interface's guest (interface.rs), procedural rain audio, PSPLINK telemetry |
crates/pocket3d-place-psp |
Validated PLPS payload: shared GE vertex buffers, spatial index chunks, swizzled RGBA4444/RGBA8888 mip chains, animation and camera data; no JSON on the device |
n3ds/, tools/atlas-3ds.ts |
PICA renderer, native cooker, paired wireless deployment, capture and performance measurement |
tools/atlas.ts |
cook (places and the atlas pack), build, deploy over USB, status/capture/profile/sweep/shots, shader lint, standalone VPK |
tools/atlas-psp.ts |
PSP cook/build, PSPLINK serve/run/control/capture/shot measurements, standalone EBOOT package |
ipod/, tools/atlas-ipod.ts |
iPod touch 4 app (the GLES 2 renderer, the globe and the shell that hosts the interface) and its cook/build/install/control/capture/measure tool |
android/, tools/atlas-android.ts |
Android app, measured on the Redmi 1S (a NativeActivity: the GLES 3 renderer, the globe, the title card and the shell that hosts the interface), and its cook/build/package/install/control/capture/measure tool; the APK holds armeabi-v7a and arm64-v8a libraries |
wgpu/, tools/wgpu.ts |
browser tab: the atlas screen's globe (globe.rs), the shell and the interface channel (app.rs), the seam of a renderer of places (place.rs), the PS Vita's renderer of places on wgpu (places/, shaders/place/), the page; the tool builds, serves, writes the deployable directory and checks the page in Chrome |
vendor/pocketjs/devices/web/pocket-web-wgpu |
Shared browser mechanisms: the WebGPU device and its screens, the interface's overlay pass, ranges of a pack over HTTP, the title card and the frame loop, the interface's guest in a realm, a handheld's controls and screens on the page; no scene |
vendor/pocketjs |
PocketJS: the interface's framework, UI core and per-device guest runtimes; Vita dev host and wired debug transport; 3DS paired transport and native installer; pinned PSP toolchain resolver; iPod touch 4 sysroot, startup objects and installer; the QuickJS and Rust pins the Android guest builds with |
Everything flat on a handheld's screen (the atlas screen's lists, cards and search, a place's shots, settings and hints, the loading and error screens) is one PocketJS app, ui/. A renderer draws the globe or the place and hosts the app as a guest: PocketJS's UI core and QuickJS on the device, the guest's picture laid over the frame. The two talk in JSON lines over PocketJS's overlay service, answered in the process (ui/app/protocol.ts): the renderer says where things stand (scene, the places on the device, shots, what can be set), the interface says what the visitor asked for (turn the globe, enter, leave, cut to a shot, set an option, drive the camera).
ui/pocket.json declares a presentation per form of device, and PocketJS picks the one a device's modality asks for. They share every part (ui/app/parts.tsx) and all behaviour (browse.ts, visit.ts); a presentation decides where things go and which control means what:
| Presentation | Devices | Atlas | In a place |
|---|---|---|---|
single.tsx, 480 × 272, a pad |
PSP, Vita (whose panel also takes taps) | globe at the left, a card and four rows at the right; d-pad moves, ○ visits, □ saves, △ searches, L/R change list, the stick spins the globe | the scene has the screen; title and shot name fade after a few seconds; × atlas, △ menu, L/R shot, START pause |
dual.tsx, 400 × 240 over 320 × 240 |
3DS | globe and card on the top screen; lists with pictures on the touch screen, scrolled by stylus or d-pad; A visits, Y saves, X searches | top: the scene; bottom: the shots as rows to tap, a pad to drag the view with, Pause, Menu and Atlas buttons |
touch.tsx, 480 × 320 or 640 × 360, touch only |
iPod touch, Redmi 1S | a finger spins the globe and scrolls the list; Save and Visit are buttons | two sticks at fixed places in the lower corners (left walks, right looks); a tap calls up a bar (atlas, shots, tour, menu); on the phone the back key closes a sheet or leaves and the menu key opens the menu |
Featured, Explore (nearest where the globe faces), Saved and Search are the lists everywhere. Search types into PocketJS's own keyboard, which is a grid for a d-pad and keys for a finger. A place's menu lists what its renderer offers there (frame rate, quality, effects the place has, exposure, statistics).
A renderer need not give the guest every turn: while no button or touch is down and neither the state nor the guest's picture has just changed, it looks in about once a second (Rest in crates/pocket-atlas-interface, n3ds/src/guest.c). The interface's own timers follow the wall clock for that reason (ui/app/clock.ts). A renderer sends only the fields of its state that changed.
bun tools/atlas-ui.ts <psp|vita|3ds|ipod|android> # → .pocket-build/ui/<device>/atlas.js, atlas.pak (each device's build runs this)
(cd vendor/pocketjs && bun install && bun tools/wasm.ts) # once, for the host tests and previews
bun test ui/test # each presentation on PocketJS's wasm core: presses and touches in, commands out
bun test ./tools/platforms.test.ts # a device with a package is a device of the browser page
bun ui/test/preview.ts <psp|3ds|ipod|android> # pictures of the screens → .pocket-build/ui/preview/Place cards come from each place's preview (web/scripts/preview-place.ts); a place without one shows a wash of its accent.
Every launch starts with the Pocket3D title card: the mark and the name "Pocket3D" in white on the plum ground for 144 vertical blanks at 60 Hz (2.4 s), then the atlas. PocketJS's pocket3d-title (vendor/pocketjs/engine/pocket3d/crates/pocket3d-title) draws it into the frame buffer before the renderer starts and before the interface boots, so it holds no texture, program or draw afterwards:
| Target | Call | Where |
|---|---|---|
| PS Vita | pocket3d_title::vita::play() |
vita/src/main.rs, first in main, before graphics::init_with_pool |
| PSP | title() → pocket3d_title::play |
psp/src/main.rs, in run() before renderer::init (sceGuInit); play leaves the frame buffer as zero bytes |
| Nintendo 3DS | pocket3d_title_play() |
n3ds/src/main.c, after gfxInitDefault (both screens BGR8) and before C3D_Init |
| Redmi 1S | title() → atlas_title_draw (android/title, pocket3d_title::draw in its RGBA layout) |
android/src/main.c, first in run(), before graphics() and before the interface's bundle is read; a tick that differs from the one shown is uploaded to one texture and covers the window, and the texture is deleted when the card ends |
| Web reference | playTitle() |
web/src/main.ts; App builds the first stage under the card and shows it when the card ends |
| Browser tab | titleCard(playTitle) |
wgpu/page/main.js, first in start; the shell, the interface and the globe's surface are read under the card, and the canvas is shown when it ends |
A Vita development build skips the card when the USB share holds atlas/boot.json with {"title": false}; packaged builds do not read that file, and the PSP and 3DS builds have no switch. An Android development build skips it while /data/local/tmp/<package>/no-title exists (bun tools/atlas-android.ts native --no-title); a release build does not look. The web reference skips it under ?shot and ?export, which export-place.ts, export-atlas.ts and preview-place.ts pass. The iPod touch app has no card: pocket3d-title has no drawer for it. The Pocket3D License (vendor/pocketjs/pocket3d/LICENSE) makes showing the card first a condition of distributing a product built on Pocket3D.
On the PSP, the PS Vita, the Nintendo 3DS, the iPod touch and Android the icon in the launcher is the Pocket3D icon, the same picture for every game built on Pocket3D. PocketJS holds one file per console under vendor/pocketjs/engine/pocket3d/icon/ and each build reads it from there: this repository holds no icon file, and a new drawing arrives with the submodule pin.
| Target | File under vendor/pocketjs/engine/pocket3d/icon/ |
Read by |
|---|---|---|
| PSP | psp/ICON0.PNG (144×80) |
xmb_icon_png in psp/Psp.toml (cargo-psp), and POCKET3D_ICON.psp as the third argument of pack-pbp in tools/atlas-psp.ts, which writes the EBOOT that ships |
| PS Vita | vita/icon0.png (128×128, 8-bit indexed) |
packageVitaVpk({ …, icon: POCKET3D_ICON.vita }) in tools/atlas.ts: it replaces sce_sys/icon0.png in the development build and in the standalone VPK |
| Nintendo 3DS | 3ds/icon.png (48×48) and 3ds/icon-small.png (24×24) |
ICON and SMALL_ICON in n3ds/Makefile, both given to smdhtool --create |
| iPod touch 4 | ios/Icon.png (57×57) and ios/Icon@2x.png (114×114) |
tools/atlas-ipod.ts copies both into the bundle; Info.plist lists them in CFBundleIconFiles and sets UIPrerenderedIcon |
| Android | android/mdpi.png (48×48), hdpi.png (72×72), xhdpi.png (96×96), xxhdpi.png (144×144) | tools/atlas-android.ts copies each to res/drawable-<density>/icon.png in the staged resources (POCKET3D_ICON_ANDROID); android/AndroidManifest.xml names @drawable/icon, and aapt --no-crunch packs the files as they are |
The name beside the icon stays "Pocket Atlas": TITLE in PARAM.SFO, the SMDH title, CFBundleDisplayName, android:label. A capture of the game goes where a console shows a picture behind or beside the icon: psp/assets/pic1.png (the XMB background) and the LiveArea pictures under vita/assets/sce_sys/livearea/contents/. tools/app-icon.test.ts fails when an icon file is tracked outside vendor/ or when a build stops reading PocketJS's. The procedure, with the checks from a built EBOOT, VPK and SMDH, is PocketJS's pocket3d-brand skill (vendor/pocketjs/skills/pocket3d-brand/SKILL.md).
cd web
bun install
bun run dev # http://127.0.0.1:5173Controls and URL switches are listed in web/README.md.
web/ above is the three.js reference. wgpu/ is the game in a tab: wgpu/README.md has the loop, the seam, the deployable directory and the measurements in full.
bun tools/wgpu.ts build # → .pocket-build/wgpu/site
bun tools/wgpu.ts serve # http://127.0.0.1:8788/ ?device=vita|psp|3ds|ipod
bun tools/wgpu.ts check # Chrome over WebGPU, every device- The atlas screen and the seven places. The globe is
ipod/src/globe.c's mesh and three programs in WGSL, on wgpu 25; one renderer runs over WebGPU in a tab and over Metal on the build machine, where the two frames differ by 0 of 255. - The interface is the bundle a device loads, compiled by
tools/atlas-ui.tsfor each of the four, as a guest in a realm of the page on PocketJS's UI core built for wasm. It is turned 30 times a second and told so, and rests at one turn a second. Its lines pass throughcrates/pocket-atlas-interface, unchanged. - The page shows one handheld and changes it while it runs: PS Vita, PSP, Nintendo 3DS with its lower screen under the pointer, iPod touch with the pointer as a finger. A browser whose pointer is a finger gets the device's buttons on the page, and the iPod touch first.
- The page is PocketJS's Pocket3D player: each layout in its device's shell with the shell's keys as the controls, the devices as text, a Simulated mark with one sentence on what a real device draws, and a dock with two links to the game's page in Pocket Studio: Download and Remix. Atlas hands it its name and its sentence (
wgpu/page/main.js). - A place is the PS Vita's renderer and pack (
wgpu/src/places/): the sun maps, the mirror, the HDR scene with 4 samples a pixel, the light fields, the haze, the bloom and the composite, with the Cg variants as WGSL sources and pipelines made at their first draw. It needstexture-compression-bc; without it the interface shows "Places need a desktop browser for now." A first frame needs 9 to 24 MB of a pack, and the textures arrive after it. - The Pocket3D title card plays first, over the page. A browser without WebGPU is told so in one sentence.
- Measured (Chrome 154, M3 Max): 30 frames a second on the PS Vita's, the PSP's and the 3DS's screens and 60 on the iPod touch's, 0.1 ms of the processor and the GPU a frame; a redraw of the interface takes 3.8 ms at the PS Vita's 960 × 544 and 0.9 to 1.5 ms on the others. The deployable directory is 32 files and 11.7 MB; on a line of 16 Mbit/s the globe and the interface are both there after 8.3 MB, at 4.1 s.
The app opens on the atlas: a night globe drawn by the GE (a lit sphere with city lights, a halo and a pin per place) under the interface, which lists the places and enters the ones packed into its EBOOT.PBP (or beside it, during development). In a place the analog stick moves and the d-pad looks; L/R change shots, START pauses or resumes the tour, △ opens the place's menu (tour, rain, sound and reflections where the place has them, statistics) and × returns to the atlas. HOME exits.
All seven places run on the PSP. The daytime and dusk places have a graded sky panorama, baked directional light and static shadows; rain and wet-road reflections are disabled by the pack's features. Griffith Observatory's vista is drawn in a depth range of its own, its city lights are GE sprites and its haze is in the vertex colours; the sea at Kamakura is a polar grid with its Fresnel mix in the vertex colours. Export a place, then cook --place <id>; see the PSP details and limitations.
Rainy Night Konbini runs at 480×272, with baked lighting, alpha-tested shelf facings, planar reflections of lit surfaces and moving objects, rain, lamp halos, the six authored camera shots, the taxi and skinned pedestrians. Walking near the entrance opens the doors and plays the door chime; the rain bed quiets indoors.
The package asks for the large memory of a PSP-2000 or later (MEMSIZE in PARAM.SFO): there the 24 MiB pack buffer fits beside the interface (about 5 MiB with its runtime). On a PSP-1000 a place whose pack does not fit is listed as not on the device. The package is one file, PSP/GAME/PocketAtlas/EBOOT.PBP, with the interface, the globe and every place in its DATA.PSAR section.
Requirements: usbhostfs_pc and pspsh, PSPLINK running on the console, and PocketJS's pinned PSP toolchain. Run bun tools/bootstrap.ts in vendor/pocketjs to provision it. An existing SDK may be selected with PSP_SDK=/absolute/path/to/mipsel-sony-psp; the toolchain resolver checks that override. PocketJS's submodule stays unchanged.
# Export and cook the current checkout, with the web dev server running.
(cd web && bun scripts/export-place.ts --place tokyo-konbini --seconds 20)
bun tools/atlas-psp.ts cook # one place (--place ID); cook each place the PSP should carry
bun tools/atlas-psp.ts build # the app, the interface, the globe and every cooked place → the USB share
# Keep exactly one PSP USB host running in a terminal.
bun tools/atlas-psp.ts serve
# If a host already owns the cable, use --share /its/existing/host0 on later commands.
# In another terminal:
bun tools/atlas-psp.ts run --no-build
bun tools/atlas-psp.ts status
bun tools/atlas-psp.ts ctl '{"place":"tokyo-konbini"}' # enter a place; {"press":["down","circle"]} presses the interface's buttons
bun tools/atlas-psp.ts ctl '{"shot":0,"time":10}' # fixed halfway view
bun tools/atlas-psp.ts capture --out .pocket-build/validation/psp/view.bmp
bun tools/atlas-psp.ts shots # every authored shot, captures + measurements
bun tools/atlas-psp.ts ctl '{}' # live clock
# Standalone files beside each other; no USB host needed after installation.
bun tools/atlas-psp.ts package # dist/PSP/GAME/PocketAtlas/EBOOT.PBP, every file insideThe PSP cook starts from the same PlaceIR as Vita and 3DS, and writes <id>.psp.place with separate PLPS magic/version. It rejects unsupported place kinds and packs above 24 MiB. It preserves rigid and skeletal tracks, uses the shared cooker's coarse geometry, bakes the Products material onto world-space shelf cards, shares static vertex buffers across spatial chunks, and combines only visible chunks at draw time. GPU pointers, indices, texture layouts and animation ranges are validated before upload. The current 20-second export follows the existing Vita workflow; it does not contain the web traffic simulation's full, longer schedule.
This is a fixed-function adaptation: it does not reproduce Vita's HDR/PBR shaders, normal maps, volumetric haze, per-pixel wet ripples, dynamic per-pixel lights or bloom. The PSP's 16-bit depth and reduced texture sizes also limit fine facade detail and lettering. Reflection geometry is limited to lit surfaces and moving objects. PSP workMs includes CPU submission and waiting for the GE; gpuWaitMs is only the wait after submission, not serialized GPU pass timing. Captures and USB transfers must be kept outside measurement windows. Host build, physical runtime, installed-file readback, manual control feel and listening to the sound are separate evidence.
On the connected PSP (333 MHz CPU, 166 MHz bus, PSPLINK, 2026-10-01), five 30-frame windows per fixed halfway camera at t=10 with rain and reflections enabled measured: Konbini 19.9 fps, Puddles 15.0, Vending 15.0, Crossing 20.0, Inside 20.0, Wires 30.0. These are fixed-view measurements, not a claim that the live sequence or every free-camera position sustains 30 fps. The pack is 15.38 MiB with 68,206 triangles across the whole place, 38 textures, 111 animated nodes and 16 skinned chunks. PSP support remains a first port with performance and visual quality below the Vita renderer.
PSPLINK control and status live in an optional mailbox module. Standalone startup probes the control file once; without a host it performs no per-frame host0 I/O. Control writes are atomic and commands are acknowledged by nonce before measurement. The runtime validates camera bases, finite transforms, skin weights, texture grids and GE draw counts before submitting geometry.
The EBOOT carries two pictures for the XMB. ICON0.PNG is the 144×80 Pocket3D icon from PocketJS (see App icon). PIC1.PNG, the background, is psp/assets/pic1.png, a 480×272 capture from the PSP renderer; psp/assets/README.md records where it was taken.
Requirements: VitaSDK at ~/vitasdk, cargo-vita, Rust nightly-2026-05-28 with rust-src, a Vita with HENkaku/Ensō and Pocket Devkit installed (build it with bun tools/vita.ts devkit --release in vendor/pocketjs; see its docs/VITA-USB.md), and ur0:data/libshacccg.suprx (extracted from Sony's PSM Runtime, for example with ShaRKBR33D) on the development console. Packaged builds carry compiled programs and do not need the compiler.
git submodule update --init
# 1. Export a place (dev server running) and cook it
(cd web && bun run dev) &
(cd web && bun scripts/export-place.ts --place tokyo-konbini --seconds 20) # → .pocket-build/places/tokyo-konbini/scene.glb (the device loops the 20 s of traffic)
bun tools/atlas.ts cook --place tokyo-konbini # → .pocket-build/places/tokyo-konbini/tokyo-konbini.place
# The atlas: the globe, and each place's preview card for the interface
(cd web && bun scripts/export-atlas.ts) # → .pocket-build/atlas/globe/ (maps, view-ray bakes, places.json)
(cd web && bun scripts/preview-place.ts) # → .pocket-build/places/<id>/preview.png (the registry's `preview` shot)
bun tools/atlas.ts cook-atlas # → .pocket-build/atlas/atlas.pack
# 2. Development loop on a console running Pocket Devkit (PocketJS apps/devkit)
bun tools/atlas.ts serve & # USB host
bun tools/atlas.ts native # sync pack + shaders, build, run in Devkit's native slot
bun tools/atlas.ts status # renderer telemetry under `engine`
bun tools/atlas.ts profile --shot Konbini # GPU time per scene
bun tools/atlas.ts sweep # frame time per shot × quality step
bun tools/atlas.ts capture # → .pocket-build/validation/captures/
# 3. Standalone package (title PKAT00001): the atlas and every cooked place
bun tools/atlas.ts vpk # → dist/vita/pocket-atlas-PKAT00001.vpkThe app opens on the atlas: the web globe (sky, halo and atmosphere baked for the device's fixed camera; surface, clouds and city lights shaded per pixel at 720×408 with 4× MSAA) under the interface, which says which place it faces and which pin is lit. Saved places are kept in interface.json in the data folder (ux0:data/pocket-atlas). Leaving a place frees its video memory before the next one loads.
△ in a place opens its menu: frame rate profile (vita30, vita60, cinematic), quality step (the governor's, or one held), resolution, 4× MSAA, bloom, the place's lit haze, reflections and rain when it has them, exposure, and the statistics line. A resolution whose targets do not fit in video memory is refused. While the menu is on screen the governor holds its step. Choices carry to the next place and are kept in settings.json in the data folder; the ones not made follow the renderer's profile.
ctl messages naming a place enter it and {"atlas": true} returns; {"press": ["down", "circle"]} presses buttons on the interface one after another, as a thumb would.
Commands that cook, sync or measure take --place ID (default tokyo-konbini). Shader sources in vita/shaders hot-reload: bun tools/atlas.ts sync copies them to the USB share and the device recompiles the programs whose expanded source changed. Compiled programs are cached on the share by content hash; vpk packages the ones listed in the device's gxp/manifest.txt, which holds what the running process has asked for so far. bun tools/atlas.ts programs collects the list a release needs (see "Releases").
If a USB host is already running from another checkout, --share /absolute/path/to/its/share directs sync, native replacement, control, capture and measurements through that live session. Use it only when the device is available for this task; the command does not restart the existing host. Back up shared shader/atlas files before replacing them from another branch.
bun tools/atlas.ts ctl '{"renderProfile":"vita30","view":…,"time":…,"settings":{…}}' steers the camera and the renderer: shot cuts to a camera shot by index; time freezes the loop at that second and view pins a camera until a message without them. Naming a profile resets its switches and governor; settings then overrides them: reflection, haze, bloom, rain, msaa, maxLights, fx, skip (bits of material classes; 0x80 the sky, 0x100 the light fields), flat, hud, profile (serialized GPU timing), step and hold (pin a quality step), the step or profile values detailM, lodPixels, cullSize, hazeSize, hazeLights, bloomFull, reflSize, streaks, steam, detailMaps, vertexLights, and for light fields fieldMin and fieldMax (every field's sprite range, pixels of a 272-pixel-high frame).
vita/src/profile.rs fixes a frame period and what the renderer may spend to meet it. Frames are shown at that period (every second refresh for 30 fps). A governor walks the profile's quality steps from the measured frame time: it steps down after 10 frames over the period and probes one step up after holding it, doubling the wait after each failed probe.
vita30 (default) |
vita60 |
cinematic |
|
|---|---|---|---|
| Period | 33.3 ms | 16.7 ms | 50 ms |
| Scene | 480×272 | 720×408 → 480×272 | 960×544 → 640×362 |
| Materials | normal, ORM and streak maps within 8 m (4 m at the last step) | baked diffuse and environment specular, no detail maps | detail maps within 12–18 m |
| Haze | 160×90 with 6 lights → 4 lights → 120×68 → off | 120×68, 2 lights, off below step 3 | 160×90, 6 lights |
| Bloom | two levels → one level | one level, off at the last steps | two levels |
| Reflection | 240×136, every frame | 240×136, alternate frames | 480×272, alternate frames |
| Rain | 7000 streaks, steam | 1500 streaks | 7000 streaks, steam |
| Moving lights | one per pixel on baked surfaces (only the wet ground beyond the detail distance), per vertex on people and the taxi | same | one per pixel on baked surfaces, four per pixel on people and the taxi |
Above step 0, vita30 climbs to 544×308 and then 640×362 while the GPU has room. The renderer measures each frame's GPU time without serializing it: it polls the frame's last scene's notification in 0.5 ms steps while the CPU waits for the refresh. It climbs one level when the time predicted there (the measured time × (1 + 0.35 × (pixel ratio − 1))) stays under 80 % of the period for two seconds, and drops one after 3 late frames or a smoothed GPU time over 92 %, doubling the wait before the next climb. A resolution fixed in the settings sheet, or a pinned step, turns it off. Griffith Observatory holds 30.0 fps at 640×362 in every shot under the camera rig (measured GPU 14.5–24.5 ms per frame).
Static draws carry LOD1 (≤ 6 cm) and LOD2 (≤ 25 cm) index lists (chunks beyond 1 km three coarser ones, see Dusk vistas): meshoptimizer with only the vertices on chunk-cell cuts locked, and parts of plain lit surfaces narrower than a level's error (window bars, rails, curb pieces) removed at that level. A draw takes the coarsest level whose error projects under the step's pixel threshold; the mirror pass uses twice the threshold. Shelf stock switches to one card per item.
Rigid moving draws also receive LODs, with additional 1 cm and 2.5 cm candidates for small mechanical details. Their non-emissive standard surfaces can drop subpixel parts; skinned meshes keep all their joint and weight seams and do not use this simplifier. Identical complete packed vertex records and identical geometry/animation byte ranges share storage. Unused standard-material UVs and tangents are canonicalized before welding. Rain lookup textures are generated only when the place uses them; dry glass gets a small neutral bead texture.
Opaque solid standard materials without maps or special surface effects can share a vertex_pbr palette: sRGB vertex colour carries the albedo and UV carries each surface's roughness/metalness. Static geometry keeps its spatial chunks; fixed siblings in an animated hierarchy can share their parent's frame, keeping independent wheel or gate tracks intact. Sidedness, environment strength, depth state and other retained material fields remain batch boundaries. Static palettes separate rough non-metal surfaces from those requiring a sun highlight; moving assemblies keep one palette. The renderer reads these PBR constants in full, distant and reflection variants.
This Vita encoding requires PLCE/ATLS container version 7 (version 6 introduced light fields and vista haze). Readers reject other container versions before interpreting the payload; re-cook every Vita place and the atlas when updating the renderer. Vita Place META uses version 7, while AtlasMeta remains version 1. PICA independently keeps its PLCE v5 envelope and v3 table; PSP keeps PLPS v1. Native lowerings consume source data from PlaceIR, not the Vita palette or pack.
bun web/scripts/place-budget.ts --in PACK.place --out REPORT.json validates a cooked pack and estimates vita30 step-0 main-pass geometry over the full animation loop at each shot's start, middle and end camera positions. Its draw and triangle counts are CPU planning evidence, not a GPU measurement or frame-rate claim.
Variants that drop a material's ORM map (distant, LITE and mirror programs) scale roughness, metalness and occlusion by the map's per-channel means, stored in the pack.
A place exported with a directional light gets the sun per pixel: the static scene is drawn once from the sun into a 2048² shadow map (normalized distance in a single-channel R32F colour target). An RG16 cache pairs adjacent depths; lit materials compare four depths from two point reads and blend them by the sub-texel position. Only smooth or metallic materials (roughness under 0.6 or metalness over 0.3) evaluate the sun's highlight; draws beyond the detail distance skip the shadow lookup. The sun is not baked.
Places with rigid moving opaque casters use a separate 512² shadow map, refreshed each frame with sun-frustum culling and shadow-texel LOD selection. Standard materials combine it with the cached static map; distant materials keep the moving lookup so a train's shadow remains visible across the crossing. Glass and skinned particles do not cast into this layer. Shadow coordinates and depth comparisons stay float; bounded filter weights and sunlight use half precision. Sangubashi completed device compilation and six-camera capture review, including the corrected train shadows. Its picture quality was accepted with a place-specific waiver of the step-0 30 fps requirement; the place notes retain measured limits and distinguish pre-integration device evidence from final host builds.
Receiver bounds are projected into the light’s UV/depth space, including the normal offset and PCF footprint. Draws outside every moving caster use a shader without moving-shadow sampling. Both variants are prewarmed; camera and shadow bounds also rely on skin weights quantized to an exact sum of 255.
shared/rigid-particles.ts batches independent small rigid pieces into ordinary skins of at most 24 joints. The exporter records their position/rotation tracks; the runtime bounds the full joint set and uploads the shader's complete 24-joint uniform array. This keeps windborne petals animated without a place-specific particle renderer. Mesh sizes are baked into vertices, because the pack's node animation tracks carry translation and rotation, not scale.
extras.bake.skyOcclusion in a place's export makes the cooker cast cosine-weighted rays (48 within 1.5 m for Suga Shrine Stairs, the web's N8AO radius) from every baked vertex against a BVH of the static triangles; the unblocked share scales the hemisphere and environment terms. Edges split for it only down to 1 m near where the camera goes, coarser with distance.
A gradient-sun-cloudpanorama sky annotation draws the web's daytime sky (sky_day_f.cg; places/shared/sky.ts holds the day dome, the twilight dome and the cloud-panorama bake), and extras.post carries the tone curve (ACES or AgX), grade, vignette, grain and bloom the device bakes into its colour table.
A material annotated kind: "sign" cooks as an unlit HDR surface (color multiplies its texture). frames, cols, rows and fps play the texture as a flipbook: frame f = ⌊(t + phase) · fps⌋ mod frames sits in column f mod cols, row ⌊f / cols⌋ from the top left, and the mesh's coordinates span frame 0's cell (glTF UV space, v down). scroll: [u, v] then moves the coordinates in texture widths per second, wrapped to 0..1. Both store as UvAnim in the pack; the device offsets the draw's coordinate transform each frame, so an animated sign costs what a still one does. The cooker stops a flipbook texture's mip chain while a cell is still 4 texels across, so filtering does not mix frames (the Radio Kaikan band's 32 px cells keep 4 levels), and cooks the same image separately per flipbook grid.
Every cell of a shared atlas texture (places/shared/atlas.ts) gets a border filled with its own edge pixels (16 px on a 4096² atlas; 8 px on Radio Kaikan's 2048² ones; 2 px on the konbini's and Suga's full 4096² atlases): a distant sign samples low mip levels, where a black border would bleed in and BC1 blocks would turn it into dark squares.
A dusk-street place is lit by its signs after sunset:
- Animated signs (
places/shared/signs.ts): see Signage. - Twilight sky (
places/shared/sky.ts): thegradient-sun-cloudpanoramasky with the sun below the horizon, no disc, no cloud panorama, and atwilightobject: an afterglowbandalong the horizon weighted toward the sun's azimuth, the pink anti-twilightbeltopposite the sun, and the Earth'sshadowunder it. The formulas are in the file header;sky_day_f.cgevaluates them underTWILIGHT(DaySky::twilightin the pack).
Radio Kaikan at Blue Hour bakes 25 panel lights (signs, the LED band and screen, shopfronts) and 22 point and spot lights (lantern lamps, soffit downlights, under the Sobu Line bridge) into the vertices, with bake.skyOcclusion (48 rays within 6 m) so the street canyon darkens toward the ground; lamp pools split edges down to 0.45 m.
A daytime-coast place adds open water to the daytime pipeline (sun with a shadow map, sky occlusion, the cloud-panorama sky):
- Water (
places/shared/water.ts): a material annotatedkind: "water"cooks asKind::Water(water_f.cg,surface_v.cgunderWAVES) and stays one draw however far it reaches (the cooker does not chunk it). Its normal map (BC5) is laid twice on the world's x/z plane,waves: [[repeatsPerMetre, scrollX, scrollZ], …]in m/s;normalScale.xscales the slopes,roughnessis the GGX α near the camera anddistanceRoughnessadds α² per metre while the slopes flatten as 1 / (1 + 40 · d · distanceRoughness);masktilts the wave faces toward the eye (the backs of the waves hide at grazing views, so far water reflects less sky). The environment probe is reflected by Schlick Fresnel (f0 = 0.02, the reflection folded above the horizon);body× the hemisphere sky fills the rest, mixed towardshallowby the mesh's vertex colour (red) over a sandy bottom; the sun adds a GGX highlight; FogExp2 on top, no shadows. The web material patches three.js' standard program to evaluate the same expressions, so the probe, sun and hemisphere it reads are the ones the exporter writes. - Surf (
foamMaterialin the same file): alpha-blended lit strips whose vertex alpha fades the foam across the surf zone and whose texture scrolls shoreward (scroll, the cooker'sUvAnim); each strip is one moving node, one draw. - Draw budget: within 140 m of the origin the cooker chunks static geometry into 32 m cells per material, beyond that into 256 m cells (and beyond 1 km into the growing cells of Dusk vistas), so a view along a coast pays one draw per material per cell. Kamakura-Kōkōmae Crossing paints its small props (posts, wires, fences, housings, cabinets) from one equipment atlas, merges each vehicle and each train body into one mesh and keeps far land to a few large triangles.
Kamakura-Kōkōmae Crossing loops 120 s: one Fujisawa-bound train, the crossing's 35 s warning, lamps alternating every 0.6 s, four gate arms, six vehicles on 60 s cycles and a cyclist on a 120 s one.
A dusk-vista place is a lookout over a lit city at blue hour, its scene reaching tens of kilometres. What follows is the Vita's; the 3DS and the iPod touch cook the haze into vertex colours and the lights into sprites (n3ds/README.md, ipod/README.md):
- Light fields (
places/shared/lights.ts): aTHREE.Pointswhose material is annotatedkind: "lights"exports as glTF POINTS with COLOR_0 and the custom attributes_LIGHT,_PATHand_BLINK(annotations:lights). The cooker sorts the lights into the cells of far terrain below, no smaller than 512 m (a wide shot over a uniform ±40 km field draws 24–32 cells and processes about 30 % of the lights where 16–19 % are in view; cells twice that size drew a third fewer field draws at Griffith Observatory's Lawn but cost 0.64 ms more GPU, the clipper's work on the extra lights outside the view), at most 16 384 per draw, and stores each light as one 40-byte vertex (VertexLayout::Lights): quantized position and phase, sRGB colour and twinkle, intensity, radius, path and cycles, blink cycles and duty. The Vita draws a field draw as a GXM point list (SCE_GXM_PRIMITIVE_POINTS, polygon modePOINT_01UV):lights_v.cgmoves the light along its path (position + path · fract(phase + cycles · t / loop)), blinks it (on whilefract(phase + blink cycles · t / loop) < duty), sizes the sprite (D = radius · H / (d · tan(fovY/2))render pixels,S = clamp(D, minPixels, maxPixels)with the range in pixels of a 272-pixel-high frame, at least 2 render pixels on the Vita: below that the pixel centres under a sprite no longer sum to its area), moves its depth toward the eye byclamp(depthPull · d / 1 km, 0.002, 0.5)of the distance d (screen position unchanged, as on the web; at grazing angles the ground under the pixels below a far light is nearer than the light), keeps its energy ((D / S)²while D < S), twinkles it (1 + twinkle · min(1, d / 8 km) · 0.35 · sin(2π(13.7 · phase + 4t))) and dims it by the vista haze's T; it writes the size toPSIZE, andlights_f.cgspreads the value over a(1 − r²)²profile across the sprite, whose coordinate it reads asPOINTCOORD(the device's SceShaccCg rejectsSPRITECOORD; the coordinate is generated only under thePOINT_01UVandPOINT_10UVpolygon modes). Additive, depth-tested against the scene, no depth write, after the sky and before blended surfaces. Measured on the Vita (vita30, 480×272, 4× MSAA, serialized main pass with and without the light pass): 1.3–1.5 ms per 10 000 lights in view (2-pixel sprites; rasterization dominates: 4× MSAA adds about 35 %), 0.35–0.4 ms per 10 000 lights the clipper drops, the same for moving and static lights (one program). - Vista haze (
places/shared/haze.ts, scene annotationhazewith aninversion): extinction ρ0 up to the inversion top H and ρ0 · e^(−(y − H)/s) above, optical depthd · (G(y_p) − G(y_e)) / (y_p − y_e)with G its antiderivative,T = e^(−τ). Every material with fog takesc · T + inscatter · (1 − T)(premultiplied glass: the inscatter × its coverage) underVISTAinstead of FogExp2; the light fields take T.surface_v.cgevaluates it per vertex (vista.cgh): T, and the inscattergain · (base + w · sun) + glow · ρ(y_p)/ρ0: base and sun are the two parts of the sky on the horizon toward the point (the gradient's horizon and the belt; the glow lobes and the afterglow band; both under the Earth's shadow), andw = band + (1 − band) · (1 − T)lets the afterglow's share grow with optical depth, so far terrain meets the sky in every azimuth. The horizon depends only on the azimuth to the sun; the CPU tabulates both parts at 17 knots ofsqrt((1 − a)/2)(VistaHaze::sky_tables, a the azimuth cosine; within 2 % of the dome). The fragment programs read one extra varying. - Cells and LOD for far terrain: static geometry beyond 1 km of the origin chunks into cells as wide as the octave of distance they sit in (1 km cells from 1 to 2 km, 2 km cells from 2 to 4 km, up to 64 km), and those chunks carry three LOD levels at 3·10⁻⁴, 1.2·10⁻³ and 4.8·10⁻³ of their distance (a pixel of a 5° telephoto at 480×272 at the first, of a 40° view at the last). The renderer's choice by projected error holds a telephoto: its pixel is smaller, so it keeps the finer levels.
- Window grids: GXM has no anisotropic filtering, and its mip choice follows the denser of a texture's two directions. A window grid laying 5.3 texels per metre across and 2 up (16 windows of 3 m, 32 floors of 4 m in 256²) loses its floors from a few kilometres at 480×272. A material annotated
lodBias: "auto"makes the cooker measure that ratio over the area the texture covers and store a negative LOD bias of its log2 (−1.42 for Griffith Observatory's towers), so the mip follows the sparser direction. - Depth: reversed infinite depth into a 32-bit float buffer (
DF32M) resolves about 3 mm at 45 km; positions quantize per chunk (16-bit over the chunk's box: 25 cm in a 16 km cell). - No sun: a sun below the horizon draws no shadow map and lights nothing; the floodlights and lamps are point and spot lights baked into the vertices, and bloom carries the lit windows and the city.
The day sky's tight glow lobe takes its weight (glow.tight[0]) on the Vita as on the web; the places before Griffith Observatory all used 1.
Rainy Night Konbini, measured on a PS Vita 2000 (CPU 444 MHz, GPU 222 MHz) in Pocket Devkit, vita30, 4× MSAA, 480×272 composited and scaled to 960×544. bun tools/atlas.ts sweep --time T pins each step for each shot's halfway view:
| Shot | t = 100 s: step 0 | t = 72 s (taxi passing): first step at 33.3 ms |
|---|---|---|
| Konbini | 33.3 ms | step 2 (step 0: 38.0 ms) |
| Puddles | 33.4 ms | step 1 (34.4 ms) |
| Vending | 33.4 ms | step 1 (36.7 ms) |
| Crossing | 33.3 ms | step 3 (37.4 ms) |
| Inside | 33.4 ms | step 0 |
| Wires | 33.4 ms | step 0 |
With the camera rig and governor running (bun tools/atlas.ts shots --seconds 130), every shot averages 29.5–30.0 fps; the longest smoothed frames are 41–44 ms, in Konbini and Puddles, where the governor steps down while the taxi passes. Serialized GPU time at step 0 (t = 100 s) runs from 31.2 ms (Wires) to 40.7 ms (Puddles): main pass 16–23 ms, haze 5.6 ms, bloom 3.8 ms, composite 2.2 ms, reflection 3.1 ms, display scale 1.2 ms.
Suga Shrine Stairs holds 33.3–33.4 ms at step 0 in every shot (sweep --time 5): Stairs, Rails, Below, Lane and Canopy draw 180–720 draws and 81k–137k triangles.
Kamakura-Kōkōmae Crossing holds 30.0 fps at step 0 in every shot with the camera rig and governor running (shots --seconds 160): Crossing, Postcard, Platform, Route134, Seawall and Park draw 81–263 draws and 81k–141k triangles, Platform the most. Serialized GPU time is 21.3 ms in Crossing, 20.9 ms in Platform and 23.7 ms in Seawall (main pass 13.4–16.2 ms), with the sun's shadow map drawn once at load.
Radio Kaikan at Blue Hour holds 30.0 fps at step 0 in every shot with the camera rig and governor running (shots --seconds 130): Arrival, Facade, Band, Vista, Corner and Clock draw 148–399 draws and 34k–61k triangles. Serialized GPU time (profile --time 5) is 17.9–19.5 ms: main pass 10.2–11.7 ms, bloom 4.2 ms, composite 2.1 ms, display scale 1.4 ms.
All seven places on a Redmi 1S (Android 4.3, Adreno 305), the release build as installed, at the app's 30 frames a second with the window's height left to the guard (bun tools/atlas-android.ts shots --rate 30): each authored shot at its midpoint, the loop frozen at 25 s, 240 frames. "Shown" is the compositor's record of each shot's last 125 frames (dumpsys SurfaceFlinger --latency); "GPU" is the time from the swap's call to the frame's last tile, by a fence, at whatever clock the GPU's governor chose. The phone had rested to 41 °C and read 49 – 58 °C during the run, with one or two of its four cores online.
| Place | Shots | Window lines | fps | Shown for two refreshes | GPU ms | CPU ms | Triangles | Draws |
|---|---|---|---|---|---|---|---|---|
| Rainy Night Konbini | 6 | 720 | 29.9 – 30.0 | 748 of 752 frames (99.5 %) | 17.0 – 24.7 | 4.3 – 7.1 | 73 – 172k | 31 – 132 |
| Suga Shrine Stairs | 5 | 648 → 576 | 30.0 | 623 of 627 frames (99.4 %) | 20.3 – 27.5 | 2.0 – 2.7 | 115 – 157k | 36 – 43 |
| Radio Kaikan at Blue Hour | 6 | 720 | 29.9 – 30.0 | 748 of 755 frames (99.1 %) | 16.8 – 24.8 | 1.6 – 3.7 | 51 – 113k | 43 – 112 |
| Kamakura-Kōkōmae Crossing | 6 | 720 | 29.9 – 30.0 | 752 of 755 frames (99.6 %) | 19.6 – 22.7 | 1.6 – 2.8 | 117 – 188k | 52 – 71 |
| Sangubashi in Bloom | 6 | 576 | 30.0 | 753 of 753 frames (100.0 %) | 21.9 – 26.6 | 3.2 – 7.8 | 142 – 196k | 61 – 274 |
| Griffith Observatory at Blue Hour | 6 | 720 | 30.0 | 740 of 752 frames (98.4 %) | 18.6 – 24.6 | 1.9 – 4.0 | 62 – 159k | 29 – 92 |
| Lombard Street in Bloom | 6 | 720 | 30.0 – 30.1 | 754 of 754 frames (100.0 %) | 19.6 – 28.1 | 1.6 – 3.3 | 84 – 199k | 18 – 37 |
At 60 frames a second (shots --rate 60, the phone at 58 – 68 °C) the guard takes the window to 540 – 648 lines: the konbini, Radio Kaikan, Kamakura and Griffith show 99.9 % of their frames for one refresh and Lombard Street 97.5 %; Suga Shrine Stairs (42.7 – 59.7 fps) and Sangubashi (41.9 – 50.4 fps) do not hold it. These are fixed views; tours and walks were not measured. See android/README.md.
bun tools/release.ts builds every device's package from the checked-out commit and writes them to dist/release/, which Git ignores:
bun tools/release.ts [--export] [--targets vita,psp,3ds,ipod-touch,android] [--out dist/release] [--vita-gxp DIR] [--no-build] [--upload]| Target | File | Holds |
|---|---|---|
vita |
pocket-atlas-<version>.vpk |
the program, atlas.pack, every Vita place, the interface and the programs a console compiled |
psp |
pocket-atlas-<version>-psp.zip |
PSP/GAME/PocketAtlas/EBOOT.PBP for the root of a Memory Stick: the program with the interface, globe.psp and each PSP place in it |
3ds |
pocket-atlas-<version>-3ds.zip |
the root of the SD card: 3ds/pocket-atlas.3dsx, and pocket-atlas/manifest.json with each place as pocket-atlas/<sha256>.place. The .3dsx is the only form: no CIA is built |
ipod-touch |
pocket-atlas-<version>-ipod.ipa |
Payload/PocketAtlas.app, with the globe and each iPod place |
android |
pocket-atlas-<version>.apk |
the release build for Android 4.3 and later with OpenGL ES 3.0 (minSdkVersion 18, targetSdkVersion 34): libmain.so and libatlas.so for armeabi-v7a and for arm64-v8a, the interface, the globe and each Android place, signed |
The exports come from a browser. --export writes them first, from this commit: tools/place.ts export --place <id> for each live place of web/src/places/registry.ts (→ .pocket-build/places/<id>/: scene.glb, its environment, export.json; Sangubashi with --geometry handheld, because its full geometry cooks to 51.7 MB for the 3DS, whose reader takes 24 MiB), then web/scripts/export-atlas.ts (→ .pocket-build/atlas/globe/) and web/scripts/preview-place.ts (→ each place's preview.png) against the reference served on a port of its own. It needs bun install in web/, Google Chrome and the GPU. Without --export the tool uses the exports on disk and refuses a place whose export.json names other web sources than the checkout holds (sourceSnapshot in web/scripts/export-source.ts, the identity tools/place.ts export seals), has another geometry than the release takes, or lists a file whose hash differs. The globe's export has no receipt; release.json records its hash.
It needs the toolchains of the four device tools: VitaSDK at ~/vitasdk with cargo-vita and Rust nightly-2026-05-28 (Vita); PocketJS's pinned PSP toolchain (bun tools/bootstrap.ts in vendor/pocketjs) with pack-pbp; Docker, for the devkitARM container, and the Rust nightly vendor/pocketjs/hosts/3ds/core/rust-toolchain.toml pins; the Xcode command line tools, ldid and PocketJS's iPod touch 4 toolchain (bun ipodtouch4 doctor in vendor/pocketjs); ImageMagick's magick, which cuts the interface's cards from the previews; the Android SDK's NDK 21.4.7075529 and build-tools 34.0.0 with a JDK and the pinned Rust toolchain's armv7-linux-androideabi and aarch64-linux-android targets, which bun tools/atlas-android.ts doctor checks.
For each target the tool cooks every live place the registry gives that target with this commit's compiler and runs the build a developer runs: tools/atlas.ts cook, cook-atlas and vpk; tools/atlas-psp.ts cook and package; tools/atlas-3ds.ts cook and package; tools/atlas-ipod.ts cook and package; tools/atlas-android.ts cook and apk --release. The PSP build holds PocketJS's psp:usb lease while it runs, as tools/atlas-psp.ts build does. The version is the one in ui/pocket.json. A target that fails is listed with its error, the other targets build, and the exit status is 1. Each step's output is in .pocket-build/release/logs/.
release.json, beside the packages, records the commit, the version, each package's size and SHA-256, the SHA-256 of the web sources, of the globe export, of each place's scene.glb and preview.png and of each cooked pack, and the toolchains: the pinned PocketJS revision, the rustc of each target, VitaSDK's compiler and version_info.txt, the PSP SDK's hash, the devkitARM image's digest, the NDK, the build-tools and the JDK, the SHA-256 of the certificate the Android package is signed under, and the Chrome version and GPU the exports were made with.
The Vita's programs are an input, collected by a required pass on a console. SceShaccCg runs on a console, so the package carries .gxp files a console compiled, and a release build reads them from app0:gxp. A place asks for its programs when it loads and when a setting changes: the Frame rate's profile lights surfaces per vertex (30 fps, 60 fps) or per pixel (Cinematic), and Bloom and Haze choose the composite. Before a Vita release, run bun tools/atlas.ts programs with the console in Pocket Devkit. It starts the development build in a new process, enters every Vita place under each of the three frame rates, and in each sets what PROGRAM_SETTINGS in tools/vita-programs.ts lists: Anti-aliasing off and on, the four pairs of Bloom and Haze, Wet reflections off and on, Rain off and on, and each Quality step. It stops when a program fails. It writes the console's list (manifest.txt, a program's name and label per row, such as standard_f.cg[BAKED,FOG,LIGHTS=0]), the .gxp files and coverage.json to .pocket-build/vita-programs/; no development run writes there. The pass took 12 minutes for seven places on 2026-10-06.
The release tool refuses a program set the pass did not collect. --vita-gxp DIR names the directory; the default is .pocket-build/vita-programs. The Vita build stops, with the reason, when manifest.txt or a .gxp is missing; when a program's name differs from the one the tool computes from vita/shaders as vita/src/shaders.rs does (FNV-1a over the source with its includes expanded, the label's definitions and the stage), which is a program compiled from a source that has changed since or has left the repository; and when coverage.json is missing or records other places, other frame rates, a missing switch, other shader sources or another list than the one beside it. release.json records the pass: its time, the development build, the places, the frame rates, and how many programs the console compiled during it. What the check cannot see: a setting that selects a program and is not in PROGRAM_SETTINGS. A new switch in vita/src/settings.rs that changes a program's definitions goes into that list in the same commit.
A console without the shader compiler draws only what the package holds. A release build loads ur0:data/libshacccg.suprx when the console has it and compiles a program the package lacks on each run, without a cache. Without it, a draw whose program is missing is skipped and no message stays on screen: the place shows with those surfaces left out, and when the missing program is the composite the picture is not drawn. The package of v0.1.0 that was first built held 864 programs, the set of the default settings; Cinematic in Rainy Night Konbini and in Griffith Observatory and Bloom off with Haze on asked for 30 more.
All five packages are byte-identical across two builds of a commit on one computer, from the same exports. The tool writes the .zip files, the .ipa and the .vpk itself: entries in the order of their names (in the .vpk, sce_sys/param.sfo and eboot.bin first, as vita-pack-vpk has them), every date 1980-01-01, modes 0644 and 0755, deflate at level 6. A development build of the Vita or PSP program carries a random build id. For a release the tool names it (POCKET_RELEASE_BUILD, which tools/atlas.ts and tools/atlas-psp.ts read): 32 hex digits from the commit, the hashes of the exports and of the target's packs and, for the Vita, the hash of the programs' manifest; release.json records both. In the .apk, aapt dates its entries 1980-01-01, tools/atlas-android.ts gives the four libraries that date before zip -X adds them in the order of their names, and apksigner dates its own entries by an entry it is given; the signatures are RSA with PKCS #1 v1.5 padding, which gives the same bytes for the same input. Two exports of a commit on one computer give the same seven scene.glb files and the same globe; a place's preview.png is a capture of the running scene and differs between exports, and the card the interface compiles from it, so every package differs after --export.
The Android package is signed with the Pocket Nexus Android release key, which is in no repository. One key signs every Pocket Nexus Android package. bun tools/atlas-android.ts apk --release signs with the PKCS #12 keystore POCKET_NEXUS_ANDROID_KEY names, by default ~/.config/pocket-nexus/signing/pocket-nexus-android-release.p12 (alias pocket-nexus); the password is the first line of the .password file beside the keystore, and apksigner reads it from that file. A missing keystore or password file stops the command before it builds, with the path it looked for. The release tool then reads the certificate from the package and refuses one that is not ANDROID_SIGNER in tools/release.ts (SHA-256 fb36ca93…67ca9c21), and release.json records it. apk --release leaves out the door for pushed code and packs and sets android:debuggable to false; the tool refuses a release package whose badging says debuggable.
A development package keeps PocketJS's Android debug key in ~/.cache/pocket-nexus/android/signing/ (alias androiddebugkey, made when that directory holds none). A phone installs a package over an installed one only under the same certificate: with a development package on the phone, adb install -r of a release answers INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, and adb uninstall dev.pocketnexus.atlas goes first (what the app saved goes with it).
Packages go to Pocket Studio and to no page on GitHub. --upload runs pocket-studio package <file> --target <id> --version <version> for each package from the repository's root, where pocket-studio register --title "Pocket Atlas" wrote .pocket-studio.json (ignored by Git). It refuses a checkout with uncommitted changes. It does not register the game, publish it or change its address; when the link file is missing it prints the commands that write it. --no-build --upload sends the packages release.json lists, after checking their hashes.
bun tools/listing.ts films what the game's page on Pocket Studio shows and writes it to dist/listing/, which Git ignores:
bun tools/listing.ts [--only FILE | --words] [--upload]
- The words are
listing/listing.json, in Git: one sentence, the paragraphs, and for each clip and still its file, its size in pixels, what drew it (from) and a caption. The tool stops when the JSON names a file it has no take for, or when it has a take the JSON does not name. - The Japanese words are
translations.jain the same file: the sentence, the paragraphs and a caption for each picture, under the English limits; Pocket Studio shows them on the game's Japanese page and shows the English for a word they leave out.bun tools/listing.ts --wordswritesdist/listing/listing.jsonfor the pictures already indist/listing/, without filming. - The pictures are filmed from the game.
wgpu/src/bin/film.rs(atlas-film) runs the browser tab's renderer of places on this machine's GPU, at the PS Vita's 960 × 544 with four samples a pixel, and writes one frame for each line of a list: an authored shot at a part of its length, or any eye, with the place's loop at a second of its own.tools/listing.tswrites the lists and hands the frames to ffmpeg. - A clip is a row of cuts, each one of the place's authored shots between two parts of its length. The picture dips to black over ten frames on each side of a cut and at the clip's two ends, as the tour's own cuts do, so a clip loops through a dip. A clip is H.264 at 30 frames a second with no sound, held under 12 MiB by a ceiling on its rate; its poster is that frame drawn again, as a JPEG.
| File | What it shows | Length |
|---|---|---|
konbini.mp4 |
Rainy Night Konbini: the corner, the front from the wet road, the side street with Tokyo Tower at its end | 22 s |
sangubashi.mp4 |
Sangubashi in Bloom: the lamps and the arms coming down (the loop's seconds 1.5 to 11.5), the train arriving, the train from down the lane, the last cars and the arms rising (29.5 to 40.5) | 29.5 s |
kamakura.mp4 |
Kamakura-Kōkōmae Crossing: the train along Route 134, then on the crossing from the slope road | 13 s |
griffith.mp4 |
Griffith Observatory at Blue Hour: the domes before downtown, then the terrace over the basin | 12.5 s |
suga.jpg, kaikan.jpg, lombard.jpg |
one shot each of Suga Shrine Stairs, Radio Kaikan at Blue Hour and Lombard Street in Bloom | |
globe.jpg |
the atlas screen's globe (atlas-shot --globe), turned to Japan, with a pin on each live place |
|
card.jpg |
the share picture a link preview carries: the Konbini's corner at 1200 × 630, with no text on it |
Thirteen files, 33.6 MiB. The cuts keep clear of the seconds when a figure walks close past the camera (the Konbini's loop from 11 to 16 s).
A frame is a function of its line: the loop's second, the view and the dip are in the line, the grain follows the count of frames, and the first line is drawn eight times before the film starts, because a place's first three frames differ from run to run. Three runs gave the same bytes for the stills, the posters, the share picture and griffith.mp4. konbini.mp4, sangubashi.mp4 and kamakura.mp4 differed between runs: in the one case looked at, two pixels of one frame in 300 differed by one level of 255.
It needs the packs under .pocket-build/places, the globe's export under .pocket-build/atlas/globe, ffmpeg and ffprobe. --upload runs pocket-studio listing dist/listing from the repository's root, where pocket-studio register wrote .pocket-studio.json; POCKET_STUDIO_CLI names the command when it is not on PATH. No picture goes to Git or to a GitHub release.
MIT
See Authoring a place for defineDayPlace, the compatible
createStage adapter, unified export/IR/recipe commands, reproducibility limits
and identity-bound device evidence. Start from the typechecked
daytime template; keep scene-family changes in Atlas.