Forever
12. 3. 2026

Celý život jsem psal kód výhradně pro ZX Spectrum. Z80 assembler znám slušně, paměťová mapa od $4000 mi nečiní problémy, a po těch pár demech, které jsem spáchal, jsem se na Spectru cítil relativně pohodlně. Jenže pak přišla myšlenka, která mě zaujala víc, než jsem čekal.

Moje první C64 intro – pohledem Spectristy

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.


Problém: příliš málo registrů, příliš jiná filosofie

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.


Tahák jako most mezi dvěma světy

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:

DJNZ → DEX + BNE

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:

Z806510
  ld b, 100
loop:
  ; ... kód
  djnz loop
  ldx #100
loop:
  ; ... kód
  dex
  bne loop

Žádný dramatický rozdíl, ale když to člověk neví, hledá DJNZ v seznamu instrukcí a nenajde nic.

16-bitové sčítání

Na Z80: ADD HL,DE — jedna instrukce. Na 6510 je to celá procedura:

Z806510
add hl, de
lda num_lo
clc
adc add_lo
sta num_lo
lda num_hi
adc add_hi ; Carry z nižšího bajtu
sta num_hi

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.

Čekání na VSYNC

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 SpectrumC64
halt
cekej:
  lda $d012
  cmp #250
  bne cekej

Funkčně ekvivalentní — ale kdybych to nevěděl, trvalo by mi to hledat hodně dlouho.


Architektura intra

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í.

Color RAM trik

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čí.

Fixed-point matematika

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.

Rozšíření znaménka

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.


Co mi dalo největší práci

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.


Co jsem se naučil

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

  1. Busy napsal:

    Tak uz aj Shrek nam dezetoval na C64 ! 🙂 😀

    • Shrek napsal:

      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 😉

Napsat komentář: Shrek Zrušit odpověď na komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *