Astéroric › Artikelen › Asteroids naar een Oric-1 porten: wat tegenstribbelde
Asteroids naar een Oric-1 porten: wat tegenstribbelde
Veertig fasen om een vectorspel uit 1979 in 20.000 cycli per beeld te passen. Wat toegaf, wat tegenstribbelde, en wat de emulator voor ons verborg.
Asteroids draait op een kast met vectorscherm: een straal die segmenten trekt, geen pixels, geen beeldgeheugen. De Oric-1 is precies het tegendeel — een bitmap van 240 × 200 punten, een 6502 op 1 MHz, 48 KB waarvan een deel door het scherm wordt ingenomen, en een geluidschip met drie blokgolfkanalen. Het project Astéroric is een studiekloon: de arcadelogica nemen zoals ze in de rev 4-ROM staat (dezelfde processor, dat helpt) en ze op deze machine laten passen. Dit artikel gaat over wat tegenstribbelde. De fasenummers verwijzen naar de CHANGELOG van de repository, die leidend is.
1. Het budget dat niet klopte
De eerste kadering deed het rekenwerk: op 1 MHz en 50 beelden per seconde heb je 20.000 cycli per beeld. Een correcte 6502-Bresenham-routine zonder zelfmodificerende code kost 15 tot 22 cycli per pixel; met 30 segmenten van 20 pixels, getekend en dan gewist, is het budget op voordat er iets berekend is. Het document noemde drie hefbomen: 25 Hz aanvaarden, de vormen vereenvoudigen, of zwaar investeren in de lijnroutine.
De meting van fase 2 gaf de doorslag: ~97 cycli per pixel, vijf keer de aanname. De 25 Hz werd meteen vastgelegd — als nominaal doel, niet als achteruitgang, en het is de beeldfrequentie van veel Oric-spellen. De zelfmodificerende code van fase 2b (dx/dy-operanden als immediates gepatcht, richting van de Y-stap bij binnenkomst gepatcht) leverde maar 4 cycli per pixel op, ver van de beoogde 50. De echte besparing kwam later, en van elders.
2. Vectoren tekenen op een bitmap
Alles wordt in XOR getekend: een segment een tweede keer tekenen wist het, zonder de achtergrond te bewaren. Dat is elegant en heeft een onmiddellijk gebrek, ontdekt in fase 9g: een pixel gedeeld door twee segmenten — de neus van het schip, elk hoekpunt van een gesloten veelhoek — wordt twee keer ge-XOR'd en dus gewist. Alle hoekpunten van de asteroïden waren onzichtbaar. De eerste fix was een expliciete replot van gedeelde hoekpunten (drie XOR's = pixel aan). De goede fix kwam in fase 24: een half-open Bresenham die ]P0, P1] tekent, alle pixels behalve het startpunt. Op een veelhoek die in een cyclus wordt doorlopen, wordt elk hoekpunt dan precies één keer ge-XOR'd. Nettokost nul — één pixel minder per segment betaalt de test bij binnenkomst.
De beslissende prestatiewinst kwam niet van de lijnroutine maar van het idee om ze niet meer aan te roepen. Asteroïden draaien nooit: hun twaalf silhouetten (vier vormen × drie groottes) worden bij het bouwen gerasterd door een Python-script dat de Bresenham van line.s nabootst, en uitgestuurd als HIRES-bitmaps die vooraf zijn verschoven naar hun zes fasen binnen een byte (3.402 bytes aan gegevens). Het renderen wordt een XOR-blit byte voor byte, ~23 cycli per niet-nul byte. Een detail dat telt: omdat 240 deelbaar is door 6, behoudt de spookinstantie van de wraparound (±240 px) dezelfde fase — één enkele set bitmaps. De decimatie van de vormen tot 6-7 hoekpunten, in fase 18h opgelegd om de beeldfrequentie te halen, kon weg: de fijne inkepingen van de Atari-vormen kwamen terug, omdat de kost niet langer van het aantal hoekpunten afhangt.
Een verrassing in de profiler, onderweg: sizeof(Asteroid) = 13. Omdat dat geen macht van twee is, dwong elke toegang asteroids[i].veld cc65 tot een aanroep van de softwarevermenigvuldiging mul8x16 — 7,1 % van de CPU-tijd. Itereren via een pointer deed ze uit het profiel verdwijnen. Alles samen brachten fasen 24 tot 26 de vrije CPU-tijd van 17,9 % naar 39,8 %.
3. Tijd: de VSync die niet bestaat
Fase 9 synchroniseerde de lus op de CB1-pin van de VIA, die zogezegd het VSync-signaal van de ULA ontving. Het spel draaide perfect onder Phosphoric. Toen richtte Phosphoric 1.16.11 zich op de hardware, en het spel bleef eindeloos op het titelscherm hangen: op een echte Oric is CB1 niet met VSync verbonden. De emulator pulseerde de pin voor het gemak en maskeerde de fout. De vervanger is Timer 1 van de VIA, vrijlopend op 20 ms — draagbaar naar echte hardware, Phosphoric, Oricutron en MAME, tegen de prijs van een theoretische tearing die nooit is waargenomen.
Er bleef een subtielere timingbug over, aanwezig sinds fase 18 onder de naam “effectief 17 Hz in drukke scènes”. frame_wait() las de teller bij binnenkomst en wachtte dan twee ticks: zolang het werk in de eerste tick paste, gaf het timerraster 40 ms per beeld; zodra een beeld meer dan 20 ms duurde, vertrok het opnieuw voor twee volle ticks — 60 ms in plaats van 40. Fase 28 installeerde een planner met vaste stap en blijvend doel (frame_target += 2), inhalen over de volgende beelden en hersynchronisatie na een lange bevriezing. Adaptieve 50 Hz is bestudeerd en verworpen: alle fysica zit in pixels per beeld, een vijftiental timerfamilies tellen in beelden, en de UFO beweegt ±1 pixel — af en toe naar 50 Hz gaan zou de spelsnelheid verdubbelen tenzij alles herschaald werd. Een constante, betrouwbare 25 Hz is beter dan een wisselvallige, riskante 50 Hz.
4. Flikkering
Op 25 Hz is een object dat aan het begin van een beeld wordt gewist en aan het einde hertekend, ~80 % van de tijd van het scherm afwezig: het oog ziet een aanwezig/afwezig-cyclus op de slechtst mogelijke frequentie. De fix was structureel — erase → tick → draw per entiteit dicht bij elkaar houden en alles wat geen rendering is (geluid, score, tekst) buiten dat venster plaatsen. Voor de asteroïden ging het venster van ongeveer 20 ms naar 7 ms.
De torpedo's vertoonden een gemenere variant, aan het licht gebracht door een tester op echte hardware (“fire beam hardly visible”). De oude pijplijn wiste ze aan het begin van de lus en hertekende ze aan het einde: afwezig uit het videogeheugen tijdens bijna de hele berekening. Op echte hardware drijft de fase tussen de CRT-scan en Timer 1, dus verschenen ze met tussenpozen; onder Phosphoric is de fase vergrendeld en kregen we 0 opnames op 20 met een zichtbare torpedo — terwijl het RAM zei dat ze actief was. Het commit-schema (wissen op de positie van de laatste tekening, meteen daarna hertekenen, schermtoestand losgekoppeld van de logische toestand) gaf 8 opnames op 8. De torpedo's gingen ook naar 2 × 2 px.
5. De ROM die we omzeilen
Het toetsenbord wordt zonder de ROM-buffer gelezen, rechtstreeks uit de matrix — wat veronderstelt dat de matrix begrepen is. Eerste versie fout: de code testte verschillende kolommen met een vast R14-masker. Op de hardware zitten SPACE en de vier pijltjes allemaal op kolom 4 en worden ze onderscheiden door hun rij, gekozen via register 14 van de PSG. Alleen de rechter SHIFT gaf een vals positief. Tweede valkuil, eigen aan de 6522: ORB lezen geeft terug wat op de als uitgang geconfigureerde bits geschreven is, niet de pintoestand. Als de ROM DDRB = $FF heeft achtergelaten, leest PB3 zijn eigen waarde terug en wordt de scan willekeurig — vandaar gemiste schoten en onsamenhangende rotaties. key_scan forceert PB3 nu als ingang zolang de scan duurt.
Derde valkuil, gehoord voordat ze begrepen was: om poort A van de PSG in matrixmodus aan te sturen schreef key_scan elk beeld R7 = $7F — “mixer volledig gedempt”. De zes toon- en ruisbits werden één keer per beeld afgesneden en bleven dat tot de volgende even geluidstick: een hakkeling op 25 Hz van alle effecten, het hoorbaarst bij de lange. Dat was de “audiobug van de schipexplosie” uit de backlog. De fix schrijft de laatste echte mixerwaarde terug, bewaard in een mixer_shadow.
De geluidsspeler onder interrupt (fase 20) vereiste te weten wat de ROM doet voordat ze naar de gebruikersvector springt: niets. De 6502 zet alleen PC en P op de stack; het bewaren van A, X en Y gebeurt binnen de ROM-handler, die we kortsluiten door de vector te patchen. Zonder eigen PHA/TXA/TYA haalt de drievoudige PLA aan het einde P en het terugkeeradres van de stack, en gaat de RTI om het even waar heen. Ook moeten alle andere IRQ-bronnen van de VIA worden uitgeschakeld voordat T1 wordt gewapend, anders vuurt een CA1/CB1/T2-restant van de ROM een handler af die het nooit bevestigt — oneindige lus. Gevalideerd onder Phosphoric met --trace-irq: 493 ingangen, 493 RTI, 19.930 tot 19.970 cycli tussen twee IRQ's.
De handler hangt aan beide vectoren, $0228 (Oric-1) en $0244 (Atmos). Dat volstond niet: met de Atmos-ROM werkt het spel niet, noch onder Phosphoric noch onder Oricutron. Atmos-compatibiliteit blijft een aparte sprint, en de site zegt dat ook.
6. De BSS, twee keer
C gaat ervan uit dat een statische variabele bij de start nul is; de crt0 moet dat garanderen. Fase 9b: met een BSS groter dan $83 bytes ging het HIRES-scherm niet meer aan — de zelfgemaakte zerobss verschilde subtiel van die van cc65, en de oplossing was de officiële routine via initlib over te nemen. Fase 36, een maand later: bij het eerste schot verschijnt een spookblok op positie (85, 85). 85 is $55, het patroon van niet-geïnitialiseerd RAM. De opmerking “initlib roept zerobss aan” was onjuist — cc65 roept ze apart aan — en de BSS werd in feite nog steeds niet gewist. Twee codereviews hadden een opmerking niet herlezen.
7. Het geluid van een andere machine
De kast uit 1979 maakt haar geluiden met discrete analoge oscillatoren; er valt niets te porten. Het vertrekpunt was een andere 8-bitter: Mine Storm, de Asteroids-kloon van de Vectrex (1982). Een andere processor (6809), maar dezelfde AY-3-8912-chip en dezelfde VIA 6522 — de code laat zich niet omzetten, de registerwaarden wel. De tabellen SS.THR, SS.BLT en SS.EXP dienden als eerste benadering.
De rest gebeurde op het gehoor en met het spectrum: FFT-analyse van opnames van de kast. De drie explosies (kleine, middelgrote, grote asteroïde) duren ongeveer even lang en verschillen alleen in ruisfrequentie — ~167, 246 en 306 Hz, dus R6 = 12, 8 en 6. Het schot ging van een lage ruis naar ~740 Hz. De architectuur volgde de kast: drie onafhankelijke circuits worden drie eigen kanalen — A voor de effecten, B voor de thump-thump die nooit door een explosie wordt onderbroken, C voor de schotel — met een mixer die uit de toestand van alle drie wordt herberekend. De titeljingle (Grieg, publiek domein) was drie fasen lang onhoorbaar: de routine bewaarde de nootindex in sound_tmp en riep dan psg_write aan… die sound_tmp als kladruimte gebruikt. De teruggelezen noot was de laatste waarde die naar de PSG was geschreven. Een eigen kladruimte volstond, in BSS geplaatst omdat de nulpagina tot op de byte vol zat.
8. De emulator die liegt, de hardware die antwoordt
Astéroric is ontwikkeld onder Phosphoric, en de twee projecten hebben elkaar gecorrigeerd. De emulator maskeerde de onbestaande VSync (§3), en daarna een verkeerd IJK-model: de eerste joysticklezing liep via register 14 van de PSG, zoals Phosphoric het simuleerde, en werkte niet op de fysieke interface van een tester. Het echte protocol loopt via de printerpoort — VIA-poort A rechtstreeks, PB4-strobe, aanwezigheidsbit 5 — en is gevalideerd tegen Oricutron; Phosphoric is tegelijk gecorrigeerd, met een herhaling van de exacte 6502-sequentie van het spel in zijn tests.
De hardware onthulde ook wat geen enkele emulator kon tonen. Het video-attribuut $1C dat bij de initialisatie wordt geschreven, kiest HIRES op 60 Hz; bit 1 kiest 50 Hz, en er is $1E nodig. De inhoud van het videogeheugen is in beide gevallen identiek, dus de schermopname ook — maar de RGB2HDMI van de tester vergrendelde niet op het signaal. Zonder gevolg voor de spelsnelheid, die door Timer 1 wordt bepaald en niet door de scan. Omgekeerd leverde de emulator de instrumenten: headless opnames bit voor bit vergeleken (make check), een profiel van 25 miljoen cycli, IRQ-tracering, gesymboliseerde RAM-dumps, gescripte --type-keys-scenario's. Met een valkuil: een gescripte toetsaanslag duurt 40 ms, precies één beeld op 25 Hz, en kan bij deterministische uitvoering systematisch tussen twee toetsenbordscans vallen.
9. Wat overblijft
- Atmos: werkt niet met de BASIC 1.1-ROM; nominaal doel Oric-1.
- Beste scores en toetsen alleen in RAM; opslag op cassette/diskette en een Microdisc-
.dsk-image op de roadmap. - Hoogstens zes asteroïden tegelijk — de kast toont er meer.
- De documentatie noemt nog 15-22 cycli per pixel voor de lijnroutine; de tweede review meet er ~45 echte. Nog bij te werken.
game.ctelt 1.429 regels; de hosttests dekken tabellen, nog niet de echte logicabronnen.- De oorspronkelijke code staat onder EUPL 1.2; de logica en vormen die van de Atari-ROM zijn afgeleid, vallen daar niet onder, en daarom heet het spel Astéroric.
Bronnen & links
- Repository: github.com/benedictemarty/oric-asteroids — CHANGELOG, ROADMAP, kaderingsgids, NOTICE.
- Technische notities: VSync en CB1, analyse van de arcadegeluiden, IRQ-validatie.
- Disassemblage van de arcade-ROM rev 4: 6502disassembly.com; vectorvormen: werk van Nick Mikstas.
- Mine Storm (Vectrex, GCE): becommentarieerde bron.
- Ontwikkelemulator: Phosphoric.
- Spelen: handleiding · asteroric.tap.