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, branchesp32-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:
| Kenmerk | Waveshare ESP32-S3-Touch-LCD-2 | DIY handheld (Seeed XIAO ESP32S3 Sense) |
|---|---|---|
| Paneel | ST7789, 320x240 | ILI9341, 320x240 native |
| Flash | 16 MB | 8 MB |
| Game data | microSD, of een getrimde STAGE.DIR in flash | microSD (volledige STAGE.DIR) |
| Input | toetsenbord 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=gnu17en-mlongcallszijn beide verplicht. De standaardgnu23van GCC 14 wijst deze code af, en Xtensacall8reikt slechts +/-512 KB.psyzenMGShebben beide eencommon.h, dus de include-paden van MGS moeten eerst komen.- 12
cast-as-lvalueexpressies, een GCC 2.x extensie, werden herschreven. - MGS vervangt de
printfvan libc door zijn eigen stub. Ik hernoemde deze naarmts_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 werdenSCRPAD_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:
- MGS engine: gedecompileerde C, gecompileerd naar Xtensa.
- psyz: PlayStation SDK en GTE in C, software rasterizer.
- Platform layer: mts scheduler op FreeRTOS, vblank tick, virtuele CD, LCD, input.
- 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:
divide.cbucket via de low byte en parkeert de high byte in de lengte van de tag.sort.cre-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_PrimInfosplaatste de eerste vertex op +8 in plaats van +12.LinkModelToParentberekende parent-vertex offsets als52 + 8in plaats van56 + 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.
xTaskCreategevolgd doorvTaskSuspendis 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
ExitCriticalSectionaanroept zonder een bijbehorendeEnter. - 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_GetTcbEntrylas ook de BIOS tabel-van-tabellen op het vaste adres0x100en 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:
- Seriële toets-aanhoudtijden worden gedegradeerd in
DGActFirst, direct naGVUpdatePadSystem, 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. - De LCD stuurt een frame alleen wanneer een sequentienummer in
DG_SwapFrameis 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
xTaskCreatestilletjes faalde. - Stacks in PSRAM werkten tot een task
fopenaanriep. 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). CDBIOSReadRequestlevert data sector voor sector en roept na elke sector een callback aan. De callback kan de resterende grootte herschrijven.FSCdStageFileInitis daarop aangewezen om de grootte van deSTAGE.DIRtabel te leren uit de eerste 4 bytes. De request behandelen als een simpele byte-copy en de callback negeren, zorgde ervoor dat elke stage-lookupNOT FOUNDteruggaf.- 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.
ReadRequestregistreert nu alleen de request, enReadSyncpompt 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.tziet 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 = 0x7fff0000het scherm zwart maakte, gokte ik twee keer fout. Het taggen van de vier schrijvers met "file:line" wees direct naargame/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.replacestilletjes 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 vanDGPcxRead4Bpp, 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 stagesis.
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_Wwas 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:
SOFTRASTERIRAMtoevoegen (IRAM_ATTR), wat -2% opleverde.- PSRAM en flash op 120 MHz zetten, wat -12% opleverde.
- 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:
printfover native USB blokkeert als er geen host is die de data weghaalt.port/mgs_printf.hschrijft nu met een zero timeout.
Wat werkt nog niet
- Codec oproepen: De
RADIO.DATindex 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
- De mts-stacks optimaliseren en hot texture pages naar intern SRAM verplaatsen.
- De 120/120 MHz geheugenklokken op de XIAO meten.
- De
RADIO.DATindex fixen en controleren of de lift daarna werkt. - Streaming implementeren in de virtuele CD voor cutscenes en stemmen.
- Audio toevoegen via een software SPU.
- 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.
Groetjes,