PSP Web Recomp

Prestaties en Voorbeelden

De eerste titel die op deze manier is opgestart, is God of War: Chains of Olympus. Het spel draait vanaf het opstarten via de menu's, tussenscènes en gevechten met 60 frames per seconde (fps) in Chrome en Firefox op een laptop. Dit is bereikt bij een resolutie tot vier keer die van de PSP, en op telefoons met on-screen touch-besturing. Muziek, spraak en geluidseffecten werken; films worden vooralsnog overgeslagen.

God of War: Ghost of Sparta volgde via dezelfde scripts. Dit spel vereiste de DRM-ontcijfering van de PSP voor één klein bestand, een handvol systeemoproepen en een correctie voor de belichting. Er was geen verdere prestatie-optimalisatie nodig: het spel draait met 55 tot 60 fps bij drie keer de native resolutie.

Er worden geen game-gegevens meegeleverd. Gebruikers dienen een disc-image van een spel dat zij bezitten te gebruiken; de scripts in deze repository zetten deze vervolgens om in een webpagina op de lokale machine.

Hoe het werkt

Recompilatie

PSPRecomp analyseert het ontcijferde executable, vindt de functies en genereert daar C++ voor in vertalingsunits van elk 16 KiB aan guest-code. Deze code draait tegen een registerbestand en een model van het geheugen van de PSP. Dit project voegt een aantal fixes toe aan PSPRecomp (in patches/) en een nieuw doel in profile/ voor het webprofiel.

Een kleine PSP-kernel

Alle verzoeken van het spel aan het besturingssysteem van de PSP worden beantwoord door high-level emulatie (HLE) in profile/host. Dit omvat:

  • Coöperatieve threads met semaforen, event-flags en callbacks.
  • Geheugenpartities.
  • Het bestandssysteem (waarbij disc-data via HTTP Range-requests wordt gestreamd, zodat alleen het executable vooraf wordt gedownload).
  • De controller, audio-output, save-data en berichtendialogen.
  • De administratie van de movie player.

De guest-tijd schuift op in frames, waardoor een spel een constante 60 Hz ziet, ongeacht hoe snel de host draait.

Grafische verwerking

De GE (de grafische chip van de PSP) wordt gevoed met display-lijsten. ge.cpp decodeert deze op de CPU, inclusief vertex-formaten, skinning, belichting, textuurgeneratie, clipping en backface culling, en stuurt gebundelde driehoeken en render-states door naar ge_gl.cpp.

Hierbij worden framebuffers WebGL render-targets, geïndexeerd op hun positie in het VRAM. Hierdoor blijven effecten die naar een textuur renderen en deze vervolgens weer uitlezen op de GPU. Ook de reinterpretatie van pixelformaten (waarbij games 32-bit buffers lezen als 16-bit textures en vice versa), de stencil-in-alpha van de PSP, mist (fog) en block transfers worden op de GPU geëmuleerd. Alles kan renderen op één tot vier keer de native resolutie van 480×272.

Threading

Net als op de PSP wordt er met twee threads gewerkt. Op de PSP verwerkt de grafische chip de display-lijst van één frame terwijl de CPU het volgende frame voorbereidt. In dit project draaien de GE en de WebGL-context op een worker-thread met een OffscreenCanvas. Het plaatsen van een lijst in de wachtrij gebeurt onmiddellijk; alleen expliciete synchronisatie-oproepen wachten. De kosten per frame worden bepaald door de drukste thread, in plaats van de som van beide.

Geluid

Geluidseffecten komen voort uit een reimplementatie van de voice-synthesizer van de PSP (32 stemmen van ADPCM met pitch en ADSR-enveloppen). Muziek en spraak worden afgeleid van ATRAC3+ streams die worden gedecodeerd met de FFmpeg-decoder, waarna de mix naar een AudioWorklet gaat.

Lessen in optimalisatie

De eerste speelbare versie draaide met 6 frames per seconde. De weg naar 60 fps werd voornamelijk afgelegd door te analyseren waar de tijd precies verloren ging, in plaats van enkel de renderer sneller te maken.

  • Frame-swapping: God of War wisselt zijn framebuffer zonder te wachten op de vertical blank. Hierdoor tekende het spel ongeveer acht frames voor elk frame dat daadwerkelijk het scherm bereikte. Door de thread die twee keer binnen één blank wisselt vast te houden tot de volgende blank (een techniek die ook PPSSPP gebruikt), werd de werklast per getoond frame met een factor acht verminderd.
  • WebAssembly Timing: In WebAssembly gaat het uitlezen van de klok via std::chrono door clock_gettime en een BigInt-conversie in JavaScript. Profiling-timers die dit per primitief uitlezen, kostten ongeveer een derde van het frame. Dit is opgelost door performance.now() alleen uit te lezen wanneer profiling is ingeschakeld.
  • Firefox Buffer-optimalisatie: Firefox kopieert elke WebGL buffer-upload naar zijn GPU-proces en valideert na elke wijziging in een index-buffer de volledige buffer bij de volgende draw-call. Een gedeelde index-ring van 4 MB zorgde ervoor dat Firefox zakte naar 3 fps. Dit is opgelost door elke draw-call een eigen kleine index-buffer te geven. Het tegenovergestelde (vertex-data stukje bij beetje in één grote buffer schrijven) hielp geen enkele browser en zorgde op telefoons voor haperingen.
  • Stencil-buffer: De PSP bewaart zijn stencil-buffer in het alpha-kanaal van de framebuffer. God of War gebruikt dit voor geprojecteerde schaduwen en om een blur-pass te beperken. Het spiegelen van stencil en alpha via full-screen passes was correct, maar kostte bij 4× resolutie 60 miljoen extra pixels per frame. Door bij te houden welke rechthoeken, stencil-bits en constante waarden daadwerkelijk veranderden, werd dit teruggebracht naar ongeveer 7 miljoen.
  • Parallelle verwerking: God of War plaatst de display-lijst van elk frame in een wachtrij en werkt alvast aan het volgende frame voordat hij wacht. Door de GE op een eigen thread te draaien, werd de kostprijs van een druk gevecht in Firefox verlaagd van ongeveer 17 ms (game plus graphics) naar de hoogste van de twee, ongeveer 10 ms.

Een tweede game: Ghost of Sparta

Ghost of Sparta werd via port.sh omgezet zonder wijzigingen aan de scripts of de recompiler, maar bleef bij het opstarten hangen. Het spel opent een bestand van 176 bytes met de DRM-vlag van de PSP, overhandigt de sleutel aan sceIoIoctl en controleert de output. Het bestand staat in PGD-formaat, dat door de KIRK crypto-engine wordt ontcijferd via AES-128 met drie sleutels uit de KIRK key vault. Dit is geïmplementeerd in profile/host/pgd.cpp.

Een tweede probleem was een witte lucht en een witte waas in de menu's. Analyse van de draws van één frame leidde naar een bewolkingslaag die met belichting werd getekend, waarvan de opaciteit afhankelijk is van de alpha van het globale omgevingslicht—een factor die in de belichtingscode was weggelaten. De prestaties vereisten geen aanpassingen: een frame kost 6 tot 8 ms, net als bij Chains of Olympus.

Zelf een game porten

Vereisten

  • Git, CMake, Ninja, een C++20 compiler en Python 3.
  • Omgeving: Alles is getest op Linux. macOS zou de browserversie moeten kunnen bouwen (niet getest); de native test-runner vereist EGL en OpenGL ES headers (libegl-dev en libgles-dev op Debian en Ubuntu).
  • Schijfruimte: Reken op ongeveer 2 GB per game voor de geëxtraheerde disc, de gegenereerde code en de builds.

Installatie en gebruik

git clone https://github.com/snuri00/psp-web-recomp.git
cd psp-web-recomp
scripts/setup.sh                                   # Installeert PSPRecomp, patches en Emscripten SDK
PSP_DECRYPT=/path/to/decrypter scripts/port.sh mygame "My Game.iso"
scripts/serve.sh mygame                            # Start server op http://localhost:8613/

port.sh extraheert de disc (ISO, ZIP of map), ontcijfert PSP_GAME/SYSDIR/EBOOT.BIN, vertaalt dit naar C++, schrijft het manifest voor het streamen van de rest van de disc en bouwt de pagina. Dit duurt enkele minuten; God of War is in ongeveer vier minuten klaar op een 8-core laptop. Voeg --native toe om ook een headless runner te bouwen die frames naar afbeeldingen kan dumpen en audio kan opnemen.

Ontcijfering

Executables op commerciële discs zijn versleuteld. De variabele PSP_DECRYPT moet verwijzen naar een tool die wordt aangeroepen als tool <in> <out> en een plain ELF schrijft, zoals DecEboot of pspdecrypt. Ook PPSSPP kan een ontcijferd executable dumpen tijdens het draaien van een game (Settings → Tools → Developer tools).

Verwachtingen

Er zijn tot nu toe twee games succesvol opgestart, beide van Ready at Dawn en gebouwd op dezelfde engine. Dit zegt weinig over games van andere studio's. Een andere game zal waarschijnlijk stoppen bij een systeemoproep die nog niet is geïmplementeerd (gelogd als [hle] unimplemented ...) of een GE-functie gebruiken die deze renderer niet ondersteunt. In docs/internals.md worden de tools beschreven om te achterhalen wat er ontbreekt.

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 verzorgt beide headers en ondersteunt de Range-requests waar het streamen van de disc op vertrouwt. Om het op een telefoon te testen, kan een tunnel worden gebruikt, bijvoorbeeld: cloudflared tunnel --url http://127.0.0.1:8613

Browsers die geen WebGL2 op een OffscreenCanvas kunnen tekenen, vallen automatisch terug op een enkele thread. Dit kan worden geforceerd met ?threads=0. Met ?profile kan een timing-overzicht per frame in de statusbalk worden toegevoegd.

Juridische informatie

Deze repository bevat uitsluitend originele code, de PSPRecomp-patches en third-party code onder hun eigen licentie. Er is geen game-code of game-data inbegrepen. De gegenereerde C++ en de gebouwde WebAssembly zijn vertalingen van het executable van het spel en behoren dus toe aan de eigenaren van het spel; houd deze op je eigen machine en publiceer ze niet. Gebruik disc-images van spellen die je zelf bezit. God of War is een handelsmerk van Sony Interactive Entertainment; dit project is niet gelieerd aan of onderschreven door Sony of andere game-uitgevers.

Credits

  • PSPRecomp (MIT): Verantwoordelijk voor de statische recompilatie.
  • PPSSPP en JPCSP: Documenteerden veel van het hardwaregedrag dat hier wordt geëmuleerd.
  • FFmpeg: De standalone kopie van de ATRAC3/ATRAC3+ decoder van PPSSPP wordt gebruikt voor muziek en spraak (LGPL 2.1 of later, in profile/thirdparty/at3standalone).
  • Emscripten: Wordt gebruikt voor het bouwen van de WebAssembly.

Licentie

De code in deze repository is beschikbaar onder de MIT License (zie LICENSE). De component in profile/thirdparty/at3standalone behoudt zijn LGPL 2.1 licentie.