Metal Gear Solid draait native op de ESP32-S3

Het board is de Seeed Studio XIAO ESP32S3 Sense: een dual-core ESP32-S3 met 8 MB PSRAM en 8 MB flash, op een module van 21 × 17,8 mm. De Sense-versie bevat een uitbreidingsbord met een camera en een microSD-slot; dat microSD-slot is de plek waar de game-data staat.

Ik heb een tijd besteed aan het uitzoeken of dit mogelijk was. Eerst zocht ik naar een versie van de game die al was herbouwd als C-code die op een pc draait, en daarna controleerde ik of al die logica in het flashgeheugen van de ESP32-S3 en op een microSD-kaart paste. Het kostte meerdere pogingen, maar uiteindelijk werkte het. Een fantastische game draait nu op een microcontroller-board dat ongeveer 15 USD kost.

In dit artikel beschrijf ik hoe Metal Gear Solid (PlayStation, 1998) als een native port op de ESP32-S3 draait. De C-broncode van de game, gereconstrueerd door de mgs_reversing decompilatie van FoxdieTeam, is direct gecompileerd naar Xtensa-machinecode. Alles wat de PlayStation-hardware normaal gesproken leverde, is vervangen door software:

  • De SDK.
  • De GPU.
  • De GTE geometry coprocessor.
  • De CD-drive.
  • De vertical blank interrupt.
  • De controller.

Het artikel behandelt de hardware, de platformlaag, geheugenplaatsing, de debugmethode, de belangrijkste bugs, de prestaties, de handheld en wat er nog niet werkt.

Code en gerelateerde projecten:

  • MGS port: davidmonterocrespo24/mgs_reversing, branch esp32-port.
  • Handheld hardware gids: HARDWARE.md.
  • psyz fork: davidmonterocrespo24/psyz.

Let op: Je hebt je eigen disc nodig. De repository bevat niets dat is afgeleid van de game; port/extract_disc.py haalt de bestanden uit je eigen disc-image.

Hardware

De ESP32-S3 heeft twee Xtensa-cores op 240 MHz en 512 KB intern SRAM. Beide hier gebruikte boards hebben ook 8 MB PSRAM, die achter dezelfde cache zit als de flash.

Vergelijking gebruikte hardware:

KenmerkWaveshare ESP32-S3-Touch-LCD-2DIY handheld (Seeed XIAO ESP32S3 Sense)
PaneelST7789, 320x240ILI9341, 320x240 native
Flash16 MB8 MB
Game datamicroSD, of een getrimde STAGE.DIR in flashmicroSD (volledige STAGE.DIR)
Inputtoetsenbord via serial (play.py)drone-gimbal stick, 6-knops weerstandsladder, 2 directe knoppen

De kloof meten vóór het schrijven van de code

Voordat ik aan de port-code begon, heb ik gemeten hoe ver de decompilatie verwijderd was van een build voor Xtensa:

De engine-libraries (libgv, libdg, libgcl, libhzd, libfs) compileerden bijna direct met xtensa-esp32s3-elf-gcc 14.2 tegen de psyz-headers: 57 van de 58 bestanden. Geen van hen bevat absolute 0x800xxxxx adressen. Alle grafische output verloopt via 11 libgpu calls (DrawOTag, LoadImage, ClearOTagR enzovoort). Dat is precies de laag die de software-renderer van psyz vervangt.

Ik liet één falend bestand, libdg/chanl.c, bewust falen. De cast die het zou hebben opgelost, zou namelijk stilletjes de stride hebben veranderd (zie de sectie over de ordering-table).

De rest vereiste mechanische passes, waarbij elke pass een hele klasse problemen oploste:

  • -std=gnu17 en -mlongcalls zijn beide verplicht. De standaard gnu23 van GCC 14 wijst deze code af, en Xtensa call8 reikt slechts +/-512 KB.
  • psyz en MGS hebben beide een common.h, dus de include-paden van MGS moeten eerst komen.
  • 12 cast-as-lvalue expressies, een GCC 2.x extensie, werden herschreven.
  • MGS vervangt de printf van libc door zijn eigen stub. Ik hernoemde deze naar mts_printf, wat later leidde tot een van de meest misleidende bugs ("De boot die vast leek te zitten").
  • 346 letterlijke scratchpad-adressen (0x1f8000xx) over 16 bestanden werden SCRPAD_ADDR + offset, ondersteund door een array.

Het resultaat was dat 497 van de 497 C-bestanden naar Xtensa zijn gecompileerd. De ELF had 0 ongedefinieerde referenties en 0 dubbele definities (text 987.073 B, bss 2.434.276 B). Het eigen geheugen van de game is 804 KiB (428 KiB heap plus twee 188 KiB packet buffers), wat comfortabel in het PSRAM past.

Architectuur

De port heeft drie lagen tussen de game en de SoC:

  1. MGS engine: gedecompileerde C, gecompileerd naar Xtensa.
  2. psyz: PlayStation SDK en GTE in C, software rasterizer.
  3. Platform layer: mts scheduler op FreeRTOS, vblank tick, virtuele CD, LCD, input.
  4. ESP32-S3 (Hardware).

psyz en de GTE

psyz, de reimplementatie van de PlayStation SDK door Xeeynamo, modelleerde al het volledige GTE-registerbestand en elke GTE-operatie in C, gevalideerd tegen de PCSX-Redux GTE-testsuite.

Mijn eerste schatting was dat er 64 tot 70 macro's misten. Die schatting was te hoog. MGS levert zijn eigen MIPS inline-asm headers (inlinen.h, inlinex.h), en -fsyntax-only controleert geen registernamen. Zodra die headers naar psyz werden omgeleid, bleek de werkelijke kloof 41 macro's te zijn. Ik heb deze toegevoegd aan psyz, waarbij ik de assembly van de game zelf als specificatie gebruikte voor welke COP2-registers elke macro raakt. Daarna heb ik mijn andere psyz-gebaseerde projecten opnieuw gebouwd om te controleren of er niets kapot ging.

Wanneer je een GTE-macro aliast, moet je controleren of elke constraint een input of een output is. Bijvoorbeeld: gtereadopz heeft een =r output, dus zijn alias moet &x nemen.

De ordering-table tag: één word op PSX, twee in psyz

Dit is het belangrijkste verschil in lay-out en het veroorzaakte de meeste bugs. Op de PlayStation begint elke primitief met één 32-bit tag: 24 bits voor het adres van de volgende primitief en 8 bits voor de lengte. Een host-pointer past niet in 24 bits, dus psyz gebruikt twee words: tag en len. Elke primitief groeit met 4 bytes, en elk veld na de tag verschuift met één word.

MGS misbruikt ook de lengte-byte. libdg voert een two-pass radix sort uit op 16-bit diepte:

  1. divide.c bucket via de low byte en parkeert de high byte in de lengte van de tag.
  2. sort.c re-bucket via die high byte en schrijft de werkelijke lengte.

Omdat psyz de twee velden scheidt, is de vertaling exact:

  • PSX: pack->tag = (hi << 24) | ot; ot = pack;
  • psyz: addPrim(ot, pack); setlen(pack, hi);

Het herdefiniëren van 42 lokale declaraties en 100 parameters naar OT_TYPE* was het makkelijke deel. Het moeilijke deel was de code die uitging van de packed lay-out zonder dit expliciet te vermelden:

  • DG_PrimInfos plaatste de eerste vertex op +8 in plaats van +12.
  • LinkModelToParent berekende parent-vertex offsets als 52 + 8 in plaats van 56 + 12.
  • De primitief-constructors (beschreven bij "Het witte scherm").

Er is ook een verzwarende factor: op de console herschreef addPrim() bij elke invoeging de hele tag-word, waardoor een vergeten lengte nooit opviel. In psyz wordt een lengte die nooit is ingesteld, simpelweg nooit geschreven.

Konami’s scheduler binnen FreeRTOS

MGS draait op Konami's eigen cooperatieve scheduler. De boot-banner vermeldt nog steeds Multi Task Scheduler for PSX ver2.02 Jul 11 1998. Deze leunt op de BIOS thread API (OpenTh/ChangeTh) en gebruikt de vblank-interrupt om taken te wekken waarvan de timers zijn verlopen.

In de port is elke mts-thread een FreeRTOS-task, waarbij er slechts één tegelijk uitvoerbaar is. Het uiteindelijke ontwerp:

  • Alle mts-tasks zijn gepind aan core 0, samen met de vblank tick op prioriteit 10, zodat de tick de game pre-empt overneemt zoals de PSX-interrupt dat deed.
  • De LCD-scanout draait op core 1. Het kruisen van deze toewijzingen veroorzaakte races die geparkeerde tasks midden in hun stack heractiveerden. Eén log liet twee kopieën van Main() zien die lijn voor lijn afwisselden.
  • Een nieuwe task suspend zichzelf in zijn trampoline. xTaskCreate gevolgd door vTaskSuspend is niet atomair, en op twee cores draaide de task voordat zijn controleblok was ingevuld.
  • Critical sections zijn essentieel. Elke task heeft een absolute vlag, die bij elke switch wordt opgeslagen en hersteld, net als het MIPS statusregister. Het is een vlag in plaats van een teller omdat de game ExitCriticalSection aanroept zonder een bijbehorende Enter.
  • De tick voert de mts-callback alleen uit terwijl de idle-task (11) actief is. Die task is Konami’s SIO console loop, de enige code die nooit de scheduler-status raakt. Op de console draaide dit met interrupts ingeschakeld, precies om deze reden. De game passeert elke frame de idle-state, waardoor de wake-latency onder één frame blijft. Deze wijziging verwijderde intermitterende hangs.
  • mts_GetTcbEntry las ook de BIOS tabel-van-tabellen op het vaste adres 0x100 en schreef via het resultaat. Buiten een PSX bevat dat adres onzin; het gebruikt nu een private TCB-tabel.

De vblank tick en de twee klokken

port/esp32_vblank.c vuurt de VSync-callback van de game elke 16 ms af. Dat vereiste dat VSyncCallback() werd geïmplementeerd in psyz, waar het voorheen niet eens een pointer bewaarde.

VSync(-1) moet een free-running vblank-telling teruggeven. De eerste versie gaf het aantal gepresenteerde frames terug, wat stopt met tellen als er niets wordt getekend, waardoor timers nooit verliepen.

De framerate varieert: 2 vblanks per frame in een gang, 4 tot 5 in een open kamer. Daarom horen twee zaken die leken op vblank-zaken eigenlijk bij het game-frame:

  1. Seriële toets-aanhoudtijden worden gedegradeerd in DGActFirst, direct na GVUpdatePadSystem, het enige punt dat betekent: "een frame heeft de pad gelezen". Vblanks tellen op die plek zou Snake te ver laten lopen of toetsen loslaten voordat de game ze zag.
  2. De LCD stuurt een frame alleen wanneer een sequentienummer in DG_SwapFrame is gewijzigd, met een fallback van 250 ms.

Geheugenplaatsing

Intern SRAM is schaars en snel. PSRAM is overvloedig en traag. Flash-uitlezingen schakelen de cache uit waar PSRAM ook van afhankelijk is.

De RAM-map van de game hangt af van één define: MEMBOTTOM. Zowel de packet arenas, de heap als de libfs sector-buffer komen hiervan voort door aftrekking. Deze is verplaatst naar een statische PSRAM-array (mgsmainram[0xC9000], EXTRAMBSSATTR). Dit moet statisch zijn omdat RESIDENT_BOTTOM wordt gebruikt als statische initializer, wat malloc uitsluit.

Het 1 MB PSX VRAM leeft in PSRAM. Zonder EXTRAMBSS_ATTR zou dram0 met ongeveer 900 KB overlopen.

Task-stacks bewogen heen en weer:

  • Stacks van 32 KB uit intern SRAM zorgden ervoor dat de vierde xTaskCreate stilletjes faalde.
  • Stacks in PSRAM werkten tot een task fopen aanriep. De flash-driver schakelt dan de cache uit, de task verliest zijn eigen stack en de SoC reset zonder een panic te printen.
  • Stacks zijn nu 20 KB in intern SRAM. High-water marks laten zien dat vijf tasks ongeveer 2 KB gebruiken en één task 14.484 B.

Een stub met het verkeerde type belandde in read-only flash. sputransferCallback is een functiepointer. Hem declareren als functie plaatste hem in .text, waardoor de eerste = 0 crashte. Ik vond dit door nm te draaien op een werkende psyz-gebaseerde ELF en deze te vergelijken met deze port via comm, en symbolen te zoeken die daar B/D zijn en hier T.

Één klasse bugs bestaat alleen op deze geheugenmap: pointer-tests gebaseerd op de sign-bit. MGS maakt een script-block pointer onderscheid van een 16-bit proc-id met procid < 0. PSX RAM zit op 0x8xxxxxxx, wat negatief is als signed value. PSRAM zit op 0x3Cxxxxxx, wat positief is. Het resultaat was PROC 3C1987D2 NOT FOUND, gevolgd door een LoadProhibited op EXCVADDR 0x00000003. Een GCLISBLOCKPTR macro lost de plek op waar dit gebeurde. Andere plekken met hetzelfde patroon moeten nog worden geaudit.

Een virtuele CD via FAT

libfs werkt in absolute sectoren. Het leest de ISO9660-directory één keer en gebruikt daarna nooit meer een bestandsnaam. port/virtual_cd.c vervangt de CD BIOS:

  • Elk bestand krijgt een synthetisch bereik van 1.000.000 sectoren, zodat een sectornummer via een deling terugmaapt naar (bestand, offset).
  • CDBIOSReadRequest levert data sector voor sector en roept na elke sector een callback aan. De callback kan de resterende grootte herschrijven. FSCdStageFileInit is daarop aangewezen om de grootte van de STAGE.DIR tabel te leren uit de eerste 4 bytes. De request behandelen als een simpele byte-copy en de callback negeren, zorgde ervoor dat elke stage-lookup NOT FOUND teruggaf.
  • Levering moet asynchroon zijn. De parser en de "disc" zijn coroutines die één buffer-window delen. Alles in één keer leveren zorgde ervoor dat bestand 2 bestand 1 overschreef voordat het was geparsed. ReadRequest registreert nu alleen de request, en ReadSync pompt sectoren totdat de callback 2 teruggeeft ("buffer redirected").

De data leefde eerst in PSRAM, wat de game beperkte tot een handvol stages. De echte oplossing was de microSD. De kaart is een SPI-perifeer, gescheiden van de XIP flash, dus het lezen ervan schakelt de instructie-cache niet uit. Bestanden blijven op de kaart en kosten geen PSRAM.

De volledige 71.892.992-byte STAGE.DIR (alle 96 stages) wordt on-demand gelezen. Van de 93 stage-bestanden hebben er 65 volledig symbolische actor-tabellen. De andere 28 bevatten nog ruwe MIPS-adressen omdat hun actor-code niet is gedecompileerd. Een generator bouwt één gededupliceerde actor-tabel en een whitelist: 56 stages en 149 actor-klassen. De script-stage-change command controleert de whitelist terwijl de huidige wereld nog intact is.

De kaart en de LCD delen SPI2, en ESP-IDF arbitreert niet tussen hen. Eén LCD-frame bestaat uit 30 gekeuwde asynchrone transfers, dus een kaart-uitlezing midden in een frame veroorzaakte assert failed: spihaliram.c:134 en een reboot-loop. De oplossing is expliciet bus-eigendom (Mgs_SpiBusTake/Give), waarbij in-flight transfers worden afgehandeld voordat de bus wordt vrijgegeven.

Methode: Meten, niet gissen

Ongeveer een derde van de voor de hand liggende verklaringen in dit project bleek onjuist. Wat werkte, was het board zo krijgen dat het een getal print dat tussen hypotheses beslist. De nuttigste lessen kwamen van probes die foute antwoorden gaven:

  • Een teller die niemand incrementeert, verzint een bevinding. [trap] scanning 63 triggers, 0 entered so far, werd bijna "het trigger-systeem is dood". De teller had geen ++. Met een echte teller vuurden triggers vanaf de eerste seconde.
  • Een tautologische check geeft vals vertrouwen. De skeleton-audit vergeleek |R·t| met |t|. Die zijn gelijk voor elke rotatie, dus bleef de check wekenlang groen terwijl elk skeleton vervormd was (zie "Strict aliasing verwijderde een rotatie").
  • Controleer waar een veld wordt geschreven voordat je het interpreteert. eye_inv.t ziet eruit als een camerapositie, maar is een geroteerde richtingvector. Het lezen als positie produceerde een gedetailleerd, foutief verhaal over een wegvliegende camera.
  • Houd een probe breed tot de data hem vernauwt. Ik filterde de full-screen-primitive probe naar ongetextureerde primitieven, vanuit de gedachte dat een flash geen texture heeft. Een searchlight-cone heeft er echter wel één.
  • Splits tellers per oorzaak. "65% van de seams zijn stale" waren vier oorzaken in één bucket; de grootste was onschadelijk. Na de splitsing vuurde mijn fix nul keer, en heb ik hem verwijderd.
  • Instrumenteer alle N schrijvers. Toen DG_UnDrawFrameCount = 0x7fff0000 het scherm zwart maakte, gokte ik twee keer fout. Het taggen van de vier schrijvers met "file:line" wees direct naar game/gamed.c:476.
  • Decodeer de data voordat je de interpreter verdacht. Het script van de dock leek één commando uit te voeren. Het decoderen van de retail-bytecode liet 19 nodes zien, waarvan er slechts één print (zie "De lege dock was een two-pass script").
  • Controleer of de probe in de binary zit. Een breadcrumb die "(nothing)" las, was nooit ingevoegd omdat een scripted str.replace stilletjes was gefaald. Grep de string in de .bin.
  • Gebruik hardware watchpoints voor rogue writers. espcpusetwatchpoint(0, addr, 4, STORE) liet zien dat ongeveer 230 GPU-desyncs kwamen van DGPcxRead4Bpp, een texture-decoder wiens scratch-buffer overlapte met live packets.
  • Wees argwanend bij plotselinge verbeteringen. Toen de kaart niet monteerde, viel het board terug op 9 stages in flash, en lades tijden dropten van 693 naar 21 vblanks. Dat lijkt op een 35x versnelling, maar het betekent dat de kaart ontbreekt. Controleer eerst of de microSD is gemount en table_size 1152 -> 96 stages is.

De Bugs

Strict aliasing verwijderde een rotatie

Symptoom: Elk personage had armen die losgekoppeld waren van het torso, een knie die achterstevoren boog en voeten die verschillende kanten op wezen. De wereld was in orde. Oorzaak: CompMatrix schreef de translatie via (VECTOR*)&tmp.t[0] over een long[3]. GCC 14 bij -O2 concludeerde dat niets die stores las en verwijderde de call. De disassembly had geen multiply-instructies. Het berekende simpelweg m2->t = m0->t + m1->t. Fix: long long rekenkunde zonder type punning, plus -fno-strict-aliasing voor de hele game. Een tweede identiek geval bestaat in libdg/display.c:318.

Het witte scherm: vijf foute hypothesen, drie echte oorzaken

Symptoom: Wanneer een vijand Snake spotte, bedekte een wit laken de scène. De HUD bleef zichtbaar en er waren kleurflitsen. Oorzaak 1: SetDrawStp deed ((ulong*)p)[1] = 0xE6000000 | stp. In psyz is word 1 de lengte (len), en het commando hoort in code[0]. De reader zag een packet van miljarden words lang, dropte dit en vervolgde midden in een primitief, waarbij vertices als commando's werden gedecodeerd. Oorzaak 2: DGMakePrim initialiseerde de lengte nooit. Dit is nu gefixt door len = psize/4 - 2 af te leiden en te controleren tegen de psyz macro's. Oorzaak 3: De PSX GPU weigert elke polygoon groter dan 1023x511. Onze rasterizer tekende ze wel. Polygonen die achter de camera doorlopen, worden gepind aan de GTE-saturatielimiet, wat resulteert in screen-spanning quads. De grootte-test draait nu in soft_raster.inc.c vóór het clipping.

Square0 verschoof 12 bits: elke afstand /64

Symptoom: Snake nam drie stappen, raakte een muur, kaatste terug en maakte nooit progressie. Oorzaak: In PSY-Q is het numerieke suffix de shift: Square0 shift niet en Square12 wel. In de port shiftte Square0 met 12, waardoor elke Square0 -> sum -> sqrt lengte met /64 uitviel. Dit brak bewegingen, collsies, vijandelijk zicht, kogels, de infraroodsensor en licht-attenuatie. Fix: Het verwijderen van de shift maakte de trajectorie weer monoton.

Vijf GTE opcodes gekopieerd uit de verkeerde rij

Symptoom: Snake was uit de wereld gevallen (Y = -3250), beweging zat vast op 0,1 per frame, en de dock had geen water. Oorzaak: psyz’s PsyzGteRtv0/Rtv1/Rtv2/Ll/Llv0 gebruikten MVMVA opcodes uit de verkeerde rij van de inlinen.h tabel van MGS. "Roteer deze vector" werd "roteer hem en voeg de oude translatie toe", en lighting gebruikte de rotatiematrix in plaats van de light-matrix. Fix: Na de fix was de Y-positie weer 250, was het draaien vloeiend en steeg het aantal textured pixels per frame van 23.500 naar 107.877.

De lege dock was een two-pass script

Symptoom: De dock had slechts een handvol object-groepen en het script leek één commando uit te voeren. Oorzaak: De structuur van s00a is: if (var[5] < 6) { zet de oproep van de Colonel klaar } else { bouw de dock }. De wereld wordt pas bij de tweede pass gebouwd, en het beantwoorden van de codec triggert dit. Omdat ik de codec had uitgeschakeld om een crash te voorkomen, werd de dock nooit gebouwd. Fix: MENU_RadioDrainHeadless beantwoordt nu de oproep en voert de proc uit zonder het codec-scherm te openen.

Delen door nul: MIPS overleeft het, Xtensa reboot

Symptoom: De board resette willekeurig. Oorzaak: libhzd/near.c deelt door een gekwadrateerde 2D-lengte die in degeneratieve gevallen nul kan zijn. De R3000 (PSX) gaat door met een ongedefinieerd resultaat. Xtensa genereert een IntegerDivideByZero en reset het board. Fix: HZD_DIV is toegepast op de 7 runtime-divisoren.

De freeze van 17,6 minuten

Symptoom: De game bevroor na precies 17,6 minuten. Oorzaak: psyz's VSync truncateerde het resultaat naar 16 bits, maar VSync(-1) moet de 32-bit field count teruggeven. Bij 65.536 fields (17,6 minuten bij 60 Hz) wrapte de mts-klok van 64.800 naar 1.064 terwijl een task wachtte op 65.537. Fix: De volledige count teruggeven voor mode < 0.

De boot die vast leek te zitten

Symptoom: De boot leek urenlang vast te zitten. Oorzaak: Konami’s mtsprintf was een lege stub op de PSX. Door #define printf mtsprintf ging alle output van de game naar deze stub, waardoor de boot stilletjes vorderde. Fix: mts_printf laten aanroepen van vprintf.

Een priority deadlock die een gefaalde malloc was

Symptoom: De file daemon (task 10) kreeg nooit CPU-tijd. Oorzaak: Ik had de stacks verhoogd naar 32 KB. FreeRTOS haalt deze uit intern SRAM, en de vierde task paste niet meer. xTaskCreate faalde, OpenTh gaf -1 terug, en ChangeTh weigerde die tid voor altijd. Fix: Stacks verlagen naar 20 KB in intern SRAM.

Zwart scherm na toevoegen cutscene data

Symptoom: Het kopiëren van DEMO.DAT, VOX.DAT en ZMOVIE.STR naar de kaart maakte het scherm zwart. Oorzaak: De stage-change code wacht tot GM_StreamStatus() == -1. De cutscene-stream advanced nooit, omdat de virtuele CD sector-reads ondersteunt, maar niet het continue ring-buffer streaming pad. Fix: Alle input-streams worden nu geweigerd in NewStreamControl door hun callback uit te voeren, met een watchdog van 600 frames. Dit betekent dat cutscenes en stemmen ontbreken, maar de game is speelbaar.

Kleinere bugs:

  • PSX_W was 256 (geërfd), maar MGS rendert 320 kolommen; een vijfde van het beeld ging verloren.
  • Boot race: de tick las USB serial voordat de driver was geïnstalleerd.
  • Frame dropper: Konami's frame dropper veroorzaakte strobing; vlaggen worden nu geforceerd naar 0 onder __psyz.

Prestaties

De raster-tijd is gemeten over 120 frames op dezelfde positie. In een kleine kamer (s07a) ging de raster-tijd van 7,69 ms naar 6,18 ms per frame door:

  1. SOFTRASTERIRAM toevoegen (IRAM_ATTR), wat -2% opleverde.
  2. PSRAM en flash op 120 MHz zetten, wat -12% opleverde.
  3. De scanout matchen met de game-rate (in plaats van vaste 62.5 Hz), wat -6,6% opleverde.

In de dock (s00a) is het zwaarder: 40,9 ms rasterisatie plus ongeveer 25 ms logica en GTE, wat neerkomt op ongeveer 15 fps. Het doel is 30 fps. De kosten zitten in textured pixels (~207 cycli per pixel), waarbij bijna elke pixel een cache miss is door de geroteerde driehoeken.

Laden is traag: ongeveer 11,5 s voor 1,1 MB vanaf de kaart (~95 KB/s).

De handheld samenstellen

De handheld is gebouwd uit:

  • Een XIAO ESP32S3 Sense (voor de microSD slot).
  • Een ILI9341 SPI paneel.
  • Een drone-controller gimbal.
  • 8 knoppen: 6 op een weerstandsladder (ADC pin), plus 'fire' en 'aim' op eigen pinnen.

Belangrijke bevindingen tijdens de bouw:

  • Ontbrekende ground: Het paneel gaf een wit licht door zichzelf te voeden via de beschermingsdiodes; een ontbrekende ground was de oorzaak.
  • Paneel type: Het bleek een ILI9341 (320x240) te zijn in plaats van de geplande ILI9488 (480x320). Dit was gunstig: native resolutie zonder scaling.
  • Reset pin: RST moest naar een echte pin (D4) omdat software-reset onbetrouwbaar was.
  • ADC reads: ADC-uitlezingen in de vblank tick bevroren alles. Een task op core 1 samplet nu elke 10 ms.
  • USB-blocking: printf over native USB blokkeert als er geen host is die de data weghaalt. port/mgs_printf.h schrijft nu met een zero timeout.

Wat werkt nog niet

  • Codec oproepen: De RADIO.DAT index is onjuist; de berekende startsector is 31.392 terwijl het bestand slechts 867 sectoren heeft.
  • De intro: Booten via het titelscherm veroorzaakt 11 panics.
  • De lift: De lift wordt nooit aangevraagd door het stage-script; waarschijnlijk door een story-variabele die alleen via een codec-oproep vordert.
  • Overig: CD-streaming (cutscenes, stemmen), FMV (geen MDEC), audio, en memory card saves.
  • Ontbrekende stages: 28 stages waarvan de actor-code nog in MIPS assembly staat.
  • Prestaties: Frames per seconde boven de 15.

Volgende stappen

  1. De mts-stacks optimaliseren en hot texture pages naar intern SRAM verplaatsen.
  2. De 120/120 MHz geheugenklokken op de XIAO meten.
  3. De RADIO.DAT index fixen en controleren of de lift daarna werkt.
  4. Streaming implementeren in de virtuele CD voor cutscenes en stemmen.
  5. Audio toevoegen via een software SPU.
  6. Debug-probes verwijderen.

Conclusie

Een PlayStation tolereert type punning, delen door nul, reads buiten arrays en sign-tested pointers. Een moderne compiler en CPU doen dat niet. De aanpak die werkte voor deze port was om elke meting zo te ontwerpen dat deze kan falen.