Story-Time
There’s probably something fundamentally wrong with me, because at some point while lying on my couch, I caught myself wondering why nobody had ever ported Counter-Strike to the PlayStation 2. Not a stripped-down experiment, not a Half-Life mod with some Counter-Strike maps awkwardly shoved into it, but an actual proper GoldSrc-era Counter-Strike experience running natively on Sony’s aggressively weird early-2000s hardware.
And instead of dismissing that thought like a healthy person would, I decided to do something about it.
I grew up with Counter-Strike. Not modern Counter-Strike. I’m talking about the late-2000s “whatever cracked Russian torrent build somehow worked on the family PC” era of CS 1.5 and 1.6 (WON-Era GUI). The kind where every install came bundled with mysterious DLLs, half the menus were untranslated Cyrillic, and every server either blasted distorted Linkin Park or redirected you to a zombie mod server against your will.

I spent an embarrassing amount of my childhood inside those games. I still vividly remember sitting in the backseat of a ‘94 Opel Astra during a roadtrip across Italy, building maps in Hammer on a tiny laptop when I was maybe eight years old. So, maybe, in hindsight.. this project was probably inevitable.
Now yes, before anybody points it out: Half-Life already exists on PS2. I know. I beat it on PS2 years ago. I also know somebody made a Counter-Strike mod for that version which ports some maps over. But honestly, it never felt like Counter-Strike to me. No proper economy system, incorrect weapon models, just no authentic GoldSrc gameplay loop; it feels more like somebody exported a few maps and called it a day. Technically impressive? Absolutely. But not the thing I had in mind.
To this day I still own a physical copy of Xbox Counter-Strike and have never actually played it. From what I understand, it’s also much closer to Condition Zero internally than classic CS 1.6 anyway, which makes sense considering the timeline. So my only real memory of Counter-Strike on a console remains CS:GO on the PS3.
But that stuck with me, because despite all its flaws, there was something magical about Counter-Strike existing on hardware it absolutely had no business running on.
So the goal became simple in theory and catastrophically unreasonable in practice:
Build a fully fledged Counter-Strike experience for the PlayStation 2.
Naturally, I also decided to make things significantly harder for myself.
I set a few rules before starting. The biggest one was no generative AI for code. Using it for documentation is fine. Research is fine. Using it to read decade-old PS2DEV forum posts written by sleep-deprived reverse engineers is fine. But the actual implementation work has to be handwritten. If the engine crashes, I want to know why it crashes, not stare at autogenerated code I don’t understand.
The second rule was even dumber:
No prebuilt PS2 engine.
There are existing PS2 engines out there. One of the more modern ones is AthenaEnv, which is honestly impressive and allows you to build PS2 applications using JavaScript. I even use it for a separate Cuphead PS2 port project, and for 2D work it’s genuinely solid.
But every time I attempted serious 3D rendering with it, things exploded. I even rebuilt parts of the stack from source because I was convinced I had somehow broken my toolchain. Nope.
I also experimented with another engine whose name I unfortunately forgot, but its model importer mangled geometry so badly that every imported mesh looked like it had survived atmospheric reentry.
So eventually I arrived at the completely rational conclusion:
“I should build my own PlayStation 2 engine from scratch.”
At some point your brain becomes so thoroughly cooked from low-level programming that “port a 27-year-old PC game onto a 20-year-old console while simultaneously writing a custom engine” starts sounding less like a terrible life decision and more like a fun side project.
And honestly, once you commit to that level of nonsense, there’s no turning back.
Implementation
To create Dust2, one (me) must first invent the universe.
Three days and an unhealthy amount of C code later, I had my first major breakthrough:

Not a textured cube. Just a cube.
But importantly: a cube rendered directly through the PS2 graphics pipeline using my own code.

Which admittedly is still very far away from rendering de_dust2, but every engine starts somewhere.
The PS2 rendering pipeline is bizarre compared to modern hardware. There’s no unified GPU pipeline like you’d expect today. Instead, the Emotion Engine (EE), Vector Units (VU0/VU1), DMA controller, GIF, and Graphics Synthesizer (GS) all work together in what can best be described as “a distributed rendering conspiracy”.
My first renderer was extremely primitive. Vertices lived in main memory, transformations happened directly on the EE in software, and GIF packets were manually constructed and pushed to the GS through DMA chains. No scene graph and definitely no abstraction layer. Just raw packet construction and enough matrix math to get triangles onto the screen.
The first major milestone was simply understanding how to talk to the GS without immediately hard-locking the console.
Unlike modern APIs where you call something civilized like DrawIndexed(), the PS2 expects you to build command packets manually and stream them into graphics memory using DMA. You are effectively programming a graphics coprocessor by yelling binary instructions at it fast enough.
Once I had basic geometry rendering working reliably, the next issue was content.
Counter-Strike maps are stored as BSP files using Valve’s GoldSrc format, alongside WAD archives for textures. Existing BSP conversion tools either crashed, exported invalid geometry, broke UV coordinates, or refused to handle certain brush layouts correctly. So naturally I started writing my own BSP/WAD pipeline. Because, yes, building a renderer wasn’t enough suffering already.
I ended up building a custom exporter that parses GoldSrc BSP structures directly, reconstructs polygon faces from brush data AND extracts texture references AND converts indexed WAD textures into TGA assets AND FINALLY exports the final scene into OBJ/MTL format.
Ironically, the broken open-source tools were still extremely useful because reading their source code helped me understand how Valve’s BSP traversal and lump structures actually worked internally.
GoldSrc BSPs are fascinatingly cursed formats. Faces reference texture info structures which reference mip textures which reference palette indices stored elsewhere in the WAD archive. Nothing is where you initially expect it to be. Every structure feels like it was designed by someone actively trying to punish future reverse engineers. Once exports worked, the next step was obvious: build an OBJ renderer.
Naturally, the first thing I rendered wasn’t a Counter-Strike map. It was the Utah teapot, because computer graphics law requires it.
And somehow… it worked.

At this point I had a renderer pipeline functioning end-to-end, but there was still one small issue: You couldn’t actually do anything.
So the next task became controller support. I wired up libpad through PS2SDK, then initialized the pad subsystem over the IOP, and built a simple input abstraction layer for analog movement and camera rotation.
Which sounds straightforward until you realize the PS2 effectively has two systems running simultaneously.
The EE handles game execution while the IOP (I/O Processor), a separate MIPS R3000-based subsystem inherited from the PS1 architecture, manages hardware communication through IRX modules. So even controller input involves RPC communication between processors. The PS2 truly is less a console and more a small network of angry processors forced to cooperate under threat.
Once controls worked, texture support became the next bottleneck.
Initially, everything rendered completely untextured (just colored), which made the engine feel less like Counter-Strike and more like abstract geometry floating through the void. Which is exactly what it was. To fix this, I implemented a multi-mesh renderer with material parsing through OBJ/MTL files, texture uploads into GS VRAM, and support for TGA decoding.
Texture handling on the PS2 is an entire nightmare category on its own because the Graphics Synthesizer only has 4MB of embedded VRAM total. That means texture management quickly turns into a constant exercise in not immediately running out of memory. Modern GPUs let you waste VRAM like an irresponsible billionaire while the PS2 demands financial discipline But honestly, in my head, humanity got to the moon with roughly 72KB of memory on the Apollo Guidance Computer, so surely we can figure out how to render Dust2 on a PS2. Also, and this is the most important argument of all: Rockstar somehow made GTA run on this thing. Counter-Strike will be fine.
Every texture upload has to be carefully packed into GS memory pages with explicit control over pixel storage formats, texture buffers, CLUT layouts, and transfer alignment. Even small mistakes produce spectacular corruption.
The issue is that my current clipping pipeline runs directly on the Emotion Engine in plain C. That means vertex transforms, clipping checks, projection calculations, and visibility handling all happen on the main CPU. Which, by the way, is absolutely not how commercial PS2 games handled rendering. The correct approach is VU1 microcode.
And this is where the PS2 architecture graduates from “quirky” into “absolute black magic”.
Sitting between the Emotion Engine and the Graphics Synthesizer is VU1, a dedicated 128-bit SIMD vector coprocessor specifically designed for geometry processing. Commercial PS2 games offloaded transforms, lighting, clipping, animation skinning, and packet generation onto VU1 because it can process vector workloads massively faster than the EE alone. Right now my renderer is basically brute-forcing geometry on the CPU like an idiot.
The proper solution (and the thing I’m currently building) is a custom VU1 microprogram written in assembly.
The idea is roughly this: Vertex data gets streamed from EE memory through DMA -> VU1 performs matrix transformations in parallel -> Perspective division happens on-vector -> Near-plane clipping executes directly in microcode -> GIF packets are emitted immediately afterward -> The output streams directly into the GS pipeline
All of this happens in a deeply pipelined fashion designed specifically around the PS2’s bizarre hardware architecture; which sounds absurdly overengineered until you realize this is literally how actual PS2 games rendered graphics.
The GS itself is also hilariously unconventional. It has enormous raw fillrate for its time but almost no modern rendering features. There’s no programmable pixel shading, no conventional Z-buffer behavior, extremely limited texture memory, and almost everything relies on careful fixed-function tricks The console achieves performance less through elegance and more through brute-force parallelism and developers performing ritual sacrifices to DMA alignment.
So somewhere between importing Dust2 and accidentally learning vector assembly for a 25-year-old console, this project stopped being “what if Counter-Strike existed on PS2?” and turned into a full-scale archaeology expedition into one of the strangest hardware architectures ever built.
And honestly?
I’m having an absurd amount of fun with it.
(to be continued)

Leave a comment