How PocketVibe works
Short version: PocketVibe sets up a tiny website and a browser inside your handheld. The menus and the games are all web pages, running on the device itself, with no internet needed.
Why a browser engine?
three.js draws 3D with WebGL, and WebGL only exists in browsers. So the handheld needs a browser.
Chrome and Firefox are too heavy for it, and they want a desktop around them. We use WPE WebKit: Safari's engine, in the version made for small devices like TV boxes and car screens. No address bar, no window; it draws straight to the screen. Cog is the smallest browser built on it: one page, full screen.
A frame of a game reaches the screen through these layers:
- The gamethree.js, in JavaScriptWebGL
- WPE WebKitinside Cog, full screenOpenGL ES
- Mesa Panfrostthe open-source GPU driver
- Mali-G31 GPUand the screen
Getting the engine onto the handheld
ROCKNIX is very lean: it has no WPE, and no package manager to install one. So we packed WPE, Cog and Mesa into a small Debian file system.
- It is a 147 MB download. The "downloading the game engine" screen on the first start unpacks it on the SD card (690 MB).
- PocketVibe runs Cog "inside" that Debian, with Linux mount namespaces and
pivot_root. Think of a lightweight container. - A
chrootwas not an option: WebKit runs every page in a sandbox (bubblewrap), and bubblewrap refuses to run in a chroot.
What runs on the handheld
PocketVibe.sh
What the Ports menu starts. Sets things up, starts the service, opens the browser, and reopens it if it crashes.
pocketvibed
A small web server in Python, the handheld's backend. Downloads games, keeps settings and saves, installs updates.
Cog
The browser. It opens the launcher, and the games from there.
- The launcher is the menus you see: a plain web page. The handheld on our home page runs exactly this.
- Each game is a folder of web files built with Vite.
Why does each game get its own port? Browsers keep saves per address. A port per game means its own storage, so its own saves, and no game can read another's.
What happens when you press A
- The launcher asks the service to start the game.
- The service starts a small server for that game on its own port, and says where it is.
- The browser opens the game's shell there.
- The shell shows the cover and the name, and loads the game sized for the screen.
- The game sets up WebGL, reads the buttons and starts its loop. Once the first frames are drawn, the cover fades away.
Going back: hold Start + Select. The service reads those buttons straight from Linux, not through the browser, so you can leave even if a game freezes. Hold them for three seconds to quit.
Buttons and sound: games read the controls through the browser's Gamepad API. WebKit only allows sound after a real key press, and gamepad buttons do not count. So the service taps a virtual F13 key that nothing uses. A bit of a hack, but it works.
On Android
The launcher, the shell and the games are the same files. Only the layers under them change:
- Browser engine: Android's WebView (Chrome's engine) instead of WPE.
- Service: the same job in Kotlin, a small web server inside the app.
- Buttons: Android's key events become the keyboard keys the pages read.
That is why the same game can run smoother on an Android handheld: Chrome's graphics path is very mature, and the maker's own driver is faster than Mesa on a Mali-G31.
The store, and this website
- The store runs on Cloudflare: a Worker, a D1 database for games and versions, and R2 for the zips and covers.
npx pocketvibe publishbuilds and uploads a game; once reviewed, it is in the store. - The website runs the same launcher inside a drawn handheld. A service worker in your browser stands in for the handheld's service, so the demo behaves exactly like the real thing.
The hard part: speed
The setup itself is simple. The hard part is 60 fps on a small GPU through a browser engine. So we measured what everything costs on the handheld: triangles, draw calls, shadows, and more. Those numbers became the rules the AI follows when it writes a game. That is the difference between 3 to 6 fps for typical three.js code and 60 fps for games that follow the rules.