VBI, DLI, problikávání a co s tím

raster/c.p.u., 2009

Každý kodér na malém Atárku narazí dříve či později na nějaké záhadné problémy. Zdá se, že jeho program nedělá to co by měl, něco se někde přepisuje, procesor zatuhává na kódu v místech, kam by se teoreticky nikdy neměl dostat, a tak podobně. Tohle bývají chyby, které člověk hledá třeba i hodně dlouho, ale obecná rada, jak je nedělat nebo jak je rychle najít, neexistuje.

Pak je však jiná skupina problémů, se kterými programátoři velmi často bojují (podle e-mail dotazů co mi chodí), přitom jsou zbytečné a lze se jich vyvarovat. Mluvím o problikávání obrazovek při změnách DListu a o zápasení s rutinami ve VBI či DLI (oboje jsou tzv. NMI).

Často je důvodem jen pouhá neznalost základních principů, případně víra v mylné informace, které jsme zaslechli někdy před mnoha lety, ale věříme jim dodnes. Mezi nejčastější mýty třeba patří, že VBI se provádí jen v době kdy je vykreslovací paprsek mimo viditelný obraz, nebo že využitím kódu ve VBI získáme nějaký výpočetní výkon navíc (cca 20000 taktů), který by se jinak promrhal.

Přitom si stačí uvědomit, že to "I" ve zkratkách VBI, DLI, znamená interrupt, neboli přerušení. Procesor tedy v tomto okamžiku přeruší práci na kódu, který zpracovával doposud, odskočí si někam jinam, a poté se opět vrátí k původnímu kódu. Aby věděl, kam se má na konci přerušení vrátit, musí si na zásobník uložit návratovou adresu (stejně jako při JSR instrukci), navíc však i "flag" status (jako instrukce PHP). Proto se rutiny pracující v přerušení ukončují instrukcí RTI, která ze zásobníku vybere a obnoví status a návratovou adresu, narozdíl od RTS, které použije jen návratovou adresu. Je to nutné proto, aby původní kód mohl správně pokračovat, neboť přerušení může nastat v libovolném okamžiku. Ze stejného důvodu je nutné v rutině přerušení zachovat obsah všech registrů (A,X,Y), proto ty, které chceme využívat, musíme též nejprve uschovat (nejčastěji na zásobník) a před koncem rutiny přerušení jejich obsah obnovit.

Okamžik, kdy se NMI přerušení vyvolávají, je přesně daný. U DLI je to na začátku posledního mikrořádku displaylist instrukce, která má nastaven sedmý bit. Toto je také častý důvod zápasení s kódem, kdy si programátor nastaví v displaylistu u prvního textového řádku sedmý bit a diví se, proč jeho rutina mění barvu až od řádku druhého.

VBI se vyvolává na začátku vertikálního zatemnění, což je, zjednodušeně řečeno, na prvním mikrořádku pod spodní viditelnou hranou obrazu. To je však jen jeho začátek a nikde není zaručeno, že bude tato rutina i v době mimo viditelný obraz dokončena - záleží jen na její délce. Opět velmi častý údiv programátora, jakto že se činnost jeho kódu ve VBI projevuje během vykreslování. A odpověď jednoduchá: Má rutinu příliš dlouhou, takže se provádí i během vykreslování dalšího snímku. Oněch často zmiňovaných 20 tisíc taktů pro rutinu ve VBI vychází z prostého zaokrouhleného výpočtu: 105 taktů na mikrořádek krát 312 mikrořádků (PAL), mínus cca 8000 taktů DMA ANTICu během vykreslování obrazu, mínus cca 1000 taktů činnost OS ROM, rovná se 23760, tedy zhruba 20 tisíc. Z nich však jen cca pouhých 6 tisíc ((312-256)*105) připadá na dobu před začátkem vykreslování dalšího snímku, delší akce už mohou být "viditelné".

Přitom VBI i DLI jsou nemaskovatelná (NMI), tudíž následující DLI může přerušit i předcházející DLI, případně se mohou navzájem přerušovat s VBI a naopak. Pokud to tedy například s délkou VBI přeženete drsně, může vám jeho provádění přerušit VBI zahájené na konci následujícího snímku, které bude opět tak dlouhé, že ho přeruší až další VBI, a další... a jestli nedošlo k zamrznutí, přerušují se dodnes. ;-)

Využitím rutin v přerušení se tedy nezískává žádný výkon navíc, ale sebere se "hlavní" rutině.

Od správného pochopení výše uvedených skutečností je již jen krůček k úspěšnému vyřešení všech problémů druhého typu, což je nežádoucí ošklivé "probliknutí" či nepochopitelné občasné zátuhy při změně displaylistu a/nebo vektorů přerušení. Problém je v chybějící synchronizaci.

Jak je známo, vektor pro displaylist se nastavuje přes adresy $230 a $231. Změna obsahu těchto buněk však sama o sobě vůbec nic neprovede. Teprve až při prvním následujícím VBI, když se vyvolá příslušná OS ROM rutina, se v rámci jejího kódu obsah buňky $230 zkopíruje do $d402 a obsah $231 do $d403. Až toto je skutečný vektor pro displaylist čipu ANTIC, jehož změna se projeví vykreslováním odpovídajícího displaylistu.

Metoda zápisu na dolní registry a přepisu ve VBI do registrů čipu se používá proto, neboť kdybychom měnili přímo obsah registrů čipu zrovna během vykreslování, začal by se od toho okamžiku vykreslovat nový obraz, klidně od poloviny rozkresleného aktuálního snímku. VBI nám tedy zajistí, že se tento přepis provede až po dokončení vykreslování, a projeví se to až pěkně shora od začátku následujícího snímku. Jenomže na něco jsme zapomněli.

Pokud nebudeme činnost našeho kódu žádným způsobem synchronizovat s vykreslováním, může dojít k VBI zrovna v té nešťastné chvíli, kdy máme teprve nastaven jeden ze dvou dolních registrů adresy displaylistu. OS ROM rutina tedy zapíše ANTICu nižší bajt hodnoty pro nový displaylist, zatímco vyšší bajt nastaví ještě dle starého displaylistu. Výsledkem je vykreslení celého následujícího snímku obrazu displaylistem dle obsahu paměti [nový nižší bajt, předchozí vyšší bajt], což se může projevit velmi divoce, a až po tomto snímku, kdy dojde k dalšímu VBI, se přepíše správně i ten vyšší bajt a teprve další snímek bude správně.

Obdobně nehezky může dopadnout, když například před VBI stihneme nastavit oba bajty displaylistu, ale zápis barvových registrů (adresy $2c0 až $2c8) nebo přepnutí šířky obrazu (adresa $22f) se již před tím samým VBI nestihne. Výsledkem je probliknutí jednoho snímku se špatnými barvami, neodpovídající šířkou obrazu atd. Pravděpodobnost, že k tomu dojde, nemusí být ani příliš velká, ale tím hůře pro programátora. Probliknutí barev nebo jeden snímek zmateného obrazu by se ještě s přimhouřením obou očí dalo strpět, jenomže důsledky mohou být fatální. Například může displaylist z nesprávné adresy obsahovat hodnoty vyvolávající několik DLI, s jejichž vyvoláním se nepočítalo, případně zmatený obraz může nastavit obsahy PMG kolizních registrů, ze kterých se vyvodí kolize k nimž dojít nemělo atd.

Chudák programátor pak stojí před problémem, že "vždycky to fungovalo" (i když to výjimečně nějak divně probliklo), ale teď to občas zatuhává, nebo třeba při hře raketka vybuchuje, aniž by k tomu měla mít důvod.

Rada zní jednoduše: Neignorujte podivné nežádoucí jevy a snažte se zjistit kdy a proč k nim dochází.

A jak takové chyby odstranit, nebo lépe vůbec nedělat? Je důležité si důsledně uvědomovat, zda provádíte změnu dolních registrů, jejichž přepis do čipů se provádí až ve VBI, nebo zda nastavujete přímo horní registry čipů. A vždy synchronizovat změny s vykreslováním!

Pokud před inicializační část přidáte třeba jen jednoduchou synchronizační podmínku

lda $14
waicmp $14
beq wai

máte zaručeno, že váš kód bude proveden vždy ihned po dokončení VBI OS ROM rutiny (která obsah registru $14 inkrementuje). Pokud ve VBI neprovádíte i nějakou vlastní akci, zbývá procesoru do okamžiku začátku vykreslování následujícího snímku cca 6000 taktů, tedy dost času na provedení změn mimo viditelnou oblast a bez hrozby přerušení inicializace dalším VBI. Žádná ze změn dolních registrů se však neprojeví ihned v následujícím snímku, ale až v o jedna dalším. Proto musíte buďto provést změny současně v dolních i ve sledujících registrech, nebo na uskutečnění změny počkat ještě jeden snímek. První synchro čekání tedy zajistí, že náš kód nebude přerušen VBI, a po druhém čekání si můžeme být jisti, že již všechny přepisy hodnot z dolních registrů byly (během toho následujícího VBI) provedeny.

Příklad využití dvou synchronizací:

;1.synchro
lda $14
wa1cmp $14
beq wa1
;
;displaylist
lda #dlist
sta $231
;
;barvy
lda #$04
sta $2c5
lda #$08
sta $2c6
lda #$0c
sta $2c7

Nyní jsou hodnoty adresy nového displaylistu a třech barvových registrů zapsány do dolních registrů, následující celý snímek však bude ještě vykreslen starým displaylistem a předchozími barvami. Počkáme tedy na dokončení tohoto snímku a provedení následujícího VBI:

;2.synchro
lda $14
wa2cmp $14
beq wa2

Nyní je již po dalším VBI, takže všechny hodnoty z dolních registrů byly přepsány do sledujících. Následující snímek tedy bude vykreslen celý správně naším novým displaylistem a se všemi změněnými barvami.

Neberte prosím zmíněné příklady jako nějaké dogma. Jde jen o to uvědomit si, že pokud měníte dolní a horní registry související s vykreslováním obrazu bez jakékoliv synchronizace, koledujete si v lepším případě o nehezky problikávající zmatený snímek, častěji však i o záhadné nahodilé zátuhy. Je přitom úplně jedno, zda budete synchronizovat přes zjišťování změny hodnoty adresy $14 nebo třeba porovnáváním obsahu VCOUNT registru (čítač vertikálních řádků) s nějakou konstantou, ale mějte vždy na paměti, které registry měníte a kdy se tato změna skutečně projeví.