Tokyo, flown over on a PS Vita, a PSP, a Nintendo 3DS, an iPod touch 4 and a Redmi 1S. The city is the real one: 14 753 buildings of the Shiba district around Tokyo Tower, from Japan's open 3D city model (Project PLATEAU), OpenStreetMap and the national elevation survey. The sun crosses the sky, the shadows turn with it, the windows and the street lamps come on at dusk, and on the Vita the traffic runs on the real road graph. 960 × 544 with 4× multisampling at 60 frames per second on the Vita; 480 × 320 with 4× multisampling at 60 on the iPod touch; the panel's own 1280 × 720 at 60 on the Redmi 1S; 30 frames per second on the PSP and the 3DS.
It plays in a browser at tokyo.studio.pocket.nexus. Its page on Pocket Studio, studio.pocket.nexus/games/tokyo, has recordings and the packages for each device, which members download. Compiled city data keeps the terms of its sources (Attribution).
| Screen | Renderer | Measured | |
|---|---|---|---|
| PS Vita | 960 × 544, 4× MSAA, shadows that follow the clock, night glow | GXM, programs compiled on the device | 150 s of the tour from 15:36 to 20:04, with the traffic and the interface: 9 010 frames, 0 late, worst frame 17.6 ms, 86 800 to 200 200 triangles a frame (mean 158 200), 222 draws |
| PSP | 480 × 272, shadows that follow the clock | GE, fixed function, one display list a frame | 150 s from the title into the tour, with the interface over it (PSPLINK, 333 MHz): 4 500 frames, 5 late (3 of them in the first 5 s, while the tour's first cells are read), worst frame 50 ms, 22 800 to 42 100 triangles a frame (mean 37 600), 1 255 draws |
| Nintendo 3DS | 400 × 240 on the upper screen, shadows that follow the clock; the area from above and the day as a bar on the lower one | PICA200: four vertex programs, three combiner stages | Old 3DS, 90 s of the tour with the interface on both screens: 2 728 frames, 0 late, 21 400 to 53 600 triangles a frame (mean 40 000), 276 draws, 17.1 ms of CPU and 17.5 ms of GPU a frame |
| iPod touch 4 | 480 × 320, 4× MSAA, shadows that follow the clock, the tower floodlit and its lamps by night | OpenGL ES 2 on the SGX535: seven programs | 150 s of the tour with the interface over it, from 15:35 to 20:05 (iOS 6.1.6): 8 965 frames, 37 late (0.41 %), worst frame 39.8 ms, 16 600 to 27 500 triangles a frame (mean 22 200), 205 draws |
| Redmi 1S | 1280 × 720, shadows that follow the clock, the lamps' light on the ground, the tower floodlit and its lamps by night | OpenGL ES 3.0 on the Adreno 305: eight programs | 150 s of the tour with the interface over it, from 15:50 to 20:22 (MIUI V5; the SoC at 47 °C at the start and 60 to 65 °C from 25 s in, two cores online): 8 959 frames, 48 late (0.54 %; 29 of them within 10 s of the dusk, when the walls read the shadows and their rooms), worst frame 35.2 ms, 43 800 to 97 600 triangles a frame (mean 67 100), 135 draws |
| Browser tab | the iPod touch's pack and passes, shown as a PS Vita, a PSP, a Nintendo 3DS, an iPod touch or an Android phone: its screens, its presentation of the interface, its buttons from the keys | wgpu 25 over WebGPU (wasm32): the iPod touch's programs in WGSL, the interface on PocketJS's UI core for wasm | Chrome 154 on an M3 Max, each device on the tour with its interface: 60 and 30 frames a second held, 0.3 to 0.6 ms of the processor and the GPU a frame; for an eye the iPod touch reported, the same triangles and draws; beside its capture of a flight with the interface, a mean difference of 1.0 of 255 |
The city is modelled once as Three.js content that runs in a browser, and a compiler lowers it to what one console draws:
web/is the model: Procedural Tokyo by Yong Su (MIT), with its pipeline from public records to tiles (web/tools/pipeline) and its three.js client.web/src/pocket/adds the export page.crates/tokyo-cookis the city compiler: CityIR in, one pack and a compile receipt out, for a device profile (profiles/vita60.json,psp30.json,n3ds30.json,ipod60.json,redmi1s60.json).crates/tokyo-packis the pack: tables, vertex layouts and sections shared by the compiler and the runtimes.crates/tokyo-simis what moves, the same on every device: the camera and its tour, the clock and the sun, the sweep that turns heights into shadows, the traffic; and what a frame draws: the cells, blocks and regions in view at their levels of detail.ui/is the interface: one PocketJS app, compiled for each device and drawn over the city by every runtime.crates/tokyo-interfaceis the renderer's side of it and the flow around a flight (the title, the flight, the menu).vita/,psp/,n3ds/,ipod/andandroid/draw a pack. The iPod touch's core is the 3DS's Rust source built forarmv7-apple-ios; the Android app's core is its own (android/core, OpenGL ES 3.0) and reads the Vita's layouts.wgpu/draws the iPod touch's pack with wgpu: in a browser tab over WebGPU, with the interface over it and the page as a PS Vita, a PSP, a Nintendo 3DS, an iPod touch or an Android phone, and on the build machine, where a frame goes to a file. Its core is the same Rust source again; what no game owns of it is PocketJS's browser kernel (vendor/pocketjs/devices/web/pocket-web-wgpu).
PocketJS (pinned in vendor/pocketjs) supplies the device toolchains, the dev host, the GXM kernel and packaging. It also supplies what every Pocket3D game shows: the title card at launch and the app icon in the console's launcher (vendor/pocketjs/engine/pocket3d/icon/: 144 × 80 for the XMB, 128 × 128 for the Vita's bubble, 48 × 48 and 24 × 24 for the 3DS, 57 × 57 and 114 × 114 for SpringBoard, 48 to 192 pixels for an Android launcher). This repository holds no icon file; psp/assets/pic1.png and the Vita's LiveArea pictures are captures of this game.
bun tools/tokyo.ts fetch --area shiba # PLATEAU, OpenStreetMap, GSI → web/data/raw (3 GB)
bun tools/tokyo.ts tiles --area shiba # → web/public/tiles/shiba (the reference's own compiler)
bun tools/tokyo.ts export --area shiba # the city as the reference draws it → .pocket-build/city/shiba/ir (CityIR)
bun tools/tokyo.ts cook --area shiba # CityIR → .pocket-build/city/shiba/vita60/city.pack + receipt.json
bun tools/tokyo.ts native # build, and replace the binary Pocket Devkit runs
bun tools/tokyo.ts bench # frame timings over the tour → .pocket-build/validation/vita/…
bun tools/tokyo.ts cook --profile psp30 # the PSP's pack
bun tools/psp.ts run # build, stage on the PSPLINK share, start
bun tools/psp.ts bench # → .pocket-build/validation/psp/…
bun tools/psp.ts emu # the same program in PPSSPPHeadless: a frame and its status
bun tools/tokyo.ts cook --profile n3ds30 # the 3DS's pack
bun tools/n3ds.ts install # build the .3dsx (the pack in its ROMFS), send it over the paired wire, start
bun tools/n3ds.ts bench
bun tools/n3ds.ts emu # the same .3dsx in Azahar
bun tools/android.ts cook # the Redmi 1S's pack (profiles/redmi1s60.json)
bun tools/android.ts install # build the signed package with the pack inside, install it
bun tools/android.ts apk --release # the package members install, signed with the release key
bun tools/android.ts bench # → .pocket-build/validation/android/…
bun tools/wgpu.ts cook # the browser tab's pack: the iPod touch's (profiles/ipod60.json)
bun tools/wgpu.ts serve # the page, the wasm and the four interfaces, with the pack by byte range
bun tools/wgpu.ts check # the page in Chrome, each device driven from its title into a flight
bun tools/wgpu.ts dist # the directory a static host serves → .pocket-build/wgpu/dist
The export takes 28 s and the compile 3 to 6 s for the 180 tiles of Shiba; bun run dev serves the reference itself.
web/export.html loads the reference's mesher and materials in headless Chrome and hands the compiler what they produce (tools/export.ts receives it). Nothing of the reference's look is written a second time.
- Per tile, the geometry: the triangles
buildTilemakes for the buildings, with the attributes its facade material reads (window bay, height above the base, storey height, category), the footprints, PLATEAU's own models, the props. - Per tile, a picture from above (
top_x_z.png, 0.25 m a texel): the ground, the road paint, every roof under its aerial photo, tree crowns and parked cars, drawn by the reference's own materials under a light that leaves each surface its own colour, through a camera that looks straight down. A second, small picture (far_x_z.png) also shows the elevated roads and railways. - The facade material, drawn flat (
facade.day.png,facade.night.png): eight kinds of wall, each eight bays by eight storeys. The day picture is the wall in a neutral grey with a mask of what is wall and what is window; the night picture is what the rooms and the shop signs emit.
A device draws three kinds of surface, each with one program.
- Tops: the ground and every roof. Their texture is the picture from above and their texture coordinates are their own x and z, so a vertex is 12 bytes and the 11 million triangles of road surface the reference drapes over the terrain are no triangles at all: they are in the picture.
- Walls: the facade pictures, tinted by the building's colour. A wall is cut only where it is longer than the picture's eight bays; storeys repeat upwards.
- Solids: painted geometry, for the lattice tower, structures and trees.
The city is cut three ways, and each cut is a level of detail:
| Level | Place | Geometry |
|---|---|---|
| near | a cell, 128 m | the reference's own triangles: PLATEAU's roof shapes and wall planes, structures within 15 cm, a crown on every tree |
| mid | a block, 512 m, with one picture of 1 024² | prisms |
| far | a region, 1 024 m, with one picture of 512² | prisms from a coarser grid, of buildings over 14 m; structures are in the picture |
Prisms. Every roof of the area is drawn into one grid of heights, 1 m a cell. Cells next to each other within 4 m of height form a terrace, whoever's roof they are; a terrace's outline is straightened and becomes walls under the facade pictures and a flat top under the picture from above. Two houses of one height with a shared wall become one prism. The mid level has a quarter of the near level's triangles and the far level a twentieth.
Order in the index buffer is the culling structure. A mid or far batch has its indices ordered by cell, and its walls within a cell by the sector of the compass they face (16 sectors). The pack stores where each group starts. A frame draws, of each cell in view, the ground and roofs, and of its walls only the arc of sectors that can face the eye: a third of the walls in view are never sent to the GPU.
The city as heights. The same raster, at 2 m, goes into the pack (HMAP), with the light of 9 128 street lamps as a second picture over the same grid (LAMP).
- Shadows come from
HMAP. For the light's direction,tokyo_sim::shadow::sweepwrites into each cell the height below which a point over it is in shadow: the cell's own top, or what the cell one step towards the light passes on, lowered by the light's slope. A thread of its own sweeps the 2 048 × 1 536 cells whenever the sun has moved a tenth of a degree (410 ms a sweep); the result is a 16-bit texture. Every fragment, of any program, compares its own height with one texel. The step towards the light lands between two cells, so a shadow's edge softens with its distance from what casts it. - The scene is drawn straight into the display's multisampled surface. There is no off-screen target and no pass that copies the frame. At night a quarter-size chain takes the frame the display is showing, keeps what is bright and blurs it, and adds it over the next frame.
- The level of detail follows a triangle budget (200 000) and the GPU: the near and the mid distance shrink when a frame draws more, and when a frame has to wait more than 6 ms for the GPU to finish the frame before last; they grow back slowly, two seconds after such a wait. The GPU can run most of a refresh behind and still show every frame on time, and a view that fills more pixels than its triangles say (low over the park at dusk, 175 000 triangles) is what puts it there. While a list is up the budget is 150 000.
- The traffic is 800 cars on 6 777 lanes; the ones in view are built into one draw each frame.
- The interface (
ui/, thesinglepresentation) is drawn by PocketJS's Vita host library into the display scene, after the city and the night's glow: 480 × 272 logical, rastered at two samples a pixel. The guest turns 30 times a second and its list is drawn every frame. Its vertices come from vita2d's pool, which each frame takes one half of in turn, so a frame's draws stay valid until the GPU has used them. A tap on the panel reaches a list row through the guest's own hit test. The guest's collector walks its whole heap, 35 ms, and does not start by itself: it runs when the load ends and when the title comes back. The flight istokyo_sim::flight::Flightand the flow around ittokyo_interface::Session, as on the handhelds; the bundle is read from the USB share in a development build and from the package otherwise, and what the interface asks to keep isux0:data/pocket-tokyo/interface.json. - A clip is a pass over the whole screen's stencil here. vita2d makes a clip of two stencil writes, in the multisampled scene the city is drawn in. The compass fades its marks at the ends of its window and the map's picture is transparent beside the map, so the flight's instruments use none; the panel of a list uses one, and the lower budget pays for it.
- The interface's glyph pages and map take 4 MB of video memory: PocketJS's Vita host keeps a font atlas at one byte a texel (the pages for 24 px and 36 px type are 1 024 texels square at two samples a pixel). At 32 bits a texel they took 15 MB, and the city's ground pictures had to be 512 texels to fit; with the pages as coverage the ground pictures are 1 024 texels (65 MB of city, 69 MB of video memory reserved in all). Textures stay in video memory: with the two shadow targets in GPU-mapped main memory a night tour had 26 late frames in 30 s.
- The Pocket3D title card plays first.
Measured on the console (clocks at 444 / 222 MHz):
| Scene pass | 3.0 ms + 0.053 ms per 1 000 triangles |
| A draw call | 2 µs of GPU time, 4 µs of CPU time |
| GXM's parameter buffer at its default 16 MB | overflows near 150 000 triangles in view; the frame then takes three times as long. vita/build.rs raises it to 32 MB |
| Shadow lookup | 1.1 ms a frame with its own texture coordinate, 2.9 ms read from the .zw of a shared one |
| Off-screen scene, bloom chain and composite | 3.75 ms; drawing into the display surface removes it |
| The interface | 1.0 ms a turn (script, layout, two ticks of the UI core) at 30 turns a second; 0.75 ms of CPU a frame to draw its list; 35 ms a collection of its heap; a list is built the first time it is shown, in one turn of up to 71 ms (one late frame), and stays built |
| The tour with the interface | 150 s from 15:36 to 20:04, ground pictures of 1 024 texels: 9 010 frames, 0 late, worst frame 17.6 ms, 86 800 to 200 200 triangles (mean 158 200); a turn of the guest 0.5 ms, its draw 0.8 ms. Before the interface: 9 010 frames, 0 late, worst 17.0 ms, mean 161 400 |
| The flight's instruments on the GPU | with them the tour's heaviest stretch at dusk had 6 late frames in 9 000 under the triangle budget alone; none with the governor that follows the GPU |
| One clip in the instruments | a night tour's first 30 s: 8 to 11 late frames with the compass clipped, 2 to 4 with its marks faded instead (both before the governor followed the GPU) |
The PSP has 24 MB and no programs; the 3DS has vertex programs, three texture units and a 32 MiB limit on what the wire installs. Their packs differ from the Vita's in what follows from that.
- The near level is prisms too, from a raster of 2 m, and each cell's near level is one record: its vertices, its indices and a picture of its own ground (0.5 m a texel on the PSP, 1 m on the 3DS). A device keeps the records of the cells before the eye in 40 slots; a thread reads them while the frame thread waits for the GPU. A cell that has not arrived is drawn at the mid level. The rest of the pack stays in memory: 10 MB on the PSP.
- No vertex has a normal. Walls and painted geometry are ordered by the sector of the compass they face (16), at every level. The PSP draws one sector per draw with the GE's ambient colour as its light; the 3DS keeps the sector in the vertex and picks one of 17 lights in its vertex program. A ground vertex is its position and what it sees of the sky: 8 bytes.
- Day and night. PSP: every picture is 8-bit indices with a day palette and a night palette, clustered over both colours of a texel at once; the palette the GE reads is their mix by the hour. 3DS: the ground in ETC1 by day, and the night as light: the lamps' light is one texture over the whole city, added by a combiner stage; the facades have a second picture of what their windows emit.
- The iPod touch takes the 3DS's vertices (target
ipod) and its pictures as 16-bit texels in row order: a block's ground at 512² (1 m a texel), a cell's at 256² (0.5 m), the facades by day and what their windows emit, the lamps' light. Nothing limits the pack to 32 MiB there: it is 177 MB, 134 MB of it the cells' records, of which 40 are in memory at a time. A wall vertex's sector byte also carries, in its upper three bits, how late in the dusk the building's rooms come on. - Shadows. Both sweep the pack's heights (4 m a cell) for the sun's direction on a thread of low priority, when the sun has moved a degree. PSP: ground pictures keep to 128 colours, the upper half of each palette is the lower half in shadow, and the thread sets the top bit of the indices where the ground is in shadow. 3DS: the result is an 8-bit texture over the whole city, multiplied in by a combiner stage.
A landmark is a building the reference builds member by member from a model of its own, where the city's data has only its outline; web/src/world/tower.js (a steel lattice tower) is one. A model reports what it is made of through web/src/world/landmark.js: beams with a rank (0 carries the outline, 1 the frame, 2 is bracing), boxes, lamps. The export records the members (landmarks.cir), and the compiler makes a handheld's model from them at three levels of detail, chosen by the landmark's distance alone:
| Level | Members | Tokyo Tower |
|---|---|---|
| near, to 450 m | all 1 412: a beam is a ribbon of two triangles seen from both sides, an outline beam two ribbons crossed | 3 388 triangles |
| mid, to 1 100 m | the outline and the frame, no thinner than 1.6 m | 1 732 |
| far | the outline, no thinner than 3.2 m | 564 |
A model for another landmark reports its members the same way and needs nothing else in the compiler or on a device. The Vita draws the reference's own triangles of the tower.
A member's alpha is how far it shines by its own colour once the night has come. A landmark's steel and decks stand in floodlights: alpha 191, 0.75 of their colour, which the vertex program of the 3DS and of the iPod touch adds to the light of the hour (a wall keeps 0.16 to 0.26 of its colour by night, and the tower stood that dark among lit windows). The Vita's pack gives its tower the same 0.75. The PSP draws every painted face at its full colour by night and reads no alpha: its pack is the same bytes.
A profile that gives landmarks.lamps a size per level also gets the model's lamps: each is three squares crossed, in the lamp's colour with alpha 255, lit whole. The iPod touch's profile does (118 lamps on the tower: 4 096, 2 440 and 1 272 triangles); a machine that lights a group of faces with one colour has no use for them.
- One display list a frame. The draws are chosen (
tokyo_sim::view::select) while the GE draws the frame before; the list is written while the GE draws it. - A list costs 1.5 ms plus 0.63 ms per 1 000 triangles, with or without lighting, fog, texture or clipping. At 30 frames a second that is 45 000 triangles; the budget is 42 000.
- Two ranges of the 16-bit depth buffer: the cells near the eye, then the mid and far levels. A landmark is drawn in the range its distance puts it in, or in both.
- Reading a cell (126 KiB at most) takes 8 ms over USB; a sweep of the shadows takes 320 ms and writing them into the pictures 1.7 s, both in the time the frame thread waits.
- The interface (
ui/, thesinglepresentation) runs on PocketJS's PSP host library: QuickJS, the UI core and its GE backend, with PocketJS's one-block arena as the program's allocator. The city's buffers come at their exact size from the 2 MB the arena leaves the kernel, then from the arena's uncarved tail (psp/src/mem.rs). The guest takes 5.0 MB once the title is up and 5.7 MB once every list has been opened, so the package asks for the larger user memory of the 2000 and later models (MEMSIZE=1: 53.7 MB free at the start, 21.7 MB of the arena in use after loading). With 24 MB the guest is not started: nothing is drawn over the city, and START hands the eye to the tour and takes it back. - A turn of the guest runs while the GE draws the frame before. The frame's own work on this CPU is 9 ms (choosing the draws 4.4 ms, writing the list 4.4 ms) against 28 ms of the GE's, so a whole turn fits in the wait: its script 6.4 ms, then layout and the list of what it shows 2.9 ms. During a flight the guest takes 10 turns a second, one per refresh of the numbers, 3.4 ms of this CPU a frame. Its draw is the last pass of the display list and costs the GE 0.9 ms during a flight.
- A list over the city is pixels the GE blends. The title's cost it 1.9 ms and the menu's 6.7 ms, and the city gives that up in triangles: the governor's budget is 39 000 behind the title and 31 000 behind the menu. 60 s of each on the console: 0 late frames.
- A list coming up or leaving costs one frame. The turn that does it takes about 50 ms (script 36 ms, layout 16 ms). Every list stays built once it has been shown (
Rowsinui/app/parts.tsx); the first time, the hours take 0.27 s to build and the settings 0.12 s. A collection of the guest's heap stops this CPU for 60 to 80 ms; it waits for a list to be up.
- One pass: the depth buffer has 24 bits. The near plane is at 12 m: the haze is a table of 128 steps over the depth buffer's own values, and a nearer plane leaves the whole city to its first step.
- Ground: (light × shadow + lamps × night) × picture, in three combiner stages over three textures. Walls: light × tint × facade by day, plus night × what the windows emit. Painted geometry: (light + night × the vertex's alpha) × its colour, in the vertex program.
- Every texture is in linear memory, and every vertex program writes all three texture coordinates: a unit that stays bound is read at them.
- The interface is the PocketJS guest of
ui/(thedualpresentation), compiled in beside the core: PocketJS's 3DS UI core, its citro3d backend and QuickJS. It boots before the pack is read and shows the reading. Its turn runs beforeC3D_FrameBegin, beside the GPU's work on the frame before, and its draws read texture unit 0 alone. - The lower screen is the interface's second surface, a colour target of its own: the area from above with the eye's mark, the Menu and Tour keys, the day as a bar a stylus turns, and the lists. The upper surface is drawn over the city every frame; the lower one on the frames its list differs from the one it was last drawn from, 8 times a second on the tour.
- A turn of the guest takes 12.3 ms on an Old 3DS (its script 8.3 ms, layout and the two lists 4.1 ms) and is taken 15 times a second during a flight: 6.2 ms a frame. The tour over the same hours, 90 s, before and with the interface: 0 late frames in 2 717 and in 2 728; CPU 9.7 ms → 17.1 ms a frame; GPU 16.2 ms → 17.5 ms mean, 21.2 ms → 22.6 ms at most. Opening a list builds its rows in one turn of up to 49 ms.
- The guest takes 4.5 MB of heap and 5.0 MB of linear memory; 12.3 MB of linear memory stay free. The
.3dsxis 32.03 MB with the bundle (0.26 MB of script, 0.70 MB of pak) in its ROMFS; the wire installs up to 33.55 MB (32 MiB).
ipod/README.md has the loop and the measurements in full.
- The 3DS's passes as fragment programs, into a target with four samples a pixel that is resolved into the screen's buffer. Ground: (light × shadow + lamps × night) × picture; by day the program reads no lamps and by night no shadows. Walls: light × tint × facade, plus what the rooms emit by night. Haze is the Vita's formula, per vertex.
- A flight holds one refresh a frame up to about 34 000 triangles with four samples (about 50 000 with one). Drawn alone, a frame takes the GPU 5.9 ms plus 0.39 ms per 1 000 triangles; consecutive frames overlap in it.
- The interface is one more read in every program of the scene, of its texture at the pixel's own place. As a blended quad over the scene it left 38 % of the tour's frames late, and as a pass of its own 49 %; read in the programs, 12 % at the same 32 000 triangles. The budget with the interface is 24 000: it takes 8 000 triangles. The guest takes a turn 13 times a second (1.0 ms) and its texture is redrawn 11 times a second (5.1 ms of the CPU).
- Every vertex attribute starts on a 4-byte boundary. With the two bytes of light at offset 6 read as an attribute of their own, the driver copied and converted vertices at every draw: 242 draws took 34.7 ms of the CPU, and 3.3 ms once those bytes were read with the two before them.
- Every colour in a fragment program is
lowp. Withmediumpintermediates a night frame of 43 000 triangles (one sample a pixel) missed every eighth refresh and a dusk frame every fourth; inlowpthe same frames miss none. - A cell's record and the shadows reach the GPU on the render thread, one cell and one strip of the shadows a frame. The shadows have two textures, and a sweep is uploaded into the one no frame reads: a tile-based driver copies a texture that a queued frame reads before it writes into it.
android/README.md has the loop and the measurements in full.
- The pack is the Vita's shape (target
redmi1s): every place in memory, the near level the reference's own triangles, a normal on every vertex. Its pictures are ETC2 blocks, and a landmark is a model with three levels of detail in no cell's batches (20 024, 2 440 and 1 272 triangles for Tokyo Tower). The tower stands in its floodlights by night at every level: the two farther ones carry the alpha of the reference's own steel (120), which this target's program, the Vita's, multiplies by 1.6. - The scene goes straight into the window's buffer at the panel's 1280 × 720, with no target off the screen. The interface is drawn over it at 640 × 360 with two pixels a point.
- One package for the Redmi 1S and for a 64-bit phone:
minSdkVersion18,targetSdkVersion34, the libraries forarmeabi-v7a(API 18) and forarm64-v8a(API 21, segments aligned to 16 KB). In a window that is not 16:9 the title card, the city and the interface go into the largest 16:9 rectangle about its middle, with black beside it, and a finger is placed through the same rectangle: 1920 × 1080 in a 2400 × 1080 window. On the Redmi 1S that rectangle is the whole buffer, and a held frame is the same 921 600 pixels as before it. - The window has no samples of its own. With two a pixel from the EGL config, a held frame of 81 100 triangles under the interface went from 14.6 ms of the GPU and a frame a refresh to 20.2 ms and a frame every 23.5 ms; at the governor's shortest distances (69 100 triangles), from 13.2 ms to 18.4 ms and a frame every 21.9 ms.
- A frame costs what its triangles interpolate. With 12 to 16 numbers on every vertex a frame of 150 000 triangles took the GPU 20.3 ms; each number 58 000 triangles carry is 0.4 to 0.6 ms. The ground hands its fragments six numbers, a wall nine, and what is painted is lit, shadowed and hazed in the vertex stage: a colour crosses the triangle. Drawing nearest first, farthest first or in the pack's order came within 1.5 ms of each other, and a pass into the depth buffer first made the frame 33.6 ms.
- A program reads the shadows or the lights: by day shadows, by night the lamps' light on the ground and the rooms' on the walls. With both read, a frame of 74 400 triangles takes 14.7 ms; with the day's programs 12.7 ms. Only the walls have a set with both, for the dusk; the lamps' light comes onto the ground over two seconds once the sun's is out.
- The governor follows the GPU's own time for a frame (
EXT_disjoint_timer_query, read four frames later): the distances of the near and mid levels come in while it is over 13 ms and go out under 11 ms. The timer leaves out 2.4 ms of a frame, so 14.2 ms of it is a refresh. Over the tour the scale went from 0.25 to 0.80 (mean 0.35): the near level reached 75 to 240 m and the mid level 325 to 1 040 m. - Indices are 16 bits from a batch's first vertex and each batch has a vertex array of its own: with 32-bit indices over one array the driver walked the indices on the render thread, 147 µs for one block's ground.
- Above 60 °C the system leaves the phone two cores at 1.0 GHz. The shadows' sweep runs below every thread of a frame: at the frame's priority a minute of the tour had 29 late frames, below it 10.
wgpu/README.md has the loop, the devices, the deployable directory and the measurements in full.
- The same core, pack and programs. The flight, the slots and what a frame draws are the handheld core's (
n3ds/core/src, built with itshostfeature); the pack is the iPod touch's, unchanged; the seven programs areipod/src/render.c's in WGSL, on wgpu 25. One renderer runs over WebGPU in a tab and over Metal on the build machine. - The interface is the bundle a device loads, compiled by
tools/ui.ts, as a guest in a realm of the page on PocketJS's UI core built for wasm. Its lines pass throughtokyo-interface's channel, and the flow is theSession's. The guest is offered a turn as often as the device's own host offers one, 30 times a second on the PS Vita, the PSP and the 3DS and 60 on the iPod touch, and its clock is told that rate. The UI core rasterizes the picture once with its alpha; it is drawn again when its draw hash changes, 14 to 16 times a second in a flight: 1.6 ms a redraw at the PS Vita's 960 × 544, 0.44 ms at the PSP's size. The 3DS's lower screen is drawn again when its own hash changes, 7 times a second. - The page shows one handheld in its shell and changes it in a flight: PS Vita, PSP, Nintendo 3DS with its lower screen under the pointer, iPod touch with the pointer as a finger. The screen's size, its samples, the triangles a frame may draw, the frames a second and what the buttons do are values of the renderer. The shell's own keys and sticks take a pointer or a finger, and go down when the keyboard holds them. A browser whose pointer is a finger gets the iPod touch first.
- The page is PocketJS's Pocket3D player, the same for every game: the game's name, the devices as text, the word Simulated with one sentence on what a real device draws, the keys as a list, and a dock with two links to the game's page in Pocket Studio: Download and Remix. The page has no line of this game's own; the city's sources are in the interface's About list.
- The pack is read a range at a time. The head is 9.3 MB; a block's picture of the ground (699 KB) is read after the first frame, the nearest block first, with flat ground until it arrives; a cell's record when a slot wants it. A read is an HTTP
Rangerequest on the pack's file, or whole pieces of a pack cut into files of 2 MiB for a host that limits a file's size (bun tools/wgpu.ts dist: 130 files, 184.3 MB). The first frame of the city needs 13.8 MB: 6.8 s on a line of 16 Mbit/s. - The shadows are swept in a worker: 4.2 ms of an M3 Max in wasm, beside the frames.
- The Pocket3D title card plays first, over the page, while the interface and the head are read. A browser without WebGPU is told so in one sentence.
- Checked against the iPod touch: for an eye the device reported, the same triangles of each kind, draws, blocks and regions; beside its capture of a flight at night with its interface on the screen, a mean difference of a colour of 1.0 of 255, none of it in the interface's pixels.
Every 2D pixel comes from one PocketJS app, ui/: the title, the instruments over a flight, the menu, the hours, the settings and, on a touch panel, the controls. A renderer draws the city and no text. ui/pocket.json declares three presentations, and PocketJS picks one at build time from the device's modality (screens, touch, buttons):
| Presentation | Devices | Screen | What is its own |
|---|---|---|---|
presentations/single.tsx |
PSP, PS Vita | 480 × 272 logical; the Vita rasters it at 2× | Lists walked by the pad under a legend; on the Vita a row also takes a tap |
presentations/dual.tsx |
Nintendo 3DS | 400 × 240 over 320 × 240 | The lower screen: the area from above with the eye on it, the Menu and Tour keys, and the day as a bar a stylus turns |
presentations/touch.tsx |
iPod touch 4 | 480 × 320 | A stick, three keys (up, down, fast), a finger on the city to turn the view; a tap on the clock opens the day as a bar; rows a finger presses, marked under it |
the same, entered through main-touch-wide.tsx |
Redmi 1S | 640 × 360 logical, rastered at 2× for the panel's 1280 × 720 | Nothing: panel(640, 360) gives the presentation its screen before it mounts. The title's rows stand at the left where the iPod touch's do, a list's panel in the middle of the screen at the iPod touch's size, and each control at the edge it hangs from |
A presentation decides where things go and how large they are. What the clock, the compass tape, a list row or the map looks like is in ui/app/parts.tsx once, and what the lists hold is in ui/app/flight.ts once.
The title, the lists, the menu and the strip under them stand in the same place on every device. A presentation changes a row's height (26 to 30 px under a pad; 36 px on the touch panel's title and 44 px in its lists) and adds the controls its device alone has. The touch panel's strip names no button, the way back stands in a list's heading, and the six hours stand two abreast under the day's bar, so that each is 44 px tall.
- The protocol (
ui/app/protocol.ts,crates/tokyo-interface) is JSON lines over PocketJS'spocket.overlayservice, answered in the process: the QuickJS API on the Vita and the PSP, thesvcwiresymbols of PocketJS's C hosts on the 3DS, the iPod touch and the Redmi 1S. The renderer sends the members of its state that changed since the last line; the interface sendsstart,menu,tour,hour,title,option,prefs, and from a touch paneldrive(the stick and the held keys) andlook(pixels a finger dragged). - The flow is
tokyo_interface::Session, one implementation for every device, aroundtokyo_sim::flight::Flight: behind the title the tour flies; a flight is the tour's or the pad's; under the menu the pad is the interface's and the city keeps moving. With no guest on the screen (its files are missing, or it threw) the menu button hands the eye to the tour and takes it back. - The numbers in flight (the clock, the height, the speed, the heading, the eye on the map) travel as one array,
t. Each is written to its node (@pocketjs/framework/hot) in a cell of fixed size: one native call and no layout. The compass is one tape that slides. - The place under the view is worked out in the interface from those numbers and the places the area file names (
areas/shiba.json: a name, a latitude and a longitude each): the named place nearest to where the view meets the ground. A new area brings its own places. - The map is the export's own picture from above (
far_*.png), reduced bytools/ui.tsto 15 m a pixel and compiled into the app. The map and the scene come from one drawing. - The hour: the interface asks for an hour (a row of the list, a point of the bar) and the clock sweeps there at 9 hours a second by the shorter way round, so the sky is seen turning.
Clockin the settings is how fast the day runs by itself. - A turn of the guest is offered 30 times a second and taken when it is worth its cost (
tokyo_interface::Pace): the interface says when nothing is scheduled (idle), and from then a turn is taken when the renderer has news, when the menu button changes, while a finger is down, and for 12 turns after any of those. - A hint's seconds are counted by the renderer. A timer of the guest's counts its turns, so a hint that stands for 7 s on such a timer makes a device take 30 turns a second for 7 s (9 ms each on a PSP). The interface asks to be woken instead (
wake, answered inwoke), and between a hint's two fades nothing is scheduled. - Handed back to the tour, the eye travels to the tour's path in 1.5 to 5 seconds (by the distance) and keeps above what it crosses; it does not cut there.
- What is kept between runs (the settings) is one JSON text the interface hands to the renderer, which stores it as
interface.jsonand hands it back at the start.
bun tools/ui.ts preview runs each device's compiled bundle on PocketJS's UI core built for wasm, against a renderer that exists only as state (ui/test/harness.ts), and writes every screen as a picture to .pocket-build/ui/preview/. bun tools/ui.ts test drives the same rig through a flight and checks what the interface asked for.
| Fly | Look | Climb, descend | Faster | Clock | Tour | Menu | |
|---|---|---|---|---|---|---|---|
| PS Vita | left stick | right stick | R, L | ✕ | d-pad left, right | SELECT | START |
| PSP | stick (ahead, turn) | △, ✕ | R, L | □ | d-pad left, right | SELECT | START |
| Nintendo 3DS | Circle Pad (ahead, turn) | X, B | R, L | Y | d-pad left, right; the bar on the lower screen | SELECT; the Tour key | START; the Menu key |
| iPod touch | the stick on the panel | a finger on the city | UP, DOWN | FAST | a tap on the clock, then the bar | the TOUR key | the key at the upper left |
| Redmi 1S | the stick on the panel | a finger on the city | UP, DOWN | FAST | a tap on the clock, then the bar | the TOUR key | the key at the upper left; the back key under the panel (it leaves the app from the title) |
A stick, or a finger on the city, takes the eye off the tour where it is. On the 3DS, L + R + START leaves.
In a browser tab the keys are the buttons of the device the page shows: W A S D the stick, the arrows the d-pad, Z X C V the face buttons at the right, the bottom, the left and the top (I K J L by their place on a device with one stick; the right stick on a PS Vita), Q and E the shoulders, Shift SELECT, Space START. The pointer is the finger on a touch screen and the stylus on the 3DS's lower one, and it presses the keys and moves the sticks of the shell the page shows.
On the handhelds the eye keeps 30 m above what stands under it and around it. On the tour it starts to rise two seconds before a tower and comes down after it.
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 [--targets vita,psp,3ds,ipod-touch,android] [--out dist/release] [--vita-gxp DIR] [--no-build] [--upload]
| Target | File | Holds |
|---|---|---|
vita |
pocket-tokyo-<version>.vpk |
the program, the vita60 pack, the interface and the programs a console compiled |
psp |
pocket-tokyo-<version>-psp.zip |
PSP/GAME/PocketTokyo/ for the root of a Memory Stick: EBOOT.PBP, the psp30 pack and the interface |
3ds |
pocket-tokyo-<version>.3dsx |
the program, with the n3ds30 pack and the interface in its ROMFS |
ipod-touch |
pocket-tokyo-<version>-ipod.ipa |
Payload/PocketTokyo.app, with the ipod60 pack |
android |
pocket-tokyo-<version>.apk |
the app for Android 4.3 and later with OpenGL ES 3.0 (minSdkVersion 18, targetSdkVersion 34): the engine and the loader for armeabi-v7a and for arm64-v8a, the interface and the redmi1s60 pack, signed |
It needs the toolchains of the four device tools: VitaSDK at ~/vitasdk with cargo-vita and Rust nightly-2026-05-28; PocketJS's pinned PSP toolchain (bun tools/bootstrap.ts in vendor/pocketjs) with pack-pbp; Docker, for the devkitARM container, and Rust nightly-2026-07-02; the Xcode command line tools, ldid and PocketJS's iPod touch 4 toolchain (bun ipodtouch4 doctor in vendor/pocketjs); the Android SDK's NDK 21.4.7075529 and build-tools 34.0.0 with a JDK, and the Rust targets armv7-linux-androideabi and aarch64-linux-android on nightly-2026-07-02, which bun tools/android.ts doctor checks. The exported area is an input: fetch, tiles and export in From records to a frame write .pocket-build/city/shiba/ir. It carries no record of the sources it came from, so the tool records its hash and cannot tell a stale export.
For each target the tool cooks the profile's pack from .pocket-build/city/shiba/ir with this commit's compiler and runs the build a developer runs: tools/tokyo.ts vpk, tools/psp.ts package, tools/n3ds.ts build, tools/ipod.ts package, tools/android.ts apk. 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 target's build 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 CityIR and of each 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, and the SHA-256 of the certificate the Android package is signed under.
The Vita's programs are an input. SceShaccCg runs on a console, so the package carries the .gxp files a development run left in its share's tokyo/gxp, the ones manifest.txt there lists. --vita-gxp DIR names that directory; the default is .pocket-build/vita-usb/share/tokyo/gxp. Before the Vita build the tool checks each program against vita/shaders: the console names a program by an FNV-1a hash of its source, an FNV-1a step can be taken back, and a name taken back through this commit's file arrives at the hash's starting value or at the one state every scene program shares. The build stops, with the reason, when the manifest is missing, when a program was compiled from a source that has changed since, and when a source has no program. The numeric #defines a scene program starts with are not checked on the host (scene_defines in vita/src/city.rs: constants, and the pack's block size, margin and height range); release.json records the state they hash to.
All five packages are byte-identical across two builds of a commit on one computer. The tool writes the .zip, 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 program carries a random build id. For a release the tool names it (POCKET_RELEASE_BUILD, which tools/vita.ts reads): 32 hex digits from the commit, the vita60 pack's hash and the hash of the programs' manifest; release.json records it. In the .apk, aapt dates its entries 1980-01-01, tools/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.
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. The release tool runs bun tools/android.ts apk --release, which 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 build 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.
A release package is not android:debuggable and reads no development copy. --release writes android:debuggable="false" and compiles the loader and the shell with TOKYO_RELEASE, so neither looks in files/dev; tools/android.ts refuses a release package whose badging says debuggable. A development package (apk, install without --release) stays debuggable and signed with the phone's debug key (~/.cache/pocket-nexus/redmi1s/signing/pocketjs-redmi1s-debug.keystore, alias pocketjs, made when absent): native, launch, status, ctl, capture and bench reach the app's data through run-as. 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.tokyo goes first (the settings the app kept go 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 Tokyo" 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 description credits the city's sources (Attribution). 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(tokyo-film) runs the browser tab's renderer 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 of the development host's words. The flight advances two sixtieths of a second a frame, and a frame is drawn again until every cell near the eye and every block's picture has been read, so no frame shows a cell on its way.tools/listing.tswrites the lists and hands the frames to ffmpeg. - A clip is the tour from a second of its route, or an eye carried through a row of points, with the clock at an hour and running at a rate. A carried eye keeps over open ground and streets, as the flight's own keeps out of the buildings. A clip is H.264 at 30 frames a second with no sound, with a fade of ten frames at each end so that it loops through a dip, held under 12 MiB by a ceiling on its rate; its poster is that frame filmed again, as a JPEG.
| File | What it shows | Length |
|---|---|---|
tower.mp4 |
the tour round Tokyo Tower from its second 15, the clock from 17:03 to 18:48: the sun sets at 17:45 and the lamps and the rooms come on | 22 s |
bay.mp4 |
an eye from Azabudai past the tower, over Zojo-ji and above the avenue through Daimon to Kyu-Shiba-rikyu Garden, the clock from 15:30 | 18 s |
sunrise.mp4 |
one view to the east past the tower, the clock from 5:20 to 8:00, with sunrise at 6:15 | 12 s |
zojoji.jpg, lattice.jpg, hamamatsucho.jpg, night.jpg |
the tower over Zojo-ji at 17:15, the tower from 120 m at 14:00, the garden and the railway at Hamamatsucho at 11:00, the tower at 18:30 | |
card.jpg |
the share picture a link preview carries: the tower at 17:52, 1200 × 630, with no text on it |
Eleven files, 19.8 MiB. Two runs gave the same bytes for every file: the reads come from the pack's file, the shadows are swept in the frame that asks for them, and no clock of the machine enters a frame.
It needs the iPod touch's pack (bun tools/wgpu.ts cook), 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: a picture of the city is made from the compiled area and keeps its sources' terms, as a pack does.
3D city model, roof photos, bridges and street furniture: Project PLATEAU (MLIT Japan). Elevation and aerial photos: Geospatial Information Authority of Japan (GSI). Road network, railways and places: © OpenStreetMap contributors (ODbL); a compiled area is a derived database under the ODbL. Textures: Poly Haven (CC0). Trees: ez-tree (MIT). The model in web/ is Procedural Tokyo by Yong Su, under the MIT License (web/LICENSE).
The code is under the MIT License; the code in web/ keeps its own notice. The licence covers the code only: the city data keeps the terms of its sources listed above. Pocket3D's title card, app icon and device kernels come from PocketJS under their own licence.