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życiewrite
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 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_cmd
jest 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ńread
iwrite
.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ń przezwrite
nie czeka na zakończenie poleceń wysłanych wcześniej,read
nie 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 harddoom2
Upewnić 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ć
./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
Wykonać
make
Zainstalować 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 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
lubfreedoom2.wad
z projektu Freedoom (https://freedoom.github.io/) – klon gry Doom 2 dostępny na wolnej licencji.doom.wad
lubdoom2.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łączonew razie zauważenia braku miejsca w kolejce:
wyzerować przerwanie
PONG_ASYNC
wINTR
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
wINTR_ENABLE
czekać na przerwanie
z powrotem wyłączyć przerwanie
PONG_ASYNC
wINTR_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
zCOLORMAP
: używane do rysowania wszystkich ścian i większości obiektów.DRAW_COLUMN
zTRANSLATE
: 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_LINE
iFILL_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.