Tryb graficzny w języku asembler

Na początek uprzedzam: jeśli myślicie o wysokich rozdzielczościach, to się zawiedziecie, gdyż ten kurs nie będzie takich omawiał. Jeśli naprawdę wolicie wysokie rozdzielczości, to poszukajcie w Internecie opisu standardu VESA lub DirectX API.

A my, zamiast bawić się w te wszystkie skomplikowane sprawy, zajmiemy się trybem 13h. Ten tryb oferuje rozdzielczość 320x200 w 256 kolorach (co widać też w Liście Przerwań Ralfa Brown'a - RBIL).
Ale najważniejszą jego cechą jest to, że 320x200 = 64000 < 64kB, więc cały ekran mieści się w jednym segmencie, co znacznie ułatwia pracę.

Ekran w trybie graficznym mieści się w segmencie 0A000h oraz:
0A000:0000 - pierwszy piksel (bajt, 256 możliwości)
0A000:0001 - drugi piksel
0A000:0002 - trzeci piksel
...

Do zmiany trybu graficznego używa się przerwania 10h, funkcji 0 (opis wyjęty z Listy przerwań Ralfa Brown'a):


(przeskocz opis int 10h, ah=0)
	INT 10 - VIDEO - SET VIDEO MODE
	        AH = 00h
	        AL = desired video mode (see #00010)
	Return: AL = video mode flag (Phoenix, AMI BIOS)
	            20h mode > 7
	            30h modes 0-5 and 7
	            3Fh mode 6
	        AL = CRT controller mode byte (Phoenix 386 BIOS v1.10)
	Desc:   specify the display mode for the currently active display
		 adapter

Jak widać, zmiana trybu graficznego na omawiany tryb 13h nie jest trudniejsza niż:

	mov ax, 13h
	int 10h

Powrót do tradycyjnego trybu tekstowego 80x25 wygląda tak:

	mov ax, 3
	int 10h

Pytanie brzmi: jak cokolwiek narysować?
Nic prostszego! Po prostu pod adres:
wiersz*320 + kolumna
zapisujemy odpowiedni bajt, na przykład tak (składnia TASM):

	mov ax, 0a000h
	mov es, ax
	xor di,di
	mov byte ptr es:[di],15		; NASM/FASM:   mov byte [es:di],15

No ale 1 piksel to za mało na nasze ambicje, prawda?
Spróbujmy narysować poziomą linię (NASM):


(przeskocz program rysujący linię poziomą)
	; nasm -O999 -o liniapoz.com -f bin liniapoz.asm

	org 100h

		mov ax, 13h
		int 10h			; uruchom tryb graficzny 13h

		mov ax, 0a000h
		mov es, ax
		xor di, di

		mov al, 15
		mov cx, 10

		rep stosb		; przenieś 10 bajtów wartości 15 pod
					; es:di = 0a000:0000

		xor ah, ah
		int 16h

		mov ax, 3
		int 10h			; powrót do trybu tekstowego

		mov ax, 4c00h
		int 21h

To chyba nie było zbyt trudne, prawda? No to spróbujmy coś trudniejszego: linia pionowa.

Cała filozofia w tym przypadku polega na tym, aby po narysowaniu piksela przejść o 1 wiersz niżej (czyli o 320 bajtów dalej). Piszmy więc (NASM):


(przeskocz program rysujący linię pionową)
	; nasm -O999 -o liniapio.com -f bin liniapio.asm

	org 100h

		mov	ax, 13h
		int	10h

		mov	ax, 0a000h
		mov	es, ax
		xor	di, di

		mov	al, 15
		mov	cx, 100

	rysuj:
		mov	[es:di], al
		add	di, 320
		loop	rysuj

		xor	ah, ah
		int	16h

		mov	ax, 3
		int	10h

		mov	ax, 4c00h
		int	21h

Na razie było łatwo: rysować zaczynaliśmy w lewym górny rogu, więc DI był równy 0. A co, jeśli chcemy wyświetlać piksele gdzieś indziej?
Cóż, są dwie możliwości:

  1. W czasie pisania programu (czyli przed kompilacją) znasz dokładną pozycję, gdzie będziesz rysować.

    W takim przypadku kompilator policzy DI za ciebie, wystarczy wpisać coś takiego:

    		mov di, wiersz*320 + kolumna	

    wstawiając w miejsce słów wiersz i kolumna znane przez siebie wartości.

  2. Pozycja, gdzie będziesz rysować jest zmienna i zależy na przykład od tego, co wpisze użytkownik.

    Tutaj jest gorzej. Trzeba wpisać do programu instrukcje, które przemnożą wiersz przez 320 i dodadzą kolumnę. Należy raczej unikać powolnej instrukcji (I)MUL. Ten problem rozwiążemy tak (wiersz i kolumna to 2 zmienne po 16 bitów):

    		mov ax, [wiersz]
    		mov bx, [wiersz]	; BX = AX
    		shl ax, 8		; AX = AX*256
    		shl bx, 6		; BX = BX*64 = AX*64
    		add ax, bx		; AX = AX*256 + AX*64 = AX*320 =
    					;     = wiersz*320
    		add ax, [kolumna]	; AX = wiersz*320 + kolumna
    
    		mov di,ax	

Ostatni przykład: rysowanie okręgu (no, w każdym razie czegoś co miało być okręgiem a ma kształt bardziej przypominający elipsę...). Program ten wykorzystuje koprocesor do policzenia sinusów i kosinusów dla kątów od 0 do 360 stopni, przerobionych na radiany. Komentarze obok instrukcji FPU oznaczają stan stosu, od st(0) z lewej.


(przeskocz program rysujący koło)
	; nasm -O999 -o kolo.com -f bin kolo.asm

	org 100h

		mov ax, 13h
		int 10h

		mov ax, 0a000h
		mov es, ax

		mov cx, 360

		finit
		fldpi
		fild word [sto80]

		fdivp st1, st0		; pi/180

		fld1
		fild word [r]		; r, 1, pi/180
		fldz			; kąt=0, r, 1, pi/180

		mov al, 15

	rysuj:
		fld st0			; kąt, kąt, r, 1, pi/180

		fmul st4		; kąt w radianach
		mov di, 100*320 + 160	; środek ekranu

		fsin			; sin(kąt), kąt, r, 1, pi/180
		fmul st2		; sin(kąt)*r, kąt, r, 1, pi/180

		fistp word [wys]	; kąt, r, 1, pi/180

		fld st0			; kąt, kąt, r, 1, pi/180
		fmul st4		; kąt w radianach
		fcos			; cos(kąt), kąt, r, 1, pi/180
		fmul st2		; r*cos(kąt), kąt, r, 1, pi/180

		fistp word [szer]	; kąt, r, 1, pi/180

		add di, [szer]		; dodajemy odległość poziomą

		mov dx, [wys]
		mov bx, dx
		shl dx, 8
		shl bx, 6
		add dx, bx		; dx = wys*320

		sub di, dx		; odejmujemy odległość pionową

		mov [es:di], al		; wyświetlamy piksel

		fadd st0, st2		; kat += 1

		dec cx
		jnz rysuj

		finit

		xor ah, ah
		int 16h

		mov ax, 3
		int 10h

		mov ax, 4c00h
		int 21h

	r	dw	50
	szer	dw	0
	wys	dw	0
	sto80	dw	180

Podobnie, używając FSIN i FCOS, można rysować na przykład linie ukośne, które pominąłem w tym kursie.

Mam nadzieję, że po lekturze tego odcinka każdy bez problemów będzie rysował w tym dość prostym (zwłaszcza do nauki) trybie graficznym.

Miłego eksperymentowania!



Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)