Bardzo często zdarza mi się odtwarzać muzykę mplayerem. Jest to odtwarzacz konsolowy, co sprawia pewne kłopoty. Brakuje mu także opcji odtwarzania wszystkich utworów z danego katalogu i podkatalogów.
Oto rozwiązanie tego problemu za pomocą narzędzia find:
$ find -type f -printf "`pwd`/%p\n" | shuf > /tmp/pls && mplayer -playlist /tmp/pls
Kilka uwag:
- używam komendy "shuf" aby zrandomizować listę odtwarzanych utworów. mplayer posiada opcję -shuffle, ale powoduje ona losowanie przy każdej zmianie utworu. shuf ustala jedną permutację listy, dzięki czemu można się cofnąć do odegranego już utworu
- lista tworzona jest w /tmp/pls. Można uprościć polecenie find jeżeli zapiszemy ją w aktualnym katalogu:
$ find -type f | shuf > pls && mplayer -playlist pls
Ja nie chcę jednak sobie zaśmiecać katalogów playlistami.
Można wreszcie nie używać pliku pomocniczego, a korzystać z przekierowania strumieni:
$ find -type f | shuf | mplayer -playlist -
Wtedy jednak nie można sterować mplayerem, np. zmniejszając głośność lub zmieniając utwór. W tym momencie shuf staje się zbędny.
$ find -type f | mplayer -shuffle -playlist -
Na dobrą sprawę możemy pominąć także parametr "-type f" który mówi find by wyświetlał jedynie pliki, a pomijał katalogi.
$ find | mplayer -shuffle -playlist -
W ostateczności można też słuchać plików w kolejności podawanej przez find:
$ find | mplayer -playlist -
I tak wygląda komenda od której zacząłem :-)
Pokazywanie postów oznaczonych etykietą snippet. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą snippet. Pokaż wszystkie posty
wtorek, marca 31, 2009
wtorek, listopada 11, 2008
I gdzie jest błąd?
Ostatnio na liście mailingowej haskell-cafe kilka razy padło pytanie o niedziałający z jakiegoś powodu program. W każdym z tych okazało się że problem leżał po stronie programisty :-)
Zadanie jest proste: uruchamiamy pewien proces, karmimy go danymi i oczekujemy od niego odpowiedzi. Naiwny kod wygląda tak:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
Powyższy kod może, ale nie musi zadziałać. Co więcej: może raz działać dla danego programu, a raz nie działać. Prowadzi to do trudnych do debugowania i przez to irytujących błędów. Niestety: problem wynika z nieznajomości semantyki kanałów (ang. pipe - moje tłumaczenie jest chałupnicze) pomiędzy procesami.
Połączenia te mają ograniczoną pojemność "bufora". Po jego zapełnieniu proces który próbuje go "przepełnić" jest usypiany, aż bufor zostanie nieco opróżniony. Jest to (całkowicie słuszny) środek zaradczy przeciwko zużyciu przez nadgorliwy proces wszelkich zasobów systemu - w przeciwnym wypadku system musiałby przechować dowolnie dużo danych które zapisał dany proces. Bufor ten może być dość niewielki, np. 300 kb.
Jaki to ma związek z powyższym kodem? Ano taki, że w wyniku takiego właśnie działania kanałów kod ten powoduje często deadlocks - zakleszczenia.
Oto co się dzieje.
Uruchamiamy wysyłanie danych do naszego procesu:
> hPut p_stdin jakies_dane
Ale ów proces nie konsumuje ich na raz w całości. Zamiast tego zaczyna wysyłać częściowe porcje danych które wędrują do p_stdout. Wysyła ich na tyle dużo, że bufor p_stdout zapełnia się. Zostaje więc uśpiony. Aby został obudzony musimy odebrać z p_stdout porcję danych. Ale nie możemy tego zrobić - jeszcze nie skończyliśmy wysyłać mu danych na p_stdin!
Rozwiązanie jest proste: należy uruchomić wysyłanie danych w innym wątku:
> forkIO (hPut p_stdin jakies_dane)
W tym momencie jeden wątek będzie realizował wysyłanie danych, a drugi odbieranie. Co prawda jeden z nich może zostać uśpiony (bo np. proces nie odebrał jeszcze wszystkich danych i wykonuje teraz jakieś obliczenia) ale nie spowoduje to zakleszczenia.
Scenariusz może się jeszcze bardziej skomplikować, jeżeli interesuje nas równocześnie wyjście z p_stderr. W tym momencie ten kod także będzie błędny:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
> odpowiedz_stderr <- hGet p_stderr
Dlaczego? Proces może zapełnić bufor p_stderr i zostać uśpiony zanim zamknie swoje standardowe wyjście (dzięki czemu wywołanie "hGet p_stdout" się skończyłoby się i zaczelibyśmy opróżniać p_stderr).
Rozwiązanie w tym przypadku jest nieco bardziej skomplikowane, jednak zaczyna się tutaj pojawiać pewien schemat:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
(Dla opisu MVar przeczytaj ten post)
Co się tutaj wydarzyło? To co poprzednio: dodaliśmy nowy wątek który zajmuje się obsługą wejścia/wyjścia dla dokładnie jednego uchwytu (Handle).
Zauważmy, że w poprawnym kodzie mamy dokładnie jeden wątek dla jednego uchwytu: jeden "główny" oraz dwa utworzone przez forkIO. Jest to ogólna reguła by unikać tego typu zakleszczeń.
Uważny czytelnik zauważy, że w pewnym momencie odszedłem od słowa "kanał" (pipe) na korzyść słowa "uchwyt" (handle). O ile te pierwsze występują w przypadku komunikacji między procesami - i ten przypadek rozważamy - o tyle ten typ błędu występuje ogólnie dla typu uchwytów, które w GHC wykorzystywane są dla wielu typów operacji wejścia wyjścia - w szczególności dla połączeń sieciowych. W ich przypadku również może dochodzić do tego typu błędów.
W każdym z powyżej zacytowanych kawałków kodu jest czai się jeszcze jeden typ błędu, wynikający z semantyki uchwytów w GHC. Jak możemy przeczytać w dokumentacji nieużywany uchwyt jest automatycznie zamykany przez odśmiecacz (GC - garbage collector). Ma to ważną implikację: nie mamy gwarancji kiedy to nastąpi. Dlatego też może się zdarzyć, że otworzymy zbyt wiele plików na raz i system odmówi nam otwarcia nowych deskryptorów pliku. RTS wyrzuci nam w tym momencie wyjątek którego prawdopodobnie nie złapiemy - i nasz program zostanie zabity. Stąd ważny nawyk programistyczny: nieużywane uchwyty zamykamy tak szybko jak tylko przestają nam być potrzebne i nie liczymy w tym przypadku na pomoc systemu.
Dla kompletności oto poprawny (mam nadzieję...) kod:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane >> hClose p_stdin)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv >> hClose p_stdout)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
Zadanie jest proste: uruchamiamy pewien proces, karmimy go danymi i oczekujemy od niego odpowiedzi. Naiwny kod wygląda tak:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
Powyższy kod może, ale nie musi zadziałać. Co więcej: może raz działać dla danego programu, a raz nie działać. Prowadzi to do trudnych do debugowania i przez to irytujących błędów. Niestety: problem wynika z nieznajomości semantyki kanałów (ang. pipe - moje tłumaczenie jest chałupnicze) pomiędzy procesami.
Połączenia te mają ograniczoną pojemność "bufora". Po jego zapełnieniu proces który próbuje go "przepełnić" jest usypiany, aż bufor zostanie nieco opróżniony. Jest to (całkowicie słuszny) środek zaradczy przeciwko zużyciu przez nadgorliwy proces wszelkich zasobów systemu - w przeciwnym wypadku system musiałby przechować dowolnie dużo danych które zapisał dany proces. Bufor ten może być dość niewielki, np. 300 kb.
Jaki to ma związek z powyższym kodem? Ano taki, że w wyniku takiego właśnie działania kanałów kod ten powoduje często deadlocks - zakleszczenia.
Oto co się dzieje.
Uruchamiamy wysyłanie danych do naszego procesu:
> hPut p_stdin jakies_dane
Ale ów proces nie konsumuje ich na raz w całości. Zamiast tego zaczyna wysyłać częściowe porcje danych które wędrują do p_stdout. Wysyła ich na tyle dużo, że bufor p_stdout zapełnia się. Zostaje więc uśpiony. Aby został obudzony musimy odebrać z p_stdout porcję danych. Ale nie możemy tego zrobić - jeszcze nie skończyliśmy wysyłać mu danych na p_stdin!
Rozwiązanie jest proste: należy uruchomić wysyłanie danych w innym wątku:
> forkIO (hPut p_stdin jakies_dane)
W tym momencie jeden wątek będzie realizował wysyłanie danych, a drugi odbieranie. Co prawda jeden z nich może zostać uśpiony (bo np. proces nie odebrał jeszcze wszystkich danych i wykonuje teraz jakieś obliczenia) ale nie spowoduje to zakleszczenia.
Scenariusz może się jeszcze bardziej skomplikować, jeżeli interesuje nas równocześnie wyjście z p_stderr. W tym momencie ten kod także będzie błędny:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
> odpowiedz_stderr <- hGet p_stderr
Dlaczego? Proces może zapełnić bufor p_stderr i zostać uśpiony zanim zamknie swoje standardowe wyjście (dzięki czemu wywołanie "hGet p_stdout" się skończyłoby się i zaczelibyśmy opróżniać p_stderr).
Rozwiązanie w tym przypadku jest nieco bardziej skomplikowane, jednak zaczyna się tutaj pojawiać pewien schemat:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
(Dla opisu MVar przeczytaj ten post)
Co się tutaj wydarzyło? To co poprzednio: dodaliśmy nowy wątek który zajmuje się obsługą wejścia/wyjścia dla dokładnie jednego uchwytu (Handle).
Zauważmy, że w poprawnym kodzie mamy dokładnie jeden wątek dla jednego uchwytu: jeden "główny" oraz dwa utworzone przez forkIO. Jest to ogólna reguła by unikać tego typu zakleszczeń.
Uważny czytelnik zauważy, że w pewnym momencie odszedłem od słowa "kanał" (pipe) na korzyść słowa "uchwyt" (handle). O ile te pierwsze występują w przypadku komunikacji między procesami - i ten przypadek rozważamy - o tyle ten typ błędu występuje ogólnie dla typu uchwytów, które w GHC wykorzystywane są dla wielu typów operacji wejścia wyjścia - w szczególności dla połączeń sieciowych. W ich przypadku również może dochodzić do tego typu błędów.
W każdym z powyżej zacytowanych kawałków kodu jest czai się jeszcze jeden typ błędu, wynikający z semantyki uchwytów w GHC. Jak możemy przeczytać w dokumentacji nieużywany uchwyt jest automatycznie zamykany przez odśmiecacz (GC - garbage collector). Ma to ważną implikację: nie mamy gwarancji kiedy to nastąpi. Dlatego też może się zdarzyć, że otworzymy zbyt wiele plików na raz i system odmówi nam otwarcia nowych deskryptorów pliku. RTS wyrzuci nam w tym momencie wyjątek którego prawdopodobnie nie złapiemy - i nasz program zostanie zabity. Stąd ważny nawyk programistyczny: nieużywane uchwyty zamykamy tak szybko jak tylko przestają nam być potrzebne i nie liczymy w tym przypadku na pomoc systemu.
Dla kompletności oto poprawny (mam nadzieję...) kod:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane >> hClose p_stdin)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv >> hClose p_stdout)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
sobota, października 25, 2008
Wywoływanie funkcji w C z języka C#
Udało mi się, po paru próbach, dojść do takiego kodu:
[C#]
[DllImport("mytest.dll", EntryPoint="makeFFT")]
private static extern void makeFFT_(IntPtr inputArray, IntPtr outputArray);
public static void makeFFT(float[] inpArr, float[] outArr)
{
IntPtr inpArrBuf = Marshal.AllocHGlobal(sizeof(float) * inpArr.Length);
IntPtr outArrBuf = Marshal.AllocHGlobal(sizeof(float) * outArr.Length);
Marshal.Copy(inpArr, 0, inpArrBuf, inpArr.Length);
makeFFT_(inpArrBuf, outArrBuf);
Marshal.Copy(outArrBuf, outArr, 0, outArr.Length);
Marshal.FreeHGlobal(inpArrBuf);
Marshal.FreeHGlobal(outArrBuf);
}
Gdzie funkcja w C ma typ:
[C]
void makeFFT( float* inArr, float* outArr )
Kod, choć niezbyt piękny, działa i robi to o co od niego oczekiwałem: wywołuje zawartą w bibliotece funkcję makeFFT gdzie inArr jest parametrem wejściowym, zaś outArr - wyjściowym. Zasadnicze pytanie brzmi: czy da się to zrobić krócej, bardziej elegancko?
[C#]
[DllImport("mytest.dll", EntryPoint="makeFFT")]
private static extern void makeFFT_(IntPtr inputArray, IntPtr outputArray);
public static void makeFFT(float[] inpArr, float[] outArr)
{
IntPtr inpArrBuf = Marshal.AllocHGlobal(sizeof(float) * inpArr.Length);
IntPtr outArrBuf = Marshal.AllocHGlobal(sizeof(float) * outArr.Length);
Marshal.Copy(inpArr, 0, inpArrBuf, inpArr.Length);
makeFFT_(inpArrBuf, outArrBuf);
Marshal.Copy(outArrBuf, outArr, 0, outArr.Length);
Marshal.FreeHGlobal(inpArrBuf);
Marshal.FreeHGlobal(outArrBuf);
}
Gdzie funkcja w C ma typ:
[C]
void makeFFT( float* inArr, float* outArr )
Kod, choć niezbyt piękny, działa i robi to o co od niego oczekiwałem: wywołuje zawartą w bibliotece funkcję makeFFT gdzie inArr jest parametrem wejściowym, zaś outArr - wyjściowym. Zasadnicze pytanie brzmi: czy da się to zrobić krócej, bardziej elegancko?
niedziela, października 19, 2008
Prosty szablon wizualizacji
Wielu osobom wydaje się, że Haskell nie nadaje się do pisania czegokolwiek poza interpreterem innego języka :-) Nic bardziej mylnego! Jest to język ogólnego zastosowania, a przy tym jest on bardzo elegancki - znacznie bardziej niż ubogi w abstrakcje C czy zatłoczony składnią C++. No dobra, wystarczająco wkurzyłem fanów tych języków, czas zabrać się za konkrety ;-)
Zbudujemy dzisiaj w Haskellu prostą aplikację wykorzystującą OpenGL. Nie chcę tutaj pokazywać tajników programowania w OpenGL, od tego są inne strony. Będziemy potrzebować:
Zbudowanie ich wymaga odpowiednich plików nagłówkowych, ale to jest już mniej ciekawe zagadnienie.
Choć post ten wyszedł na znacznie dłuższy niż początkowo zamierzałem, sam kod jest zwarty i niewielki. Można go obejrzeć tutaj i tutaj. Kod źródłowy dostępny do ściągnięcia tutaj.
Mam nadzieję, że post wyszedł choć trochę ciekawie :-)
Zadania dla czytelników:
Znaleźć błąd w programie - łatwo go zauważyć po jego uruchomieniu
Zbudujemy dzisiaj w Haskellu prostą aplikację wykorzystującą OpenGL. Nie chcę tutaj pokazywać tajników programowania w OpenGL, od tego są inne strony. Będziemy potrzebować:
Zbudowanie ich wymaga odpowiednich plików nagłówkowych, ale to jest już mniej ciekawe zagadnienie.
- Nazwanie modułu
- Importowanie odpowiednich bibliotek.
- Inicjalizacja GLFW
- Funkcje pomocnicze
- Dalsze ustawianie środowiska
- Główna pętla renderingu
- A jak stąd wyjść?
- Sprzątamy po sobie...
Warto jest nazwać jakoś nasz program. Umieszczamy więc w pierwszej linii:
>>> module Main where
Potrzebować będziemy OpenGL i GLFW (do robienia okienek i obsługi klawiatury):
>>> import Graphics.Rendering.OpenGL -- lots of modules re-exported
>>> import Graphics.UI.GLFW -- for window creation etc.
Przydadzą się też biblioteka ułatwiająca programowanie wielowątkowe:
>>> import Control.Concurrent
Biblioteka do monad też się przyda:
>>> import Control.Monad
Z kolei ta biblioteka udostępnia funkcję printf, podobną do tej znanej z C:
>>> import Text.Printf
Początek funkcji main może wyglądać tak:
>>> main = do
>>> initOk <- initialize -- [1]
>>> if not inikOk then error "Błąd inicjalizacji GLFW!" else return () -- [2]
>>> vmodes <- get videoModes -- [3]
>>> let bestVM = last vmodes -- [4]
>>> setWindowModeOk <- openWindow (videoModeToSize bestVM) [videoModeToRGBBits bestVM] FullScreen -- [5]
>>> if not setWindowModeOk then error "Nie udało się otworzyć okna" else return () -- [6]
W linijce [1] próbujemy zainicjować GLFW. Funkcja ta zwraca wartość logiczną, którą sprawdzamy w [2], jeżeli nie udało się (ma ona wartość False) przerywamy działanie programu za pomocą funkcji error. Po pobraniu informacji o dostępnych rozdzielczościach ([3]) i wybraniu potencjalnie najlepszej z nich ([4]) robimy znów podobną rzecz: próbujemy otworzyć okno w tej rozdzielczości ([5]), jeżeli nam się nie powiedzie przerywamy działanie programu ([6]). Korzystamy przy tym z funkcji pomocniczych, o których opowiem za chwile.
Niby działa, ale... można to napisać lepiej.
>>> main = do
>>> tryTo initialize (error "Błąd inicjalizacji GLFW!")
>>> bestVM <- last `fmap` get videoModes
>>> tryTo (openWindow (videoModeToSize bestVM) [videoModeToRGBBits bestVM] FullScreen)
>>> (error "Nie udało się otworzyć okna")
Co się zmieniło? Dwie rzeczy:
- wprowadziliśmy funkcję tryTo
- użyliśmy funkcji fmap
Definicja funkcji tryTo jest prosta:
>>> tryTo :: IO Bool -> IO () -> IO ()
>>> tryTo doAction actionIfFailed = do
>>> b <- doAction
>>> if not b then actionIfFailed else return ()
Przy pomocy funkcji when i rezygnując z niepotrzebnego cukru syntaktycznego można tą definicje istotnie skrócić:
>>> tryTo doAction actionIfFailed = doAction >>= \b -> when (not b) actionIfFailed
Nieco nieczytelnie można to zapisać jako:
>>> tryTo doAction actionIfFailed = doAction >>= (flip unless) actionIfFailed
Ale to już jest lekka przesada :-)
Pozostaje jeszcze sprawa funkcji fmap. Pozwala ona nakarmienie funkcji argumentem pochodzącym z monady (tak na prawdę jej definicja jest nieco ogólniejsza, ale to nieistotne). Możliwa jej definicja w kontekście monad jest taka:
>>> fmap pureFunc monadAction = monadAction >>= \ v -> return (pureFunc v)
Dzięki tym dwóm funkcjom kod został skrócony i stał się bardziej przejrzysty. Przejdźmy więc nieco dalej.
W kodzie powyżej wykorzystałem dwie funkcje o następujących definicjach:
videoModeToSize vm = Size (fromIntegral $ videoWidth vm) (fromIntegral $ videoHeight vm)
videoModeToRGBBits vm = DisplayRGBBits (videoRedBits vm) (videoGreenBits vm) (videoBlueBits vm)
Są to swego rodzaju funkcje "rzutowania", czy też "ekstrakcji": z trybu wideo (vm :: VideoMode) otrzymujemy za pomocą pierwszej funkcji rozmiar okna, zaś za pomocą drugiej - liczbę bitów poszczególnych kolorów. Nie jest to parametr, który koniecznie musimy podać, jednak pozwala on wymusić pewne zachowanie.
Skoro mamy już okno, warto by nadać mu tytuł
>>> windowTitle $= "My first Haskell + OpenGL app"
Co się wydażyło w tej linijce?
Skorzystaliśmy z funkcji $= do ustawienia parametru windowTitle. Jest to funkcja implementowana przez typy należące do klasy HasSetter. W większości popularnych języków programowania (C++, C#, Delphi, Ruby...) mamy możliwość tworzenia parametrów, choć różnie się nazywa w tych językach te mechanizmy. Zasadniczo chodzi o pewien parametr klasy, który można ustawić na pewną wartość. W momencie wykonania przypisania nie jest jednak wprost zapisywana w pamięci pewna wartość, lecz wykonywana jest odpowiednia metoda klasy z argumentem będącym wartością "przypisywaną" parametrowi. Jest to mechanizm wygodny i użyteczny, o czym świadczy chociażby fakt szerokiego wsparcia w językach. W Haskellu możemy ten mechanizm zaimplementować sami i - co więcej - jest to bardzo proste. Po szczegóły odsyłam do źródeł.
W podobny sposób ustawimy callbacki (brrr co za słowo) do reagowania na zdarzenia środowiska:
>>> keyCallback $= myKeyCallback
>>> charCallback $= myCharCallback
Gdzie myKeyCallback i myCharCallback to funkcje:
>>> -- wywoływany przy naciśnięcu jakiegokolwiek klawisza
>>> myKeyCallback key Press = do
>>> case key of
>>> SpecialKey ENTER -> restoreWindow
>>> SpecialKey ESC -> iconifyWindow
>>> _ -> return ()
>>> myKeyCallback _ _ _ = return ()
>>> -- wywoływany przy naciśnięcia klawisza, któremu odpowiada jakiś znak
>>> myCharCallback chr st = putStrLn (printf "Znak '%c' został %s." chr (if st == Press then "naciśnięty" else "puszczony"))
myKeyCallback reaguje na naciśnięcie entera i klawisza escape: jedno z nich chowa okno, drugi je pokazuje. Z kolei myCharCallback wypisuje informacje o tym, że ktoś nacisnął lub puścił jakiś klawisz któremu odpowiada pewien znak. Można zawrzeć funkcjonalność myCharCallback w myKeyCallback, ale nie byłoby to wygodne.
Zmienimy jeszcze rozmiar punktu:
>>> pointSize $= 4.0
Nieco inaczej wygląda ustawienie dwóch innych parametrów:
>>> enableSpecial KeyRepeat -- powtarzanie klawiszy
>>> enableSpecial MouseCursor -- widoczny kursor myszy
>>> --enableSpecial AutoPollEvent -- automatyczne pobieranie nowych eventów [*]
Linia [*] jest wykomentowana, gdyż nie chcemy tak na prawdę tej funkcjonalności od GLFW. Jeżeli jest ona włączona to eventy pobierane są automatycznie przy wywołaniu funkcji swapBuffers. Ponieważ czasami wyświetlamy niewiele klatek na sekundę, nasza aplikacja zachowywałaby się w takich momentach nieprzyjemnie. Zamiast tego uruchomimy w tym celu oddzielny wątek:
>>> forkIO (forever pollEvents)
Zostanie on automatycznie zabity przy końcu działania programu.
Chcielibyśmy też ustawić jakieś opcje widoku. Nie będziemy wymyślni, wystarczy nam ortogonalny widok "2D":
>>> let Size sizeX sizeY = videoModeToSize bestVM
>>> ortho2D 0 (fromIntegral sizeX) 0 (fromIntegral sizeY)
Użycie funckji fromIntegral jest w paru miejscach konieczne, by zmienić typ z jednego całkowitoliczbowego na jakiś inny typ liczbowy (np. Int -> Double, GLint -> Int).
Jesteśmy teraz gotowi na wywołanie pętli rysowania. Robi się to tak:
>>> mainLoop
;-)
Oczywiście musimy podać definicję mainLoop:
>>> mainLoop :: IO ()
>>> mainLoop = do
>>> (Position px py) <- get mousePos
>>> clear [ColorBuffer]
>>> let gray = Color3 0.5 0.5 0.5 :: Color3 Float
>>> let myPoint = (Vertex2 (fromIntegral px) (fromIntegral py)) :: Vertex2 Float
>>> renderPrimitive Points $ color gray >> vertex myPoint
>>> swapBuffers
>>> mainLoop
Rysujemy więc w aktualnym położeniu kursora duży (ustawiliśmy to przed chwilą) szary punkt.
Wszystko wygląda fajnie, ale czasami chcemy wyjść z programu, prawda? Aby to zrealizować wykorzystamy pewien rodzaj zmiennych - MVar - których obsługę znajdziemy w module Control.Concurrent (a dokładniej tutaj).
Obiekt typu MVar Int jest "pudełkiem" które może być albo puste, albo przechowywać obiekt typu Int. Stworzymy więc nową, pustą zmienną MVar która gdy zostanie zapełniona będzie sygnalizować konieczność zakończenia aplikacji. Proste, prawda?
Do funkcji main dopisujemy przed wywołaniem mainLoop jedną linijkę:
>>> quitMVar <- newEmptyMVar :: IO (MVar ())
Teraz quitMVar będzie pudełkiem w którym może znaleść się wartość typu () - jest to typ o jednym elemencie, który także wygląda jak () - jest to krotka o zerze elementów:
()
-- nie ma krotek jednoelementowych: (1)
(1,2)
(1,2,3)
(1,2,3,4)
(1,2,3,4,5)
Nie potrzebujemy zatrudniać tutaj typu Bool czy też Int, więc nie robimy tego.
quitMVar przekazujemy teraz do zmodyfikowanych wersji myKeyCallback i mainLoop:
>>> keyCallback $= myKeyCallback quitMVar
>>> mainLoop quitMVar
Definicje te wyglądają tak:
>>> -- wywoływany przy naciśnięcu jakiegokolwiek klawisza
>>> myKeyCallback quitter key Press = do
>>> case key of
>>> SpecialKey BACKSPACE -> iconifyWindow
>>> SpecialKey ENTER -> restoreWindow
>>> SpecialKey ESC -> putMVar quitter () -- umieszczamy wartość w "pudełku"
>>> _ -> return ()
>>> myKeyCallback _ _ _ = return ()
>>> mainLoop :: MVar () -> IO ()
>>> mainLoop qMV = do
>>> (Position px py) <- get mousePos
>>> clear [ColorBuffer]
>>> let gray = Color3 0.5 0.5 0.5 :: Color3 Float
>>> let myPoint = (Vertex2 (fromIntegral px) (fromIntegral py)) :: Vertex2 Float
>>> renderPrimitive Points $ color gray >> vertex myPoint
>>> swapBuffers
>>> tryTo (not `fmap` isEmptyMVar qMV) (mainLoop qMV) -- jeżeli qMV jest pusta, to kontynuujemy
Kiedy wyszliśmy już z pętli renderingu nie zostało wiele do zrobienia. Zamykamy okno i idziemy spać:
>>> closeWindow
Choć post ten wyszedł na znacznie dłuższy niż początkowo zamierzałem, sam kod jest zwarty i niewielki. Można go obejrzeć tutaj i tutaj. Kod źródłowy dostępny do ściągnięcia tutaj.
Mam nadzieję, że post wyszedł choć trochę ciekawie :-)
Zadania dla czytelników:
Znaleźć błąd w programie - łatwo go zauważyć po jego uruchomieniu
sobota, października 18, 2008
Haskell + OpenGL
Krótki szablon jak w Haskellu zapisać zrzut ekranu renderowanego przez OpenGL.
Plik zapisywany jest do formatu PAM, można go potem przekonwertować do dowolnego innego formatu rastrowego, np. za pomocą narzędzia convert z pakietu ImageMagick. Można też skorzystać z pakietu Netpbm.
Link na hpaste.org
Link na moim serwerku
Plik zapisywany jest do formatu PAM, można go potem przekonwertować do dowolnego innego formatu rastrowego, np. za pomocą narzędzia convert z pakietu ImageMagick. Można też skorzystać z pakietu Netpbm.
Link na hpaste.org
Link na moim serwerku
Subskrybuj:
Posty (Atom)