{"id":51511755,"url":"https://github.com/doodlewind/dreamcart","last_synced_at":"2026-07-08T07:02:11.010Z","repository":{"id":80193175,"uuid":"410604257","full_name":"doodlewind/dreamcart","owner":"doodlewind","description":"Isomorphic JavaScript game runtime — the same .js game runs unchanged on Sony PSP, the Web, and Nintendo 3DS. Powered by QuickJS.","archived":false,"fork":false,"pushed_at":"2026-07-02T08:08:29.000Z","size":29272,"stargazers_count":24,"open_issues_count":6,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-02T10:07:44.526Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://dreamcart.games","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doodlewind.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-09-26T16:31:31.000Z","updated_at":"2026-07-02T08:05:35.000Z","dependencies_parsed_at":"2023-07-20T10:46:43.814Z","dependency_job_id":null,"html_url":"https://github.com/doodlewind/dreamcart","commit_stats":null,"previous_names":["doodlewind/dreamcart"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/doodlewind/dreamcart","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doodlewind%2Fdreamcart","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doodlewind%2Fdreamcart/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doodlewind%2Fdreamcart/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doodlewind%2Fdreamcart/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doodlewind","download_url":"https://codeload.github.com/doodlewind/dreamcart/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doodlewind%2Fdreamcart/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35255356,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-08T02:00:06.796Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-08T07:02:09.918Z","updated_at":"2026-07-08T07:02:11.001Z","avatar_url":"https://github.com/doodlewind.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"web/static/logo.png\" alt=\"DreamCart\" width=\"150\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eDreamCart\u003c/h1\u003e\n\u003cp align=\"center\"\u003e\u003cb\u003eSelf-contained game cartridges for tiny worlds.\u003c/b\u003e\u003c/p\u003e\n\nDreamCart is an **isomorphic** JavaScript game runtime — the *same* game `.js` runs\nunchanged on Sony **PSP**, the **Web**, Nintendo **3DS**, and a dual-screen\n**Android** handheld, powered by [QuickJS](https://bellard.org/quickjs/) (plus\n[rust-psp](https://github.com/overdrivenpotato/rust-psp) on PSP and\n[libctru/citro2d](https://github.com/devkitPro/libctru) on 3DS).\n\nEach platform implements the same tiny native contract — `gfx.clear(r,g,b)`,\n`gfx.fillRect(x,y,w,h,r,g,b)`, `log(msg)`, and a `frame(buttons)` function called\nonce per frame with a fixed controller bitmask — so games in\n[`runtime/src/game/`](runtime/src/game/) are written once and run everywhere. An\n**optional** `g3d` contract adds hardware-accelerated 3D the same way: meshes\nuploaded once + one batched draw-list per frame, with all scene/physics/math logic\nin shared JS and a native engine per platform (see\n[`docs/3d-design.md`](docs/3d-design.md) and the `cube3d`/`racing3d`/`fps3d` games).\n\n### Live\n\n| | |\n|---|---|\n| 🎮 **Playground** | \u003chttps://dreamcart.games/play/\u003e — run any game in a themeable handheld console, in your browser |\n| 📖 **Docs** | \u003chttps://dreamcart.games/docs/\u003e — architecture + lib API reference |\n| 🗓 **Changelog** | \u003chttps://dreamcart.games/changelog/\u003e — capability summaries by date |\n| 🏠 **Home** | \u003chttps://dreamcart.games/\u003e |\n\n| Platform | Host | Graphics (2D / 3D) | Status |\n|----------|------|--------------------|--------|\n| PSP | Rust (rust-psp) + QuickJS | sceGu / sceGu+sceGum | ✅ runs (PPSSPP) |\n| Web | Canvas + RAF | Canvas2D / WebGL2 | ✅ runs ([Playground](https://dreamcart.games/play/)) |\n| 3DS | C (libctru) + QuickJS | citro2d / citro3d | ✅ runs (Azahar/hardware) |\n| Android (dual-screen) | Kotlin + WebView (web engine) | Canvas2D / WebGL2 | ✅ runs (3DS-style handheld; top=game, bottom=native UI) — see [`android/`](android/) |\n\n| Flappy (`flappy.js`) | Tactical 3D (`tatical3d.js`) |\n|:---:|:---:|\n| ![Flappy — a Flappy-Bird clone on the 2D contract](docs/flappy.png) | ![Tactical 3D — skinned soldier in an arena on the g3d contract](docs/tatical3d.png) |\n| 2D framework game | 3D framework game (hardware-skinned glTF) |\n\n## Games\nThere are two kinds of games, by naming convention:\n\n- **`raw-*` — raw low-level demos.** Each is a single self-contained `.js` file\n  that uses only the bare native contract (`gfx.clear`, `gfx.fillRect`, `log`, and\n  a `frame(buttons)` loop; numbers drawn with a tiny rectangle pixel-font). They\n  exist to exercise the low-level API directly, with no framework.\n- **Unprefixed — framework games.** Authored against the framework (see\n  [Framework](#framework-typescript-sdk-javascript-games) below) and bundled into\n  the same runtime — including the 3D demos (`cube3d`, `racing3d`, `fps3d`,\n  `skin3d`, `outdoor3d`, `bsp3d`, …) and a Jin-Yong-flavoured **wuxia village**\n  story game (`rpg.js`).\n\nThe raw low-level demos live in [`runtime/src/game/`](runtime/src/game/):\n\n| Game | File | Controls |\n|------|------|----------|\n| Snake (default) | `raw-snake.js` | D-pad steer; walls wrap; START restart |\n| Pong | `raw-pong.js` | UP/DOWN move paddle (vs AI) |\n| 2048 | `raw-g2048.js` | D-pad slide; START restart |\n| Breakout | `raw-breakout.js` | LEFT/RIGHT paddle, CROSS launch |\n| Tetris | `raw-tetris.js` | LEFT/RIGHT move, DOWN soft-drop, CROSS/UP rotate, START restart |\n| Platformer | `raw-platformer.js` | LEFT/RIGHT run, CROSS jump, START restart |\n\nSelect which one to embed in a native build with `PSPJS_GAME`:\n\n``` sh\nPSPJS_GAME=raw-tetris.js bun run psp     # builds EBOOT.PBP for Tetris\n```\n\nBuild every raw and framework game into a PSP memory-stick layout:\n\n``` sh\nbun run psp:all\n# -\u003e dist/psp/PSP/GAME/\u003cgame\u003e/EBOOT.PBP\n```\n\nCopy `dist/psp/PSP` to the root of the PSP memory stick; each game appears as a\nseparate homebrew entry under `PSP/GAME/\u003cgame\u003e/`. The script also packs each\nEBOOT with a generated PSP menu title, `ICON0.PNG`, and `PIC1.PNG` placeholder\npreview based on the game's `// @title`.\n\nFor real PSP/Vita startup debugging, build the trace EBOOT:\n\n``` sh\nbun run psp:trace\n# -\u003e dist/psp-trace/PSP/GAME/dreamcart-raw-snake-trace/EBOOT.PBP\n```\n\nOn PS Vita Adrenaline, the final path is\n`ux0:pspemu/PSP/GAME/dreamcart-raw-snake-trace/EBOOT.PBP`.\n\n### Debug on real PSP hardware over USB\nRun a freshly compiled game on an actual PSP in seconds — no memory-stick copy:\n\n``` sh\nbun run psp:hw walk3d   # build, then load onto the PSP; press Enter to hot-reload\n```\n\nIt serves the cargo output to the PSP as `host0:` (usbhostfs) and `ldstart`s it\nthrough PSPLINK (pspsh), keeping a tight edit → build → run loop on real silicon.\nNeeds a one-time host-tools + PSPLINK setup — see\n[`docs/psp-hardware-debugging.md`](docs/psp-hardware-debugging.md).\n\n## Play (one command)\n`bun run play \u003cweb|psp|3ds\u003e [game]` builds the chosen game and launches the\nmatching emulator:\n\n``` sh\nbun run play web              # open the Playground (pick a game from the list)\nbun run play web maze         # Playground, jump straight to a game\nbun run play psp raw-tetris   # build EBOOT + launch PPSSPP\nbun run play 3ds rpg          # build .3dsx + launch a 3DS emulator (Azahar/Citra/…)\n```\n\nRun `bun run play` with no args to see the game list. PSP needs PPSSPP\n(`brew install --cask ppsspp`); 3DS needs a 3DS emulator in `/Applications`.\n\n## Framework (TypeScript SDK, JavaScript games)\nThe raw `gfx`/`frame` contract is deliberately tiny. On top of it lives a small,\n**isomorphic** game framework ([`framework/`](framework/)) that runs the same on\n**all platforms**. The SDK ([`framework/src/`](framework/src)) is written in\n**TypeScript**, but games themselves are authored in **plain JavaScript** — the\nproject's firm boundary is that game business logic is JS. Games still get full\neditor/CI type-checking via `// @ts-check` + JSDoc types imported from the SDK\n(see [`framework/games/tsconfig.json`](framework/games/tsconfig.json)). Each game\nis bundled (framework inlined) per platform, so PSP/Web/3DS execute identical code.\n\nIt provides: a scene/entity tree (`Scene`/`Node` with `update`/`draw`), the game\nloop, edge-detecting `Input`, a seeded deterministic `Rng`, `Graphics`\n(`rect`/`sprite`/`text`), palette `Bitmap`s with a baked **8×8 ASCII font** and\nsprites, a `TileMap` with camera, a `DialogueBox`, a shared `CharController` +\nanalog input contract, an `ActionMap`, and data-driven scene descriptions. The 3D\nstack adds the `g3d` contract, `Scene3D`, `mesh`/`material`/`light`/`skin`,\ndeterministic `math`, glTF hardware skinning, and a retained **native scene**\n(cull + draw in Rust, not per-node QuickJS — the key PSP performance unlock).\nAssets are *baked* to the binary [`.dcpak`](docs/dcpak-format.md) container\n(`framework/bake/`). Full API: \u003chttps://dreamcart.games/docs/\u003e.\n\n## The web site \u0026 Playground\nThe site at **[dreamcart.games](https://dreamcart.games)** — home, Playground,\nDocs and Changelog — is a static **Bun + React** multi-page app built into\n`web/site/` and deployed to Cloudflare Pages. It uses a single CSS **token\ncontract** with **themes-as-data** (switched via `\u003chtml data-theme\u003e`) and headless\ncomponents keyed by `data-part`, so one theme picker re-skins the whole site —\nincluding the **handheld console** the Playground renders games inside (desktop:\nhorizontal, PSP-like; mobile: vertical, Game Boy SP-like). Four themes ship:\n`cartridge` (default), `psp-silver`, `dmg` (Game Boy green), and `light`.\n\nThe Playground runs the *same* game files as every other platform: it implements\nthe identical `gfx`/`input`/`frame` contract on Canvas/WebGL2\n([`web/engine.js`](web/engine.js), the global `window.PSPJS`) and loads the games\nfrom a generated manifest ([`web/build-games.ts`](web/build-games.ts) →\n`window.GAMES`). Each game's menu title, order and on-screen controls come from a\nheader comment in its own source (`// @title` / `// @order` / `// @controls`) — the\nsingle source of truth, so adding a game needs no edit to the build script. Game\nsource is hidden by default behind a **Code** drawer (raw games stay editable +\nrunnable; framework games are read-only). Docs and Changelog are authored as\nMarkdown in [`web/content/`](web/content/) and rendered to static HTML at build\ntime (syntax highlighting via highlight.js, theme-recolored through tokens).\n\n``` sh\nbun run serve        # build + serve the full site -\u003e http://localhost:8123\nbun run site         # build the static site into web/site/\nbun run deploy       # build + publish web/site/ to Cloudflare Pages\n```\n\n## Build \u0026 test\n``` sh\nbun install\nbun run build          # bake assets -\u003e typecheck -\u003e bundle games -\u003e web manifest\nbun run test           # golden + smoke tests\n```\n\n`bun run build` bundles each `framework/games/*.js` (framework inlined, via\n`Bun.build`) to `runtime/src/game/\u003cname\u003e.js`, so the PSP/Web/3DS build steps embed\nthem exactly like the raw demos (e.g. `PSPJS_GAME=rpg.js bun run psp`).\n\n### Golden tests\n`framework/test/golden.ts` renders each framework game's bundle **headlessly under\nBun** (a gfx mock → RGBA framebuffer), runs a deterministic seeded sequence with\nscripted input, and byte-compares the framebuffer to a committed golden\n(`framework/test/goldens/*.png`); it also runs a no-crash **smoke pass** over the\n`raw-*` demos (seeded `Math.random`). A sibling\n[`framework/test/contract.ts`](framework/test/contract.ts) asserts the controller\nbutton bitmask is identical across the Web and 3DS hosts, the framework SDK, and\nevery raw demo's `BTN_*` constants (the raw games are eval'd as a string and so\ncan't `import` the canonical `Btn`, so the test enforces they never drift).\nBecause the same bundle runs on every platform, this catches crashes and visual\nregressions in the shared code. Run with `bun run test`; regenerate goldens with\n`UPDATE=1 bun framework/test/golden.ts`.\n\n## Architecture\n- **Rust host** (`runtime/src/main.rs`) owns the process: it sets up the GU\n  (double-buffered 480x272), the controller, and a QuickJS runtime/context, then\n  registers the native API and runs the per-frame loop (Rust opens the GE display\n  list, calls JS `frame(buttons)`, then finishes/syncs/swaps).\n- **2D graphics + input bridge** (`runtime/src/gfx.rs`, `runtime/src/bridge.rs`)\n  exposes to JS:\n  - `gfx.clear(r, g, b)`\n  - `gfx.fillRect(x, y, w, h, r, g, b)`\n  - `log(msg)`\n  - `frame(buttons)` is defined by the game; `buttons` is the PSP controller bitmask\n    (`UP=0x10`, `RIGHT=0x20`, `DOWN=0x40`, `LEFT=0x80`, `CROSS=0x4000`, `START=0x08`).\n- **QuickJS allocator** (`runtime/src/qjs_alloc.rs`): QuickJS is created with\n  `JS_NewRuntime2` so it allocates through the Rust/PSP allocator (rust-psp's\n  startup sets up no C heap, so newlib `malloc` is unusable).\n- **FFI bindings**: extended in `quickjs-rs/libquickjs-sys/src/lib.rs`.\n- **Web host** (`web/engine.js`): the same `gfx`/`input`/`frame` contract on\n  Canvas2D, with an optional WebGL2 layer for the `g3d` 3D pass.\n\n## Setup (from a fresh clone, macOS)\nInstall [Bun](https://bun.sh) and [Homebrew](https://brew.sh) (and Docker, e.g.\n[OrbStack](https://orbstack.dev), for the 3DS build), then one command sets up\n**everything**:\n\n``` sh\ngit clone https://github.com/doodlewind/dreamcart.git\ncd dreamcart\nbun install\nbun run bootstrap\n```\n\n`bun run bootstrap` ([scripts/bootstrap.ts](scripts/bootstrap.ts)) is idempotent\nand installs/configures:\n- submodules (`bun run setup`)\n- **LLVM** (`brew install llvm` — Apple clang can't target MIPS)\n- **PPSSPP** (`brew install --cask ppsspp`) — PSP emulator\n- **Azahar** (downloaded from GitHub releases) — 3DS emulator\n- **Rust** `nightly-2026-05-28` + `rust-src`, and pins the repo override\n- **cargo-psp / prxgen / pack-pbp / mksfo** (built from the `rust-psp` submodule)\n- the **PSPSDK** from `doodlewind/pspdev`\n  (`sdk-noabicalls-normalized-2026-06-19`, clang-built no-abicalls newlib/glue\n  with normalized archive metadata) into `mipsel-sony-psp/`\n- the **`devkitpro/devkitarm`** Docker image (3DS toolchain)\n\nIt reports anything it can't auto-install (Homebrew/Docker not present, Docker\ndaemon stopped); fix those and re-run. Then everything runs on Bun — there's no\nPython/Make glue.\n\n\u003e The `quickjs-rs` and `rust-psp` submodules point at `doodlewind/*` forks.\n\u003e Those forks carry DreamCart's PSP C/stdio shims, 32-bit `size_t` ABI/API\n\u003e exports, and PSP nightly/tooling compatibility fixes. LLVM `llvm-ar` is used\n\u003e for the static archive (Apple `ar` silently drops MIPS objects →\n\u003e `undefined symbol: JS_*`).\n\n## Run on PSP\nOpen the EBOOT in [PPSSPP](https://www.ppsspp.org/):\n\n``` sh\nopen -a PPSSPPSDL --args runtime/target/mipsel-sony-psp/debug/EBOOT.PBP\n```\n\n## Run on the Web\n``` sh\nbun run serve        # -\u003e http://localhost:8123  (PORT=3000 to change)\n```\n\nOpen \u003chttp://localhost:8123/play/\u003e (or `/play/?game=rpg.js` to pick one). The dev\nserver ([`web/serve.ts`](web/serve.ts)) builds the full site and serves it with\nCloudflare-Pages-style directory routing.\n\n## Run on 3DS\nBuilds a `.3dsx` homebrew app using the `devkitpro/devkitarm` Docker image — no\nhost toolchain install or sudo needed (just Docker, e.g. OrbStack/Docker Desktop):\n\n``` sh\nPSPJS_GAME=raw-tetris.js bun run 3ds   # -\u003e runtime-3ds/dreamcart-3ds.3dsx\n```\n\nRun the `.3dsx` in [Azahar](https://azahar-emu.org/) (the maintained Citra fork)\nor on real hardware. The 3DS host ([`runtime-3ds/source/main.c`](runtime-3ds/source/main.c))\nembeds QuickJS and renders the same games via citro2d (scaled to the 400×240 top\nscreen), with logs on the bottom screen.\n\n## Run on Android (dual-screen)\nA 3DS-style dual-screen handheld app (game on the top screen, native UI on the\nbottom) that runs the **web engine** inside a WebView. See [`android/`](android/):\n\n``` sh\nbun run android          # assemble the debug APK\nbun run android:install  # install + launch on a connected device/emulator\n```\n\n## License\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoodlewind%2Fdreamcart","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoodlewind%2Fdreamcart","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoodlewind%2Fdreamcart/lists"}