.. _z2-driver: ============================================ Zadanie 2: sterownik urządzenia HardDoom ][™ ============================================ Data ogłoszenia: 11.04.2019 Termin oddania: 16.05.2019 (ostateczny 30.05.2019) .. contents:: .. toctree:: :hidden: harddoom2 Materiały dodatkowe =================== - :ref:`z2-harddoom2` - Symulator urządzenia: https://github.com/koriakin/qemu (branch ``harddoom2``) - Plik nagłówkowy z definicjami rejestrów sprzętowych (do skopiowania do rozwiązania): https://github.com/koriakin/qemu/blob/harddoom2/hw/misc/harddoom2.h - Mikrokod potrzebny do uruchomienia urządzenia: https://github.com/koriakin/harddoom2/blob/master/doomcode2.bin lub https://github.com/koriakin/harddoom2/blob/master/doomcode2.h - Program testowy: https://github.com/koriakin/prboom-plus (branch ``doomdev2``) - Plik nagłówkowy z definicjami interfejsu urządzenia znakowego (do skopiowania do rozwiązania): https://github.com/koriakin/prboom-plus/blob/doomdev2/src/doomdev2.h Wprowadzenie ============ Zadanie polega na napisaniu sterownika do :ref:`urządzenia HardDoom ][™ `, będącego akceleratorem grafiki dedykowanym dla gry Doom 2. Urządzenie dostarczane jest w postaci :ref:`zmodyfikowanej wersji qemu `. Urządzenie powinno być dostępne dla użytkownika w formie urządzenia znakowego. Dla każdego urządzenia HardDoom ][™ obecnego w systemie należy utworzyć urządzenie znakowe ``/dev/doomX``, gdzie X to numer kolejny urządzenia HardDoom ][™, zaczynając od 0. Interfejs urządzenia znakowego ============================== Urządzenie ``/dev/doom*`` służy do tworzenie zasobów HardDoom ][™ oraz wysyłania poleceń do urządzenia HardDoom ][™. Powinno ono obsługiwać następujące operacje: - ``open``: alokuje nowy kontekst urządzenia (zestaw aktywnych buforów), który będzie służył do wysyłania poleceń. Początkowo żaden bufor nie jest aktywny. - ``close``: w oczywisty sposób. - ``ioctl(DOOMDEV2_IOCTL_CREATE_SURFACE)``: tworzy nowy bufor ramki na urządzeniu. Jako parametr tego wywołania przekazywane są wymiary bufora (szerokość i wysokość). Szerokość musi być wielokrotnością 64 z zakresu 64 .. 2048, a wysokość musi być w zakresie 1 .. 2048. Wynikiem tego wywołania jest nowy deskryptor pliku odnoszący się do utworzonego bufora. Utworzony bufor ma nieokreśloną zawartość. - ``ioctl(DOOMDEV2_IOCTL_CREATE_BUFFER)``: tworzy nowy bufor na urządzeniu (inny niż bufor ramki). Parametrem tego wywołania jest rozmiar bufora w bajtach (maksymalnie 4MiB). Wynikiem jest deskryptor pliku odnoszący się do utworzonego bufora. Utworzony bufor ma nieokreśloną zawartość, a użytkownik powinien wypełnić ją stosownymi danymi przez użycie ``write`` na buforze. - ``ioctl(DOOMDEV2_IOCTL_SETUP)``: wybiera bufory, które będą używane do wykonywania poleceń na tym kontekście urządzenia. Parametrami są deskryptory plików wskazujące na odpowiednie bufory. Zamiast deskryptora, użytkownik może podać ``-1`` aby nie wybierać żadnego bufora (w tym wypadku wysłanie polecenia wymagającego odpowiedniego bufora skończy się błędem). - ``write``: wysyła polecenia do urządzenia, używając aktywnych buforów dla danego kontekstu. Zapisane dane powinny być ciągiem struktur ``doomdev2_cmd``, a rozmiar przekazanego bufora musi być wielokrotnością rozmiaru tej struktury (w przeciwnym wypadku należy zwrócić ``-EINVAL``). Sterownik stara się wykonać jak najwięcej poleceń z zadanej listy, zatrzymując się w razie błędu bądź przyjścia sygnału i zwraca rozmiar przetworzonej części bufora (bądź kod błędu, gdy nie udało się nic przetworzyć). Kod użytkownika jest odpowiedzialny za ponowienie próby w przypadku niepełnego wykonania. Struktura ``doomdev2_cmd`` jest unią kilku różnych struktur, opisujących poszczególne polecenia. Typ polecenia można poznać przez sprawdzenie pola ``type`` (jest to jedna z wartości ``DOOMDEV2_CMD_TYPE_*``). Pola struktur odpowiadają bezpośrednio parametrom poleceń w sprzęcie. Na buforach ramki oraz zwykłych buforach można wywołać następujące operacje: - ``lseek``: ustawia pozycję w buforze dla następnych wywołań ``read`` i ``write``. - ``read``, ``pread``, ``readv``, itp: czeka na zakończenie wszystkich wcześniej wysłanych operacji rysujących do danego bufora (jeśli jest to bufor ramki), po czym czyta gotowe dane z bufora do przestrzeni użytkownika. W razie próby czytania poza zakresem bufora, należy poinformować o końcu pliku. - ``write``, ``pwrite``, ``writev``, itp: itp: czeka na zakończenie wszystkich wcześniej wysłanych operacji rysujących używających danego bufora, po czym kopiuje dane z przestrzeni użytkownika do bufora. W razie próby pisania poza zakresem bufora, należy zwrócić błąd ``-ENOSPC``. Sterownik powinien wykrywać polecenia z niepoprawnymi parametrami (zły typ pliku przekazany jako ``*_fd``, współrzędne wystające poza bufor ramki, wartości parametrów większe niż sprzęt obsługuje, itp.) i zwrócić błąd ``-EINVAL``. W przypadku próby stworzenia tekstur czy buforów ramki większych niż obsługiwane przez sprzęt, należy zwrócić ``-EOVERFLOW``. Sterownik powinien rejestrować swoje urządzenia w sysfs, aby udev automatycznie utworzył pliki urzadzeń o odpowiednich nazwach w ``/dev``. Numery major i minor dla tych urządzeń są dowolne (majory powinny być alokowane dynamicznie). Plik nagłówkowy z odpowiednimi definicjami można znaleźć tutaj: https://github.com/koriakin/prboom-plus/blob/doomdev2/src/doomdev2.h Sterownik może przyjąć ograniczenie do 256 urządzeń w systemie. Założenia interakcji ze sprzętem ================================ Można założyć, że przed załadowaniem sterownika, urządzenie ma stan jak po resecie sprzętowym. Urządzenie należy też w takim stanie zostawić przy odładowaniu sterownika. Pełnowartościowe rozwiązanie powinno działać asynchronicznie -- rysujące operacje ``write`` powinny wysyłać polecenia do urządzenia i wracać do przestrzeni użytkownika bez oczekiwania na zakończenie działania (ale jeśli bufory poleceń są już pełne, dopuszczalne jest oczekiwanie na zwolnienie miejsca w nich). Oczekiwanie na zakończenie poleceń powinno być wykonane dopiero przy wywołaniu ``read``, które będzie rzeczywiście potrzebowało wyników rysowania. .. Implementując sterownik można zignorować funkcje suspend itp. - zakładamy, .. że komputer nie będzie usypiany (jest to bardzo zła praktyka, ale niestety .. nie mamy możliwości sensownego przetestowania suspendu w qemu ze względu na .. bugi w virtio). Zasady oceniania ================ Za zadanie można uzyskać do 10 punktów. Na ocenę zadania składają się trzy części: - pełne wykorzystanie urządzenia (od 0 do 2 punktów): - operacja w pełni asynchroniczna (``write`` na urządzeniu nie czeka na zakończenie wysłanych poleceń, rozpoczęcie wysyłania poleceń przez ``write`` nie czeka na zakończenie poleceń wysłanych wcześniej, ``read`` nie wymaga zatrzymania całego urządzenia): 1p - użycie bloku wczytywania poleceń: 1p - wynik testów (od 0 do 8 punktów) - ocena kodu rozwiązania (od 0 do -10 punktów) Punktacja --------- Punkty ujemne za kod można było dostać za: 1. Niepoprawna obsługa tekstury długości niepodzielnej przez 64 (-0.2 pkt) 2. Bezpośrednie odwołanie do danych w userspace (bez copy from/to user) (-1pkt) 3. Brak readme (-0.1 pkt) 4. Brak lub błędna implementacja lseek (-0.2 pkt) 5. Brak sprawdzenia czy zasoby należą do urządzenia (-0.5 pkt) 6. Brak obsługi więcej niż jednego urządzenia (-0.5 pkt) 7. Wywołanie close() zwalnia bufory za szybko (-0.5 pkt) 8. Niepoprawna weryfikacja argumentów poleceń (-0.5 pkt) 9. Zwracanie 0 lub nieokreśloną wartość zamiast błędu jeśli pierwsze polecenie w batchu się nie powiedzie. (-0.2 pkt) 10. Zwracanie błędu w przypadku gdy copy_from_user jednego z dalszych poleceń nie powiedzie się (-0.2 pkt) 11. Brak możliwości wyładowania modułu lub pozostawienie urządzenia włączonego (Enable!=0 / interrupt enable != 0) (-0.5 pkt) 12. Wykorzystanie bitfieldów (-1 pkt.) 13. Użycie packed struct (-0 pkt) 14. Brak synchronizacji przy odczycie (-0.5 pkt) 15. Niepoprawna obsługa wielokrotnego open/close /dev/dooom* (-0.5 pkt) 16. Konsekwentne nieustawianie DOOMDEV2_CMD_FLAGS_TRANMAP (-0.5 pkt) 17. Niepoprawna kolejność parametrów polecenia (-0.5 pkt) 18. Use after free (-0.5 pkt) Testy: 1. DRAW_BACKGROUND 2. DRAW_FUZZ 3. FILL_RECT 4. DRAW_LINE 5. DRAW_COLUMN z COLORMAP, TRANSLATE, TRANSMAP 6. DRAW_COLUMN bez flag 7. DRAW_SPAN 8. DRAW_COLUMN na zmianę z COPY_RECT pomiędzy kilkoma buforami, wymagający INTERLOCK 9. COPY_RECTS naprzemiennie z FILL_RECTS (z zamianą buforów) 10. wszystkie powyższe, na wielu buforach Forma rozwiązania ================= Sterownik powinien zostać zrealizowany jako moduł jądra Linux w wersji 4.20.8. Moduł zawierający sterownik powinien nazywać się ``harddoom2.ko``. Jako rozwiązanie należy dostarczyć paczkę zawierającą: - źródła modułu - pliki Makefile i Kbuild pozwalające na zbudowanie modułu - krótki opis rozwiązania Rozwiązania prosimy nadsyłać na adres ``p.zuk@mimuw.edu.pl`` z kopią do ``mwk@mimuw.edu.pl``. .. _z2-qemu: QEMU ==== Do użycia urządzenia HardDoom ][™ wymagana jest zmodyfikowana wersja qemu, dostępna w wersji źródłowej. Aby skompilować zmodyfikowaną wersję qemu, należy: 1. Sklonować repozytorium https://github.com/koriakin/qemu 2. ``git checkout harddoom2`` 3. Upewnić się, że są zainstalowane zależności: ``ncurses``, ``libsdl``, ``curl``, a w niektórych dystrybucjach także ``ncurses-dev``, ``libsdl-dev``, ``curl-dev`` (nazwy pakietów mogą się nieco różnić w zależności od dystrybucji) 4. Uruchomić ``./configure`` z opcjami wedle uznania (patrz ``./configure --help``). Oficjalna binarka była kompilowana z:: --target-list=x86_64-softmmu --python=$(which python2) --audio-drv-list=alsa,pa 5. Wykonać ``make`` 6. Zainstalować wykonując ``make install``, lub uruchomić bezpośrednio (binarka to ``x86_64-softmmu/qemu-system-x86_64``). Aby zmodyfikowane qemu emulowało urządzenie HardDoom ][™, należy przekazać mu opcję ``-device harddoom2``. Przekazanie tej opcji kilka razy spowoduje emulację kilku instancji urządzenia. Aby dodać na żywo (do działającego qemu) urządzenie HardDoom ][™, należy: - przejść do trybu monitora w qemu (Ctrl+Alt+2 w oknie qemu) - wpisać ``device_add harddoom2`` - przejść z powrotem do zwykłego ekranu przez Ctrl-Alt-1 - wpisać ``echo 1 > /sys/bus/pci/rescan``, aby linux zauważył Aby udać usunięcie urządzenia:: echo 1 > /sys/bus/pci/devices/0000:/remove Testy ===== Do testowania sterownika przygotowaliśmy zmodyfikowaną wersję ``prboom-plus``, który jest uwspółcześnioną wersją silnika gry Doom 2. Aby go uruchomić, należy: - zainstalować w obrazie paczki: - ``libsdl2-dev`` - ``libsdl2-mixer-dev`` - ``libsdl2-image-dev`` - ``libsdl2-net-dev`` - ``xfce4`` [albo inne środowisko graficzne] - ``xserver-xorg`` - ``autoconf`` - pobrać źródła z repozytorium https://github.com/koriakin/prboom-plus - wybrać branch ``doomdev2`` - skompilować źródła i zainstalować program (bez instalacji, program nie będzie w stanie znaleźć swojego pliku z danymi, ``prboom-plus.wad``): - ``./bootstrap`` - ``./configure --prefix=$HOME`` - ``make`` - ``make install`` - pobrać plik z danymi gry. Można użyć dowolnego z następujących plików: - ``freedoom1.wad`` lub ``freedoom2.wad`` z projektu Freedoom (https://freedoom.github.io/) -- klon gry Doom 2 dostępny na wolnej licencji. - ``doom.wad`` lub ``doom2.wad`` z pełnej wersji oryginalnej gry, jeśli zakupiliśmy taką. - ``doom1.wad`` z wersji shareware oryginalnej gry. - załadować nasz sterownik i upewnić się, że mamy dostęp do ``/dev/doom0`` - uruchomić X11, a w nim grę: ``$HOME/bin/prboom-plus -iwad `` - w menu Options -> General -> Video mode wybrać opcję "doomdev" (domyślne ustawienie "8bit" wybiera renderowanie programowe w trybie bardzo podobnym do naszego urządzenia i można go użyć do porównania wyników). Aby w grze działał dźwięk, należy przekazać do qemu opcję ``-soundhw hda`` i przy kompilacji jądra włączyć stosowny sterownik (Device Drivers -> Sound card support -> HD-Audio). Wskazówki ========= Do plików dla buforów polecamy użyć funkcji ``anon_inode_getfile``. Niestety, tak utworzone pliki nie pozwalają domyślnie na ``lseek``, ``pread``, itp -- żeby to naprawić, należy ustawić flagi ``FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE`` w polu ``f_mode``. Aby dostać strukturę ``file`` z deskryptora pliku, możemy użyć ``fdget`` i ``fdput``. Aby sprawdzić, czy przekazana nam struktura jest odpowiedniego typu, wystarczy porównać jej wskaźnik na strukturę ``file_operations`` z naszą. Polecamy zacząć implementację od operacji ``FILL_RECT`` i ``DRAW_LINE`` (wymagają tylko bufora ramki i pozwalają zobaczyć mapę). Następnie polecamy ``DRAW_COLUMN`` (można na początku pominąć flagi i mapy kolorów/przezroczystości) -- jest odpowiedzialna za rysowanie większości grafiki w grze i bez niej niewiele zobaczymy. Rozmiar bufora ramki rzadko kiedy jest dokładnie wielokrotnością strony -- możemy to wykorzystać, umieszczając tabelę stron w nieużywanej części ostatniej strony. Pozwoli to uniknąć osobnej alokacji na (zazwyczaj bardzo małą) tabelę stron. Do rozwiązania w niżej punktowanej wersji synchronicznej nie jest konieczne użycie rejestrów ``FENCE_COUNTER`` i ``fENCE_WAIT`` -- wystarczy sama flaga ``PING_SYNC``. W rozwiązaniu w pełnej wersji asynchronicznej konieczne będzie użycie ``FENCE_COUNTER`` (w połączeniu z rejestrem ``FENCE_WAIT`` lub flagą ``PING_SYNC`` do oczekiwania w ``read``). Może się zdarzyć, że nie będziemy w stanie wysłać polecenia ze względu na brak miejsca w kolejce poleceń (tej wbudowanej w urządzenie bądź naszej własnej w pamięci wskazywanej przez ``CMD_*_IDX``). Żeby efektywnie zaimplementować oczekiwanie na wolne miejsce, polecamy: - wysyłać z jakąś minimalną częstotliwością (np. co 1/8 .. 1/2 wielkości naszego bufora poleceń bądź sprzętowej kolejki) polecenie z flagą ``PING_ASYNC`` - domyślnie ustawić przerwanie ``PONG_ASYNC`` na wyłączone - w razie zauważenia braku miejsca w kolejce: - wyzerować przerwanie ``PONG_ASYNC`` w ``INTR`` - sprawdzić, czy dalej nie ma miejsca w kolejce (zabezpieczenie przed wyścigiem) -- jeśli jest, od razu wrócić do wysyłania - włączyć przerwanie ``PONG_ASYNC`` w ``INTR_ENABLE`` - czekać na przerwanie - z powrotem wyłączyć przerwanie ``PONG_ASYNC`` w ``INTR_ENABLE`` Aby urządzenie działało wydajnie, należy unikać niepotrzebnego wysyłania poleceń ``SETUP`` (mogą one czyścić pamięć podręczną i blokować paczkowanie sąsiednich kolumn przez mikrokod), poleceń z flagą ``INTERLOCK`` (blokują równoległe przetwarzanie poleceń ``COPY_RECT``), oraz poleceń z flagą ``FENCE`` (blokują paczkowanie sąsiednich kolumn przez mikrokod). Jeśli chcemy czytać z bufora, do którego ostatnio rysowaliśmy przed wysłaniem ostatniego polecenia z ``INTERLOCK``, nie ma potrzeby wysyłać kolejnego -- w szczególności, w przypadku serii wywołań ``COPY_RECT`` między różnymi buforami ramki, nie należy wysyłać ``INTERLOCK`` między wywołaniami. Jeżeli chcemy tymczasowo (do testów) pozmieniać coś w grze (np. wykomentować operacje, których jeszcze nie wspieramy), kod odpowiedzialny za obsługę urządzenia możemy znaleźć w ``src/i_doomdev.c``. Jeżeli popsujemy konfigurację ``prboom-plus`` tak, że przestanie się uruchamiać w stopniu wystarczającym do zmiany ustawień, możemy znaleźć jego plik konfiguracyjny w ``$HOME/.prboom-plus/prboom-plus.cfg``. Usunięcie go spowoduje przywrócenie ustawień domyślnych. Aby zobaczyć różne operacje w akcji: - ``DRAW_COLUMN`` bez żadnych flag: używane do rysowania grafik interfejsu (menu, itp). - ``DRAW_COLUMN`` z ``COLORMAP``: używane do rysowania wszystkich ścian i większości obiektów. - ``DRAW_COLUMN`` z ``TRANSLATE``: używane do rysowania nowego HUD (dostępnego pod przyciskiem F5 -- być może po naciśnięciu kilka razy). Jeśli zmiana palety działa, część cyfr powinna nie być czerwona. - ``DRAW_FUZZ``: wpisujemy kod ``idbeholdi``, żeby uczynić się niewidzialnym (po prostu naciskając kolejne litery w trakcie rozgrywki). - ``DRAW_SPAN``: używane do rysowania podłogi / sufitu. - ``DRAW_LINE`` i ``FILL_RECT``: używane do rysowania mapy (przycisk Tab). - ``COPY_RECT``: używane do efektu przejścia między stanami gry (choćby rozpoczęcie nowej gry czy ukończenie poziomu). - ``DRAW_BACKGROUND``: zmniejszamy rozmiar ekranu z pełnego (naciskając przycisk ``-`` kilka razy) -- ramka ekranu powinna być wypełniona.