PS2 Web Recomp

Toegepaste Voorbeelden

Dit project is een zustersysteem van psp-web-recomp en volgt dezelfde methode.

Gran Turismo 4

De eerste titel die op deze manier werd opgestart is Gran Turismo 4, gekozen omdat dit een van de meest veeleisende games op het systeem is. Het spel maakt intensief gebruik van beide vector units, vereist exact floating-point gedrag voor replays en licentietesten, en gebruikt grafische trucs waarbij gerenderde beelden als paletten worden teruggelezen.

Het spel draait volledig, van het opstarten en de openingsfilm tot de menu's en races, inclusief muziek en geluidseffecten. Voortgang wordt opgeslagen op een geheugenkaart die in de browser wordt bewaard. Op een laptop met een geïntegreerde GPU in Chrome draait een race op volle snelheid (50 frames per seconde voor deze PAL-release) op een tweevoudige resolutie ten opzichte van de PS2.

God of War II

God of War II werd via dezelfde scripts geïmplementeerd. Er was geen gamespecifieke code nodig, maar het proces bracht twee bugs aan het licht die Gran Turismo 4 niet had getriggerd: één in de recompiler en één in de timing van de vector unit, naast enkele ontbrekende onderdelen van de I/O-processor van de PS2.

Het spel speelt nu van de menu's via de tussenfilmpjes tot aan de gevechten. Bij de native PS2-resolutie in Chrome haalt het spel 60 frames per seconde, zelfs tijdens zware gevechten. Bij een tweevoudige resolutie varieert dit tussen de 48 en 60 fps, beperkt door de geïntegreerde GPU van de laptop.

Let op: Er worden geen gamegegevens meegeleverd. De gebruiker dient zelf een disc-image van een eigen spel te gebruiken, waarna de scripts in deze repository dit omzetten naar een webpagina op de eigen machine.

Hoe het werkt

Recompilatie

eerecomp leest het uitvoerbare bestand van het spel, volgt de control flow om functies te vinden en genereert C++ code in eenheden van 16 KiB guest code. De output behoudt de eigenschappen van de R5900:

  • 128-bit registers en de bijbehorende multimedia-instructies.
  • Branch delay slots.
  • De floating-point unit van de PS2 (die geen infinities of denormals kent en anders afrondt dan IEEE).
  • Vector unit 0 die als coprocessor fungeert.

Sprongen tussen eenheden worden tail calls. Een thread-switch ontwindt de host stack en hervat de uitvoering bij het guest-adres, waardoor de gegenereerde code geen aparte stack per guest-thread nodig heeft.

Een compacte PS2-kernel

De EE-zijde van het besturingssysteem wordt afgehandeld via high-level emulatie: threads, semaforen, interrupt handlers voor de DMA-controller en de vertical blank, alarms, en het SIF-kanaal dat de hoofd-CPU verbindt met de I/O-processor.

De I/O-processor draait de eigen IRX-modules van het spel (geluidsdriver, disc streaming code, controller- en geheugenkaartdrivers) via een interpreter van de R3000 CPU. De IOP-kernel en de libraries die deze modules aanroepen zijn echter reimplementaties in C++. De disc wordt gelezen via HTTP Range requests, zodat alleen het uitvoerbare bestand vooraf wordt gedownload.

Vector Units

Het grootste deel van het 3D-werk van een PS2-game gebeurt in microprogramma's die het spel uploadt naar vector unit 1. Deze draaien met bit-exacte PS2 float-berekeningen en gemodelleerde pipeline-vertragingen van de MAC, status en clip flags, omdat games hun code hierop inplannen.

Microprogramma's die tijdens een testrun worden waargenomen, worden vooraf gecompileerd naar SIMD C++. Programma's die de pagina voor het eerst tegenkomt, worden ter plekke vertaald naar een WebAssembly-module (dit duurt enkele milliseconden per programma).

Grafische weergave

De 4 MB videogeheugen van de Graphics Synthesizer (GS) bevindt zich op de GPU in native swizzled layouts. Het tekenen gebeurt via WebGPU compute shaders: driehoeken worden in tiles verdeeld en gerasteriseerd volgens de eigen regels van de GS voor texturing, paletten, alpha blending, depth en destination alpha tests.

Omdat alles in het GS-geheugen blijft, werken effecten die renderen naar een textuur of een framebuffer als palet herinterpreteren precies zoals op de console. De interne resolutie kan één, twee of drie keer de native resolutie zijn. Ter referentie is er een software-rasterizer beschikbaar om de GPU-output te controleren.

Threads

De emulatie draait in een worker. Vector unit 1 heeft een eigen worker (net zoals deze parallel draait op de console). De GS-commando's gaan via een ring buffer naar de paginathread, die de WebGPU-device beheert.

Geluid en video

De SPU2 is reimplementiert met 48 ADPCM-stemmen, ADSR-enveloppen en de streaming DMA voor muziek. De output gaat via shared memory naar een AudioWorklet. Video's worden gedecodeerd door een implementatie van de IPU, de MPEG-2-decoder van de PS2.

Optimalisaties en lessen

De eerste race draaide op 3 frames per seconde (fps) met een software-rasterizer en een interpreter voor vector unit 1. Door de GS over te zetten naar WebGPU compute shaders en de vector unit-programma's te compileren, steeg dit naar ongeveer 70 fps. De resterende optimalisaties richtten zich op de browser-overhead:

  1. Compute Dispatches: De eerste GPU-renderer verstuurde circa 650 compute dispatches per frame. Door texture-uploads en palette-loads te verschuiven naar voren (hoisting), werd dit teruggebracht naar ongeveer 80.
  2. Texture Caching: Gran Turismo 4 uploadt elke frame 3 MB aan textures. Door een cache van 16 MB op de GPU aan te leggen voor recent geüploade inhoud, werd de hoeveelheid data per frame met ongeveer 60% verminderd.
  3. Shader Divergentie: Bij een drievoudige resolutie zakte de framerate naar 23 fps. Profiling toonde aan dat de per-triangle setup divergent was en dat grote uploads de paginathread blokkeerden. Door de setup uniform te laden en data in één stuk te schrijven, steeg de snelheid naar 48-50 fps op dezelfde geïntegreerde GPU.
  4. Browser Scheduling: Omdat geneste setTimeout-aanroepen beperkt zijn tot 4 ms, is de emulatie-loop nu aangepast om via een MessageChannel af te geven. Door de emulatie in een worker te plaatsen, werd de framerate gestabiliseerd op een constante 50 fps.

Technische uitdagingen bij God of War II

God of War II kon via port.sh met alleen generieke wijzigingen worden omgezet, maar legde diverse bugs bloot:

  • Geluid: Het spel was aanvankelijk stil omdat de geluidsdriver afhankelijk is van een hardware-timer van de I/O-processor die nog niet was geïmplementeerd.
  • Crashes en Rendering: In het begin verschenen er enorme platte polygonen. Het spel crashte wanneer de render-lijst het einde van het geheugen bereikte en de vertical blank-handlers overschreef.
  • Recompiler Bug: Er werd een fout ontdekt waarbij een branch als laatste instructie van een 16 KiB-unit stond. Het pad dat de branch niet nam, vervolgde de uitvoering in de volgende unit bij de delay slot in plaats van daarna, waardoor instructies dubbel werden uitgevoerd of onterecht werden overgeslagen.
  • Vector Unit Timing: De enorme polygonen werden veroorzaakt door driehoeken die geculd hadden moeten worden. De microcode van God of War II leest de clip-flags drie instructies na de CLIP-instructie; het model maakte deze pas na vier instructies zichtbaar. Gran Turismo 4 merkte dit niet omdat die microcode langer wacht.

Daarnaast waren een boot-pad (vergelijkbaar met de BIOS), modules die vanuit het geheugen in plaats van de disc worden geladen, en de destination chain mode van de scratchpad DMA nodig.

Zelf een game porten

Vereisten

  • git, CMake, Ninja
  • C++20 compiler (Clang heeft de voorkeur)
  • Python 3
  • 7-Zip (voor .7z disc-images)
  • Besturingssysteem: Linux op x86-64

Bij de eerste run wordt de Emscripten SDK geïnstalleerd in tools/emsdk en wordt wgpu-native gedownload voor de headless test runner. Houd rekening met ongeveer 300 MB schijfruimte per game voor de gegenereerde code en builds; de disc-image zelf wordt gelinkt en niet gekopieerd.

Instructies

git clone https://github.com/snuri00/ps2-web-recomp.git
cd ps2-web-recomp
scripts/port.sh mygame "My Game.iso"
python3 scripts/serve.py build/web-mygame games/mygame/disc.iso    # http://localhost:8620/

port.sh voert het volledige proces uit: het leest het boot-programma uit SYSTEM.CNF, vertaalt de machinecode naar C++ en draait het spel enkele keren in headless mode. Omdat statische analyse niet alle functies via pointers kan vinden, registreert elke run onbekende adressen, waarna het script deze toevoegt als entry points en opnieuw vertaalt. Ook de vector unit microprogramma's worden tijdens deze runs vastgelegd en vooraf gecompileerd. God of War II wordt in ongeveer 3,5 minuut omgezet op een 8-core laptop.

De exploring runs simuleren een speler door op 'Start' te drukken en elke vijf seconden op 'Kruis'. Dit kan worden aangepast met --press 900:cross voor eigen inputs en --frames voor de duur van de run. Frames van de laatste run worden opgeslagen in games/mygame/shots. Gebruik scripts/build_web.sh mygame om alleen de pagina opnieuw te bouwen.

Verwachtingen: Tot nu toe zijn twee games succesvol geport. Andere games zullen waarschijnlijk stoppen bij een niet-geïmplementeerde system call of IOP-library functie (gelogd als [sys] unhandled syscall of [iop] missing <library> #<n>), of een GS-functie gebruiken die de renderer nog niet ondersteunt.

Browserondersteuning en Bediening

WebGPU

De pagina heeft WebGPU nodig voor de GPU-renderer. Chrome en Edge ondersteunen dit op Windows en macOS. Op Linux moet dit handmatig worden ingeschakeld in Chrome/Chromium via:

  • chrome://flags/#enable-unsafe-webgpu
  • chrome://flags/#enable-vulkan

Zonder WebGPU neemt de software-renderer het over; dit is voldoende voor menu's, maar te traag voor 3D. Op laptops met twee GPU's probeert de pagina beide adapters te gebruiken en valt indien nodig terug op het kopiëren van frames via een 2D-canvas.

Besturing

  • Toetsenbord:
  • Face buttons: Z (kruis), X (cirkel), A (vierkant), S (driehoek)
  • Shoulders: Q, W, 1, 2
  • Start/Select: Enter / Backspace
  • D-pad: Pijltjestoetsen
  • Linker stick: I, J, K, L
  • Overig: Gamepads werken via de browser Gamepad API, telefoons krijgen on-screen controls, en de F-toets schakelt fullscreen. De resolutie-selector herlaadt de pagina bij een wijziging.

Opslag (Saves)

De pagina plaatst een geheugenkaart in slot 1 en bewaart deze in de browseropslag van het adres van waaruit wordt gespeeld. Games formatteren en gebruiken deze kaart zoals een echte kaart.

  • Export card: Downloadt de kaart als een .ps2 bestand (compatibel met PCSX2).
  • Import card: Plaatst een .ps2 bestand terug in het slot.
  • ?nocard start het spel met een leeg slot.

Hosting

De pagina maakt gebruik van SharedArrayBuffer voor threads en moet daarom cross-origin isolated worden geserveerd met de volgende headers:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

scripts/serve.py verzendt beide headers en ondersteunt de Range requests die nodig zijn voor disc streaming.

Juridische Informatie

Deze repository bevat uitsluitend originele code en third-party code onder de eigen licenties. Er is geen gamecode of game-data inbegrepen. De gegenereerde C++ en WebAssembly zijn vertalingen van het uitvoerbare bestand van het spel en behoren daarmee toe aan de eigenaren van het spel; bewaar deze op je eigen machine en publiceer ze niet. Gebruik uitsluitend disc-images van spellen die je zelf bezit.

Gran Turismo, God of War en PlayStation zijn handelsmerken van Sony Interactive Entertainment. Dit project is niet gelieerd aan of onderschreven door Sony, Polyphony Digital, Santa Monica Studio of andere game-uitgevers.

Credits & Licentie

  • MPEG-decoder: De VLC-tabellen zijn aangepast van Play! (BSD 2-clause).
  • Referenties: PCSX2, Play! en de ps2sdk headers dienden als referentie voor de hardware- en systeememulatie.
  • Technologie: wgpu-native en Dawn zorgen voor WebGPU, en Emscripten bouwt de WebAssembly.
  • Licentie: De code in deze repository is beschikbaar onder de MIT-licentie. De bestanden in runtime/src/ipu/tables.hpp bevatten de BSD 2-clause notice van Play!.