Od 11 let jsem psal kód výhradně pro ZX Spectrum. Z80 assembler znám slušně (aspoň myslím), paměťová mapa od $4000 mi nečiní problémy (mám downhl, uphl ;) ), a po těch pár demech, které jsem spáchal, jsem se na Spectru cítil pohodlně. Jenže pak přišla myšlenka, která mě zaujala víc, než jsem čekal.
Při jednom rozhovoru se Silliconem padl nápad: co zkusit podat stejné intro na více platformách zároveň? ZX Spectrum, Atari, C64. Zvlášť pikantní na tom bylo, že jsem celé roky dost aktivně podporoval zdravé poštuchování mezi těmito platformami — a teď bych měl programovat pro C64. To mi přišlo natolik vtipné, že jsem to musel zkusit. Popohnala mne i Silliconovo pozitivní věta: "To za měsíc nestihneš!"
Měl jsem hotové rotozoomer intro pro ZX Spectrum a chtěl jsem ho překlopit na C64. Výsledkem je rotozoomer s efektem rozpouštění textu na začátku, pulsujícím zoomem a texturou s nápisem C64, který se vejde do 1 kilobajtu. Trvalo to přibližně měsíc — a během vývoje jsem si průběžně psal tahák, který se z pracovních poznámek vypracoval v docela ucelenou referenci.
První věc, která mě na 6510 udeřila do očí: tři registry. A, X a Y. To je vše. Žádné HL, žádné BC, DE, IX, IY. Na Z80 jsem zvyklý, že když potřebuju 16-bitový ukazatel, prostě naplním HL a jedu. Na 6510 musíš vzít dva bajty kdekoli v paměti, uložit tam adresu a pak použít takzvaný indirect indexed adresovací mód. Vypadá to takto:
lda (ptr),y ; načti bajt z adresy uložené v ptr/ptr+1, posunuté o Y
Na Z80 bych napsal LD A,(HL) a bylo by. Na 6510 je třeba mít tu adresu v Zero Page — což je speciální oblast prvních 256 bajtů paměti, kde jsou instrukce o bajt kratší a o jeden cyklus rychlejší. Zero Page na C64 je v podstatě to, co jsou pro mě registry na Z80. To byl první mentální přerod, který jsem musel udělat.
Druhá věc: ADC a SBC vždy pracují s Carry. Na Z80 existuje ADD A,n, které Carry ignoruje — stačí prostě sečíst. Na 6510 musíš před každým sčítáním nejprve vymazat Carry (CLC), jinak ti přiletí jeden bit navíc a výsledek bude špatně. Po prvních pár ladících sezeních jsem si to zapamatoval bolestivě natrvalo.
Abych si přechod usnadnil, napsal jsem si tahák přímo pro Spectristy. Není to běžná reference 6510 — takových je na internetu plno. Tahák je postavený jako srovnávací tabulka: vlevo, co znám ze Z80, vpravo, jak to vyřeším na 6510. Několik příkladů, které mi nejvíc pomohly:
Na Z80 mám DJNZ loop — sníží B o jedničku a skočí, pokud není nula. Jednoduchá smyčka na N opakování. Na 6510 to samé vypadá takto:
| Z80 | 6510 |
|---|---|
|
|
Žádný dramatický rozdíl, ale když to člověk neví, hledá DJNZ v seznamu instrukcí a nenajde nic.
Na Z80: ADD HL,DE — jedna instrukce. Na 6510 je to celá procedura:
| Z80 | 6510 |
|---|---|
|
|
V rotozoomeru tohle dělám desítky krát za frame — přičítám deltu U a V ke každému pixelu. Bylo důležité vědět, že CLC patří před první ADC, ale před druhé ne — tam naopak Carry z přenosu z nižšího bajtu chci.
Na Spectru mám HALT — procesor se zastaví a čeká na přerušení od ULA, které přichází přesně 50× za sekundu. Na C64 se VSYNC dělá jinak: čteš registr $D012, který obsahuje aktuální rastrový řádek VIC-II, a čekáš, až dosáhne čísla 250 (oblast mimo viditelný obraz):
| ZX Spectrum | C64 |
|---|---|
|
|
Funkčně ekvivalentní — ale kdybych to nevěděl, trvalo by mi to hledat hodně dlouho.
Intro je rozděleno do tří fází:
1. Dissolve efekt — při spuštění se BASIC obrazovka „rozpouští": 150 snímků, každý snímek 100 náhodných znaků přepíšu plným blokem a zbarví na černo. Náhodná čísla generuji pomocí LFSR (Linear Feedback Shift Register) s polynomem $1D — deterministická sekvence, která vypadá náhodně a vejde se do pár bajtů kódu.
2. Intro část — 200 snímků ukazuje texturu s nápisem „C64" s pevným zoomem a plynulou rotací. Záměrně začínám na úhlu 5, ne 0 — v okolí 0°, 90°, 180° a 270° je sinusová tabulka „plochá" a rotace by vypadala trhaně.
3. Hlavní rotozoom smyčka — nekonečná, s pulsujícím zoomem řízeným sinusem, pohybem středu textury a plynulou rotací.
Na C64 je grafika organizovaná jinak než na Spectru. Standardní znakový mód zobrazuje 40×25 znaků, každý 8×8 pixelů. Změna rozlišení nebo přepnutí do bitmapového módu by stála příliš bajtů na inicializaci — a já mám limit 1024 bajtů.
Řešení: Screen RAM na $0400 naplním znakem 160 — to je v PETSCII plný barevný blok. Barvu každého znaku řídí Color RAM na $D800 — každý bajt odpovídá jednomu znaku na obrazovce. Každý znak se tak stane „pixelem" rotozoumeru o rozlišení 40×25. Není to hodně, ale pro efekt rotace a zoomu to bohatě stačí.
Rotozoomer počítá, kam v textuře odpovídá každý „pixel" na obrazovce. Pracuji s fixed-point čísly ve formátu 8.8 — horní bajt je celá část (0–15, indexuje do 16×16 textury), dolní bajt je desetinná část pro sub-pixel přesnost. Pro každý řádek vypočítám počáteční U a V souřadnici, pak přičítám konstantní delty du a dv odvozené z úhlu a zoomu:
du = cos(angle) * zoom / 64
dv = sin(angle) * zoom / 64
Protože cos(x) = sin(x + 90°) a moje sinusová tabulka má 256 hodnot pro celý kruh, platí cos(angle) = sintab[(angle + 64) & 255]. Ušetří to bajty i čas.
Jednou ze záludností, kterou tahák výslovně řeší: když mám 8-bitové znaménkové číslo z sinusové tabulky (třeba -16 uložené jako $F0) a chci ho použít jako 16-bitovou hodnotu, musím správně rozšířit znaménko — horní bajt musí být $FF, ne $00. Na Z80 bych použil BIT 7,A a JP Z,.... Na 6510:
signext:
cmp #$80 ; Carry = 1 pokud A >= 128 (záporné)
lda #0
bcc done ; Kladné -> vrať 0
lda #$ff ; Záporné -> vrať $FF
done:
rts
Bez tahákového vysvětlení, že CMP #$80 nastaví Carry podle znaménka, by mě tohle zabralo zbytečně dlouho.
Absence 16-bitových registrů. Opravdu. Každá 16-bitová operace je na 6510 procedura — load, clear carry, add, store, load, add with carry, store. Při optimalizaci na 1 KB jsem hledal každý bajt a každé místo, kde tohle šlo zkrátit nebo sloučit, bylo vítanou úsporou.
ACME assembler. Na Spectrum používám ASW, který má trochu jiné syntaktické zvyklosti. ACME má lokální návěstí jako - a +, která jsou sice šikovná na krátké smyčky, ale ze začátku matoucí.
Timing a velikost zároveň. Na Spectru se o 1 KB nepohybuji — tam mám prostor. Tady jsem musel myslet na každý bajt. Například místo JSR draw / RTS na konci podprogramu rotozoom jsem napsal JMP draw — úspora dvou bajtů, protože nepotřebuju návratovou adresu ukládat na zásobník.
Netvrdím, že umím 6510 assembler. Zvládl jsem napsat jedno konkrétní intro za přibližně měsíc práce, s tahákovou berlou v ruce. Ale tohle mi přijde jako dobrý výsledek — efekt funguje, vejde se do kilobajtu a já se neskutečně bavil, když byl prezentován na Foreveru - i když jsem tam nebyl osobně přítomen, věřte, že jsem měl rohlík od ucha k uchu a to mi nebylo uplně nejlépe.
Tahák, který jsem si na to napsal si můžete stáhout zde, snad někomu pomůže.
Málem bych zapomněl: moc děkuji Silliconovi za vyzkoušení intra na reálném C64 ještě před odesláním do soutěže na Foreveru. Byl jsem o něco klidnější... ;)

2 Comments
Tak uz aj Shrek nam dezetoval na C64 ! 🙂 😀
Ale nedezertoval… jak píšu z článku, byl to takový hec… 😉
Ale popravdě byla to sranda, užil jsem si to. Škoda, že jsem neudělal ještě Atari verzi, ale byl jsem opravdu KO… Commodore me opravdu vyčerpal 😉