Zadanie 2: sterownik urządzenia HardDoom ][™

Data ogłoszenia: 11.04.2019

Termin oddania: 16.05.2019 (ostateczny 30.05.2019)

Materiały dodatkowe

Wprowadzenie

Zadanie polega na napisaniu sterownika do urządzenia HardDoom ][™, będącego akceleratorem grafiki dedykowanym dla gry Doom 2. Urządzenie dostarczane jest w postaci 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.

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.

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:<idurządzenia>/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 <dane_gry.wad>
  • 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.