OpenStrike: A Counter-Strike-Shaped FPS on a 2004 PSP

Today, PocketJS released OpenStrike, a single-player FPS that plays classic GoldSrc-era maps (like de_dust2) at a locked 60 FPS on a 2004 Sony PSP. The project is open source at pocket-stack/open-strike and is the first game built on the Pocket runtime family.

The Machine: 333 MHz, 32 MB RAM, No Shaders

The PSP-1000 has a single MIPS CPU at 333 MHz with no JIT for JavaScript. It has 32 MB of RAM (24 MB available to user programs). The GPU is a fixed-function pipeline with no programmable shaders. There is no OS in the traditional sense—the game is a single executable (EBOOT.PBP) that runs on bare metal.

Architecture: Rust Engine + JavaScript Game Logic

OpenStrike splits into two parts:

  • Rust side (openstrike-core + Pocket3D renderer): handles player movement, bot AI, bullets, and rendering.
  • JavaScript side (rules.ts, hud.tsx): round flow, scoring, weapon tables, and the HUD (a Solid.js component tree styled with Tailwind).

The JavaScript engine is QuickJS (ES2023), compiled to a few hundred kilobytes. The game bundle (openstrike.js) is byte-identical on the PSP and your laptop.

Frame Tick Order (Every 16.7ms)

  1. Read controller input (pad → SimInput)
  2. sim.tick(dt) — movement, bots, bullets (Rust)
  3. strike.__dispatch(state, events) — one call into QuickJS
    • rules.ts reacts and queues commands
    • hud.tsx re-renders only changed bindings
  4. Drain commands into core (setPhase, configureWeapon, etc.)
  5. Draw (PVS → batches → GE)

JavaScript is consulted exactly once per frame and can never stall the simulation.

The Map: BSP + PVS + Baking

GoldSrc maps use BSP trees and Potentially Visible Sets (PVS). The cooker (pocket3d-cook) transforms the map and textures into a .p3d file that is exactly what the GPU needs to read, with zero parsing at load time.

Key baking steps:

  • Lightmaps → vertex colors: polygons are diced into a 32-unit grid, lightmaps sampled into vertex color channels. The GE interpolates colors for free.
  • Positions quantized to i16: half the memory and bandwidth of floats. The GE silently normalizes integer vertices (divides by 32768) in 3D mode—a behavior documented nowhere, confirmed by reading the emulator source.
  • Textures become CLUT8: 8-bit indices into a 256-color palette, 4× smaller than RGBA. Resampled to power-of-two sizes, swizzled, with full mip chains.

The .p3d is linked into the executable's read-only data. Level loads are instant because nothing loads—just one cache-writeback call at boot.

Performance: 6.8–8.4 ms Average Frame Time

On the physical PSP, a scripted tour of de_dust2 shows:

  • JavaScript: ~2.2 ms per frame
  • Simulation + bots + draw recording: the rest
  • Average work: 6.8–8.4 ms (headroom ~8 ms)

Texture Glow-Up: Three Fixes

Midway through porting, textures looked muddy. Three causes:

  1. Non-power-of-two textures were stretched with nearest-neighbor sampling. Fix: bilinear resample through the palette.
  2. Sizing rounded down (320→256), losing detail. Fix: round up (max 512).
  3. GE's auto mip selection blurred floors. Fix: −1.0 LOD bias.

Also tightened the light-bake grid from 96 to 32 world units. Dust2 went from 26k to 58k vertices, map grew 0.9 MB—still within budget.

Why It Matters

OpenStrike proves that web stack ergonomics—TypeScript, Solid.js, Tailwind—can power a real-time 3D game on hardware that predates the iPhone. The architecture (Rust engine + JS game logic, baking, fixed-function rendering) is a blueprint for anyone targeting constrained devices without sacrificing developer experience.

How to Try It

You need a copy of the original game to extract map data (not redistributed). Then run pocket3d-cook to bake the map, and build the EBOOT.PBP. Or just explore the source on GitHub.

Editor's Take

I've been following PocketJS since its early days, and this release is a masterclass in systems thinking. The decision to bake everything at build time—from lightmaps to texture swizzling—is exactly the kind of trade-off that makes constrained hardware sing. I've ported a simple game to a Raspberry Pi Pico before, and the discipline required is humbling. OpenStrike takes that to another level. If you're a web developer curious about game dev or embedded, study this project. It's a rare example of cross-disciplinary engineering that's fully documented.