← back to the game

Crash Bandicoot 2 — Web Port Changelog

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>.

Milestone 1 — Disc extraction and world rendering

(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).

the disc parses cleanly.

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).

redeploys in one step.

Milestone 2 — Models, animations, audio decoding

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).

Milestone 3 — The game runs

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.

Milestone 4 — Collision, camera rails, sprites, sound and music

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.

Milestone 5 — Title screen, death/respawn, level flow, retail camera

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.

dying 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.

Milestone 6 — Intro cutscene, speech, camera events and draw lists

and 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.

matrix 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.

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.

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.

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.

~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.

Milestone 7 — Saving, checkpoints and the difficulty table

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.

Milestone 8 — Smooth rendering, orientation fixes, boot sequence, page polish

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.

Milestone 9 — Flow verification pass

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.

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.

Milestone 10 — Warp room: lens flare, portal fade, LOAD/SAVE spot

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).

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.

Milestone 11 — Body-height collision: hanging, crawling, walking on water

(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.

Crash 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.

only 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.

and 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.

heights under a point (to compare with collision leaves); watchobj= shows the object's

frame bound.

Milestone 12 — Retail player control, ice physics and the attract-mode demos

screen 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.

acceleration 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.

the port's title exception kept the screen black after the attract demo.

Milestone 13 — Playable intro, hidden cutscene HUD, retail pause screen, logo music

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.

recalls 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.

c2-tool run … campts=ZONE:IDX dumps any camera path.

Milestone 13a — Rats in the pits

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.

button on a camera path flagged skippable also queues link mask 0x80).