2. Śnieżenie
Jako drugi program napiszemy graficzny efekt - śnieżenie ekranu. Oto jego pełny kod źródłowy w NoASM:
; 02 Śnieżenie
; ======== MAKRA ========
define width 120
define height 90
define pixels 10800 ; width*height
define size 43200 ; pixels*4u
; ======== KOD ========
; Organizujemy stos
mov4i &stos SR
; Tryb ekranu
mov1u 5 A1 ; Urządzenie: ekran
mov1u 2 A2 ; Komunikat: zmiana trybu
mov1u 2 A3 ; Tryb: graficzny
mov4i width BX ; Szerokość
mov4i height BY ; Wysokość
out
; Pętla główna
Loop:
call Frame
jmp Loop
; ======== FUNKCJE ========
; Funkcja główna - rysuje klatkę
Frame:
call Generuj
call Zapisz
call Zaprezentuj
ret
; Wygenerowanie klatki amimacji do pamięci Screen
Generuj:
namespace ; Anonimowa strefa nazw
; Inicjalizacja
zero4 BX ; BX będzie licznikiem pikseli
mov4i &Screen BY ; BY będzie przechowywał adres bieżącego piksela
; Pętla
MyLoop:
; Warunek
eq4u BX pixels A1 ; Jeśli licznik pikseli = liczba pikseli
retnz1 A1 ; to koniec funkcji
; Zapisanie piksela
rand1u 0x00 0xFF A1 ; Wylosowanie jasności
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
inc4i BY
; Zwiększenie liczników (BY już został zwiększony powyżej)
inc4u BX
; Zapętlenie
jmp MyLoop
end
; Wysłanie obrazu do ekranu
Zapisz:
mov1u 5 A1 ; Urządzenie: ekran
mov1u 7 A2 ; Komunikat: wysłanie obrazu
mov4i &Screen AY ; Adres pamięci z obrazem
out
ret
; Zaprezentowanie zmian na ekranie
Zaprezentuj:
mov1u 5 A1 ; Urządzenie: ekran
mov1u 4 A2 ; Komunikat: zaprezentowanie zmian
out
ret
; ======== PAMIĘĆ ========
; Rezerwacja pamięci na ekran
Screen:
reserve size
; To znaczy, że tutaj po końcu programu będzie się rozciągał obszar
; pamięci do wykorzystania jako stos
stos:
Jak widać, kod jest trochę dłuższy od poprzedniego i zawiera wiele nowych rzeczy, które będziemy musieli omówić. Za pomocą odpowiednich komentarzy podzielony został na logiczne części.
Najpierw przyjrzyjmy się pierwszego fragmentowi:
; ======== MAKRA ======== define width 120 define height 90 define pixels 10800 ; width*height define size 43200 ; pixels*4u
Define nie jest instrukcją. Oznacza to, że z takiej linijki
nie powstanie instrukcja do pliku docelowego z dyskiem. Jest to tzw.
dyrektywa preprocesora, która definiuje makro. Jej pierwszym parametrem
jest nazwa, a drugim treść. Począwszy od takiej deklaracji w całym kodzie
słowo width zostanie zamienione na liczbę 120,
słowo height na liczbę 90 itd. Jest to tak jakby
zdefiniowanie stałej.
Wartości dwóch ostatnich makr powstają z obliczenia na podstawie poprzednich. Niestety w NoASM nie można stosować wyrażeń matematycznych i dlatego trzeba było policzyć je ręcznie, wpisać, a w komentarzu obok dopisać skąd to się wzięło.
Jeśli jeszcze tego nie zrobiłeś, skompiluj i uruchom ten program.
Będzie to potrzebne do zrozumienia, co znaczą te stałe. Width
oznacza ilość punktów na szerokość. Height to ilość punktów
na wysokość. Iloczyn szerokości i wysokości to pixels, czyli
całkowita ilość punktów. Z kolei size to rozmiar pamięci
potrzebnej na zapamiętanie obrazu. Każdy punkt składa się z koloru,
a kolor, jak pamiętamy, zapisywany jest na 4 bajtach.
Zauważyłeś zapewne, że po odpowiednim rozszerzeniu okna wirtualnej maszyny poszczególne piksele (punkty) obrazu na wirtualnym ekranie mają postać dużych kwadratów, a nie są pojedynczymi pikselami ekranu rzeczywistego monitora. Mówiąc o punktach lub pikselach będziemy mieli na myśli te wirtualne - na ekranie NoSense - niezależnie od tego, jak są wyświetlane przez wirtualną maszynę.
Poznamy teraz rzecz, której od tej pory będziemy już zawsze używali we wszystkich programach. Stos jest bardzo ważną strukturą danych. Nie powstaje jednak w NoSense automatycznie, ale trzeba go sobie samemu zorganizować. Na szczęście potem już można używać instrukcji, które go wspierają.
Stos potrzebuje miejsca w pamięci. To miejsce robimy deklarując na samym końcu programu etykietę:
; To znaczy, że tutaj po końcu programu będzie się rozciągał obszar ; pamięci do wykorzystania jako stos stos:
Podczas uruchamiania programu maszyna ładuje go do pamięci. Nasz kod, skompilowany do postaci kodu maszynowego NoCode, zajmuje ileś tam bajtów. Dalej rozciąga się pamięć, której również można używać. Właśnie początkowi tej pamięci nadajemy nazwę definiując powyższą etykietę na końcu naszego programu.
Do utworzenia stosu potrzebne jest wpisanie adresu pamięci, od którego
się zaczyna, do specjalnie do tego przeznaczonego rejestru SR.
To jest pierwsza instrukcja w naszym programie:
; ======== KOD ======== ; Organizujemy stos mov4i &stos SR
Każda komórka (bajt) pamięci ma swój adres. Adresy są 4-bajtowymi
liczbami całkowitymi ze znakiem (typu [4i]).
W znanej nam już instrukcji mov4i widzimy użyty dodatkowy
znaczek &. Oznacza on, że do rejestru SR chcemy
wpisać adres etykiety stos. Gdyby go nie było, wpisalibyśmy
do niego to, co znajduje się w pamięci oznaczonej tą etykietą.
Właściwy kod rozpoczyna się wysłaniem do ekranu komunikatu wyjściowego zmieniającego tryb wyświetlania na tryb graficzny o żądanej szerokości i wysokości:
; Tryb ekranu mov1u 5 A1 ; Urządzenie: ekran mov1u 2 A2 ; Komunikat: zmiana trybu mov1u 2 A3 ; Tryb: graficzny mov4i width BX ; Szerokość mov4i height BY ; Wysokość out
Opis tego komunikatu znajdziesz w dokumentacji w rozdziale
Devices > Screen > Output messages > Set mode.
Wykonane tutaj operacje powinny być ci już znane. Zwróć jedynie uwagę,
że podając szerokość i wysokość używamy zdefiniowanych wcześniej makr -
width i height.
Działają one tak samo, jakgdyby w tym miejscu bezpośrednio wpisać ich
wartości.
Jednak główną częścią programu jest ta oto niepozorna pętla:
; Pętla główna Loop: call Frame jmp Loop
Widzimy tutaj etykietę i instrukcję skoku do niej. Można się więc
domyślić, że działanie pętli polega na nieustannym wykonywaniu instrukcji
call. Tylko co to za instrukcja?
Instrukcja call oznacza wywołanie. Jest podobna do
instrukcji skoku jmp, ale zanim wykona skok, odkłada na stos
adres aktualnie wykonywanej instrukji (konkretnie to instrukcji następnej
za tą, czyli u nas jmp), aby potem móc powrócić w to
miejsce.
Odłożenie na stos polega na zapisaniu danej wartości (w tym przypadku
adresu miejsca w kodzie) pod adresem przechowywanym przez rejestr
SR oraz zwiększeniu tego rejestru o rozmiar tej informacji.
W ten sposób można odkładać na stos coraz to nowe wartości, a potem
w odwrotnej kolejności zdejmować je (odczytywać). Właśnie tak działa stos.
Jednak w instrukcji call to wszystko odbywa się automatycznie
i nie musimy się tym zajmować.
Skoro call działa podobnie do instrukcji skoku, podana
do niej nazwa Frame powinna być nazwą etykiety. Okazuje się,
że faktycznie tak jest. Widzimy przy okazji, że etykiety wolno używać
nawet jeszcze wcześniej, nim zostanie zdefiniowana. Zdefiniowany pod
nią kod wygląda natomiast tak:
; ======== FUNKCJE ======== ; Funkcja główna - rysuje klatkę Frame: call Generuj call Zapisz call Zaprezentuj ret
Po przeskoczeniu do tej etykiety rozpoczyna się wykonywanie kolejnych
instrukcji call. Robią one kolejno trzy rzeczy potrzebne do
utworzenia każdej klatki animacji śnieżenia, a całość jest jakby
funkcją (podprogramem) rysującym pojedynczą klatkę.
Obecna na końcu bezparametrowa instrukcja ret oznacza
powrót (ang. "return") i jest jakby odwrotnością call.
Zdejmuje ze stosu zapamiętany adres i skacze do niego wracając w miejsce,
z którego nastąpiło wywołanie i kontynuując wykonywanie kodu znajdującego
się pod tym wywołaniem.
Żeby wygenerować nową klatkę animacji, potrzeba trzech czynności. Ich omawianie zaczniemy od końca:
; Zaprezentowanie zmian na ekranie Zaprezentuj: mov1u 5 A1 ; Urządzenie: ekran mov1u 4 A2 ; Komunikat: zaprezentowanie zmian out ret
Ta funkcja po wywołaniu wysyła do ekranu znany nam już komunikat zaprezentowania dokonanych zmian. Okazuje się bowiem, że takiego samego zaprezentowania trzeba dokonać również w trybie graficznym, a nie tylko w trybie tekstowym, żeby zmiany pokazały się na ekranie.
Zanim omówimy środkową funkcję, spójrz na deklarację pod koniec programu:
; ======== PAMIĘĆ ======== ; Rezerwacja pamięci na ekran Screen: reserve size
Reserve nie jest instrukcją, ale poleceniem kompilatora.
Oznacza ono zarezerwowanie określonej ilości bajtów pamięci. Jak
pamiętamy, size jest makrem zdefiniowanym jako liczba
43200. Normalnie taka spora ilość bajtów zapisana zostałaby do dysku
zajmując w nim miejsce. Jeśli jednak pod poleceniem reserve
nie ma już w programie żadnej instrukcji (jedynie inne takie polecenia
oraz etykiety - tak jak u nas), kompilator nie zaśmieca niepotrzebnie
dysku, a wszystko i tak działa poprawnie.
Podsumowując, pod etykietą Screen kryje się obszar 43200
bajtów pamięci wypełnionych byle czym. Będziemy go używali do
przechowywania tablicy z kolorami pikseli do wyświetlenia na ekranie.
Wyświetlenia takiego dokonuje funkcja:
; Wysłanie obrazu do ekranu Zapisz: mov1u 5 A1 ; Urządzenie: ekran mov1u 7 A2 ; Komunikat: wysłanie obrazu mov4i &Screen AY ; Adres pamięci z obrazem out ret
Jest to wysłanie komunikatu wyjściowego opisanego w dokumentacji w
rozdziale Devices > Screen > Output messages > Set image.
W trybie graficznym nie rysuje się po pikselu (tak jak w trybie tekstowym
ustawiało się kolejne znaki), ale trzeba na raz wpisać do ekranu cały
obraz z obszaru pamięci przechowującego kolory kolejnych pikseli.
Do rejestru AY wpisujemy adres obszaru pamięci oznaczonego
etykietą Screen - stąd poznany już wcześniej znaczek
&. Przypomnę, że gdyby go nie było, wpisalibyśmy to, co
znajduje się w pamięci pod tą etykietą.
Wreszcie czas na omówienie najważniejszej i najtrudniejszej funkcji.
Wypełnia ona obszar pamięci oznaczony pod Screen kolorami
punktów. Do naszego efektu potrzebne jest, aby każdy punkt w każdej klatce
otrzymał nowy, losowy kolor w odcieniach szarości (czarny, biały lub jakiś
szary).
; Wygenerowanie klatki amimacji do pamięci Screen
Generuj:
namespace ; Anonimowa strefa nazw
; Inicjalizacja
zero4 BX ; BX będzie licznikiem pikseli
mov4i &Screen BY ; BY będzie przechowywał adres bieżącego piksela
; Pętla
MyLoop:
; Warunek
eq4u BX pixels A1 ; Jeśli licznik pikseli = liczba pikseli
retnz1 A1 ; to koniec funkcji
; Zapisanie piksela
rand1u 0x00 0xFF A1 ; Wylosowanie jasności
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
inc4i BY
; Zwiększenie liczników (BY już został zwiększony powyżej)
inc4u BX
; Zapętlenie
jmp MyLoop
end
Fragment rozpoczynający się poleceniem namespace,
a zakończony odpowiadającym mu poleceniem end wyznacza tzw.
strefę nazw. Wszystkie etykiety zdefiniowane w tej strefie są lokalne
dla niej. Z każdego miejsca w kodzie można się odwołać do każdej z etykiet,
ale w miejscu na zewnątrz tej strefy wymagałoby to podania pełnej ścieżki
w postaci nazwa_strefy.etykieta.
Ponieważ po słowie namespace nie ma podanej nazwy dla
strefy, powstaje tzw. strefa anonimowa. Jej nazwa zostanie przez kompilator
wygenerowana automatycznie z jakiś losowych znaków, ale to nie ma
znaczenia.
Nam chodzi tutaj o odizolowanie etykiet zdefiniowanych i
używanych wewnętrznie w funkcji Generuj od reszty kodu,
żeby przypadkiem nie powstała kolizja nazw.
Musimy teraz zaplanować, w jaki sposób napisać samo to wypełnianie tablicy kolorami kolejnych pikseli. Trzeba będzie zorganizować pętlę, która przejdzie przez komórki pamięci odpowiadające wszystkim pikselom i wypełni je nowymi danymi. Pętla będzie musiała się zatrzymać po dojściu do końca, co wymaga sprawdzenia warunku.
Wybrane tu rozwiązanie może nie jest najlepsze ani najszybsze, ale działa nienajgorzej. Będziemy używali dwóch liczb, na które przeznaczymy dwa rejestry:
; Inicjalizacja
zero4 BX ; BX będzie licznikiem pikseli
mov4i &Screen BY ; BY będzie przechowywał adres bieżącego piksela
BX będzie zliczał przetwarzane piksele. Zaczyna od zera,
z każdym nowym będzie zwiększany o jeden, a kiedy osiągnie liczbe równą
ilości pikseli (zdefiniowaną jako makro pixels), będzie to
warunkiem zakończenia pętli.
Trzeba będzie zapisywać obliczane wartości do pamięci rozpoczynającej
się etykietą Screen. W tym celu użyjemy rejestru
BY, który będzie przechowywał adres bieżącego piksela.
Pod ten adres będziemy zapisywali.
Na początek ustawiamy go na adres etykiety Screen,
co odpowiada pierwszemu pikselowi, jaki się tam znajdzie.
Realizacja pętli przebiegającej po wszystkich pikselach wygląda tak:
; Pętla
MyLoop:
; Warunek
eq4u BX pixels A1 ; Jeśli licznik pikseli = liczba pikseli
retnz1 A1 ; to koniec funkcji
; ...
; Zapętlenie
jmp MyLoop
Na końcu znajduje się skok do początku, ale wcześniej, jeszcze przed
właściwą treścią pętli zastąpioną tutaj wielokropkiem znajduje się warunek.
Instrukcja eq4u porównuje dwie wartości. Jeśli wartość
znajdująca się w rejestrze BX jest równa (ang. "equal")
liczbie kryjącej się pod makrem pixels, do rejestru
A1 wpisana zostaje logiczna wartość prawdy (czyli jednobajtowa
liczba różna od zera). W przeciwnym wypadku wpisany zostaje fałsz, czyli
zero.
Retnz1 to powrót warunkowy. Dokonuje powrotu z funkcji,
tak jak ret, ale tylko jeśli jednobajtowa wartość znajdująca
się w rejestrze A1 jest różna od zera (ang. "not zero").
Innymi słowy - jeśli wykonane instrukcję wcześniej porównanie dało wynik
prawdziwy.
Na koniec pozostało samo wygenerowanie koloru piksela:
; Zapisanie piksela
rand1u 0x00 0xFF A1 ; Wylosowanie jasności
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
mov1u A1 *BY
inc4i BY
inc4i BY
; Zwiększenie liczników (BY już został zwiększony powyżej)
inc4u BX
Instrukcja rand1u losuje liczbę z zakresu od 0 (pierwszy
parametr) do 255 (drugi parametr - zapisany szesnastkowo) i wynik losowania
umieszcza w rejestrze A1 (trzeci parametr). Ta wartość powinna
się stać jasnością wszystkich trzech składowych (czerwonej, zielonej i
niebieskiej) danego piksela. Tylko wtedy, kiedy są one sobie równe,
piksel ma kolor czarny, szary albo biały, a nie barwny.
W tym celu przypominamy sobie, że BY przechowuje adres
bieżącego piksela. Musimy potraktować ten adres jako liczbę typu
[4u]. Jednakże będziemy zapisywali jej poszczególne bajty.
Musimy do tego wiedzieć, że bajty przechowywane są w odwrotnej kolejności,
niżby się to intuicyjnie wydawało - nie XRGB, ale BGRX.
Najpierw kopiujemy więc bajt z A1 do pierwszej składowej.
Pojawia się tu nowy operator - gwiazdka *. Gdyby jej nie było,
skopiowana zostałaby liczba z rejestru A1 do rejestru
BY (dokładnie do jej pierwszego bajta). Nam jednak chodzi
o to, żeby ta liczba wpisana została w miejsce w pamięci, którego adres
przechowuje (na które pokazuje) rejestr BY - właśnie do tego
służy gwiazdka. W językach wysokiego poziomu nazywa się to wskaźnikiem.
Wpisujemy więc liczbę pod adres przechowywany przez BY i
zwiększamy ten adres przesuwając nasz wskaźnik o jeden bajt do przodu.
W ten sposób wypełniamy składowe koloru piksela czwarty bajt pozostawiając
niezmieniony (BY zwiększamy dwa razy).
Po tym całym procesie BY przechowuje już adres do początku
następnego piksela, który zostanie wpisany w następnym cyklu pętli.
Pozostaje już tylko zwiększyć licznik pikseli BX i to
wszystko.
2004-07-02
Adam Sawicki "Regedit"
WWW: NoSense,
HomePage,
ProgrameX