Zadanie 2: sterownik urządzenia HardDoom ][™¶
Data ogłoszenia: 11.04.2019
Termin oddania: 16.05.2019 (ostateczny 30.05.2019)
Materiały dodatkowe¶
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 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życiewritena 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ć-1aby 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 strukturdoomdev2_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_cmdjest unią kilku różnych struktur, opisujących poszczególne polecenia. Typ polecenia można poznać przez sprawdzenie polatype(jest to jedna z wartościDOOMDEV2_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ńreadiwrite.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 (
writena urządzeniu nie czeka na zakończenie wysłanych poleceń, rozpoczęcie wysyłania poleceń przezwritenie czeka na zakończenie poleceń wysłanych wcześniej,readnie wymaga zatrzymania całego urządzenia): 1puż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:
Niepoprawna obsługa tekstury długości niepodzielnej przez 64 (-0.2 pkt)
Bezpośrednie odwołanie do danych w userspace (bez copy from/to user) (-1pkt)
Brak readme (-0.1 pkt)
Brak lub błędna implementacja lseek (-0.2 pkt)
Brak sprawdzenia czy zasoby należą do urządzenia (-0.5 pkt)
Brak obsługi więcej niż jednego urządzenia (-0.5 pkt)
Wywołanie close() zwalnia bufory za szybko (-0.5 pkt)
Niepoprawna weryfikacja argumentów poleceń (-0.5 pkt)
Zwracanie 0 lub nieokreśloną wartość zamiast błędu jeśli pierwsze polecenie w batchu się nie powiedzie. (-0.2 pkt)
Zwracanie błędu w przypadku gdy copy_from_user jednego z dalszych poleceń nie powiedzie się (-0.2 pkt)
Brak możliwości wyładowania modułu lub pozostawienie urządzenia włączonego (Enable!=0 / interrupt enable != 0) (-0.5 pkt)
Wykorzystanie bitfieldów (-1 pkt.)
Użycie packed struct (-0 pkt)
Brak synchronizacji przy odczycie (-0.5 pkt)
Niepoprawna obsługa wielokrotnego open/close /dev/dooom* (-0.5 pkt)
Konsekwentne nieustawianie DOOMDEV2_CMD_FLAGS_TRANMAP (-0.5 pkt)
Niepoprawna kolejność parametrów polecenia (-0.5 pkt)
Use after free (-0.5 pkt)
Testy:
DRAW_BACKGROUND
DRAW_FUZZ
FILL_RECT
DRAW_LINE
DRAW_COLUMN z COLORMAP, TRANSLATE, TRANSMAP
DRAW_COLUMN bez flag
DRAW_SPAN
DRAW_COLUMN na zmianę z COPY_RECT pomiędzy kilkoma buforami, wymagający INTERLOCK
COPY_RECTS naprzemiennie z FILL_RECTS (z zamianą buforów)
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:
Sklonować repozytorium https://github.com/koriakin/qemu
git checkout harddoom2Upewnić się, że są zainstalowane zależności:
ncurses,libsdl,curl, a w niektórych dystrybucjach takżencurses-dev,libsdl-dev,curl-dev(nazwy pakietów mogą się nieco różnić w zależności od dystrybucji)Uruchomić
./configurez 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
Wykonać
makeZainstalować wykonując
make install, lub uruchomić bezpośrednio (binarka tox86_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 harddoom2przejść 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-devlibsdl2-mixer-devlibsdl2-image-devlibsdl2-net-devxfce4[albo inne środowisko graficzne]xserver-xorgautoconf
pobrać źródła z repozytorium https://github.com/koriakin/prboom-plus
wybrać branch
doomdev2skompilować ź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=$HOMEmakemake install
pobrać plik z danymi gry. Można użyć dowolnego z następujących plików:
freedoom1.wadlubfreedoom2.wadz projektu Freedoom (https://freedoom.github.io/) – klon gry Doom 2 dostępny na wolnej licencji.doom.wadlubdoom2.wadz pełnej wersji oryginalnej gry, jeśli zakupiliśmy taką.doom1.wadz wersji shareware oryginalnej gry.
załadować nasz sterownik i upewnić się, że mamy dostęp do
/dev/doom0uruchomić 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_ASYNCdomyślnie ustawić przerwanie
PONG_ASYNCna wyłączonew razie zauważenia braku miejsca w kolejce:
wyzerować przerwanie
PONG_ASYNCwINTRsprawdzić, 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_ASYNCwINTR_ENABLEczekać na przerwanie
z powrotem wyłączyć przerwanie
PONG_ASYNCwINTR_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_COLUMNbez żadnych flag: używane do rysowania grafik interfejsu (menu, itp).DRAW_COLUMNzCOLORMAP: używane do rysowania wszystkich ścian i większości obiektów.DRAW_COLUMNzTRANSLATE: 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 kodidbeholdi, żeby uczynić się niewidzialnym (po prostu naciskając kolejne litery w trakcie rozgrywki).DRAW_SPAN: używane do rysowania podłogi / sufitu.DRAW_LINEiFILL_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.