Flashback na ZX Spectrum Next / 2. díl
Flashback na ZX Spectrum Next – 2. Grafika a vrstvy
V prvním díle jsem rozebíral cutscény – vektorovou animaci, kterou Z80 kreslí pixel po pixelu sám. Tentokrát jde o pravý opak: o grafiku, kterou kreslit vůbec nechci. Místnosti, postavy, předměty a ikony má Flashback uložené jako hotové obrázky a Next má na hotové obrázky hardware – Layer 2, sprity a tilemapu. Otázka tedy nezní „jak to nakreslit“, ale „do čeho to převést, aby to nakreslil hardware sám“.
A ukázalo se, že nejtěžší není převod pixelů, ale pořadí. Původní hra kreslí všechno do jednoho obrazu a kdo je vpředu, rozhoduje, kdo se kreslil poslední. Next skládá vrstvy sám a na pořadí kreslení mu nezáleží vůbec.
Co má Flashback a co má Next
Flashback kreslí do jednoho obrazu 256 × 224 bodů. Napřed místnost, přes ni postavy a předměty, nakonec ikony a texty. Místnost je celý obrázek, žádná skládačka z dlaždic. Postavy jsou zabalené bitmapy v PERSO.SPR a ve čtyřech souborech příšer. Předměty – všechno, co není postava – jsou poskládané z malých obdélníků vyříznutých z dlaždicových bank levelu. Ikony jsou čtverečky 16 × 16 v GLOBAL.ICN. A barvy nejsou jedna paleta, ale šestnáct slotů po šestnácti barvách: bajt pixelu je (slot << 4) | barva, a jak uvidíme, právě v tom slotu je schovaná polovina celé hry.
Next má pro tohle čtyři vrstvy: Layer 2 (bitmapu s bajtem na pixel), 128 hardwarových spritů, tilemapu a starou obrazovku ULA. Rozdělení vyšlo skoro samo. Místnost se nehýbe, takže jde do Layeru 2 v režimu 320 × 256. Všechno, co se hýbe, jsou sprity. A tilemapa i ULA zůstávají vypnuté – u tilemapy to nebylo z lenosti a dostanu se k tomu. Pomáhá i to, že Flashback nescrolluje: místnost se přepíná celá, takže odpadá celý aparát, který by jinak stál nejvíc času.
Místnost: čtyři pruhy, ne čtyři roviny
Místnosti levelu jsou v jednom souboru .MAP: 64 položek po šesti bajtech, znaménkový 32bitový offset a délka. Nulový offset znamená nepoužitou místnost – levely jich používají 26 až 56 –, kladný zabalenou a záporný nezabalenou. Na offsetu jsou čtyři čísla palet a za nimi čtyři pruhy, každý zabalený jednoduchým RLE (znaménkový bajt: záporný opakuje, kladný kopíruje) a každý rozbalený na přesně 14 336 bajtů. Čtyřikrát 14 336 je 256 × 224.
A tady jsem poprvé narazil na to, že ani referenční zdroják není bible. REminiscence má pro obě varianty jinou představu, jak ty čtyři pruhy poskládat: zabalená větev je klade za sebe, nezabalená je prokládá po sloupcích jako VGA mode X – pixel x v rovině x & 3. Obojí vypadá rozumně a obojí se dá obhájit. Rozhodlo až to, že jsem nakreslil obojí: prokládaná verze dá čtyři hřebeny šumu. Ve všech 193 místnostech DOS verze přitom není jediná nezabalená, takže druhou větev nemá co potvrdit ani vyvrátit. Vtipné je, že obrázky menu mode X opravdu jsou – stejná přípona, jiný formát.
Rozbalená místnost je pak prostě 57 344 bajtů po řádcích a bajt pixelu nese slot a barvu. Místnosti používají sloty 0–3 a 8–11, změřeno přes všech 193. Sloty 8 a výš jsou tytéž čtyři palety ještě jednou, jenže s nastaveným sedmým bitem – a ten bit je nejdůležitější věc v celém tomhle dílu. Ale nejdřív, kam to uložit.
Uložená tak, jak ji čte Layer 2
Layer 2 v režimu 320 × 256 ukládá obraz po sloupcích – z prvního dílu víme, že sloupec je 256 souvislých bajtů a spodní bajt adresy je řádek. Místnost má jen 224 řádků, ale převodník ji stejně ukládá po celých 256: každý sloupec dostane šestnáct řádků okraje nahoře a šestnáct dole. Stojí to 7 kB na místnost a ušetří to dělení 224 u každého sloupce. Místnost má pak přesně 64 kB, tedy osm 8KB stránek, a protože herní plocha začíná na sloupci 32 a 32 sloupců je jedna stránka, padne každá stránka místnosti přesně na jednu stránku obrazovky. Zobrazit místnost znamená osm kopií po 8 kB a žádný přepočet souřadnic.
Zabalit jsem to nechtěl a změřil jsem si proč. Původní balení dostane všech 193 místností na 4,4 MB, moje RLE v řádkovém pořadí na 5,3 MB a totéž RLE ve sloupcovém pořadí na 5,7 MB – sloupce se balí hůř, protože běhy v téhle grafice jsou vodorovné. Ani jedno se do paměti Nextu nevejde, takže se místnosti stejně čtou z karty. A když se čtou z karty, je místo na kartě zadarmo a čas Z80 ne. Soubor rooms.bin má tedy 12,1 MB nezabalených místností za sebou a změna místnosti je jeden seek a osm čtení rovnou do stránek Layeru 2, bez jediného dekodéru. K tomu 512 bajtů palety z druhého souboru – nová místnost ve starých barvách není jemná chyba, je to celá obrazovka špatně.
Barvy jsou dvanáctibitové 0RGB jako big-endian slova, šestnáct na paletu, a u místností mají prohozenou červenou a modrou. Conradova paleta, která je ve zdrojáku enginu a ne v datech hry, prohozená není – stejná převodní funkce, jiný příznak. Na Nextu pak každá složka ztratí jeden bit, protože jeho paleta má devět bitů a ne dvanáct. To žádná kontrola neodstraní, takže všechny porovnávají proti redukované barvě a netváří se, že se nic neztratilo.
Náhled z převodníku přitom nedokazuje nic: sdílí s převodníkem všechny předpoklady, takže místnost transponovaná obráceně by vyšla stejně špatně v obou. Kontrola proto čte hotové soubory tak, jak je čte hardware – místnost r je bajt r × 65 536, pixel X, Y je offset (X − 32) × 256 + Y, ten bajt je položka palety a ta je barva – a porovná to s obrázkem rozbaleným z .MAP jinou cestou, po řádcích a bez transpozice. Shodnout se můžou jen tehdy, když sedí transpozice, stránky, sloty palet i redukce barev zároveň. Sedí všech 193 místností a šest z nich se navíc fotí v emulátoru a porovnává pixel po pixelu.
Popředí je jeden bit
Conrad v džungli prochází za listím, za kmeny a za zábradlím. Flashback to řeší ve svém blitteru: když kreslí postavu, odmítne přepsat pixel, který má v cíli nastavený sedmý bit (dst[i] & 0x80). Pixely ze slotů 8 a výš jsou tedy popředí a postava zajde za ně. Na Nextu má každá položka palety Layeru 2 takzvaný priority bit – horní bit druhého bajtu – a pixel s takovou barvou se kreslí nad všechno ostatní, sprity včetně. Je to stejné pravidlo: v obou případech rozhoduje horní bit. Převodník proto nastaví priority bit na položkách 128 až 255 a za běhu se popředí nemaskuje vůbec. Z80 o něm neví.
Není to detail. Přes všech 193 místností je popředím 19,5 % pixelů, v nejhorší místnosti 58 %, a bez popředí je jediná. Maskovat to programem by znamenalo u každého pixelu postavy nahlédnout do místnosti – přesně tu práci, kterou na Nextu dělá hardware zadarmo.
Ten samý bit pak vyřešil dvě věci, které s popředím na první pohled nesouvisí. První je okraj. Místnost má 256 sloupců a obrazovka 320, takže po stranách zbývá po 32 sloupcích, a postava, která odchází z místnosti, má zmizet v okraji, ne stát na černé vedle obrazu. Sprity mají vlastní ořezové okno – jenže to se mi v emulátoru rozchodit nepodařilo: okno 0, 0, 0, 0, které by mělo schovat všechny sprity, nezměnilo nic, plocha spritů měla pořád stejných 7 399 měřených pixelů, ať jsem nastavil kterýkoli z dokumentovaných bitů. Okraj Layeru 2 je tedy vyplněný černou na položce 192, která priority bit má, a postava za okraj prostě zajde. Je to totéž z druhé strany a stojí to na hardwaru, který už byl ověřený.
Druhá věc je HUD – ikona předmětu v ruce, ikona a název toho, na čem Conrad stojí, příběhové texty a inventář. Nabízí se na to sprity nebo tilemapa, a obojí by skončilo za listím, protože popředí místnosti je nad nimi. Kreslí se to proto do Layeru 2: ikony na položky od 0xA0, text od 0xE0, rámeček inventáře od 0xF0 – všechno s priority bitem, všechno nad vším, přesně jako v originále, který je kreslí poslední. Kousek místnosti pod nimi se uloží do vlastní stránky a při zmizení se vrátí. Palety textu a inventáře jsou zapečené do palety každé místnosti, takže je přinese každá cesta, kterou se paleta dostane do hardwaru.
Tahle kontrola měla past zabudovanou přímo v sobě: místnost bez postav vypadá úplně stejně, ať je priority bit nastavený, nebo ne – na obrazovce není co překrýt. Snímek místnosti by prošel i bez něj, a prošel by z úplně špatného důvodu. Proto existuje zvláštní build, který přes místnost položí plochu ze všech 128 spritů, a měří se, které pixely ji přebily: pixely popředí musí, ostatní nesmí. Šest místností, nula chyb – a teprve teď to něco znamená.
Proč zůstala tilemapa vypnutá
Tilemapa je na Nextu nejlevnější způsob, jak dostat na obrazovku cokoli, co se dá poskládat z kostiček 8 × 8: jeden bajt místo šedesáti čtyř pixelů. V tomhle portu ji nepoužívám vůbec, a nebylo to tak, že bych na ni zapomněl. Kandidátem byla na dvou místech a pokaždé ji vyřadil jiný důvod.
HUD vyřadil priority bit, jak jsem popsal: dlaždice by byla za listím. Texty v cutscénách vyřadila geometrie písma. Když se ukázalo, že text je nejdražší věc v přehrávači – ve scéně MEMO stál 17 % času, kdežto výplň polygonů v téže scéně zhruba procento –, byla tilemapa první nápad: zápis jednoho bajtu místo devíti tisíc taktů na znak, a vodorovně to vychází přesně, protože text je vždycky na násobku osmi a herní plocha začíná na 32. Jenže znak má devět řádků a dlaždice osm. Devátý řádek je stín a rozteč řádků je osm, takže stín jednoho řádku textu padá vždycky do buňky toho dalšího. Potřeboval bych dlaždici pro každou dvojici „znak nade mnou, znak tady“, tedy 224² ≈ 50 000, kde Next jich pojme nejvýš 512. A svisle zarovnané to taky není: titulky sedí na y ≡ 3 a umístěné řetězce na y ≡ 2 (mod 8), kdežto posun tilemapy je jen jeden.
A předměty, které Flashback z dlaždic doopravdy skládá, by na ni nesedly taky. Stojí kdekoli po pixelu, ne na mřížce osmi, hýbou se, zrcadlí se a jsou jednou před postavou a jindy za ní – kdežto tilemapa je jedna vrstva v jednom pořadí. Dopadlo to tedy tak, že se Flashbackovy dlaždice převádějí, ale na sprity. K tomu za chvíli, nejdřív postavy.
Postavy: snímky rozřezané na čtverce
Conrad je v souboru PERSO.SPR a tabulka PERSO.OFF převádí číslo spritu na offset: šest bajtů na položku, číslo a 32bitový offset, konec na 0xFFFF. Z 682 položek ukazuje na něco 681. Snímek začíná čtyřmi bajty – kotvou dw, dh a rozměry w, h – a za nimi jsou zabalené pixely. Šířka je jen spodních šest bitů w; šestý bit říká, že pixely jsou uložené po sloupcích a šířka s výškou si prohodily role. Tak je uloženo 542 z 681 snímků. Sedmý bit by znamenal nezabalené a v celé hře ho má jediný snímek – ze souboru žoldnéřů, a není to obrázek, ale řada 31 bajtů, kterou by originál kreslil barvami z celé palety.
Balení je RLE nad čtyřbitovými pixely: bajty se rozloží na nibbly a nibble 0xF uvozuje běh. A je v tom past převzatá z REminiscence: dekodér přečte počet, vynásobí ho dvěma a tolik bajtů rozloží – tedy dvakrát víc, než pak spotřebuje. Všude jinde je to neškodné přečtení kousku dalšího snímku, jenže poslední snímek v souboru tím uteče za konec. Počet tedy znamená zabalené bajty, ne nibbly. A 344 snímků z 681 se rozbalí o bajt delší, protože poslední běh zapíše celou svou délku; originál si toho nevšimne, protože kreslí w × h z pomocného bufferu. Převodník hlídá opačný případ – snímek, který vyjde kratší –, protože ten by vadil. Nenastane.
Hardware Nextu chce vzory 16 × 16 bodů, ve čtyřbitovém režimu po 128 bajtech. Převodník tedy každý snímek rozřeže na mřížku šestnáctek od jeho levého horního rohu, prázdné čtverce zahodí – každý by stál sprite i 128 bajtů – a k snímku zapíše, který čtverec leží kde. Hodí se tu dvě shody. Nula je ve Flashbacku průhledná a na Nextu je průhledná ve všech bankách čtyřbitové palety, takže se žádný pixel nepřemapovává. A barvy si postava ve Flashbacku vybírá maskou 0x40 – slotem palety v horním nibblu –, což je přesně číslo, kterému Next u čtyřbitového spritu říká posun palety. Conrad má banku 4, příšery banku 5. Všech 681 Conradových snímků dá 2 298 vzorů, 287 kB.
Pět souborů – Conrad a čtyři druhy příšer – si čísla spritů dělí tak, že se žádné neopakuje; PERSO má mezery přesně tam, kde leží čísla příšer. Jeden plochý index tedy stačí. Originál ale drží otevřený jen PERSO a jeden soubor příšer vybraný podle seznamu levelu, a pro číslo z jiného souboru nekreslí nic. Převodník proto čísla ze souborů, které level nenačte, vynechá – jinak by se na obrazovce objevilo něco, co originál nekreslí. První level tak kreslí 738 snímků ze dvou souborů, druhý 1 063 ze čtyř.
Otočení postavy na druhou stranu vypadá jako jeden bit v atributech spritu, a v hardwaru jeden bit opravdu je – jenže zrcadlí každý čtverec 16 × 16 na jeho místě. Postava složená ze šesti čtverců by měla každý kus obrácený a celek v původním pořadí. Čtverce se proto musí přeskládat (ten, který ležel na tx, jde na šířka − tx − 16) a kotva se počítá jinak: originál kotví zrcadlený snímek na pos_x + dw − šířka, což není zrcadlo nezrcadlené kotvy. Kvůli tomu se do záznamu snímku vrátila šířka, kterou jsem z něj předtím vyhodil jako zbytečnou.
Předměty: dlaždice, ze kterých se stal sprite
Postavy jsou ale v prvním levelu menšina: ze 107 entit se jich 94 kreslí jinou cestou, jako objekty. Objekt nemá vlastní bitmapu. V GLOBAL.SPC je pro každé číslo seznam malých obdélníků – bajt, který přes LEVELn.RP vybere banku dlaždic v LEVELn.MBK, kotva, počet a pak po čtyřech bajtech číslo dlaždice, posun a velikost: osm až dvaatřicet bodů v každém směru a vlastní bit pro zrcadlení. Banka je buď syrová, nebo zabalená bytekillerem, stejně jako kolizní mřížky. Tohle je ve Flashbacku ta skutečná dlaždicová grafika – a právě tu by člověk čekal na tilemapě.
Převodník každý objekt poskládá do jedné bitmapy a kotvu posune o nejlevější a nejvyšší obdélník – a vyjde z toho obyčejný snímek: stejný záznam a stejný kód na Z80 jako u postavy, zrcadlení včetně. Že zrcadlení vyjde, jsem si napřed spočítal na papíře: originál zrcadlí každý obdélník na místě a staví ho na posx − s[1] − w, a zrcadlení celé poskládané bitmapy kolem kotvy postavy dá každý pixel na totéž místo. Dělal jsem to schválně dřív než kód, protože „je to skoro totéž“ je přesně věta, po které vzniknou dvě kreslicí cesty tam, kde stačí jedna. Celý rozdíl za běhu jsou tři věci: vlastní index (270 čísel prvního levelu chtějí postavy i objekty a znamenají pro ně něco jiného), žádný test ořezu, protože ho originál u objektů nemá, a paleta – objekt bere jednu ze čtyř palet místnosti, ne banku postav. Prvních 64 barev místnosti se proto posílá i do palety spritů, jen bez priority bitu, který sprity nemají.
V prvním levelu je to 359 objektů z 942 obdélníků. Další levely pak rozbily skoro každý bajt, který jsem si dovolil. Objekt ze druhého levelu je široký 272 pixelů a má 43 čtverců, takže šířka i poloha čtverce v bajtu přetekly. Jeden objekt prvního levelu má po poskládání kotvu 132, která by se jako znaménkový bajt zabalila na −124 a objekt by skončil o 256 pixelů jinde. Slovo velikosti banky není počet bajtů, ale dlaždic po 32 bajtech – a první level náhodou nepoužívá žádnou syrovou banku, kde by se to projevilo. A ve třech z pěti sad místností je banka nula prázdná a .RP na ni posílá všechno, co level nemá. Žádná z těch věcí nevypadala jako chyba, dokud ji data neukázala.
Kdo je před kým
Tady je podstata rozdílu mezi oběma stroji. Flashback kreslí do framebufferu a pořadí je pořadí kreslení: entity sbírá do čtyř seznamů, kreslí je v pořadí 2, 1, 0, 3 a pozdější kresba překryje dřívější; uvnitř seznamu jde od posledního přidaného, takže navrchu skončí první. Next žádné pořadí kreslení nemá. Sprity se překrývají podle čísla slotu a o pořadí vrstev rozhoduje registr. Na obrazovce tedy nakonec leží tohle:
Priority bit, nad vším: popředí místnosti, okraj kolem ní, ikony, texty, inventář.
Entity před Conradem, Conrad, entity za ním – postavy i objekty, až 128 čtverců 16 × 16.
Pozadí místnosti.
ULA a tilemapa.
Layer 2 tu hraje dvě role najednou, a to je ta hezká část: tatáž bitmapa je pozadím pod sprity i popředím nad nimi, podle toho, jakou barvu pixel má. Žádná druhá vrstva na popředí, žádná maska. Když skript otřese místností, posune se Layer 2 registrem $17 o pár řádků a hne místností, ikonami i textem naráz; sprity se posunou o totéž číslo. A když se stmívá, mění se jen barvy palety a priority bit zůstává, takže popředí drží před postavami celou cestu do černé.
O pořadí spritů rozhoduje bit 6 registru $15, který říká, jestli při překryvu vyhrává nejnižší, nebo nejvyšší slot. Z dokumentace se to vyčíst nedalo, jen změřit: s vynulovaným bitem vyhrává nejvyšší. Port ho nastavuje, takže navrchu je slot 0, a sloty rozdává v opačném pořadí, než originál kreslí – nejdřív seznam před Conradem, pak Conrad, pak seznam za ním. Kdyby to bylo naopak, vypadalo by to jako chyba hloubky v místnosti, ne jako seznam projitý z druhé strany. A nebyl by to okrajový případ: 64 ze 107 entit prvního levelu patří za Conrada a 42 před něj. Jedna ze čtyř kontrolních procházek na tom spadla na 31 pixelech – přesně tam, kde entita stojí před Conradem. Jinde v levelu se ta otázka neklade.
Pořadí se ale obrázkem zkontrolovat nedá. Do místnosti zasahují i entity ze čtyř sousedních – každý směr má vlastní pravidlo, jak daleko se musí naklonit – a ve výchozí místnosti, kde se fotí, nezasahuje nikdo. Schválné zahození všech čtyř sousedů prošlo všemi snímky bez jediného pixelu rozdílu. Porovnávají se proto čísla: pro každou z 27 místností levelu všech 128 slotů po pěti bajtech atributů, 17 280 bajtů, a rozdíl řekne, který slot a který bajt – kde snímek umí jen „31 pixelů nesedí“. A našel chybu, kterou snímky přehlédly: tabulka sousedů leží v jiné stránce a četla se oknem, které tam nechalo kreslení spritů, takže všichni sousedé vycházeli jako „nikdo“. Každý snímek přitom seděl.
Paměť vzorů a paprsek
Plán počítal s tím, že bude potřeba cache vzorů: 681 snímků je 287 kB proti 16 kB sprite paměti. Jenže nerozhoduje, kolik snímků existuje, ale kolik jich je naráz na obrazovce. Nejrušnější místnost prvního levelu (51) má po načtení na obrazovce osm entit, z toho šest objektů, a dohromady 24 čtverců – tři kilobajty. Vzory se proto nahrávají, když se snímek změní, a žádná cache není. Kdyby některá místnost přerostla svůj díl paměti, kontrola spadne, místo aby si toho všiml hráč.
A pak přišla chyba, kterou žádná kontrola v repozitáři najít nemohla. Postava při chůzi dělala artefakty – a čtrnáct zastavených snímků téže chůze sedělo na referenci pixel po pixelu. Všechny obrazovkové kontroly totiž hru napřed zastaví a teprve pak fotí, takže porovnávají, co v obraze je, a ne kdy se to tam zapsalo. Kreslení přitom běží na konci herního tiku, tedy uprostřed viditelného snímku, a pár kilobajtů vzorů přes port je mnohem víc, než se vejde do zatemnění. Postavě se přepisovaly pixely, zatímco byl paprsek v půlce. Všiml jsem si toho až při hraní. Oprava je dvojitý buffer: sprite paměť je rozdělená na dvě poloviny po 64 vzorech a píše se vždycky do té, která zrovna není vidět.
Předtím bylo blikání, a to dvojí. Na začátku každého tiku jsem schovával všech 128 spritů a kreslení je hned vracelo, takže každý snímek, který paprsek chytil mezi tím, neměl postavy žádné. A čekal jsem na řádek 0 místo na zatemnění – jenže řádek 0 je místo, kde obraz začíná, takže všechno, co tik dělá, padlo doprostřed kreslení. Teď se schovává jen to, co snímek nepoužil, a čeká se na zatemnění.
A dvě drobnosti, které stály nejvíc času na jeden řádek. Vzory se nahrávají portem a out (c),a posílá registr B na horní polovinu adresní sběrnice – první verze použila B jako čítač bajtů, takže data šla na 128 různých portů a na obrazovce nebylo nic. Špatný port je tichý: nic nehlásí a nic nespadne. A protože port $303B čísluje vzory po 256 bajtech, kdežto čtyřbitový vzor má 128, začíná každá entita na sudé polovině. První verze tím přeskočila sprite slot, který pak nikdo nepřepsal, a jedna postava chvílemi nosila kus jiné – v barvě, kterou v místnosti nic nemá, a jen v bězích dost dlouhých na to, aby se sloty posunuly o jeden. Mezera je teď v paměti vzorů a sloty jdou souvisle.
Obraz stojí, teď se musí hýbat
Grafika je převedená celá – místnosti všech pěti sad, postavy, příšery, objekty, ikony i písmo – a na obrazovce ji skládá hardware, ne Z80. Ten do obrazu sahá jen tehdy, když se něco mění: místnost, snímek postavy, ikona nebo text. Popředí, okraj, pořadí i HUD nad tím vším jsou nastavení, ne výpočet. V dalším díle bych se chtěl podívat na to, co tyhle obrázky rozpohybuje: animační tabulky, objektové skripty a interpret, který je čte – tedy na místo, kde se z grafiky stává hra.






