A from-scratch Rust/wasm/WebGPU recreation of *Crash Bandicoot 2: Cortex Strikes Back*
(SCUS-94154) that runs directly on the data files from the original disc. Nothing is
emulated: the disc's own geometry, textures, animations, sounds, music and object scripts
are parsed and driven by a new engine.
The game is served at <http://localhost:6051>. The level selector in the top bar switches
levels; this changelog is at <http://localhost:6051/changelog.html>.
.bin (Mode 2 / 2352-byte sector) reader with an ISO9660 walker; extracts all 78 files (38 levels as .NSF/.NSD pairs plus the executable) from the disc image.
their 5-character EIDs), the NSD level directory (chunk index, GOOL table, spawn points),
texture pages, wGeo scenery (vertices, triangles, quads, texinfos, vertex colors,
animated/LOD texture tables) and Zone entries (worlds, collision bounds, camera paths,
object entities with their property tables).
c2-tool CLI (ls, extract, info, zones, tex, summary) — every level onthe disc parses cleanly.
wgpu) that keeps the PlayStation texture pages as raw VRAM in an R8Uint texture and does the 4-bit / 8-bit CLUT and 16-bit lookups in the fragment shader,
reproducing the original semi-transparency modes (50/50, additive, subtractive) and
transparent-black texels. A depth buffer replaces the PlayStation's painter's ordering.
level, and lets you fly through it (WASD, arrows / mouse-drag to look, Shift for speed).
scripts/deploy.sh rebuilds andredeploys in one step.
TGeo model decoder (Crash 2 triangle-strip command stream, colours, texinfos, per-vertex compression descriptors) and SVtx animation decoder (uncompressed and bit-packed frames,
collision boxes). Verified bit-exact against every frame on the disc (67,196 frames).
add &anim=Cr10V (or any animation name) to the URL to see it play at Crash's spawn point.
VAB/SEQ music parser and a software SPU synthesizer (ADSR envelopes, pitch, pan, looping,
sequencer). Every level's music renders offline; wiring into WebAudio is next.
virtual address space for GOOL objects/entries/globals, the object pool and tree, and the
Crash 2 GOOL interpreter (all 79 opcodes decoded from the retail executable's dispatch
table; operand grammar verified against the disassembly).
fruit, enemies, gates, particles and the HUD objects are instantiated from the zone
entity tables and run their retail bytecode (including the embedded native MIPS fragments,
which are executed by a small R3000 interpreter with the retail register conventions).
responds to the d-pad (arrow keys / gamepad) and runs through the level; the camera
follows him. Objects are drawn from the engine's per-frame display list.
the original engine reads them (binary search + metavalue rows), and zone/entity pointers
are patched into memory exactly like the retail loader does.
walks on a flat floor at spawn height), the camera-rail system, sprites/text (HUD, fruit),
sound, and level transitions.
stops, wall plotting/sliding, zone-node searches, rebound vectors. Crash stands on the real
geometry, walks down the steps in Sewer or Later, is stopped by walls and slides along them.
original CamFollow/CamGetProgress algorithm, including neighbour-path transitions and
the draw lists that spawn/despawn entities as the camera progresses. Camera auto-modes 1/3
advance along the rail on their own.
from the GOOL animation tables and drawn either as world billboards or in a 2D overlay pass
that uses the PlayStation's 512×240 screen space.
ramps/glides, sustain counters) and a WebAudio bridge plays the decoded SPU-ADPCM samples
through the software SPU at 44.1 kHz, one 1/30 s block per game frame.
software sequencer, and swapped with a 30-frame cross-fade when the camera enters a zone
with a different track — the same state machine as the original midi.c. Browsers only
allow audio after a key press, so press any key once.
bit per code/event/transition entry point (the warp room and Snow Go crashed without it),
state-change instructions pass stack arguments, CHNK sub-op 3, and the zone header's
flags/water/music fields were read from the wrong offsets.
3C) plays the SCEA / Universal /Naughty Dog intro, then the Crash Bandicoot 2 logo with the "NEW GAME / LOAD GAME" menu in
the original font. Menus and HUD text use the retail text layout (per-character size
escapes, centring), with the Crash 2 screen-space and centre flags.
title menu) the client fetches that level's files and restarts the engine in place, carrying
the GOOL globals (lives, fruit, gems, previous level) across. The warp room chooses the
return pad exactly like the retail loader (level-code table from the executable), and
the player object is placed on the NSD spawn point.
LevelSaveState/LevelRestart are ported, so falling into a pit ordying replays the retail sequence — the engine-driven fade to black (GOOL globals 106/107,
drawn as a subtractive full-screen quad), respawn at the last checkpoint, fade back in.
the 0x142 distance band, look-at blending/limits from 0x119/0x183/0x16E/0x1AA, mode
transitions, the warp-room orbit camera, per-path projection distance 0x130). The
renderer now also applies the PlayStation's 5/8 vertical projection scale, which is what
gives the game its wide look — Crash and the crates now sit in the frame as on the console.
operand translation (stack pops) for the push/pointer opcodes, the fall-death event chain,
path orientation at path ends, objects updating without a player object.
1C, "New Game" from the title) now plays through: Cortex's falland the crystal cave, the "one year later" card, the space station with N. Gin, then the
beach with Coco — and hands control to the player on the beach. The camera-event
system that drives it was ported from the retail executable: GOOL opcode 72 pushes camera
events (link masks, snaps, look-at and offset overrides), path-end event records run
fly / hold-until-snap / skip-chain transitions, and per-path trigger bands and arrival
events reach the player object.
speech segments one after another; the GOOL "speech player" polls the voice until each
segment has played out. Voices now track their sample length (SPU pitch, 28 samples per
ADPCM block), report "done" and are released exactly when the retail audio code would,
so the intro dialogue plays back to back. Voice volume follows the retail rule (HUD and
speech objects play flat, everything else is spatialized), and the audio-control opcode
returns its result in the object's misc register as on the console.
0x185) are evaluated like the retailmatrix setup and exposed to scripts as GOOL global 119 — this is what keeps Crash parked
during the intro until the beach, and what enables Crash's footprints in the snow levels.
points and spawns/despawns the objects named in each point's A/B lists exactly as the
original DrawListsProcess, replacing the old "spawn everything in the zone" behaviour.
c2-tool run gained watch (per-frame player state), hold=/press= input scripts, tracecam and traceobj= for debugging scripts headlessly.
moment) now draw in screen space like the console — Crash 2 selects the projection per
object through a transform-mode register, and mode 6 is the fixed screen-space one.
and polygons beyond the zone's visibility depth (the retail far plane, 800 units short of
the header value) are culled with a short fade to the zone's far colour. The title level
has a visibility depth of 0, which is why its stray backdrop triangle is now gone.
script is gated on GOOL global 17 (game state), which the retail camera update writes each
frame (0x100 in play, 0 during skippable cutscenes) and which the engine never set.
special case), the NSD spawn record's third field is the camera-path start point, and
the player object is created by LevelUpdate when a level has a player GOOL and no
player exists yet — exactly as the console does. Standing on the LOAD/SAVE pad now opens
the memory-card menu (slot icons, percentages, menu sounds), driven by the original
CardC script.
optionally delta-masked 8-bit frames with palettes) and its four GOOL opcodes are
implemented; decoded frames are streamed into the texture pages every frame, which is
how the warp-room portal swirls and memory-card icons animate.
c2-tool gool <nsf> <name> [state] disassembles a GOOL executable (states, event map, decoded operands), and c2-tool run gained spawn=, bot= and regs=.
(all six speech lines in sequence with the hologram video), and standing on a warp pad
loads that level — Snow Go, Turtle Woods, Hang Eight, The Pits and Crash Dash from the
first room. The pads are raised platforms in the alcoves, so jump onto them. The fix
was in the sound driver: the audio-control opcode's "sub-op 13" queues a *next* sound
entry that the voice update chains in when the current sample plays out (the console
keeps it in the voice record), and the speech player polls that queue slot; the port
had been keying the next line on immediately, so the hologram's timeline never advanced.
slot 1" through the retail card-status global, so the menu shows that message and closes
on X instead of loading an empty slot and reloading the warp room. A browser-storage
memory card (real SAVE/LOAD) is on the list.
?level=02&spawn=N forces an NSD spawn index for a fresh start (testing aid). DispC pause-menu object, which freezes every other object by rewriting the display
flags, and the game clock stops until the menu has closed. RESUME / OPTIONS / WARP ROOM
work (WARP ROOM returns you to the warp room in front of the pad you came from). Pausing
also needed the display-flag globals (4 → 9 each frame) and the per-level / boot-time
globals init (4 starting lives, pause permission) ported from the executable.
~x/~y pen offsets, ~l line feed for the big fraction slashes, ~p plurals, ~s cell sizes that persist
until the next glyph), and aligns strings by the object's flags (centred, left or right)
the way the console does — menus and the pause screen are laid out correctly.
unless the console's rule applies (no change requested, demo state, outside the warp room).
(glyph colour x colour fields, exactly the console's glyph emitter), so menu text has its
yellow-orange-red gradient and the selected pause-menu item is highlighted while the others
are dimmed. ~c0/~c1 in strings switch the shading off/on per glyph.
end to end. The console's card driver (MSC 15-*: scan, read directory, prepare slot,
write, erase, load, name copy/compare) is implemented against a card image the browser
keeps in localStorage; the CardC menu script reads and writes the four 0x2A4-byte
slot records exactly as on the console (save name, unique save id, lives, crystals, gems,
the per-level entity flags and the death table). SAVE GAME → pick a slot → ENTER NAME →
DONE writes the card ("PLEASE WAIT... UPDATING MEMORY CARD."), the slot list shows the
save names (the driver rewrites the menu's four label strings in place, as the console
does), and LOAD GAME restores the globals and reloads the warp room. The card image
survives page reloads and level changes; UNFORMAT wipes it.
Crash 1 port — two state slots (checkpoint and bonus-round entry), the spawn table
restored on every respawn (boxes broken since the checkpoint come back, the checkpoint
crate stays open), the box tally reset to the checkpoint's, mode-2 merges that keep boxes
broken inside a bonus round, and the restart runs synchronously inside the player's
update (the player object survives death and enters its spawn-in state itself, so its
helper objects are no longer duplicated). Respawn/death counters live in the GOOL globals
the scripts read (5 and 108), and the respawn broadcast is the retail event 0x1300.
level, saved to the card) feeds globals 145/146; checkpoint crates hand out Aku Aku and
the game eases off after repeated deaths in the same section, as on the console.
game-over sequence.
matrix, whose y row is scaled by 5/8 so quads come out square on the 4:3 display of the
512-wide frame. Menu and HUD text has the boxy proportions of the real font instead of
being 1.6x too tall, and the save-slot icons (the animated 64x64 preview images, streamed
through five video players like the console allocates in the warp room) sit in the 2x2
slot grid.
status flag 0x200000 means right-aligned; the pause menu's level title is no longer cut
off at the left edge.
so quick menu presses never get lost.
model marks two-sided keep both). Besides being correct, this fixes the Cortex hologram in
the warp room: its polygons are all additive/subtractive, and drawing the far side too was
adding the head twice, so it looked opaque and bright instead of a dim see-through
projection.
same card menu as the save wall.
frame is now drawn from an interpolation between the last two steps — object positions,
rotations and scales, the camera (position, angles and projection) and the vertices of
adjacent animation frames are all tweened, so motion is smooth at 60, 120 or any other
refresh rate. Teleports, camera cuts and animation restarts snap instead of sliding.
Animated textures keep the game clock.
text) were built on a camera basis that was rotated 180° and so drew upside down; sprites
now also honour the object's roll and negative-scale mirroring. Object rotation uses the
console's composition (Ry(roll)·Rx(pitch)·Ry(yaw−roll)) instead of a yaw/pitch/roll chain,
which changes how pitched or rolled objects sit. Mirrored (negative-scale) models keep the
right face toward the camera.
nebula backdrop — are now placed in front of the camera at the projection distance as the
console does instead of being drawn edge-on in the world, and the ordering-table depth
bias from the object size register is modelled (a backdrop with a large negative size
sits behind everything). The full boot now plays: SCEA presents (with Crash flying past),
the Universal logo and legal text, the Naughty Dog logo, then the title. The page boots
into the title screen by default.
indicator; fullscreen; debug readout toggle with F1; build stamp), a one-time controls
hint (H shows it again), a loading card, idle-hidden cursor, and save-file export/import:
the virtual memory card can be downloaded as a card image and installed again later or in
another browser.
waterfalls) are rebuilt each game step; the many "extended texture" polygons that only
select a level-of-detail entry stay static.
lit — the console's light-source scenery lighting (camera property 0x185 bit 2, the
firefly's light) is not modelled yet, nor is the per-object colour-matrix lighting.
A systematic check of every level (headless sweep of all 38 levels, 900 frames each, with
an exploring bot) and of the main flows in the browser: boot → title menu → New Game →
intro → warp room, level entry, pause menu and its options, the title's Load Game screen,
death/respawn and game over.
console's rule that a game-over/demo state redirects to that screen no longer re-applies
while already on it, which previously reloaded the screen every frame.
console does; the game-over scene (Crash and the backdrop) is placed relative to them and
now appears instead of sitting off-screen.
with a temporarily changed pose or tint — N. Gin's hit flash) and the camera-space vector
rotation opcode (used to aim particle bursts relative to the camera) are implemented; both
were the last "unhandled" operations in the level sweep.
warp at the end; the camera stays on the wide cutscene shot until Crash has moved far
enough along, matching the level's camera-trigger data.
follow), drop Crash at a world position (tele=), and dumps full model draw items; the level
selector page keeps ?dbg=nobias,nocull render switches for diagnosing depth/culling.
Continued flow checks in the browser and headless runner, concentrating on the warp room.
position and rotation every frame). The hologram projector's beam sweeps the room and its
lens flare (LenFC) flashes the screen white when the beam crosses the camera, exactly as
the retail scripts compute it. The stale-flash write that could stall a scripted fade-out
(the flare zeroes the fade global while the camera faces its light — a console quirk that
can hit if a portal is entered within ~3 s of the hologram vanishing) is guarded: a
requested fade-out always runs to completion.
additive semi-transparency mode for the fade quad (0x80016BF0), and the pad sets it before
fading. The same flag drives the lens-flare flash.
entrance side of the warp room (sector opposite the pads, more than 1800 units from the
centre) opens the memory-card LOAD/SAVE menu (CHeOC state 5 → CardC). There is no
automatic save prompt after a level on the console — Crash reappears at the pad he came
from (spawn selection ported from 0x8001FA00) and the LOAD/SAVE spot is the only entry.
verified end to end including the arrival warp-in animation.
console's floor search is ported (the platform Crash lands on gets status flag 0x4000;
elevators and bonus platforms wait for it — the bonus exit elevator now lifts Crash back
to the level), and the collision-leaf event table is the Crash 2 one (subtypes 13–38 and
node types 5/6 were missing; leaves 29–38 queue a camera link event whose mask was also
computed backwards). Turtle Woods' entry is the hole gate past the bonus-return trigger:
the fall queues the camera link, the bonus path's event record teleports Crash into the
side-scrolling bonus stage (BONUS title, fruit/crate counters), dying there returns him to
the main path, and the exit elevator brings him back onto the platform.
checkpoint id and position that the crate (or bonus trigger) publishes in globals 69 and
102–104; the port had kept an internal id that never changed, so Crash respawned where he
stood when the crate broke.
LOAD GAME slot screen automatically, as the console does; saving from the warp room writes
the browser's memory card and the slot appears on the next boot.
slot; the player pool is a stack on the console and closed players were only released when
they were the last allocated, so slots 3 and 4 fell back to the texture's "ICON 3/4"
placeholders. All four slots show their icon now (SAVE GAME / LOAD GAME screens).
watchobj=/watchregs= append one object's state and registers to every watch line; external GOOL code blocks (e.g. Wil2C) can be disassembled; multi-point tele=;
the client accepts ?tele=FRAME:X:Y:Z for reaching remote spots; the disassembler's JAL
target field was 4 bits short.
(floor, wall band, ceiling, object-below, octree query box) uses the top of the current
frame's bound (642 standing, ~380 crouched, 686 jumping) unless the state carries flag
0x40000000, which selects the fixed 665 of the riding states. The port had 665 everywhere.
0x8002A3B0), so small drops keepCrash grounded and the stacked floor leaves of the collision octree average to the visible
floor — he stood 50 units above the ground on parts of Turtle Woods before.
StopAtCeil is the Crash 2 routine (retail 0x8002B57C + 0x8002BC38): scenery ceilingsonly stop states flagged 0x200000 (crouch/crawl), otherwise only bottom-solid objects
(crates hit from below) do; the zone's top face is the ceiling when nothing continues
above; states flagged 0x40 (the pipe-hanging states 11–15) probe the whole body height
and are pulled up to the mesh above them with event 0x3300. **Hanging from the ceiling
grates in Hangin' Out works** — Crash grabs the mesh and shimmies along it over the
water; this was impossible before.
scene flag 0x200 in a 0x24000000 state): the retail floor search consults ProcessNode
for every leaf of type 2 and up (and subtypes below 48); the port only did for types 3/4,
so Crash could walk on the rivers of Hang Eight / Air Crash instead of drowning.
0x8003C9BC): floor-like leaves (types 1and 5) use the band from the feet, other leaves wall when untyped or subtype ≥ 27, and
state flag 0x10000000 disables leaf walls for that state. The solid subtype-28 band at
the end of Turtle Woods is a wall Crash used to walk straight through.
rail to the follow camera fires once Crash is 8000 units along the camera's lateral axis
(past the stone-lined trench), matching the console's formula bit for bit.
c2-tool floorprobe <nsf> X Z prints the scenery vertices and exact surface heights under a point (to compare with collision leaves); watchobj= shows the object's
frame bound.
DEMO entries (the attract-mode recordings the console plays when the titlescreen is left alone) are decoded and replayed: the recording's saved level state and
Crash's collision bound are restored, the deterministic RNG is seeded from it, the
recorded pad is fed one record per frame with the recorded frame times (quantised to
17/34/51 ticks like a measured frame) and the script frame counter follows the recorded
ticks. The attract loop works: idling on the title for a minute plays a level demo,
any button (or the end of the recording) fades back to the title. c2-tool run … demo
replays a level's recording headlessly and reports deaths — a physics fidelity test with
real console inputs. Five of the seven recordings replay without a single death (boss
level, Snow Go, the space level, the polar-bear level, the sewer); the remaining two
desync at a late pit and at a moving river platform and are being investigated.
0x8001D850..), not Crash 1's: the 16-entryacceleration table from the executable (run speed 0xAAE60 — Crash was 14 % slower before
— with per-entry turn rates), table selection by state flags (crouch/slide states
steer with their own accelerations and keep momentum with no input, hanging and riding
entries), decelerate-instead-of-stop rules, and the pad direction register.
control (retail 0x8001D2C4) — the pad accelerates his velocity vector in camera
space, friction decays it, the speed register follows the real displacement, and the
momentum bit survives while airborne so he slides off ice edges. Snow Go's demo
recording now replays perfectly across the ice.
(a crouching Crash saw the floor as a ceiling and never stood back up after a slide).
recording and comes back through the logo sequence; slide, crouch, crawl and standing
back up; the warp room → pad → Snow Go flow with the new control code.
0x8002F310);the port's title exception kept the screen black after the attract demo.
Five problems reported from a play-through, all fixed:
to the gameplay rail behind Crash and Coco (the view of the original's "Prologue"
screenshot) and Crash can be walked down the jungle path, over the pit, past the
crates and armadillos to the warp. The beach dialogue dolly is an auto-scrolling camera
path whose end has no enabled neighbour but does carry a "hold + snap" link record; when
such a path parks, the port now runs that record (the space scenes of the intro carry a
"hold" record there too, which just parks the camera until the director snaps it, so
they are unchanged). The checkpoint the intro script saves therefore records the
gameplay camera. Auto-scroll cameras also move at the retail speed (one path point per
frame — the port ran them 20 % fast) and stay frozen while a scene director clears
display flag 2, as the console does.
centre through the whole intro: the director freezes HUD animation on its second frame,
before the counters had finished their five-frame start-up wait and hidden themselves.
The console's GOOL clock adds the real duration of the previous frame, so the frame
after a level's first one carries the whole disc load and every wait started on frame 0
is over; the port now does the same. CoreObjectsCreate also creates the fourth HUD
object (the crystal/gem icon) and publishes the HUD objects in globals 6/7/14 like the
retail routine.
object-display flags, so it is drawn on black — stone tablets with Crash's face, the
level name, RESUME / OPTIONS / WARP ROOM, the box and fruit counts with their crystal,
gem and box icons and the percentage. The port ignored both flags and drew the text over
the running level, and the menu's 3D icons (transform modes 13/14, screen-space like
the HUD's mode 6) landed in the world instead of on the screen.
no length byte (FF 51 tt tt tt); read as a standard MIDI meta the title theme's bar-5
tempo change became 6.7 seconds per beat and the music crawled after six seconds, so the
Naughty Dog logo never got its hits. 1385 of the theme's 1427 notes now play in 48 s.
Hrecalls the card): arrows or WASD move, Space (or X) jumps, Shift (or Z) spins, C (or
V) crouches / slides / body-slams, Q/E/1/2 are the shoulder buttons, Enter pauses.
0x8001CBA4); c2-tool run … campts=ZONE:IDX dumps any camera path.
for GOOL event 0x3100, which the console raises from the trench floor (collision
subtype 28) and broadcasts to every object (0x80029CB0 → 0x8001D194, request
type 0). The port treated that broadcast as a no-op, so a player who dropped into a pit
found it empty and, with its 1800-unit walls, inescapable. Floor events 0x3000/0x3100
now reach every object: stepping onto the trench floor releases the rats (blue, pink
tails, dust and sparks), which chase and kill Crash exactly as on the console, and the
checkpoint respawn puts the player back on the path around the pit.
0x4300 (a facebutton on a camera path flagged skippable also queues link mask 0x80).