npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rbayuokt/react-native-psp

v0.1.2

Published

React Native for the Sony PSP. Real React and react-reconciler on a custom renderer, with Yoga layout and QuickJS.

Readme

react-native-psp

react-native-psp

Documentation · Install · Your first app

npm create @rbayuokt/react-native-psp@latest my-psp-app

Watch it run on a real PSP-1000: deploying over USB, walking the showcase through layout, text, images, animation, lists, controls etc, then editing a screen and watching it live reload.

An experimental React Native target for the Sony PSP.

It uses real React and a real react-reconciler host config, with a custom PSP renderer, layout engine, controller focus model, and JavaScript runtime sized for 32 MB of RAM and a 333 MHz MIPS CPU.

It builds one EBOOT.PBP for the whole PSP family. Development and testing have been on a PSP-1000, which is the model to target: it has half the RAM of every later one, so what fits there fits everywhere. The 2000, 3000, Go and Street run the same homebrew but have not been tried. The Street has no Wi-Fi at all, so fetch and the log transport cannot work on it.

This is not a React-inspired UI library. React itself runs on the device.

Why this exists

My younger brother bought a PSP for Rp 250,000, about 14 US dollars. It sat around running games from 2004, and I wanted to do something else with it.

Specifically I wanted to write apps for it in React Native. Not a React-looking API on top of C, the actual thing, so the UI work I already know how to do would still apply.

So I read how you draw a UI on a PSP. It is all C. You talk to the graphics unit by building a display list of vertices and flushing it once a frame. The GU samples that ship with the SDK are the real documentation.

That is what made the rest possible. Yoga is C++, compiles for MIPS, and is the layout engine React Native already uses, so flexbox comes along with it. React and react-reconciler are plain JavaScript and run on anything that fits in memory, which QuickJS-ng does. So the whole thing became React on the device, a host config holding the node tree in C, Yoga for the rectangles, and the GU to paint them.

How it draws, since you were going to ask

Nothing on screen is a widget.

flowchart TD
  subgraph js["JavaScript, runs when state changes"]
    A["View, Text, Pressable"] --> B["react-reconciler"]
  end
  subgraph native["C, runs every frame"]
    C["flat array of nodes"] --> D["Yoga resolves every rectangle"]
    D --> E["paint walk"]
    E --> F["GU display list"]
  end
  B -->|"int16 handles"| C
  F --> G["flush, swap buffers, 480x272"]

<View> and <Text> are host component names, and react-reconciler turns each one into an int16 handle into a flat array of nodes that lives in C. JavaScript holds the handles and never mirrors the tree.

Once a frame, Yoga walks that array and resolves every rectangle, then a paint pass walks it again and pushes two vertices per rectangle into the display list. Two, because an unrotated rectangle is a sprite to the GU. Rotate it and it becomes triangles. Text is one draw call per string out of a bitmap atlas baked at build time, tinted by the vertex colour so a colour change costs no extra texture. Then the list is flushed, the buffers swap, and that is the frame.

Live reload, so this is bearable to work on

Rebuilding the binary and copying it to a memory stick for every change would have killed the project early. So the running app watches for a new bundle and reloads itself instead.

flowchart TD
  A["you save a file"] --> B["esbuild rebuilds the bundle"]
  B --> C["the PSP sees the same folder over USB"]
  C --> D["app checks the file every 30 frames"]
  D --> E["tear down the JS runtime, boot a fresh one"]
  E --> F["evaluate, with a progress bar on screen"]
  F --> G["new UI, about 2.5 seconds after you saved"]

Three terminals started once, and after that you just save. The progress bar matters more than it sounds, because a PSP sitting frozen for two seconds looks exactly like a PSP that has crashed. It is driven from QuickJS's interrupt handler, so it animates during the evaluation rather than freezing at whatever the last step set.

This is live reload and not Fast Refresh. The whole runtime is replaced, so hook state resets on every update. Details and the plan for real Fast Refresh are in docs/fast-refresh.md.

What was hard

I thought memory would be the problem. It is not. React, react-reconciler and QuickJS come to about half a megabyte, and the PSP gives you 21.75 MB.

The limit is how much of the tree React can rebuild in 16.6 ms. A 78 node screen takes 73 ms of JavaScript to build, and only 2.6 ms of that is the C side. So most of the work here went into not rebuilding things. Screens stay mounted and navigating flips a style prop. Animation runs in C, so a spinning logo costs zero renders. Anything that changes every frame lives outside React.

What cost me the most time was trusting the emulator. PPSSPP gets memory exactly right and runs between 1.6x and 3.2x too fast, which you can adjust for. The problem is the bugs it does not reproduce. Yoga uses a quiet NaN for "undefined" and does arithmetic on it. The PSP's FPU raises an exception on NaN where a normal FPU carries it through. Ran fine in PPSSPP, died on the first frame on hardware. Three of the four worst bugs in this project only happened on the device.

The rest is what the PSP does not have. No touchscreen, so the D-pad moves a focus cursor, and typing means handing over to the system keyboard. One font at one size in a baked atlas, so no font sizes. No JIT. 1024 nodes, fixed. Full list in docs/limitations.md.

Status

The MVP vertical slice is verified on real PSP-1000 hardware: the app boots, renders, lays out with flexbox, takes D-pad focus, and fires onPress from the Cross button back into React state.

Columns are where each thing has actually been run. PSP-1000 is the model on the desk; the rest of the family should behave the same but is untested.

| | PPSSPP | PSP-1000 | | --- | --- | --- | | Boot, GU renderer | Verified | Verified | | View, Text, Pressable | Verified | Verified | | Flexbox layout (Yoga) | Verified | Verified | | D-pad focus, focus ring | Verified | Verified | | Cross to onPress to setState | Verified | Verified | | React 19 + react-reconciler on device | Verified | Verified | | Live reload on save | Verified | Verified | | Image, baked art with tinting | Verified | Verified | | Animation, shared values driven in C | Verified | Verified | | ScrollView, focus-driven | Verified | Verified | | Stack navigation | Verified | Verified | | Text wrapping, numberOfLines | Verified | Verified | | borderRadius, borderWidth | Verified | Verified | | FlatList, windowed | Verified | Verified | | Modal with focus trap | Verified | Verified | | TextInput on the system keyboard | Verified | Verified | | AsyncStorage, battery | Verified | Verified | | SectionList, onLongPress, onArrow | Verified | Verified | | Focus restored on the screen you go back to | Verified | Verified | | Button, Switch, Slider, ActivityIndicator | Verified | Verified | | focusStyle, focus colours swapped in C | Verified | Verified | | Live reload back onto the screen you were editing | Verified | Verified | | Alert on the system dialog | Verified | Verified | | textAlign | Verified | Verified | | Sound effects | Verified | Verified | | fetch over Wi-Fi | Verified | Verified | | Wi-Fi handshake for the log transport | Verified | Needs testing | | Fast Refresh (state preserving) | Planned | Planned |

Timings in this README come from PPSSPP, which is optimistic by 1.6x to 3.2x. The behaviour above is confirmed on a PSP-1000; the numbers are not.

Example

import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from '@rbayuokt/react-native-psp';

const s = StyleSheet.create({
  screen: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 10 },
  button: { paddingHorizontal: 10, paddingVertical: 5, backgroundColor: '#282c34' },
  label: { color: '#61dafb' },
});

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <View style={s.screen}>
      <Text style={s.label}>Hello PSP</Text>
      <Pressable style={s.button} onPress={() => setCount((n) => n + 1)}>
        <Text style={s.label}>{`count ${count}`}</Text>
      </Pressable>
    </View>
  );
}

The demo that ships with the repo is examples/showcase: a welcome screen with the runtime facts, then a menu of eight topics covering layout, text, images, animation, lists, controls, device APIs and the export list. There is also a playground screen, which is an empty canvas you edit while the app is running to watch live reload work. Run it with npm run build and open EBOOT.PBP.

Measurements

Taken on a physical PSP-1000 unless noted. PPSSPP matches hardware exactly on memory and is optimistic on timing by 1.6x to 3.2x, so never read speed off the emulator.

| | Value | Where | | --- | --- | --- | | Usable heap | 22,806,528 bytes (21.75 MB) | PSP-1000 | | QuickJS runtime + context | 54,960 bytes | PSP-1000 | | React + react-reconciler heap | 446,928 bytes | PPSSPP (memory matches hardware) | | QuickJS engine code | 627,097 bytes text | MIPS build | | Yoga | 84,962 bytes text | MIPS build | | Bundle (React + reconciler + host config) | 180,371 bytes raw, 54,958 gzip | esbuild | | Precompiled bytecode | 801,107 bytes | qjsc-host | | EBOOT.PBP | 1,721,026 bytes | build output | | Idle frame, 48 nodes | 59 fps, 23 us JS | PPSSPP | | Loading precompiled bytecode | 139,255 us | PPSSPP | | Worst frame during a navigate | 33,366 us | PPSSPP |

The idle figure matters more than it looks. An earlier demo that called setState every single frame cost 12.6 ms of JS per frame. A normal event-driven UI does no work between inputs. React's cost here is proportional to how often state actually changes.

One React commit costs about 10 ms on this hardware whatever it touches, so anything that changes on every button press has to stay out of React. That is why the focused look is focusStyle, a set of colours the C side swaps during paint: moving the cursor down a list renders nothing at all. Same reason animation runs in C.

Tree size, not memory, is the practical constraint. Screens are kept mounted so navigating rebuilds nothing, which means the node count only grows. The showcase boots at 48 nodes with one screen built, and reaches 477 of the 1024 available once all twelve have been opened. See docs/navigation.md.

Requirements

  • macOS (the font atlas generator uses CoreText)
  • PSPDEV toolchain, plus brew install libmpc
  • Node.js 22+
  • PPSSPP for iteration
  • A PSP with custom firmware for hardware validation. Any model runs the binary; a PSP-1000 is the one to test against, since it has the least RAM

See docs/getting-started.md for exact setup steps.

Development

Use live reload. Save a file and the running PSP updates itself, with a progress bar while the bundle re-evaluates. No rebuild command, no redeploy, no button press on the device.

Three terminals, started once:

npm run usb        # 1: serves this directory to the PSP over USB
npm run shell      # 2: pspsh, then run  ./rnpsp.prx  once
npm run watch      # 3: rebuilds the bundle on every save

Then just edit. Terminal 3 reports each rebuild, terminal 2 shows [Dev] bundle changed, reloading, and the PSP is updated about 2.5 seconds after you hit save.

It works in PPSSPP too, which maps host0: to the EBOOT's directory.

This is live reload, not Fast Refresh: useState resets on each update. See docs/fast-refresh.md for the difference and for what Fast Refresh would add.

Build

npm install
npm run build          # bundles TS, then builds EBOOT.PBP and rnpsp.prx
npm test               # 377 host assertions + 24 navigator, host-side
npm run typecheck

Run EBOOT.PBP in PPSSPP, or copy it to PSP/GAME/rnpsp/ on a Memory Stick. Manual rebuild and redeploy is the fallback for when USB is unavailable; prefer live reload above.

Architecture

App.tsx
  -> esbuild (es2015, production)
  -> QuickJS on MIPS
  -> React 19.2.8 + react-reconciler 0.33.0
  -> react-native-psp host config
  -> C node tree (JS holds int16 handles, the tree lives in C)
  -> Yoga layout
  -> sceGu renderer
  -> 480x272

Controller input runs the other way: sceCtrl to a focus manager to an event dispatched into React, which sets state and re-renders.

More detail in docs/architecture.md.

Documentation

Everything below is on the docs site too, with search.

Installing

npm install @rbayuokt/react-native-psp [email protected]

The scope is not a flourish. npm rejected the plain name for being too close to react-native-fs, so it lives under @rbayuokt. Everything else, the repo, the docs and the binary, is still react-native-psp.

Full steps in docs/installation.md.

Licence

MIT, see LICENSE. The vendored engines are MIT too; see NOTICE for what is bundled and where it came from.

Credits

Built on QuickJS-ng, PSPDEV, and React.


Created by @rbayuokt, made with ❤️ and 🎵