Self-contained game cartridges for tiny worlds.
DreamCart is an isomorphic JavaScript game runtime — the same game .js runs
unchanged on Sony PSP, the Web, Nintendo 3DS, and a dual-screen
Android handheld, powered by QuickJS (plus
rust-psp on PSP and
libctru/citro2d on 3DS).
Each platform implements the same tiny native contract — gfx.clear(r,g,b),
gfx.fillRect(x,y,w,h,r,g,b), log(msg), and a frame(buttons) function called
once per frame with a fixed controller bitmask — so games in
runtime/src/game/ are written once and run everywhere. An
optional g3d contract adds hardware-accelerated 3D the same way: meshes
uploaded once + one batched draw-list per frame, with all scene/physics/math logic
in shared JS and a native engine per platform (see
docs/3d-design.md and the cube3d/racing3d/fps3d games).
| 🎮 Playground | https://dreamcart.games/play/ — run any game in a themeable handheld console, in your browser |
| 📖 Docs | https://dreamcart.games/docs/ — architecture + lib API reference |
| 🗓 Changelog | https://dreamcart.games/changelog/ — capability summaries by date |
| 🏠 Home | https://dreamcart.games/ |
| Platform | Host | Graphics (2D / 3D) | Status |
|---|---|---|---|
| PSP | Rust (rust-psp) + QuickJS | sceGu / sceGu+sceGum | ✅ runs (PPSSPP) |
| Web | Canvas + RAF | Canvas2D / WebGL2 | ✅ runs (Playground) |
| 3DS | C (libctru) + QuickJS | citro2d / citro3d | ✅ runs (Azahar/hardware) |
| Android (dual-screen) | Kotlin + WebView (web engine) | Canvas2D / WebGL2 | ✅ runs (3DS-style handheld; top=game, bottom=native UI) — see android/ |
Flappy (flappy.js) |
Tactical 3D (tatical3d.js) |
|---|---|
![]() |
![]() |
| 2D framework game | 3D framework game (hardware-skinned glTF) |
There are two kinds of games, by naming convention:
raw-*— raw low-level demos. Each is a single self-contained.jsfile that uses only the bare native contract (gfx.clear,gfx.fillRect,log, and aframe(buttons)loop; numbers drawn with a tiny rectangle pixel-font). They exist to exercise the low-level API directly, with no framework.- Unprefixed — framework games. Authored against the framework (see
Framework below) and bundled into
the same runtime — including the 3D demos (
cube3d,racing3d,fps3d,skin3d,outdoor3d,bsp3d, …) and a Jin-Yong-flavoured wuxia village story game (rpg.js).
The raw low-level demos live in runtime/src/game/:
| Game | File | Controls |
|---|---|---|
| Snake (default) | raw-snake.js |
D-pad steer; walls wrap; START restart |
| Pong | raw-pong.js |
UP/DOWN move paddle (vs AI) |
| 2048 | raw-g2048.js |
D-pad slide; START restart |
| Breakout | raw-breakout.js |
LEFT/RIGHT paddle, CROSS launch |
| Tetris | raw-tetris.js |
LEFT/RIGHT move, DOWN soft-drop, CROSS/UP rotate, START restart |
| Platformer | raw-platformer.js |
LEFT/RIGHT run, CROSS jump, START restart |
Select which one to embed in a native build with PSPJS_GAME:
PSPJS_GAME=raw-tetris.js bun run psp # builds EBOOT.PBP for TetrisBuild every raw and framework game into a PSP memory-stick layout:
bun run psp:all
# -> dist/psp/PSP/GAME/<game>/EBOOT.PBPCopy dist/psp/PSP to the root of the PSP memory stick; each game appears as a
separate homebrew entry under PSP/GAME/<game>/. The script also packs each
EBOOT with a generated PSP menu title, ICON0.PNG, and PIC1.PNG placeholder
preview based on the game's // @title.
For real PSP/Vita startup debugging, build the trace EBOOT:
bun run psp:trace
# -> dist/psp-trace/PSP/GAME/dreamcart-raw-snake-trace/EBOOT.PBPOn PS Vita Adrenaline, the final path is
ux0:pspemu/PSP/GAME/dreamcart-raw-snake-trace/EBOOT.PBP.
bun run play <web|psp|3ds> [game] builds the chosen game and launches the
matching emulator:
bun run play web # open the Playground (pick a game from the list)
bun run play web maze # Playground, jump straight to a game
bun run play psp raw-tetris # build EBOOT + launch PPSSPP
bun run play 3ds rpg # build .3dsx + launch a 3DS emulator (Azahar/Citra/…)Run bun run play with no args to see the game list. PSP needs PPSSPP
(brew install --cask ppsspp); 3DS needs a 3DS emulator in /Applications.
The raw gfx/frame contract is deliberately tiny. On top of it lives a small,
isomorphic game framework (framework/) that runs the same on
all platforms. The SDK (framework/src/) is written in
TypeScript, but games themselves are authored in plain JavaScript — the
project's firm boundary is that game business logic is JS. Games still get full
editor/CI type-checking via // @ts-check + JSDoc types imported from the SDK
(see framework/games/tsconfig.json). Each game
is bundled (framework inlined) per platform, so PSP/Web/3DS execute identical code.
It provides: a scene/entity tree (Scene/Node with update/draw), the game
loop, edge-detecting Input, a seeded deterministic Rng, Graphics
(rect/sprite/text), palette Bitmaps with a baked 8×8 ASCII font and
sprites, a TileMap with camera, a DialogueBox, a shared CharController +
analog input contract, an ActionMap, and data-driven scene descriptions. The 3D
stack adds the g3d contract, Scene3D, mesh/material/light/skin,
deterministic math, glTF hardware skinning, and a retained native scene
(cull + draw in Rust, not per-node QuickJS — the key PSP performance unlock).
Assets are baked to the binary .dcpak container
(framework/bake/). Full API: https://dreamcart.games/docs/.
The site at dreamcart.games — home, Playground,
Docs and Changelog — is a static Bun + React multi-page app built into
web/site/ and deployed to Cloudflare Pages. It uses a single CSS token
contract with themes-as-data (switched via <html data-theme>) and headless
components keyed by data-part, so one theme picker re-skins the whole site —
including the handheld console the Playground renders games inside (desktop:
horizontal, PSP-like; mobile: vertical, Game Boy SP-like). Four themes ship:
cartridge (default), psp-silver, dmg (Game Boy green), and light.
The Playground runs the same game files as every other platform: it implements
the identical gfx/input/frame contract on Canvas/WebGL2
(web/engine.js, the global window.PSPJS) and loads the games
from a generated manifest (web/build-games.ts →
window.GAMES). Each game's menu title, order and on-screen controls come from a
header comment in its own source (// @title / // @order / // @controls) — the
single source of truth, so adding a game needs no edit to the build script. Game
source is hidden by default behind a Code drawer (raw games stay editable +
runnable; framework games are read-only). Docs and Changelog are authored as
Markdown in web/content/ and rendered to static HTML at build
time (syntax highlighting via highlight.js, theme-recolored through tokens).
bun run serve # build + serve the full site -> http://localhost:8123
bun run site # build the static site into web/site/
bun run deploy # build + publish web/site/ to Cloudflare Pagesbun install
bun run build # bake assets -> typecheck -> bundle games -> web manifest
bun run test # golden + smoke testsbun run build bundles each framework/games/*.js (framework inlined, via
Bun.build) to runtime/src/game/<name>.js, so the PSP/Web/3DS build steps embed
them exactly like the raw demos (e.g. PSPJS_GAME=rpg.js bun run psp).
framework/test/golden.ts renders each framework game's bundle headlessly under
Bun (a gfx mock → RGBA framebuffer), runs a deterministic seeded sequence with
scripted input, and byte-compares the framebuffer to a committed golden
(framework/test/goldens/*.png); it also runs a no-crash smoke pass over the
raw-* demos (seeded Math.random). A sibling
framework/test/contract.ts asserts the controller
button bitmask is identical across the Web and 3DS hosts, the framework SDK, and
every raw demo's BTN_* constants (the raw games are eval'd as a string and so
can't import the canonical Btn, so the test enforces they never drift).
Because the same bundle runs on every platform, this catches crashes and visual
regressions in the shared code. Run with bun run test; regenerate goldens with
UPDATE=1 bun framework/test/golden.ts.
- Rust host (
runtime/src/main.rs) owns the process: it sets up the GU (double-buffered 480x272), the controller, and a QuickJS runtime/context, then registers the native API and runs the per-frame loop (Rust opens the GE display list, calls JSframe(buttons), then finishes/syncs/swaps). - 2D graphics + input bridge (
runtime/src/gfx.rs,runtime/src/bridge.rs) exposes to JS:gfx.clear(r, g, b)gfx.fillRect(x, y, w, h, r, g, b)log(msg)frame(buttons)is defined by the game;buttonsis the PSP controller bitmask (UP=0x10,RIGHT=0x20,DOWN=0x40,LEFT=0x80,CROSS=0x4000,START=0x08).
- QuickJS allocator (
runtime/src/qjs_alloc.rs): QuickJS is created withJS_NewRuntime2so it allocates through the Rust/PSP allocator (rust-psp's startup sets up no C heap, so newlibmallocis unusable). - FFI bindings: extended in
quickjs-rs/libquickjs-sys/src/lib.rs. - Web host (
web/engine.js): the samegfx/input/framecontract on Canvas2D, with an optional WebGL2 layer for theg3d3D pass.
Install Bun and Homebrew (and Docker, e.g. OrbStack, for the 3DS build), then one command sets up everything:
git clone https://github.com/doodlewind/dreamcart.git
cd dreamcart
bun install
bun run bootstrapbun run bootstrap (scripts/bootstrap.ts) is idempotent
and installs/configures:
- submodules (
bun run setup) - LLVM (
brew install llvm— Apple clang can't target MIPS) - PPSSPP (
brew install --cask ppsspp) — PSP emulator - Azahar (downloaded from GitHub releases) — 3DS emulator
- Rust
nightly-2026-05-28+rust-src, and pins the repo override - cargo-psp / prxgen / pack-pbp / mksfo (built from the
rust-pspsubmodule) - the PSPSDK from
doodlewind/pspdev(sdk-noabicalls-normalized-2026-06-19, clang-built no-abicalls newlib/glue with normalized archive metadata) intomipsel-sony-psp/ - the
devkitpro/devkitarmDocker image (3DS toolchain)
It reports anything it can't auto-install (Homebrew/Docker not present, Docker daemon stopped); fix those and re-run. Then everything runs on Bun — there's no Python/Make glue.
The
quickjs-rsandrust-pspsubmodules point atdoodlewind/*forks. Those forks carry DreamCart's PSP C/stdio shims, 32-bitsize_tABI/API exports, and PSP nightly/tooling compatibility fixes. LLVMllvm-aris used for the static archive (Applearsilently drops MIPS objects →undefined symbol: JS_*).
Open the EBOOT in PPSSPP:
open -a PPSSPPSDL --args runtime/target/mipsel-sony-psp/debug/EBOOT.PBPbun run serve # -> http://localhost:8123 (PORT=3000 to change)Open http://localhost:8123/play/ (or /play/?game=rpg.js to pick one). The dev
server (web/serve.ts) builds the full site and serves it with
Cloudflare-Pages-style directory routing.
Builds a .3dsx homebrew app using the devkitpro/devkitarm Docker image — no
host toolchain install or sudo needed (just Docker, e.g. OrbStack/Docker Desktop):
PSPJS_GAME=raw-tetris.js bun run 3ds # -> runtime-3ds/dreamcart-3ds.3dsxRun the .3dsx in Azahar (the maintained Citra fork)
or on real hardware. The 3DS host (runtime-3ds/source/main.c)
embeds QuickJS and renders the same games via citro2d (scaled to the 400×240 top
screen), with logs on the bottom screen.
A 3DS-style dual-screen handheld app (game on the top screen, native UI on the
bottom) that runs the web engine inside a WebView. See android/:
bun run android # assemble the debug APK
bun run android:install # install + launch on a connected device/emulatorMIT


