W Linuksie oczywiście nie można rysować bezpośrednio, czyli programować kartę graficzną lub zapisywać do pamięci ekranu (dostępnej na przykład w DOSie pod segmentem A000h). Zamiast tego, większość roboty wykonają za nas biblioteki oraz moduły jądra, odpowiedzialne za urządzenia (jeśli programujemy na przykład framebuffer). W tym artykule wykorzystam możliwości biblioteki SVGAlib, ze względu na prostotę jej opanowania, oraz z biblioteki Allegro ze względu na jej wieloplatformowość, łatwość używania i możliwość zapisania obrazów do pliku.
Aby móc korzystać z SVGAlib, musicie zainstalować pakiety svgalib oraz svgalib-devel lub po prostu samemu skompilować bibliotekę, jeśli pakiety nie są dostępne.
Będziemy się zajmować dwoma trybami (ale nic nie stoi na przeszkodzie, aby skorzystać z dowolnego innego). Będą to: tryb 320x200 w 256 kolorach i oczywiście tryb tekstowy (ten, do którego wrócimy po zakończeniu programu). Do ustawienia bieżącego trybu służy funkcja vga_setmode. Przyjmuje ona jeden argument - numer trybu (G320x200x256 = 5, TEXT = 0).
Do zmiany bieżącego koloru służy funkcja vga_setcolor. Jedynym jej argumentem jest numer koloru (na przykład 1-niebieski, 2-zielony, 3-jasnoniebieski, 4-czerwony, 5-fioletowy, 6-brązowy, 7-biały).
Do narysowania pojedynczego piksela służy funkcja vga_drawpixel. Przyjmuje ona dwa argumenty. Od lewej (ostatni wkładany na stos) są to: współrzędna X oraz współrzędna Y punktu do zapalenia. Punkt o współrzędnych (0,0) to lewy górny róg ekranu.
Współrzędna X rośnie w prawo, a Y - w dół ekranu.
Do narysowania linii służy funkcja vga_drawline. Przyjmuje ona 4 argumenty. Od lewej (ostatni wkładany na stos) są to: współrzędna X początku linii, współrzędna Y początku linii, współrzędna X końca linii, współrzędna Y końca linii.
Aby nasz rysunek był widoczny choć przez chwilę, skorzystamy z funkcji systemowej sys_nanosleep, podając jej adres struktury timespec mówiącej, jak długą przerwę chcemy. Więcej szczegółów w innych artykułach oraz w opisie przerwania int 80h.
Do działania programów pod X-ami potrzebne mogą być uprawnienia do pliku /dev/console a pod konsolą tekstową - do pliku /dev/mem.
Jak widać, teoria nie jest trudna, więc przejdźmy od razu do przykładowych programów.
Pierwszy z nich ma zaprezentować rysowanie pojedynczego piksela oraz dowolnych linii. Zwróćcie uwagę na sposób kompilacji. Korzystamy z bibliotek dostępnych dla programistów języka C, więc do łączenia programu w całość najlepiej użyć GCC - zajmie się on dołączeniem wszystkich niezbędnych bibliotek. A skoro używamy gcc, to funkcja główna zamiast _start, musi się nazywać main - tak samo jak funkcja główna w programach napisanych w C. I tak samo, zamiast funkcji wychodzenia z programu, możemy użyć komendy RET, aby zamknąć program.
; Program do rysowania linii dowolnej z wykorzystaniem SVGAlib ; ; Autor: Bogdan D., bogdandr (at) op.pl ; ; kompilacja: ; nasm -O999 -f elf -o graf1.o graf1.asm ; gcc -o graf1 graf1.o -lvga section .text global main ; reszta trybów dostępna w /usr/include/vga.h (wymagany svgalib-devel) %define TEXT 0 %define G320x200x256 5 ; deklaracje funkcji zewnętrznych: extern vga_setmode extern vga_setcolor extern vga_drawline extern vga_drawpixel main: push dword G320x200x256 call vga_setmode ; ustawiamy tryb graficzny: ; 320x200 w 256 kolorach add esp, 4 ; zdejmujemy argument ze stosu push dword 5 ; ustawiamy kolor (5=fioletowy) call vga_setcolor add esp, 4 push dword 100 ; współrzędna y punktu push dword 160 ; współrzędna x punktu call vga_drawpixel ; rysujemy piksel add esp, 8 push dword 6 ; ustawiamy kolor (6=brązowy) call vga_setcolor add esp, 4 push dword 160 push dword 320 push dword 0 push dword 0 call vga_drawline ; linia od lewego górnego ; narożnika do środka prawego boku add esp, 16 push dword 7 ; ustawiamy kolor (7=biały) call vga_setcolor add esp, 4 push dword 10 push dword 20 push dword 110 push dword 50 call vga_drawline add esp, 16 mov dword [t1+timespec.tv_nsec], 0 mov dword [t1+timespec.tv_sec], 5 ; czekaj 5 sekund mov eax, 162 ; sys_nanosleep mov ebx, t1 ; adres struktury mówiącej, ; ile chcemy czekać mov ecx, 0 int 80h ; robimy przerwę... push dword TEXT call vga_setmode ; ustawiamy tryb tekstowy 80x25 add esp, 4 xor eax, eax ; zerowy kod zakończenia (bez błędu) ret ; powrót z funkcji main i ; zakończenie programu section .data struc timespec .tv_sec: resd 1 .tv_nsec: resd 1 endstruc t1 istruc timespec
Drugi program rysuje okrąg. Środek tego okręgu jest w środku ekranu, kolejne punkty (łącznie będzie ich 360) obliczam następująco: współrzędna x = współrzędna x środka + r*cos(t), y = y_środka + r*sin(t), po przerobieniu kąta t na radiany. Do liczenia tych sinusów i kosinusów wykorzystuję FPU.
; Program do rysowania okręgu z wykorzystaniem SVGAlib ; ; Autor: Bogdan D., bogdandr (at) op.pl ; ; kompilacja: ; nasm -O999 -f elf -o kolo_linux.o kolo_linux.asm ; gcc -o kolo_linux kolo_linux.o -lvga section .text global main ; reszta trybów dostępna w /usr/include/vga.h (wymagany svgalib-devel) %define TEXT 0 %define G320x200x256 5 extern vga_setmode extern vga_setcolor extern vga_drawpixel main: push dword G320x200x256 call vga_setmode ; ustawiamy tryb graficzny: ; 320x200 w 256 kolorach add esp, 4 ; zdejmujemy argument ze stosu push dword 2 ; ustawiamy kolor call vga_setcolor add esp, 4 mov ebx, 360 finit ; poniżej będę zapisywał stan ; rejestrów FPU, od st0 do st7 fldpi ; pi fild word [sto80] ; 180, pi fdivp st1, st0 ; pi/180 fld1 ; 1, pi/180 fild word [r] ; r, 1, pi/180 fldz ; kąt=0, r, 1, pi/180 rysuj: fld st0 ; kąt, kąt, r, 1, pi/180 fmul st4 ; kąt w radianach fsin ; sin(kąt), kąt, r, 1, pi/180 fmul st2 ; sin(kąt)*r, kąt, r, 1, pi/180 fistp dword [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 dword [szer] ; kąt, r, 1, pi/180 mov eax, [wys] mov edx, [szer] add eax, 100 ; dodajemy współrzędną y środka add edx, 160 ; dodajemy współrzędną x środka push eax ; umieszczamy współrzędne na stosie push edx call vga_drawpixel ; rysujemy piksel add esp, 8 fadd st0, st2 ; kąt = kąt + 1 dec ebx jnz rysuj mov dword [t1+timespec.tv_nsec], 0 mov dword [t1+timespec.tv_sec], 5 ; 5 sekund mov eax, 162 ; sys_nanosleep mov ebx, t1 ; adres struktury mówiącej, ; ile chcemy czekać mov ecx, 0 int 80h ; robimy przerwę... push dword TEXT call vga_setmode ; ustawiamy tryb tekstowy 80x25 add esp, 4 xor eax, eax ; zerowy kod zakończenia (bez błędu) ret ; powrót z funkcji main section .data struc timespec .tv_sec: resd 1 .tv_nsec: resd 1 endstruc t1 istruc timespec r dw 50 ; promień okręgu szer dd 0 wys dd 0 sto80 dw 180
Aby móc korzystać z SVGAlib, musicie zainstalować pakiety svgalib oraz svgalib-devel lub po prostu samemu skompilować bibliotekę, jeśli pakiety nie są dostępne.
UWAGA: zmieni się sposób kompilacji programu w stosunku do tradycyjnych, asemblerowych programów. Korzystamy z bibliotek dostępnych dla programistów języka C, więc do łączenia programu w całość najlepiej użyć GCC - zajmie się on dołączeniem wszystkich niezbędnych bibliotek. A skoro używamy gcc, to funkcja główna zamiast _start, musi się nazywać main - tak samo jak funkcja główna w programach napisanych w C. I tak samo, zamiast funkcji wychodzenia z programu, możemy użyć komendy RET, aby zamknąć program. Sama kompilacja przebiega następująco:
nasm -O999 -f elf -o graf2.o graf2.asm gcc -o graf2 graf2.o -lvga
Będziemy się zajmować dwoma trybami (ale nic nie stoi na przeszkodzie, aby skorzystać z dowolnego innego). Będą to: tryb 320x200 w 256 kolorach i oczywiście tryb tekstowy (ten, do którego wrócimy po zakończeniu programu). Do ustawienia bieżącego trybu służy funkcja vga_setmode. Przyjmuje ona jeden argument - numer trybu (G320x200x256 = 5, TEXT = 0).
Przed rozpoczęciem pracy ustawiamy tryb graficzny 320x200, wykonując
extern vga_setmode ... push dword 5 ; G320x200x256 call vga_setmode ; ustawiamy tryb graficzny: ; 320x200 w 256 kolorach add esp, 4 ; zdejmujemy argument ze stosu
Ale teraz zajmiemy się rysowaniem bez funkcji SVGAlib, poprzez zapis do odpowiednich komórek pamięci. Pamięć trybów graficznych znajduje się w segmencie A000, co odpowiada liniowemu adresowi A0000, licząc od adresu 0. Oczywiście system, ze względów bezpieczeństwa, nie pozwoli nam bezpośrednio pisać pod ten adres, więc musimy sobie poradzić w inny sposób. Sposób ten polega na otwarciu specjalnego pliku urządzenia, który symbolizuje całą pamięć w komputerze - /dev/mem. Na większości systemów otwarcie tego pliku wymaga uprawnień administratora.
Po otwarciu pliku mamy dwie możliwości. Pierwsza to poruszać się po nim funkcjami do zmiany pozycji w pliku, oraz odczytywać i zapisywać funkcjami odczytu i zapisu danych z i do pliku. Może to być powolne, ale sposób jest. Druga możliwość to zmapować plik do pamięci, po czym korzystać z niego jak ze zwykłej tablicy. Tę możliwość opiszę teraz szczegółowo.
Otwieranie pliku odbywa się za pomocą tradycyjnego wywołania:
mov eax, 5 ; sys_open mov ebx, pamiec ; adres nazwy pliku "/dev/mem", 0 mov ecx, 2 ; O_RDWR, zapis i odczyt mov edx, 666o ; pełne prawa int 80h ... pamiec db "/dev/mem", 0
Drugim krokiem jest zmapowanie naszego otwartego pliku do pamięci. Odbywa się to za pomocą funkcji systemowej sys_mmap2. Przyjmuje ona 6 argumentów:
Po pomyślnym wykonaniu, system zwróci nam w EAX adres zmapowanego obszaru pamięci, którego możemy używać (w przypadku błędu otrzymujemy wartość od -4096 do -1 włącznie). Przykładowe wywołanie wygląda więc tak:
mov eax, 192 ; sys_mmap2 xor ebx, ebx ; jądro wybierze adres mov ecx, 100000h ; długość mapowanego obszaru mov edx, 3 ; PROT_READ | PROT_WRITE, możliwość ; zapisu i odczytu mov esi, 1 ; MAP_SHARED - tryb współdzielenia mov edi, [deskryptor] ; deskryptor pliku pamięci, otrzymany ; z sys_open w poprzednim kroku mov ebp, 0 ; adres początkowy w pliku int 80h
Teraz wystarczy już korzystać z otrzymanego wskaźnika, na przykład:
mov byte [eax+0a0000h], 7
Kolejne adresy w pamięci oznaczają kolejne piksele określonego wiersza. Po przekroczeniu 320 bajtów, kolejny bajt oznacza pierwszy piksel kolejnego wiersza i tak dalej.
Bajty zapisywane w pamięci (czyli kolory pikseli) mają takie same wartości, jak w tradycyjnym podejściu: 1-niebieski, 2-zielony, 3-jasnoniebieski, 4-czerwony, 5-fioletowy, 6-brązowy, 7-biały.
Zmiany, które zapiszemy w pamięci, mogą jednak nie od razu pojawić się w pliku (czyli na ekranie w tym przypadku). Aby wymusić fizyczny zapis danych, korzysta się z funkcji sys_msync. Przyjmuje ona 3 argumenty:
Przykładowe wywołanie wygląda więc tak:
mov eax, 144 ; sys_msync mov ebx, 0a0000h ; adres startowy mov ecx, 4000 ; ile zsynchronizować mov edx, 0 ; flagi int 80h
Po zakończeniu pracy należy przywrócić tryb tekstowy:
push dword 0 ; TEXT call vga_setmode ; ustawiamy tryb tekstowy 80x25 add esp, 4
oraz odmapować plik:
mov eax, 91 ; sys_munmap mov ebx, [wskaznik] ; wskaźnik otrzymany z sys_mmap2 mov ecx, 100000h ; liczba bajtów int 80h
i zamknąć go:
mov eax, 6 ; sys_close mov ebx, [deskryptor] ; deskryptor pliku "/dev/mem" int 80h
Jeśli Wasza grafika ma często się zmieniać (na przykład jest to animacja), to pisanie bezpośrednio do zmapowanej pamięci (lub pamięci wideo, jeśli macie dostęp) może być zbyt powolne, by efekty graficzne były zadowalające. Ale można to obejść na dwa sposoby: uruchamiać sys_msync dopiero po zapełnieniu całego ekranu lub cały ekran najpierw zbudować sobie w osobnym buforze, po czym jednym ruchem wrzucić cały ten bufor do zmapowanej pamięci czy pamięci wideo.
Jak widać, mapowanie plików do pamięci jest wygodne, gdyż nie trzeba ciągle skakać po pliku funkcją sys_lseek i wykonywać kosztownych czasowo wywołań innych funkcji systemowych. Warto więc się z tym zaznajomić. Należy jednak pamiętać, że nie wszystkie pliki czy urządzenia dają się zmapować do pamięci - nie należy wtedy zamykać swojego programu z błędem, lecz korzystać z tradycyjnego interfejsu funkcji plikowych.
Biblioteka Allegro powinna na większości systemów być dostępna jako gotowy pakiet, ale w razie czego można ją pobrać na przykład ze strony alleg.sf.net.
Pierwszymi funkcjami, jakie w ogóle należy uruchomić przez rozpoczęciem czegokolwiek są install_allegro (w języku C - allegro_init) i install_keyboard.
Pierwsza służy do inicjalizacji biblioteki. Jako parametry oczekuje: liczbę SYSTEM_AUTODETECT=0, adres zmiennej do przechowywania błędów (ale uwaga, próba deklaracji i użycia errno w naszym programie może skończyć się błędem linkera, więc lepiej podać adres jakiegoś naszego własnego DWORDa) oraz wskaźnika na elementy uruchamiane przy wyjściu (u nas wpiszemy NULL). Druga funkcja nie przyjmuje żadnych argumentów, a służy do instalacji funkcji odpowiedzialnych za działanie klawiatury. Allegro samo zajmuje się klawiaturą, więc standardowe funkcje czytania z klawiatury mogą nie działać. Do czytania klawiszy służy funkcja readkey. Nie przyjmuje ona żadnych argumentów, a zwraca wartość przeczytanego klawisza.
Biblioteka pozwala na ustawienie wielu rozdzielczości, my zajmiemy się rozdzielczością 640x480 w 8-bitowej głębi kolorów. Do ustawienia głębi kolorów służy funkcja set_color_depth przyjmująca jeden argument - wartość owej głębi, czyli w naszym przypadku 8.
Po inicjalizacji biblioteki, instalacji klawiatury i ustawieniu głębi kolorów można przystąpić do ustawienia trybu graficznego. Robi się to za pomocą funkcji set_gfx_mode. Przyjmuje ona 5 argumentów: sterownik (u nas będziemy korzystać z autowykrywania, wpisując tu liczbę GFX_AUTODETECT=0), szerokość żądanego trybu w pikselach, wysokość trybu, szerokość okna widoku i wysokość okna widoku. U nas oknem widoku będzie cały ekran, więc ostatnie dwa parametry przyjmą wartość zero, a całe wywołanie (w języku C) będzie miało postać:
set_gfx_mode ( GFX_AUTODETECT, 640, 480, 0, 0 );
Jeśli ustawienie rozdzielczości się nie powiedzie, wywołanie funkcji zwróci wartość niezerową.
Po skończeniu pracy z Allegro należy wywołać funkcję allegro_exit w celu zamknięcia i odinstalowania biblioteki z programu. Funkcja ta nie przyjmuje żadnych argumentów.
Aby ustawić domyślną paletę kolorów, wywołujemy funkcję set_palette. Jako jej jedyny parametr podajemy zewnętrzną (pochodzącą z Allegro) zmienną default_palette.
Do czyszczenia ekranu (a właściwie wypełnienia go określonym kolorem) służy funkcja clear_to_color. Jej pierwszy parametr mówi, co ma zostać wyczyszczone - u nas chcemy wyczyścić cały ekran, więc będzie to zmienna z Allegro o nazwie screen. Drugi parametr tej funkcji to kolor, jakim chcemy wypełnić ekran. Zero oznacza czarny.
Do wyświetlania tekstu na ekranie w trybie tekstowym służy funkcja allegro_message. Jej jedyny argument to tekst do wyświetlenia. Aby wyświetlić tekst w trybie graficznym, najpierw należy podjąć decyzję, czy tekst ma być na tle, czy tło ma go przykryć. Jeśli tło ma być pod tekstem, należy jednorazowo wywołać text_mode, jako parametr podając liczbę -1 (minus jeden). Potem można już wyświetlać tekst funkcją textout. Przyjmuje ona 5 argumentów: gdzie wyświetlić (u nas znów screen), jaką czcionką (skorzystamy z domyślnej czcionki w zmiennej Allegro o nazwie font), co wyświetlić (adres naszego napisu), współrzędna X, współrzędna Y oraz żądany kolor.
Współrzędna X rośnie w prawo, a Y - w dół ekranu.
Ale przejdźmy wreszcie do wyświetlania podstawowych elementów.
Linię wyświetla się funkcją line, przyjmującą 6
argumentów: gdzie wyświetlić (tak, znowu screen), współrzędna X
początku, współrzędna Y początku, współrzędna X końca, współrzędna Y końca, kolor.
Kolor, jak w każdej innej funkcji, możemy podawać ręcznie jako liczbę, ale możemy też
uruchomić funkcję makecol, podając jej wartości od 0
do 255 kolejno dla kolorów: czerwonego, zielonego, niebieskiego, a wynik tej funkcji
podajemy tam, gdzie podalibyśmy kolor.
Okręgi wyświetla się funkcją circle, przyjmującą
5 argumentów: gdzie wyświetlić (i znowu screen), współrzędna X
środka, współrzędna Y środka, promień i kolor.
Po omówieniu tego, co ma być w programie jeszcze dwa słowa o tworzeniu programu. O ile kompilacja pliku w asemblerze jest taka jak zawsze, to linkowanie najlepiej przeprowadzić za pomocą GCC. Normalnie naszą funkcję główna nazwalibyśmy main, ale Allegro posiada własną funkcję main, a oczekuje, że nasza funkcja główna będzie sie nazywać _mangled_main (z podkreśleniem z przodu). Ponadto, Allegro oczekuje, że zadeklarujemy zmienną globalną _mangled_main_address i wpiszemy do niej adres _mangled_main. W języku C robi to za nas makro END_OF_MAIN.
Niektóre wersje biblioteki Allegro mogą od nas jednak wymagać, abyśmy to my mieli funkcję main - jeśli w czasie linkowania wystąpi błąd, to wystarczy włączyć dwie odpowiednio oznaczone linijki w poniższym programie.
Program linkuje się następującą komendą:
gcc -o program program.o `allegro-config --libs`
Zwróćcie uwagę na odwrotne apostrofy. Sprawią one, że wynik zawartej w nich komendy (a więc niezbędne biblioteki) zostanie przekazany do GCC, dzięki czemu znajdzie on wszystko, co potrzeba.
A oto przykładowy program. Wyświetla on tekst, linię i okrąg, po czym czeka na naciśnięcie jakiegokolwiek klawisza. Po naciśnięciu klawisza biblioteka Allegro jest zamykana i program się kończy.
; Program demonstracyjny biblioteki Allegro ; ; Autor: Bogdan D., bogdandr (at) op.pl ; ; kompilacja: ; nasm -O999 -f elf -o graf2.o graf2.asm ; gcc -o graf2 graf2.o `allegro-config --libs` section .text ; wymagane przez Allegro: ;global main ; TO WŁĄCZYĆ, JEŚLI ALLEGRO WYMAGA FUNKCJI MAIN global _mangled_main global _mangled_main_address ; deklaracje elementów zewnętrznych: extern install_allegro extern install_keyboard extern set_color_depth extern set_gfx_mode extern allegro_exit extern text_mode extern set_palette extern default_palette extern clear_to_color extern screen extern textout extern font extern line extern makecol extern circle extern readkey %define GFX_AUTODETECT 0 ; autowykrywanie sterownika ;main: ; TO WŁĄCZYĆ, JEŚLI ALLEGRO WYMAGA FUNKCJI MAIN _mangled_main: ; inicjalizacja biblioteki: push dword 0 push err ; nasza zmienna do błędów push dword 0 call install_allegro add esp, 3*4 ; zdjęcie parametrów ze stosu ; instalacja klawiatury call install_keyboard ; ustawienie głębi kolorów: push dword 8 call set_color_depth add esp, 1*4 ; zdjęcie parametrów ze stosu ; ustawienie rozdzielczości: push dword 0 ; wysokość okna push dword 0 ; szerokość okna push dword 480 ; wysokość całego trybu push dword 640 ; szerokość całego trybu push dword GFX_AUTODETECT call set_gfx_mode add esp, 5*4 ; sprawdź, czy się udało cmp eax, 0 jne koniec ; ustaw tło pod tekstem push dword -1 call text_mode add esp, 1*4 ; ustaw domyślną paletę push dword default_palette call set_palette add esp, 1*4 ; wyczyść ekran push dword 0 ; czyść na czarno push dword [screen] ; co czyścić call clear_to_color add esp, 2*4 ; wyświetl napis push dword 15 ; kolor push dword 10 ; współrzędna Y push dword 10 ; współrzędna X push dword napis ; napis do wyświetlenia push dword [font] ; czcionka push dword [screen] ; gdzie wyświetlić call textout add esp, 6*4 ; stwórz kolor biały do narysowania linii push dword 255 ; składowa niebieska push dword 255 ; składowa zielona push dword 255 ; składowa czerwona call makecol add esp, 3*4 ; narysuj linię push eax ; kolor push dword 240 ; współrzędna Y końca push dword 320 ; współrzędna X końca push dword 400 ; współrzędna Y początku push dword 540 ; współrzędna X początku push dword [screen] call line add esp, 6*4 ; stwórz kolor zielony do narysowania koła push dword 0 push dword 255 push dword 0 call makecol add esp, 3*4 ; narysuj koło push eax ; kolor push dword 20 ; promień push dword 240 ; współrzędna Y środka push dword 320 ; współrzędna X środka push dword [screen] call circle add esp, 5*4 ; czekaj na klawisz call readkey koniec: ; zamknij Allegro call allegro_exit ; powróć z naszej funkcji głównej ret section .data napis db "Allegro", 0 ; napis do wyświetlenia _mangled_main_address dd _mangled_main ; wymagane err dd 0 ; nasza zmienna błędów
Jak widać, biblioteka Allegro jest tylko trochę trudniejsza od SVGAlib, ale jej możliwości są znacznie większe. Tutaj pokazałem tylko ułamek grafiki dwuwymiarowej. Allegro potrafi też wyświetlać grafikę trójwymiarową, wyliczać transformacje, zapisywać wyświetlane obrazy do pliku oraz odtwarzać muzykę (w końcu to jest biblioteka do gier, nie tylko graficzna). Jak widzicie, jest jeszcze wiele możliwości przed Wami do odkrycia. Miłej zabawy!