Obsah
1. Struktura obrazové paměti grafické karty CGA, blokové přenosy a základy optimalizace
2. Vyplnění obrazovky konstantní barvou, naivní varianta
3. Úplný zdrojový kód prvního demonstračního příkladu
4. Programová smyčka realizovaná instrukcí LOOP
5. Použití instrukce REP STOSB
6. Úplný zdrojový kód třetího demonstračního příkladu
7. Porovnání rychlosti tří variant vyplnění obrazovky bílou barvou
8. Synchronizace zápisu na obrazovku s vertikálním synchronizačním pulsem
9. Úplný zdrojový kód čtvrtého demonstračního příkladu
10. Odvození snímkové frekvence na základě trvání vyplnění obrazu
11. Vykreslení rastrového obrázku získaného z binárních dat
12. Úplný zdrojový kód pátého demonstračního příkladu
13. Blokový přenos dat operací REP MOSVB či REP MOVSW
14. Realizace přenosu dat operací REP MOVSB
15. Realizace přenosu dat operací REP MOVSW
16. Problematika rozdělení obrazové paměti na liché a sudé řádky
17. Vykreslení lichých řádků získaných z rastrového obrázku
18. Korektní vykreslení všech sudých i lichých řádků získaných z rastrového obrázku
19. Repositář s demonstračními příklady
1. Struktura obrazové paměti grafické karty CGA, blokové přenosy a základy optimalizace
Ve třetím článku o programování her a grafických dem pro osobní mikropočítač IBM PC se „slavnou“ grafickou kartou CGA se zaměříme na některé důležité nízkoúrovňové operace. Popíšeme si strukturu obrazové paměti karty CGA v grafických režimech (výsledek nás nepotěší), dále si ukážeme přenosy rastrových dat do obrazové paměti a taktéž synchronizaci kódu s vertikální (snímkovou) frekvencí, což nám mj. zajistí konstantní rychlost běhu a navíc i zamezí „sněžení“, které na kartě CGA nastává při zápisu do obrazové paměti (už jsem říkal, že práce s touto kartou je peklem pro programátora?). Ovšem seznámíme se i s některými zajímavými instrukcemi mikroprocesoru Intel 8086/Intel 8088, například s instrukcemi pro blokové přenosy dat (a proč je na CGA nevyužijeme v plné síle). Toto téma do jisté míry souvisí s optimalizacemi, takže si porovnáme některé algoritmy realizované několika různými způsoby.
2. Vyplnění obrazovky konstantní barvou, naivní varianta
Ukažme si nyní, jakým způsobem je možné vyplnit (skoro) celou obrazovku bílou barvou ve chvíli, kdy máme nastaven grafický režim s rozlišením 640×200 pixelů a možností nastavení každého pixelu na barvu pozadí (černá) nebo barvu popředí (bílá). Nejprve zapíšeme do segmentového registru ES adresu segmentu 0×b800. To lze provést jen nepřímo, protože operace mov segmentový_registr, konstanta není podporována (jedno z omezení CISCové instrukční sady):
mov ax, 0xb800 ; video segment mov es, ax ; do segmentoveho registru ES
Dále vynulujeme pracovní registr BX použitý pro adresování (ze základních registrů pouze tento lze použít v roli adresního registru) v rámci video RAM (tedy segmentu 0×b800). Registr AL bude obsahovat hodnoty osmi pixelů (každý pixel=1 bit) a do registru CX zapíšeme počet bajtů video RAM (zde naschvál zapisuji nižší údaj, ale aby se obrazovka skutečně zaplnila, musíte použít hodnotu 16384).
xor bx, bx ; adresa pro zapis barev pixelu mov al, 255 ; zapisovana kombinace barev pixelu mov cx, 640*200/8 ; pocitadlo smycky
Následuje vlastní programová smyčka, v níž je operace vyplnění obrazovky realizována. Na adresu [ES:BX] jsou zapsány barvy osmi pixelů, adresa uložená v registru BX je zvýšena o jedničku, hodnota počitadla uložená v registru CX je naopak o jedničku snížena a pokud počitadlo nedosáhne nulové hodnoty, bude se programová smyčka opakovat:
fill_loop: mov [es:bx], al ; zapis barev osmi pixelu inc bx ; na dalsi pixel dec cx ; snizeni hodnoty CL jnz fill_loop ; skok pri nenulovosti vysledku
Obrázek 1: Výsledek vyplnění prvních 16000 bajtů obrazové paměti. Povšimněte si, že ve skutečnosti některé (sudé) řádky na konci vyplněny nejsou. K této problematice se ještě vrátíme.
3. Úplný zdrojový kód prvního demonstračního příkladu
Úplný zdrojový kód dnešního prvního demonstračního příkladu, který po svém spuštění vyplní (skoro) celou obrazovku bílou barvou, vypadá následovně:
; Vykresleni sady pixelu, vyplneni obrazovky. ; ; preklad pomoci: ; nasm -f bin -o gfx_6.com gfx_6_fill_1.asm ; ; nebo pouze: ; nasm -o gfx_6.com gfx_6_fill_1.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 6 wait_key mov ax, 0xb800 ; video segment mov es, ax ; do segmentoveho registru ES xor bx, bx ; adresa pro zapis barev pixelu mov al, 255 ; zapisovana kombinace barev pixelu mov cx, 640*200/8 ; pocitadlo smycky fill_loop: mov [es:bx], al ; zapis barev osmi pixelu inc bx ; na dalsi pixel dec cx ; snizeni hodnoty CL jnz fill_loop ; skok pri nenulovosti vysledku wait_key exit
4. Programová smyčka realizovaná instrukcí LOOP
V předchozím demonstračním příkladu jsme použili registr CX v roli počitadla programové smyčky. Zápis této smyčky vypadal zhruba následovně:
smyčka: ... ... ... dec cx ; snizeni hodnoty CL jnz smyčka ; skok pri nenulovosti vysledku
Ve skutečnosti je možné tyto dvě instrukce nahradit za jedinou instrukci LOOP, která provádí totožnou operaci, tj. snížení hodnoty CX následované skokem ve chvíli, kdy CX ještě nedosáhla nuly:
smyčka: ... ... ... loop smyčka ; snizeni hodnoty CX, skok pri nenulovosti vysledku
Jak je tato smyčka výhodná nám ukáže následující tabulka:
Operace | Bajtů | Cyklů |
---|---|---|
DEC+JNZ | 1+2 | 3+4 nebo 3+16 |
LOOP | 2 | 5 nebo 17 |
Druhá varianta je tedy pomalejší a současně i delší, než varianta s instrukcí LOOP.
A takto vypadá výsledek:
; Vykresleni sady pixelu, vyplneni obrazovky. ; ; preklad pomoci: ; nasm -f bin -o gfx_6.com gfx_6_ver_fill_2.asm ; ; nebo pouze: ; nasm -o gfx_6.com gfx_6_ver_fill_2.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 6 wait_key mov ax, 0xb800 ; video segment mov es, ax ; do segmentoveho registru ES xor bx, bx ; adresa pro zapis barev pixelu mov al, 255 ; zapisovana kombinace barev pixelu mov cx, 640*200/8 ; pocitadlo smycky fill_loop: mov [es:bx], al ; zapis barev osmi pixelu inc bx ; na dalsi pixel loop fill_loop ; snizeni hodnoty CX, skok pri nenulovosti vysledku wait_key exit
5. Použití instrukce REP STOSB
Ve skutečnosti je možné vyplnění (prakticky) celé obrazovky realizovat ještě snadněji, a to konkrétně opakováním instrukce STOSB. Samotná instrukce STOSB znamená „STOre String (byte)“ a provádí tyto operace:
- Zápis hodnoty uložené v registru AL na adresu [ES:DI]
- Zvýšení hodnoty registru DI o jedničku
Pokud se navíc před tuto instrukce předá prefix představovaný instrukcí REP, bude se celá operace STOSB opakovat CX-krát.
Jedna iterace takto realizované programové smyčky trvá deset cyklů, k nimž musíme připočítat inicializačních devět cyklů.
6. Úplný zdrojový kód třetího demonstračního příkladu
; Vyplneni obrazovky instrukci REP STOSB ; ; preklad pomoci: ; nasm -f bin -o gfx_6.com gfx_6_ver_fill_1.asm ; ; nebo pouze: ; nasm -o gfx_6.com gfx_6_ver_fill_1.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 6 ; nastaveni grafickeho rezimu 640x200 se 2 barvami wait_key mov ax, 0xb800 ; video segment mov es, ax ; do segmentoveho registru ES xor di, di ; adresa pro zapis barev pixelu mov al, 255 ; zapisovana kombinace barev pixelu mov cx, 640*200/8 ; pocitadlo smycky rep stosb ; vlastni vyplneni wait_key exit
7. Porovnání rychlosti tří variant vyplnění obrazovky bílou barvou
Nyní si všechny tři varianty vyplnění obrazovky bílou barvou porovnáme. Všechny smyčky zapisují v každé iteraci jediný bajt, ovšem doba trvání se bude odlišovat.
mov [es:bx], al 14 14 inc bx 3 3 dec cx 3 3 jnz fill_loop 16 4 (provedení/neprovedení skoku) ------------------------- 36 24
Celkově: 36×15999+24=574008 cyklů (to je hodně)
mov [es:bx], al 14 14 inc bx 3 3 loop fill_loop 17 5 ------------------------- 34 22
Celkově: 34×15999+22=542118 cyklů (to je hodně)
rep stosb 17 9
Celkově: 17×15999+9=271057 cyklů.
8. Synchronizace zápisu na obrazovku s vertikálním synchronizačním pulsem
U všech režimů původní varianty grafické karty CGA se vyskytuje ještě jeden fenomén, který uživatelé jiných počítačů neznali – souběžný přístup mikroprocesoru a grafického řadiče do obrazové paměti nebyl kupodivu nijak ošetřen, což způsobovalo nechvalně známé „sněžení“ (snow). Jediná možnost, jak zabránit „sněžení“, byl zápis do obrazové paměti při vertikálním zatemnění elektronového paprsku (horizontální zatemnění má příliš krátký interval). A to byl další problém, protože karta CGA negenerovala přerušení před vertikálním zatemněním – tedy něco, co uměly již mnohé osmibitové mikropočítače. Takže čekání na zatemnění muselo probíhat tím nejhorším možným způsobem – kontinuálním čtením příslušného bitu ze stavového registru karty CGA.
Realizace čekání na vertikální přerušení je sice – samozřejmě pokud nám nevadí „pálení strojového času“ – vlastně až primitivně jednoduché. Postačuje nám totiž kontinuálně sledovat čtvrtý bit portu 0×3da. Pokud je tento bit nastaven, probíhá vertikální přerušení (a zápis je tedy možné provést); jinak neprobíhá. Jenže ono to v praxi není až tak jednoduché, protože i když je tento bit nastaven, tak vlastně nevíme, zda přerušení v dalším cyklu neskončí (nevíme, ve kterém okamžiku přerušení se nacházíme). Musíme tedy provádět dvě operace: čekání na dokončení aktuálního přerušení (pokud probíhá) a čekání na nové přerušení, tedy na jeho začátek. Celá realizace čekání na začátek vertikálního přerušení vypadá takto:
wait_sync: mov dx, 0x3da ; adresa stavoveho registru graficke karty CGA wait_sync_end: in al, dx ; precteni hodnoty stavoveho registru test al, 8 ; odmaskovat priznak vertikalniho synchronizacniho pulsu jnz wait_sync_end ; probiha - cekat na konec wait_sync_start: in al, dx ; precteni hodnoty stavoveho registru test al, 8 ; odmaskovat priznak vertikalniho synchronizacniho pulsu jz wait_sync_start ; neprobiha - cekat na zacatek ret ; ok - synchronizacni kurz probiha, lze zapisovat do pameti
Synchronizovaný zápis do video RAM nám „prozradí“ její strukturu:
Obrázek 2: Začátek vyplňování (prozatím jen liché řádky).
Obrázek 3: Vyplnění prvních 8000 bajtů video RAM.
Obrázek 4: Pokračuje se vyplněním sudých řádků.
9. Úplný zdrojový kód čtvrtého demonstračního příkladu
Úplný zdrojový kód dnešního čtvrtého demonstračního příkladu, v němž se na obrazovku zapisují pixely se synchronizací této operace, vypadá následovně:
; Vykresleni sady pixelu, vyplneni obrazovky. ; ; preklad pomoci: ; nasm -f bin -o gfx_6.com gfx_6_ver_fill_1.asm ; ; nebo pouze: ; nasm -o gfx_6.com gfx_6_ver_fill_1.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 6 wait_key mov ax, 0xb800 ; video segment mov es, ax ; do segmentoveho registru ES xor bx, bx ; adresa pro zapis barev pixelu mov cx, 640*200/8 ; pocitadlo smycky fill_loop: call wait_sync mov al, 255 ; zapisovana kombinace barev pixelu mov [es:bx], al ; zapis barev osmi pixelu inc bx ; na dalsi pixel loop fill_loop ; snizeni hodnoty CX, skok pri nenulovosti vysledku wait_key exit wait_sync: mov dx, 0x3da ; adresa stavoveho registru graficke karty CGA wait_sync_end: in al, dx ; precteni hodnoty stavoveho registru test al, 8 ; odmaskovat priznak vertikalniho synchronizacniho pulsu jnz wait_sync_end ; probiha - cekat na konec wait_sync_start: in al, dx ; precteni hodnoty stavoveho registru test al, 8 ; odmaskovat priznak vertikalniho synchronizacniho pulsu jz wait_sync_start ; neprobiha - cekat na zacatek ret ; ok - synchronizacni kurz probiha, lze zapisovat do pameti
10. Odvození snímkové frekvence na základě trvání vyplnění obrazu
Předchozímu programu trvá vyplnění celé obrazovky poměrně dlouhou dobu. Vezměme tedy do ruky stopky a zkusme tuto dobu alespoň přibližně zjistit a odvodit z ní snímkovou frekvenci (tedy počet snímků zobrazených monitorem za sekundu).
Začneme s výchozím nastavením DOSBoxu, který emuluje karty VGA a SVGA. V této konfiguraci je obrazovka vyplněna za přibližně 230 sekund (měřeno běžnými stopkami, takže &plusm; 1 sekunda). Přitom se provedlo celkem 640×200/8=16000 zápisů. Z toho plyne>
16000/230 = 69,5
Za sekundu se tedy provedlo přibližně 69,5 zápisů, což odpovídá snímkové frekvenci, která dosahuje taktéž této hodnoty (necelých 70 Hz).
Ovšem korektní je měření ve chvíli, kdy je DOSBox nakonfigurován tak, aby emuloval počítač s kartou CGA. V souboru ~/.config/dosbox/dosbox-staging.conf je nutné nastavit řádek machine na hodnotu cga. Nyní bude vyplnění obrazovky trvat 268 sekund, což při stejném počtu 640×200/8=16000 zápisů vede ke snímkové frekvenci:
16000/268 = 59,70 Hz
To je velmi blízko skutečné snímkové frekvenci, která dosahuje 59,923 Hz. Tato na první pohled poněkud divná hodnota je odvozena od frekvence hodinového signálu 14,318181 MHz, která je dělena osmi a následně ještě 114. Tím získáme řádkovou frekvenci (přibližně 15699,8 řádků za sekundu) a podělením hodnotou 262 pak dostaneme onu snímkovou frekvenci 59,923 Hz.
11. Vykreslení rastrového obrázku získaného z binárních dat
Ve druhé polovině dnešního článku se zaměříme na zdánlivě jednoduchou úlohu: jak na obrazovku řízenou grafickou kartou CGA vykreslit následující obrázek ze slavné hry Golden Axe:
Obrázek 5: Obrázek ze hry Golden Axe, který máme k dispozici v binární podobě (16000 bajtů čistých obrazových dat).
Z tohoto obrázku nejdříve musíme získat „surová“ rastrová data, tj. barvy jednotlivých pixelů. Obrázek využívá grafický režim s rozlišením 320×200 pixelů, přičemž každý pixel je uložen ve dvou bitech (tedy čtyři pixely v jednom bajtu). Pokud máme k dispozici data obrázku ve formátu BMP s bitovou hloubkou 8bpp, získáme z něho surová data ve vhodném formátu následujícím konvertorem, který přeskočí (a ignoruje) hlavičku BMP, paletu (dohromady 1162 bajtů – ovšem pozor, někdy vypadá hlavička jinak či chybí paleta – níže uvedený konvertor je až příliš primitivní, aby tyto nuance odhalil) a následně přečtené hodnoty pixelů uloží ve formátu 4 pixely na bajt:
#include <stdio.h> #define PIXELS 320*200 int main(void) { FILE *fin; FILE *fout; int i; fin = fopen("golden.bmp", "r"); /* preskok hlavicky */ fseek(fin, 1162, SEEK_SET); fout = fopen("image.bin", "w"); for (i=0; i<PIXELS/4; i++) { unsigned char pixels[4]; unsigned int out; fread(pixels, 4, 1, fin); /* 4 pixely ulozit do jedineho bajtu */ out = pixels[3] & 0x03 | ((pixels[2] & 0x03) << 2) | ((pixels[1] & 0x03) << 4) | ((pixels[0] & 0x03) << 6); printf("%x ", out); /* vystup */ fputc(out, fout); } fclose(fin); fclose(fout); }
Výsledkem by měl být v každém případě soubor image.bin, který má mít přesnou velikost 16000 bajtů.
Tento binární soubor můžeme připojit do výsledné přeložené aplikace. To je možné i při použití formátu COM, protože délka souboru nepřesáhne 64 kilobajtů a tedy data budou uložena ve stejném segmentu, jako programový kód. Při překladu se binární data (obrázku) načtou a uloží tímto příkazem assembleru NASM:
; pridani binarnich dat s rastrovym obrazkem image: incbin "image.bin"
Co to znamená v praxi? Na adrese označené návěštím image je uloženo 16000 bajtů s daty rastrového obrázku, které musíme nějakým způsobem přenést do obrazové paměti, která začíná na začátku segmentu 0×d800. Budeme tedy potřebovat postupně zkopírovat 16000 bajtů. Nejprve si připravíme potřebné registry.
Do dvojice registrů DS:SI uložíme adresu prvního bajtu obrázku. Segmentový registr DS tedy nastavíme na stejnou hodnotu, jako segmentový registr pro kód CS. To sice není striktně nutné, protože tuto operaci za nás provede DOS při inicializaci programu, ale je lepší být explicitní. Povšimněte si, že nelze provádět přesuny dat přímo mezi segmentovými registry, takže si pomůžeme buď zásobníkem nebo například registrem AX:
mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku
adresa_zdroje = (DS << 4) + SI
Podobným způsobem si v registrech ES:DI připravíme cílovou adresu, tedy v našem případě adresu prvního bajtu v obrazové paměti (ta je rovna b0×b800:0 v zápisu segment:offset):
mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM
Zbývá nám nastavit si počitadlo přenášených bajtů, což je snadné. Pro tento účel se téměř vždy používá registr CX, popř. jeho spodním osm bitů CL:
mov cx, 16000 ; pocet zapisovanych bajtu
Vlastní programová smyčka, která přenese všech 16000 bajtů, může v naivním provedení vypadat následovně:
move: mov al, [ds:si] ; precteni jednoho bajtu z obrazku mov [es:di], al ; zapis tohoto bajtu do video RAM inc si ; posun ve zdrojovem "poli" inc di ; posun ve video RAM loop move ; opakovat CX-krat
Obrázek 6: Výsledek přesunu rastrových dat obrázku do obrazové paměti (špatné proložení sudých a lichých řádků).
V případě assembleru NASM je možné vynechat hranaté závorky u výpočtu adresy, takže se zápis zkrátí následovně (zda je čitelnější, ponechám na laskavém čtenáři):
move: mov al, ds:si ; precteni jednoho bajtu z obrazku mov es:di, al ; zapis tohoto bajtu do video RAM inc si ; posun ve zdrojovem "poli" inc di ; posun ve video RAM loop move ; opakovat CX-krat
12. Úplný zdrojový kód pátého demonstračního příkladu
Opět se podívejme na úplný zdrojový kód demonstračního příkladu, který po svém spuštění vykreslí na obrazovku rastrový obrázek. Přitom výsledný obrázek nebude korektní; tuto chybu opravíme v rámci navazujících kapitol:
; Vykresleni rastroveho obrazku ziskaneho z binarnich dat. ; Zakladni varianta s explicitne zapsanou programovou smyckou. ; ; preklad pomoci: ; nasm -f bin -o gfx_4.com gfx_4_image.asm ; ; nebo pouze: ; nasm -o gfx_4.com gfx_4_image.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 4 ; nastaveni grafickeho rezimu 320x200 se ctyrmi barvami mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM mov cx, 16000 ; pocet zapisovanych bajtu move: mov al, ds:si ; precteni jednoho bajtu z obrazku mov es:di, al ; zapis tohoto bajtu do video RAM inc si ; posun ve zdrojovem "poli" inc di ; posun ve video RAM loop move ; opakovat CX-krat wait_key exit ; pridani binarnich dat s rastrovym obrazkem image: incbin "image.bin"
13. Blokový přenos dat operací REP MOSVB či REP MOVSW
V programu z předchozí kapitoly je přenos dat z kódového+datového segmentu do segmentu s video pamětí (Video RAM) řešen programovou smyčkou, v níž se přenáší bajty z adresy [DS:SI] na adresu [ES:DI]. Jako počitadlo této programové smyčky opět slouží registr CX. Smyčka vypadá následovně:
move: mov al, ds:si ; precteni jednoho bajtu z obrazku mov es:di, al ; zapis tohoto bajtu do video RAM inc si ; posun ve zdrojovem "poli" inc di ; posun ve video RAM loop move ; opakovat CX-krat
Jak dlouho ovšem potrvá přenesení jednoho bajtu? Zkusme si to spočítat, opět pro případ původního čipu Intel 8086 (a Intel 8088 je v tomto případě prakticky stejně rychlý):
mov al, ds:si 14 mov es:di, al 13+5 inc si 3 inc di 3 loop move 17 ------------------------ 50 až 55
To je poměrně dlouhá doba! Ovšem již mikroprocesor Intel 8086 (a samozřejmě i Intel 8088) obsahoval instrukci movsb určenou pro přenos jednoho bajtu z adresy [DS:SI] na adresu [ES:DI] se zvýšením SI i DI o jedničku. Navíc je možné i před tuto instrukci vložit prefix rep pro její opakování CX-krát. A navíc existuje i instrukce movsw určená pro přenos celých 16bitových slov. Rychlosti těchto instrukcí jsou následující:
Instrukce | Délka (bajtů) | Cyklů |
---|---|---|
movsb | 1 | 18 |
movsw | 1 | 26 |
rep movsb | 2 | 9+17×n |
rep movsw | 2 | 9+28×n |
Pokud vám doby trvání instrukcí stále připadnou dlouhé, vězte, že na Pentiu trvají operace rep movs* pouze 3+n cyklů, kde n je počet přenášených bajtů, 16bitových slov či 32bitových slov. Tedy Pentium, i kdyby bylo taktované pouze na 4,77 MHz, jako původní PC s Intelem 8088, by stále bylo několikanásobně rychlejší, protože podobně se urychlila i většina ostatních operací.
14. Realizace přenosu dat operací REP MOVSB
Pro přenos obrazu z původního segmentu, do něhož je nahrán náš program, do segmentu video RAM můžeme využít jak rep movsb, tak i rep movsw. Nejprve použijeme rep movsb, tedy přenos po jednotlivých bajtech. Celkový počet opakování přenosové operace bude roven 16000:
mov cx, 16000 ; pocet zapisovanych bajtu
A blokový přenos bude proveden instrukcí movsb s prefixem rep:
rep movsb ; provest cely blokovy prenos po bajtech
Úplný zdrojový kód takto upraveného programu bude vypadat následovně:
; Vykresleni rastroveho obrazku ziskaneho z binarnich dat. ; Varianta zalozena na instrukci REP MOVSB ; ; preklad pomoci: ; nasm -f bin -o gfx_4.com gfx_4_image.asm ; ; nebo pouze: ; nasm -o gfx_4.com gfx_4_image.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 4 ; nastaveni grafickeho rezimu 320x200 se ctyrmi barvami mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM mov cx, 16000 ; pocet zapisovanych bajtu rep movsb ; provest cely blokovy prenos po bajtech wait_key exit image: incbin "image.bin"
15. Realizace přenosu dat operací REP MOVSW
Rychlejší bude přenos pixelů nikoli po bajtech, ale po šestnáctibitových slovech. To znamená, že se počet přenosových operací sníží na polovinu:
mov cx, 16000/2 ; pocet zapisovanych slov
A vlastní přenos zahájíme instrukcí movsw s prefixem rep:
rep movsw ; provest cely blokovy prenos po slovech
Opět se podívejme na úplný zdrojový kód takto upraveného programu:
; Vykresleni rastroveho obrazku ziskaneho z binarnich dat. ; Varianta zalozena na instrukci REP MOVSW ; ; preklad pomoci: ; nasm -f bin -o gfx_4.com gfx_4_image.asm ; ; nebo pouze: ; nasm -o gfx_4.com gfx_4_image.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 4 ; nastaveni grafickeho rezimu 320x200 se ctyrmi barvami mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM mov cx, 16000/2 ; pocet zapisovanych slov rep movsw ; provest cely blokovy prenos po slovech wait_key exit image: incbin "image.bin"
16. Problematika rozdělení obrazové paměti na liché a sudé řádky
Ve všech předchozích demonstračních příkladech, ve kterých jsme se snažili o vykreslení rastrového obrázku, bylo zřejmé, že vykreslení není korektní. Je tomu tak z toho důvodu, že v grafických režimech karty CGA (jak v monochromatickém, tak i ve „čtyřbarevném“) jsou v prvních osmi kilobajtech uloženy liché řádky (pokud řádky počítáme od jedničky) a ve druhých osmi kilobajtech pak řádky sudé. Mapa video RAM tedy vypadá zhruba takto:
==================================== 0xb800 řádek 1 0xb800+80 řádek 3 0xb800+160 řádek 5 ... ... ... ... ... ... 0xb800+7920 řádek 199 0xb800+8000 nepoužitých 192 bajtů ==================================== 0xd800 řádek 2 0xd800+80 řádek 4 0xd800+160 řádek 6 ... ... ... ... ... ... 0xd800+7920 řádek 200 0xb800+8000 nepoužitých 192 bajtů ====================================
Toto řešení sice může do určité míry vyhovovat v prokládaných (interlaced) režimech, ovšem znesnadňuje veškeré další operace, tj. vykreslování pixelů, úseček, bitmap atd.
17. Vykreslení lichých řádků získaných z rastrového obrázku
Upravme si tedy předchozí demonstrační příklady do takové podoby, aby se rastrový obrázek (který je uložen kontinuálně, tj. ve formě řádků 1, 2, 3, 4, … 99, 100, … 199, 200) vykreslil korektně. První úprava bude spočívat v tom, že vykreslíme pouze liché řádky a přitom budeme korektně ve vstupním obrázku přeskakovat řádky sudé – tedy budeme se posouvat o 80 bajtů bez přenosu. Realizace řešení poloviny našeho problému může vypadat následovně (registry budou nastaveny stejně, jako v předchozích příkladech):
mov bl, 100 ; pocitadlo radku outer_loop: mov cx, 80/2 ; velikost bloku ve slovech rep movsw ; prenest jeden obrazovy radek add si, 80 ; preskocit lichy/sudy radek dec bl jnz outer_loop ; opakovat smycku BL-krat
Povšimněte si, že původní blokový přenos realizovaný jedinou instrukcí rep movsb nebo rep movsw je nyní nutné rozdělit na přenos jednotlivých řádků – děkovné dopisy posílejte do IBM. A navíc je nutné registr SI zvýšit o 80 (bajtů), aby se ve vstupním obrázku přeskočil sudý řádek.
Výsledkem bude následující obrazovka:
Obrázek 7: Vykreslení pouze lichých řádků, korektní přeskočení řádků sudých.
Výsledný program vypadá následovně:
; Vykresleni rastroveho obrazku ziskaneho z binarnich dat. ; Korektni vykresleni vsech lichych radku obrazku. ; ; preklad pomoci: ; nasm -f bin -o gfx_4.com gfx_4_image.asm ; ; nebo pouze: ; nasm -o gfx_4.com gfx_4_image.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 4 ; nastaveni grafickeho rezimu 320x200 se ctyrmi barvami mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM mov bl, 100 ; pocitadlo radku outer_loop: mov cx, 80/2 ; velikost bloku ve slovech rep movsw ; prenest jeden obrazovy radek add si, 80 ; preskocit lichy/sudy radek dec bl jnz outer_loop ; opakovat smycku BL-krat wait_key exit image: incbin "image.bin"
18. Korektní vykreslení všech sudých i lichých řádků získaných z rastrového obrázku
Nyní již můžeme přistoupit k finální úpravě našeho vykreslovače obrázků. Nejprve budeme přenos poloviny obrazových řádků (sudých nebo lichých) realizovat v podprogramu, přičemž se předpokládá korektní nastavení adres [DS:SI] a [ES:DI]. Tento kód již známe, pouze je doplněn o instrukci ret na konci:
move_half_image: mov bl, 100 ; pocitadlo radku outer_loop: mov cx, 80/2 ; velikost bloku ve slovech rep movsw ; prenest jeden obrazovy radek add si, 80 ; preskocit lichy/sudy radek dec bl jnz outer_loop ; opakovat smycku BL-krat ret
Následuje vykreslení lichých řádků s využitím tohoto podprogramu:
mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM call move_half_image
Dále nám již postačuje posunout se ve zdrojovém obrázku o jeden obrazový řádek a upravit adresu v registru DI tak, aby ukazoval na začátek druhé poloviny Video RAM (stačilo by i pouhé přičtení hodnoty 192, to je však poněkud matoucí):
mov si, image+80; adresa prvniho pixelu na DRUHEM radku mov di, 8192 ; druha "stranka" video RAM call move_half_image
Výsledek:
Obrázek 8: Korektní vykreslení jak lichých, tak i sudých řádků.
Opět se podívejme na výslednou podobu demonstračního příkladu:
; Vykresleni rastroveho obrazku ziskaneho z binarnich dat. ; Korektni vykresleni vsech sudych i lichych radku obrazku. ; ; preklad pomoci: ; nasm -f bin -o gfx_4.com gfx_4_image.asm ; ; nebo pouze: ; nasm -o gfx_4.com gfx_4_image.asm ;----------------------------------------------------------------------------- ; ukonceni procesu a navrat do DOSu %macro exit 0 mov ah, 0x4c int 0x21 %endmacro ; vyprazdneni bufferu klavesnice a cekani na klavesu %macro wait_key 0 xor ax, ax int 0x16 %endmacro ; nastaveni grafickeho rezimu %macro gfx_mode 1 mov ah, 0 mov al, %1 int 0x10 %endmacro ;----------------------------------------------------------------------------- org 0x100 ; zacatek kodu pro programy typu COM (vzdy se zacina na 256) start: gfx_mode 4 ; nastaveni grafickeho rezimu 320x200 se ctyrmi barvami mov ax, cs mov ds, ax mov si, image ; nyni DS:SI obsahuje adresu prvniho bajtu v obrazku mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM call move_half_image mov si, image+80; adresa prvniho pixelu na DRUHEM radku mov di, 8192 ; druha "stranka" video RAM call move_half_image wait_key exit move_half_image: mov bl, 100 ; pocitadlo radku outer_loop: mov cx, 80/2 ; velikost bloku ve slovech rep movsw ; prenest jeden obrazovy radek add si, 80 ; preskocit lichy/sudy radek dec bl jnz outer_loop ; opakovat smycku BL-krat ret image: incbin "image.bin"
Obrázek 9: Po přepnutí barvové palety získáme originál (podrobnosti budou popsány příště).
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v assembleru, které jsou určené pro překlad pomocí assembleru NASM, byly uložen do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
# | Příklad | Stručný popis | Adresa |
---|---|---|---|
1 | hello.asm | program typu „Hello world“ naprogramovaný v assembleru pro systém DOS | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/hello.asm |
2 | hello_shorter.asm | kratší varianta výskoku z procesu zpět do DOSu | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/hello_shorter.asm |
3 | hello_wait.asm | čekání na stisk klávesy | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/hello_wait.asm |
4 | hello_macros.asm | realizace jednotlivých částí programu makrem | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/hello_macros.asm |
5 | gfx4_putpixel.asm | vykreslení pixelu v grafickém režimu 4 | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_putpixel.asm |
6 | gfx6_putpixel.asm | vykreslení pixelu v grafickém režimu 6 | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_putpixel.asm |
7 | gfx4_line.asm | vykreslení úsečky v grafickém režimu 4 | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_line.asm |
8 | gfx6_line.asm | vykreslení úsečky v grafickém režimu 6 | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_line.asm |
9 | gfx6_fill1.asm | vyplnění obrazovky v grafickém režimu, základní varianta | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_fill1.asm |
10 | gfx6_fill2.asm | vyplnění obrazovky v grafickém režimu, varianta s instrukcí LOOP | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_fill2.asm |
11 | gfx6_fill3.asm | vyplnění obrazovky instrukcí REP STOSB | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_fill3.asm |
12 | gfx6_fill4.asm | vyplnění obrazovky, synchronizace vykreslování s paprskem | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx6_fill4.asm |
13 | gfx4_image1.asm | vykreslení rastrového obrázku získaného z binárních dat, základní varianta | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image1.asm |
14 | gfx4_image2.asm | varianta vykreslení rastrového obrázku s využitím instrukce REP MOVSB | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image2.asm |
15 | gfx4_image3.asm | varianta vykreslení rastrového obrázku s využitím instrukce REP MOVSW | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image3.asm |
16 | gfx4_image4.asm | korektní vykreslení všech sudých řádků bitmapy | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image4.asm |
17 | gfx4_image5.asm | korektní vykreslení všech sudých i lichých řádků bitmapy | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image5.asm |
18 | gfx4_image6.asm | nastavení barvové palety před vykreslením obrázku | https://github.com/tisnik/8bit-fame/blob/master/pc-dos/gfx4_image6.asm |
20. Odkazy na Internetu
- The Intel 8088 Architecture and Instruction Set
https://people.ece.ubc.ca/~edc/464/lectures/lec4.pdf - x86 Opcode Structure and Instruction Overview
https://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf - x86 instruction listings (Wikipedia)
https://en.wikipedia.org/wiki/X86_instruction_listings - x86 assembly language (Wikipedia)
https://en.wikipedia.org/wiki/X86_assembly_language - Intel Assembler (Cheat sheet)
http://www.jegerlehner.ch/intel/IntelCodeTable.pdf - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Chip Hall of Fame: Intel 8088 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-intel-8088-microprocessor - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Apple II History Home
http://apple2history.org/ - The 8086/8088 Primer
https://www.stevemorse.org/8086/index.html - flat assembler: Assembly language resources
https://flatassembler.net/ - FASM na Wikipedii
https://en.wikipedia.org/wiki/FASM - Fresh IDE FASM inside
https://fresh.flatassembler.net/ - MS-DOS Version 4.0 Programmer's Reference
https://www.pcjs.org/documents/books/mspl13/msdos/dosref40/ - INT 21 – DOS Function Dispatcher (DOS)
https://www.stanislavs.org/helppc/int21.html - DOS API (Wikipedia)
https://en.wikipedia.org/wiki/DOS_API - Bit banging
https://en.wikipedia.org/wiki/Bit_banging - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Bootloaders
https://en.wikibooks.org/wiki/X86_Assembly/Bootloaders - Počátky grafiky na PC: grafické karty CGA a Hercules
https://www.root.cz/clanky/pocatky-grafiky-na-pc-graficke-karty-cga-a-hercules/ - Co mají společného Commodore PET/4000, BBC Micro, Amstrad CPC i grafické karty MDA, CGA a Hercules?
https://www.root.cz/clanky/co-maji-spolecneho-commodore-pet-4000-bbc-micro-amstrad-cpc-i-graficke-karty-mda-cga-a-hercules/ - Karta EGA: první použitelná barevná grafika na PC
https://www.root.cz/clanky/karta-ega-prvni-pouzitelna-barevna-grafika-na-pc/ - RGB Classic Games
https://www.classicdosgames.com/ - Turbo Assembler (Wikipedia)
https://en.wikipedia.org/wiki/Turbo_Assembler - Microsoft Macro Assembler
https://en.wikipedia.org/wiki/Microsoft_Macro_Assembler - IBM Personal Computer (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Personal_Computer - Intel 8251
https://en.wikipedia.org/wiki/Intel_8251 - Intel 8253
https://en.wikipedia.org/wiki/Intel_8253 - Intel 8255
https://en.wikipedia.org/wiki/Intel_8255 - Intel 8257
https://en.wikipedia.org/wiki/Intel_8257 - Intel 8259
https://en.wikipedia.org/wiki/Intel_8259 - Support/peripheral/other chips – 6800 family
http://www.cpu-world.com/Support/6800.html - Motorola 6845
http://en.wikipedia.org/wiki/Motorola_6845 - The 6845 Cathode Ray Tube Controller (CRTC)
http://www.tinyvga.com/6845 - CRTC operation
http://www.6502.org/users/andre/hwinfo/crtc/crtc.html - 6845 – Motorola CRT Controller
https://stanislavs.org/helppc/6845.html - The 6845 Cathode Ray Tube Controller (CRTC)
http://www.tinyvga.com/6845 - Motorola 6845 and bitwise graphics
https://retrocomputing.stackexchange.com/questions/10996/motorola-6845-and-bitwise-graphics - IBM Monochrome Display Adapter
http://en.wikipedia.org/wiki/Monochrome_Display_Adapter - Color Graphics Adapter
http://en.wikipedia.org/wiki/Color_Graphics_Adapter - Color Graphics Adapter and the Brown color in IBM 5153 Color Display
https://www.aceinnova.com/en/electronics/cga-and-the-brown-color-in-ibm-5153-color-display/ - The Modern Retrocomputer: An Arduino Driven 6845 CRT Controller
https://hackaday.com/2017/05/14/the-modern-retrocomputer-an-arduino-driven-6845-crt-controller/ - flat assembler: Assembly language resources
https://flatassembler.net/ - FASM na Wikipedii
https://en.wikipedia.org/wiki/FASM - Fresh IDE FASM inside
https://fresh.flatassembler.net/ - MS-DOS Version 4.0 Programmer's Reference
https://www.pcjs.org/documents/books/mspl13/msdos/dosref40/ - INT 21 – DOS Function Dispatcher (DOS)
https://www.stanislavs.org/helppc/int21.html - DOS API (Wikipedia)
https://en.wikipedia.org/wiki/DOS_API - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Arithmetic
https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic - Art of Assembly – Arithmetic Instructions
http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter6/CH06–2.html - ASM Flags
http://www.cavestory.org/guides/csasm/guide/asm_flags.html - Status Register
https://en.wikipedia.org/wiki/Status_register - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Programming from the Ground Up Book – Summary
http://savannah.nongnu.org/projects/pgubook/ - DOSBox
https://www.dosbox.com/ - The C Programming Language
https://en.wikipedia.org/wiki/The_C_Programming_Language - Hercules Graphics Card (HCG)
https://en.wikipedia.org/wiki/Hercules_Graphics_Card - Complete 8086 instruction set
https://content.ctcd.edu/courses/cosc2325/m22/docs/emu8086ins.pdf - Complete 8086 instruction set
https://yassinebridi.github.io/asm-docs/8086_instruction_set.html - 8088 MPH by Hornet + CRTC + DESiRE (final version)
https://www.youtube.com/watch?v=hNRO7lno_DM - Area 5150 by CRTC & Hornet (Party Version) / IBM PC+CGA Demo, Hardware Capture
https://www.youtube.com/watch?v=fWDxdoRTZPc - 80×86 Integer Instruction Set Timings (8088 – Pentium)
http://aturing.umcs.maine.edu/~meadow/courses/cos335/80×86-Integer-Instruction-Set-Clocks.pdf