ZX Spectrum Next / hardware
Copper, aneb jak nechat obrazovku, ať se mění sama
Jak se tak více ponořuji do hardware počítače ZX Spectrum Next, jsem nucen si zde odkládat svoje poznatky o nabytých zkušenostech. Jak jsem již uvedl, dělám na demu pro demo párty Forever, ale jelikož ten je až za dlouhou dobou, rozhodl jsem se (jestli to stihnu) vytvořit takové oldschool demo pro jinou demopárty, která se koná za cca 2 měsíce. A co by to bylo za oldschool demo, bez pohybujících se barevných barů, scrolltextů, které se vlní atd... A tak jsem tak nějak narazil na copper. Copper je nejmenší koprocesor, jaký v Nextu je, a zároveň ten, který nejvíc mění to, jak demo vypadá. Umí přesně dvě věci: počkat, až paprsek dojede na určené místo obrazovky, a zapsat hodnotu do některého Next registru. Žádné počítání, žádné skoky, žádná paměť, ze které by mohl číst data. Zní to jako málo, jenže Next registry ovládají palety, posuny vrstev, jejich pořadí, průhlednost i rozlišení, takže věta "na řádku 100 zapiš do registru X" znamená ve skutečnosti "od řádku 100 dolů se obrazovka chová jinak". A protože to všechno dělá hardware sám, procesor u toho nemusí být - Z80 mezitím může počítat další snímek, nebo klidně nedělat vůbec nic.
Zkuste si představit, jak by se stejná věc dělala bez něj. Musíte trefit okamžik, kdy je paprsek na konci řádku, což znamená buď hlídat přerušení na daném řádku, nebo počítat takty, a pak v tom správném mikrookamžiku poslat zápis na port. Za snímek to chcete udělat sto devadesát dvakrát, takže procesor je celou dobu, co se kreslí obraz, přivázaný k paprsku a nedělá nic jiného. Navíc každé zdržení - obsluha přerušení, o takt delší cesta smyčkou, pomalejší instrukce na hranici stránky - je vidět jako roztřepený okraj pásu, protože zápis dorazil o kousek dřív nebo později než minule. Copper tenhle celý problém odstraňuje, ne zrychluje: seznam příkazů připraví procesor a hardware ho pak vykonává deterministicky podle polohy paprsku. WAIT pracuje s vodorovnou pozicí po osmi pixelech, takže nejde o libovolnou přesnost „na jeden pixel“.
Odkud to je
Nápad není nový a Next se tím netají. Amiga měla v roce 1985 v čipu Agnus copper, jehož program (copper list) ležel v chip RAM a odjížděl se od začátku každý snímek. Uměl tři instrukce: MOVE zapsal hodnotu do registru vlastních čipů, WAIT čekal na pozici paprsku a SKIP přeskočil následující instrukci, pokud už paprsek byl za daným místem. Tím se dělalo skoro všechno, co dnes máme spojené s tím, jak vypadalo amigovské demo: barevné pásy přes celou obrazovku, obloha s víc odstíny, než měla paleta barev, rozdělení obrazovky na dvě části s jiným rozlišením, posouvání ukazatelů na bitplány uprostřed obrazu, a dokonce i odstartování blitteru na určeném řádku. Copper byl tak trochu druhý program běžící vedle toho vašeho, jen psaný v jazyce o třech slovech.
Next si z toho vzal jádro věci a pár věcí vynechal. Copper list nesedí v běžné paměti, ale v 2 KB, které jsou uvnitř FPGA, takže ho tam procesor musí nahrát bajt po bajtu; zato se o něj nikdo nepere a nezabírá vám banku. Instrukce jsou jen dvě, WAIT a MOVE - SKIP tu není a chybět vám nebude, protože podmínky si stejně vyhodnotíte na procesoru při stavbě seznamu. Zapisovat umí jen do Next registrů, ne do paměti, takže s ním nepřesunete data ani nespustíte DMA. A hlavně, copper na Nextu se nemá čemu koukat přes rameno: nemá vlastní paměť dat, takže veškerá "logika" efektu je v tom, jaký seznam mu procesor pošle. Práce se tím rozdělí velmi čistě - Z80 rozhoduje, copper vykonává.
Jak copper vypadá zevnitř
Paměť copperu je 1024 instrukcí po dvou bajtech, dohromady 2048 bajtů. Rozdíl mezi WAIT a MOVE je nejvyšší bit prvního bajtu, což je celé dekódování. MOVE má v prvním bajtu číslo Next registru (0 až 127, proto se do něj vejde bez nejvyššího bitu) a ve druhém hodnotu, která se do něj zapíše. WAIT má v prvním bajtu nastavený nejvyšší bit, pod ním šest bitů vodorovné pozice a úplně dole devátý bit čísla řádku; druhý bajt je spodních osm bitů řádku. Vodorovná pozice se zadává po osmi pixelech; v praxi se používají hodnoty 0 až 56. Svislá souřadnice má rozsah 0 až 311. Přesné časování vůči viditelné části obrazu závisí také na zvoleném video timingu, takže je lepší chápat tyto hodnoty jako souřadnice rasteru, ne jako prostý rozměr obrazovky.
U instrukce WAIT je ještě jeden detail, který se hodí znát. Vertikální část čekání míří na konkrétní řádek, ale horizontální podmínka funguje jako >=. Pokud tedy copper na řádku 123 nejdřív provede WAIT l=123,h=30 a hned za ním následuje WAIT l=123,h=20, druhý WAIT už nebude čekat na další snímek. Paprsek je stále na řádku 123 a vodorovně už je na pozici 30 nebo dál, takže podmínka h >= 20 je splněná a instrukce pokračuje bez dalšího čekání. Je dobré s tím počítat hlavně při skládání více WAITů pro stejný řádek: jejich vodorovné pozice by měly jít dopředu, ne zpátky.
; MOVE - zapiš hodnotu do Next registru
db NR_FALLBACK, $A8 ; bit 7 = 0, takže registr $4A dostane $A8
; WAIT - počkej na pozici paprsku
db $80 | (44 << 1), 99 ; vodorovně 44 (= 352 px), řádek 99
; a idiom na konec seznamu:
db $FF, $FF ; čekej na řádek 511 na pozici 63 - ten nikdy
; nepřijde, takže copper do konce snímku stojí
Ovládání je soustředěné hlavně do registrů $60, $61 a $62. Registry $61 a spodní tři bity $62 určují adresu v 2KB paměti Copperu, kam se nahrává program, a každý zápis do $60 uloží jeden bajt a adresu automaticky zvýší. Horní dva bity $62 určují režim běhu: %00 Copper zastaví, %01 vynuluje jeho program counter a spustí ho, %10 pokračuje z aktuální pozice a %11 ho spustí od začátku a při každém vertikálním blanku znovu nastaví program counter na nulu. Poslední režim je pro klasický seznam opakovaný každý snímek nejpohodlnější. Důležitý detail je, že zápis stejné hodnoty řídicích bitů znovu nemění režim Copperu; lze tedy měnit uploadovací index bez automatického zastavení běžícího programu. Při přepisování aktivního seznamu je ale stejně rozumné postupovat opatrně, případně použít registr $63, který zapisuje celou šestnáctibitovou instrukci po dvojici bajtů.
Ještě dvě věci, o které se člověk spálí hned na začátku. Za prvé, čekejte na vodorovné pozici, která je už za obrazem - v ukázce je to 44, tedy 352 pixelů, kde se zrovna nic nekreslí. Kdybyste čekali na pozici 0, zápis dopadne doprostřed viditelné části řádku a změna palety se projeví jako svislý zub uprostřed obrazovky. Za druhé, počítejte s rozpočtem. Tisíc dvacet čtyři instrukcí zní jako hodně, ale při 192 řádcích to vychází na pět instrukcí na řádek, tedy jedno čekání a čtyři zápisy. Kdo chce přepsat celou šestnáctibarevnou paletu, nemůže to udělat na každém řádku, ale klidně na dvaceti osmi pásech přes celou obrazovku, a to je pořád mnohem víc barev, než by paleta sama unesla. Rychlost samotného Copperu bývá pro běžné řádkové efekty dostatečná, ale není neomezená. MOVE i WAIT mají vlastní časování a při větším počtu změn na jednom řádku je potřeba počítat nejen kapacitu 1024 instrukcí, ale i okamžik, kdy se jednotlivé zápisy skutečně provedou.
Na co to celé je
Nejběžnější použití je paleta po řádcích a je to zároveň to, které dá nejvíc muziky za nejmíň práce. Obrázek se nakreslí jednou a nikdy se nepřekresluje; copper pak mění, co ty barvy znamenají, jak se jde po obrazovce dolů. V oldschool demu co tvořím, to takhle dělá část s mořem: snímky moře nesou šestnáct úrovní jasu, a copper těch šestnáct položek přepíše osmadvacetkrát shora dolů, takže nahoře je obloha studená a tmavá, kolem obzoru se táhne teplý opar a v blízké vodě je všechno hluboké a matné. Šestnáct indexů, a na obrazovce mnohonásobně víc barev, než kolik má paleta položek. Podobně funguje pozadí v jiné části dema: na LoRes obrazovce leží statická dvoubarevná tkanina, copper na každé druhé řádce přepíše ty dvě barvy, a z nehybné textury je najednou tekoucí gradient, který nestojí ani takt procesoru.
Druhá velká rodina efektů jsou registry posunu. Tilemap i Layer 2 mají vodorovný scroll jako obyčejný registr, takže když ho copper přepíše na každém řádku podle sinusovky, celá vrstva se rozvlní - a nikdo nepřekresluje ani pixel, protože se mění jen to, odkud si hardware bere data. Přesně tak se v jedné části dema naklání titulek na Layer 2, jen tam je místo sinusovky tabulka posunů po pásech. Co se tímhle způsobem dá dělat dál, je hlavně otázka toho, který registr vás napadne: přepnout uprostřed obrazovky pořadí vrstev, takže nahoře jsou sprity nad Layer 2 a dole pod ním; posunout paletový offset LoRes a přebarvit tím celý pás; změnit průhlednou barvu; přehodit stránku obrazu přesně na řádku, kde to není vidět; nebo v jednom snímku ukázat dvě různá rozlišení nad sebou.
Za tím vším je jedna společná myšlenka, kterou stojí za to vyslovit nahlas, protože mění způsob, jak se efekty vymýšlejí. Copper nekreslí. Copper mění pravidla, podle kterých se kreslí, a to řádek po řádku. Jakmile si tohle člověk srovná, začne se dívat na seznam Next registrů jako na seznam věcí, které se můžou po výšce obrazovky měnit zadarmo, a hledá efekt tam, kde by ho jinak nehledal - třeba místo dvanácti barev v obrázku začne uvažovat o dvanácti barvách na každém pásu. Praktický důsledek je taky ten, že efekt je najednou tabulka: animace znamená postavit v každém snímku o kousek jiný seznam, a to je pár set instrukcí Z80 nad tabulkou v paměti, ne kreslení do obrazovky. Nic se netrhá, samotné provedení seznamu je deterministické a není závislé na tom, jestli Z80 v daném okamžiku stíhá obsluhovat raster.
Ukázka: šest pruhů a nic jiného
Zde si můžete stáhnout ukázkový kód se zdrojákem, což je samostatný program, ve kterém je vlastního kódu necelých tři sta bajtů a všechno ostatní jsou tabulky a dva buffery. Přeloží se do copper_bars.nex a spustí se z NextZXOS i z emulátoru. Dělá to, co se od copperu čeká nejdřív: šest barevných pruhů plave nahoru a dolů po sinusovkách přes pomalu tekoucí pozadí. Fáze pruhů jsou na začátku rozložené po dvaceti krocích, takže první snímek vypadá jako schodiště, a rychlosti se liší jen o pár setin řádku za snímek - vlna se proto během půl minuty rozpadne, projde všemi možnými překryvy a zase se složí. Tohle je na celém efektu to jediné, co je opravdu potřeba vyladit: kdyby měly pruhy stejnou rychlost, formace stojí na místě a je to nuda po deseti vteřinách, a kdyby se rychlosti lišily o hodně, rozsype se to hned a nikdy nevznikne nic, co by vypadalo záměrně. Právě proto je fáze i krok šestnáctibitový, v pevné řádové čárce 8.8 - na celé řádky za snímek by se takhle jemný rozdíl nedal nastavit. Zajímavější než to, co dělá, je ale to, co nedělá - na obrazovce není ani jeden pixel a program nikdy nesáhne na obrazovou paměť. Všechny vrstvy jsou totiž vypnuté včetně ULA, a když nic nekreslí žádná vrstva, posílá Next do televize barvu z registru $4A. Ten jediný registr Copper přepisuje po řádcích, takže viditelná plocha efektu je v podstatě jen tabulka barevných hodnot bez klasického framebufferu.
build_copper:
ld hl,copper_list
ld de,line_buf
ld (hl),NR_FALLBACK ; řádek 0 stihneme ještě v zatemnění
inc hl
ld a,(de)
inc de
ld (hl),a
inc hl
ld b,LINES-1 ; řádky 1 až 191
ld c,0 ; a rovnou to číslo, které jde do WAITu
.line:
ld (hl),$80 | (COPPER_WAIT_H << 1) ; WAIT, vodorovně 44
inc hl
ld (hl),c ; na konci předchozího řádku
inc hl
inc c
ld (hl),NR_FALLBACK ; MOVE $4A, barva toho řádku
inc hl
ld a,(de)
inc de
ld (hl),a
inc hl
djnz .line
Skládání barev je záměrně to nejhloupější, co jde vymyslet, protože právě to je na tom hezké. Nejdřív se do bufferu opíše pozadí, což není žádný výpočet, jen výřez z delší tabulky, který se snímek od snímku posouvá o kousek dál a tím gradient teče. Pak se přes něj nakopíruje šest ramp pruhů, každá 16 bajtů, na místo, které vyšlo ze sinusové tabulky - pozdější pruh přepíše dřívější, takže překryv řeší pořadí zápisu a žádné míchání barev se nekoná. Výsledný seznam má 772 bajtů, tedy necelých 38 % z 2KB paměti Copperu, a v této ukázce se nahrává po HALTu v části snímku mimo aktivní obraz. Celý snímek stojí zlomek procesorového času a zbytek by v opravdovém demu zbyl na to, co se pod pruhy odehrává.
Jedna věc na ukázce je zjednodušení, o kterém je dobré vědět, než ji použijete jako kostru pro něco vlastního. Registr nahradní barvy je osmibitový, tedy tři bity červená, tři zelená a dva modrá, a na tmavém modrém gradientu je to znát jako viditelné pásy; skutečná paleta Nextu má devět bitů a přechody jsou na ní podstatně hladší. V reálné části dema byste proto místo $4A psali položku palety, což stojí tři zápisy místo jednoho (index, horní bajt, devátý bit), a hlavně by pod tím byl vidět skutečný obraz, ne prázdná plocha. Přechod z jednoho na druhé je ale otázka přepsání jedné rutiny - zbytek, tedy stavba tabulky, časování na HALT a nahrání seznamu v zatemnění, zůstane přesně takový, jaký je.

