Fragmenty a piksele

2008-04-22 22:25

W terminologii DirectX programowalny potok graficzny ma dwie ważne części: vertex shader i pixel shader. Nazwa tej drugiej jest zasadniczo trochę myląca. Sugeruje ona, że shader jest wykonywany dla każdego piksela na ekranie, co tak naprawdę jest dalekie od prawdy.
Tak naprawdę bowiem “piksele” te są dopiero kandydatami do zapisania w buforze tylnym. Po drodze mają bowiem wiele okazji na to, aby być całkowicie wyeliminowane z renderingu. Może to się stać z któregoś z poniższych powodów, które jednak nie wyczerpują wszystkich możliwości:

  • Test alfa. W przypadku włączenia tego rodzaju testu, możliwe jest odrzucenie “pikseli”, których wartość kanału alfa nie jest dostatecznie duża. Zwykle jako wartość porównawczą wykorzystuje się 127, 128 albo domyślnie 0. Nie zawsze można sobie oczywiście pozwolić na takie skwantowanie informacji o przezroczystości, ale ma ono niebagatelną przewagę wydajnością nad alpha-blendingiem.
  • Test głębi. To najbardziej oczywisty test, a związany jest z przesłanianiem obiektów. W wielu przypadkach możliwe jest wykonanie go przed pixel shaderem, lecz w określonych sytuacjach może być konieczne testowanie już przetworzonych “pikseli”. W zależności od powodzenia testu wartość głębi zapisana w buforze może być oczywiście uaktualniona, co wpływa na dalsze testy.
  • Test stencila. Test wykorzystujący stencil buffer jest sprzężony z testem głębi, wobec czego podlega podobnym ograniczeniom. W szczególności możliwe jest na przykład zapisanie innej wartości do wspomnianego bufora w zależności od tego, czy “piksel” wyłoży się na teście głębi czy stencila.

A zatem wyjście pixel shadera niekoniecznie musi bezpośrednio trafić na ekran. Nie powinniśmy więc brać liczby pikseli w buforze ekranu za średnią liczbę wywołań tego shadera, a już na pewno nie jako górną granicę.
Dlatego też trzeba przyznać, że używana w OpenGL nazwa ‘fragment program‘ jest o wiele lepsza niż ‘pixel shader’. Fragment (wyjście shadera) nie jest bowiem jeszcze pikselem, a jedynie kandydatem na niego, który może odpaść przy wielu okazjach.

Tags: , , , ,
Author: Xion, posted under Programming » 1 comment

Pomijanie klawiszy w C#

2008-04-21 19:50

Czasami dobrze jest reagować na wszystkie przychodzące do okna wciśnięcia klawiszy w jednym miejscu. W C# ustawia się wtedy właściwość KeyPreview dla formy. Sprawia to, że dostaje ona zdarzenia klawiatury niezależnie od tego, która z potomnych kontrolek ma aktualnie fokus. Dzięki temu można implementować polecenia aktywowane wciśnięciami określonych klawiszy lub ich kombinacji, których nie da się przypisać np. do skrótów klawiszowych w menu:

private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
    switch (e.KeyCode)
    {
        case Keys.Oemtile:
            // tylda - pokaż/ukryj konsolkę
            txtConsole.Visible = !txtConsole.Visible;
            break;
    }
}

Jeśli jednak nic w tej kwestii nie zrobimy, zdarzenia klawiatury domyślnie i trafią do kontrolki aktywnej, co niekoniecznie musi nam się podobać. W powyższym przykładzie nasza prosta konsolka "złapałaby" na przykład wszystkie wciśnięcia klawisza tyldy jako znaki ~.
Jak temu zapobiec? Wystarczy wykorzystać właściwość SuppressKeyPress z klasy KeyEventArgs:

e.SuppressKeyPress = true;

To sprawi, że zdarzenie wciśnięcia klawisza nie będzie przekazywane dalej i żadna kontrolka go nie otrzyma. To przydatne, jeśli mamy ręcznie zakodowane skróty klawiszowe, które pokrywają się ze standardowymi sekwencjami.

Tags:
Author: Xion, posted under Programming » Add comment

Nadmierne aspiracje

2008-04-19 16:54

Wygasła już chyba dyskusja w wątku na forum Warsztatu, w którym zwrócono uwagę na poziom pojawiających się w serwisie gamedev.pl projektów. Dotyczyło to zwłaszcza screenów, jakie to można niekiedy rotacyjne oglądać na stronie głównej. Dość często podnoszonym argumentem było to, iż marny ich poziom szkodzi wizerunkowi Warsztatu na zewnątrz. Proponowane przy okazji rozwiązania wahały się zwykle w przedziale między całkowitym usunięciem wszystkim tetrisów, pongów, projektów konsolowych i innych subiektywnie ocenianych "zaniżaczy poziomu", a wysłaniem ich do specjalnie przygotowanego działu (nazywanego np. 'żłobkiem'), z którego screeny nie wystawałaby na widoku publicznym na stronie głównej serwisu.

Screen z gry Game Producer
I konsolówki bywają niezłe
(Game Producer)

Troska o ów poziom Warsztatu jest doprawdy rozczulająca. Zastanawiam się tylko, jaka stoi za nią motywacja? Może to być chęć, by serwis oraz społeczność były postrzegane na zewnątrz jako bardziej profesjonalne (niż w rzeczywistości, chciałoby się dodać). Obecność screenów, które niekoniecznie powalają oglądających na kolana, ma w tym oczywiście przeszkadzać.
A ja pytam: jaki jest w tym cel oraz sens? Po pierwsze, na tle innych podobnych serwisów, Warsztat nie ma się czego wstydzić (dla porównania można obejrzeć jego odpowiedniki z innych krajów). Zaś po drugie, aspirowanie do "bardziej profesjonalnej" roli w przypadku serwisu nieanglojęzycznego nie ma większego sensu. W przeliczeniu na nadal skromny przemysł gier komputerowych w Polsce (jeden Wiedźmin wiosny nie czyni), obecna postać i poziom Warsztatu - zwłaszcza społeczności - są chyba nawet lekką nadwyżką.

Screen z techdema INQ
Screen, o którego poziom
nie trzeba się martwić :)
(INQ-Techdemo)

A mimo to zdaje się, że cierpimy od jakiegoś czasu na kompleks rozpaczliwego udowadniania światu, iż Warsztat to serwis jak najbardziej poważny i - powtórzmy jeszcze raz to kluczowe słowo - profesjonalny.
Otóż nie, to nieprawda i stanowczo przeciwko temu protestuję! Warsztat zawsze był i będzie serwisem i społecznością skupiającą amatorskich oraz zawodowych programistów gier - w tej właśnie kolejności. Wszelkie próby zmiany tego stanu rzeczy, polegające na składaniu różnorodności i walorów edukacyjnych Warsztatu na ołtarzu utrzymania 'poziomu' i poprawiania 'wizerunku' na pewno nie mogą skończyć się dobrze.

Tags:
Author: Xion, posted under Thoughts » 4 comments

Być albo nie być

2008-04-18 20:08

W programowaniu relacja 'jest' najczęściej oznacza możliwość potraktowania pewnej wartości (lub ogólniej: obiektu) jako należącego do określonego typu (klasy). Od kiedy wynaleziono dziedziczenie, obiekty mogą być polimorficzne - czyli być traktowane tak, jakby należały do kilku różnych typów danych. Dwa proste przypadki obejmują: zwykłe dziedziczenie publiczne, gdy obiekt klasy pochodnej jest też obiektem klasy bazowej, oraz implementowanie abstrakcyjnych interfejsów.

Czasami jednak to, co w związku ze słówkiem 'jest' bywa intuicyjne, nie zawsze sprawdza się w praktyce. Tak jest chociażby wtedy, gdy bierzemy pod uwagę:

  • Dziedziczenie prywatne. Ten rzadko spotykany typ dziedziczenia (obecny w C++) sprawia, że choć obiekt klasy pochodnej jest nadal obiektem klasy bazowej, to wie o tym tylko on sam. Dlatego nie jest możliwe bezproblemowe rzutowanie w górę, które dla dziedziczenia publicznego jest przeprowadzane automatycznie. Mimo tego nadal mamy do czynienia z normalnym dziedziczeniem i jeśli przy użyciu jakichś brzydkich sztuczek (czyli na przykład reinterpret_cast) skonwertujemy odwołanie do obiektu na odwołanie do klasy bazowej, będziemy mogli z niego poprawnie skorzystać.
  • Kolekcje obiektów.Teoretycznie zbiór obiektów typu B jest też zbiorem obiektów typu A, jeśli typ B jest rodzajem typu A (np. klasą pochodną). W praktyce jednak kolekcja może się zmieniać, a przy traktowaniu jej w sposób bardziej ogólny istnieje możliwość dodania do niej obiektów będących wprawdzie typu A, ale nie mających nic wspólnego z B.
    Stąd też wynika fakt, że w językach programowania nie można ot tak sobie rzutować pojemników na takie, które przechowują "bardziej ogólne" obiekty. Jeśli więc chcemy zmienić np. List<string> na List<object> w C#, musimy świadomie wykonać kopię pojemnika.
  • Niewirtualne wielodziedziczenie. W bardziej skomplikowanych przypadkach, gdy klasa ma więcej niż jedną klasę bazową, mogą się pojawiać problemy z niejednoznacznością. Jeżeli bowiem idziemy w górę więcej niż dwa pokolenia, może mieć znaczenie ścieżka w grafie dziedziczenia, którą przy okazji obieramy. Ten problem eliminuje dziedziczenie wirtualne, przy okazji wprowadzając jednak spory zestaw nowych kłopotów. I dlatego właśnie wielodziedziczenia powinno się nie unikać, o ile się nie wie dokładnie, co chce zrobić :)

Zatem proste, zdawałoby się, stwierdzenie "coś jest jakiegoś typu", w programowaniu może wcale nie być takie oczywiste. Takie są aczkolwiek uroki naszego ulubionego OOP-u :]

Tags: , ,
Author: Xion, posted under Programming » Add comment

Rozdzielanie pakietów

2008-04-16 14:06

Protokół TCP ma to do siebie, że możemy mu zaufać - zawsze mamy gwarancję, że dane wysłane trafią do odbiorcy (a jeśli nie trafią, to będziemy o tym wiedzieli). Dlatego możliwe jest traktowanie przesyłu danych tą drogą podobnie, jak chociażby wymiany danych między pamięcią operacyjną a plikiem na dysku. Z tego też powodu wiele języków programowania pozwala na opakowanie połączeń TCP/IP w strumienie o identycznym interfejsie jak te służące na przykład do manipulowania zawartością pliku.
W praktyce jednak nie da się pominąć zupełnie tego prostego faktu, iż odbierane dane pochodzą z sieci i wysyłane także tam trafiają. Dotyczy to na przykład takiej kwestii jak dzielenie informacji na małe porcje u nadawcy i ich interpretacja po stronie odbiorcy.

Jak bowiem wiadomo, dane przesyłane przez TCP/IP mogą być po drodze dzielone i składane, a zagwarantowana jest jedynie ich kolejność. Nie ma natomiast pewności, że kawałek danych wysłany jednym wywołaniem w rodzaju Send zostanie odebrany także jednym wywołaniem Receive. Granice między porcjami danych każdy protokół musi więc ustalać samodzielnie. Można to zrobić na kilka sposobów, jak chociażby:

  • Jawne zapisanie długości pakietu. Polega to na wysłaniu przed właściwymi danymi nagłówka o stałym rozmiarze, który ma ustalony format i którego częścią jest długość następującej dalej porcji danych. W najprostszym wypadku może być po prostu wysłanie najpierw np. czterobajtowej liczby z długością pakietu, a następnie reszty danych. Dzięki temu program odbierający zawsze będzie wiedział, ilu bajtów należy się spodziewać (a więc najpierw czterech, a potem tylu, ile wynosi odebrana długość).
  • Używanie ustalonego rozdzielacza. To rozwiązanie polega na określeniu jakiejś sekwencji bajtów jako znacznika końca pakietu. Bardzo często (HTTP, FTP, IRC, itd.) jest to znak końca wiersza, oddzielający poszczególne żądania i odpowiedzi, lub znak o kodzie zero (\0). Odbieranie danych polega wtedy na odczytywaniu kolejnych bajtów do bufora i interpretacji pakietu dopiero po otrzymaniu końcowego znacznika.
  • Korzystanie z własności pewnych formatów danych. Można mianowicie przesyłać informacje wyłącznie w określonym formacie, którego struktura pozwala określić, gdzie kończy się jedna porcja, a zaczyna druga. Jeśli przykładowo wykorzystamy XML i będziemy przesyłali wyłącznie pojedyncze jego elementy - np. <foo>...</foo> - to koniec takiego elementu będzie jednocześnie wiadomością o końcu pakietu. Można to więc traktować jak nieco bardziej skomplikowany wariant znaczników końca.

Jeśli tworzymy nowy protokół dla własnych aplikacji, to który wariant wybrać? Pierwszy wydaje się być dobry dla protokołów binarnych; tych jednak generalnie nie powinno się używać ze względu na liczne problemy z kodowaniem i pakowaniem danych. Druga opcja jest bardzo szeroko stosowana w wielu powszechnie używanych usługach sieciowych i wydaje się sprawdzać całkiem dobrze. Trzecia jest w gruncie rzeczy podobna, ale nieco bardziej złożona i może być kłopotliwa od strony kodu odbierającego dane.

Tags:
Author: Xion, posted under Internet, Programming » 1 comment

Konsolka dla okien

2008-04-14 20:59

Zasadniczo to systemy uniksowe znane są z intensywnego wykorzystania trybu tekstowego, czyli wiersza poleceń konsoli. W przypadku systemu Windows wydaje się, że jest to bardziej pozostałość po starym (nie)dobrym DOS-ie, która została prawie całkowicie zastąpiona przez interfejs graficzny. Problem jednak w tym, że nie wszystko da się zawsze wygodnie wyklikać - a co więcej, takie czynności niezbyt nadają się do zautomatyzowania (na przykład po to, aby wielokrotnie przeprowadzać zmiany w konfiguracji na wielu maszynach). Mając elastyczny tryb tekstowy, zwykle bowiem wystarczy napisać odpowiedni skrypt; w przypadku trybu graficznego nie ma takiej możliwości.
Ten mankament był w Windows obecny od dawna i różne były sposoby na jego obejście - począwszy od plików wsadowych (.bat) przez eksporty/importy Rejestru (.reg), pliki .inf czy skrypty WSH (Windows Scripting Host). Te ostatnie są na przykład znane z powodu... kilku wirusów, które zdołały się szeroko rozprzestrzenić, wykorzystując tę technologię (np. słynny I.Love.You).

Logo PowerShellWszystkie podobne pomysły rozwiązywały jednak cały problem dość średnio. Lecz od jakiegoś czasu istnieje Windows PowerShell, który wydaje się znacznie ciekawszym rozwiązaniem. Jest to tekstowa powłoka poleceń - wraz ze skryptowym językiem programowania - która nie tylko jest o wiele bardziej przydatna niż wcześniejsze wynalazki Microsoftu, ale też wypada wcale nieźle w porównaniu z innymi shellami, jak choćby bash. Ma ona bowiem kilka interesujących cech szczególnych:

  • Pozwala wykorzystać wszystkie trendy technologie windowsowe, jak COM, WMI (Windows Management Instrumentation) i przede wszystkim .NET. Większość czynności "systemowych" można wykonać, korzystając z któregoś z tych narzędzi.
  • Zamiast na tekście, w tym shellu operujemy na obiektach. Możemy je tworzyć, przekazywać jako dane wejściowe do poleceń, modyfikować, filtrować, a także dobierać się do ich składników - odczytywać właściwości i wywoływać metody. W szczególności operator przetwarzania potokowego | powoduje przekazanie między poleceniami (a dokładniej tzw. cmdletami) obiektów, a nie zwykłego tekstu.
  • PowerShell unifikuje też sposób dostępu do różnych składników systemu, jak system plików, Rejestr czy zmienne środowiskowe. Dzięki temu nie trzeba do każdego z nich stosować innych narzędzi.

W tym momencie zapewne przydałby się wymowny przykład, który może wyglądać choćby następująco:

ps | where { -not $_.Responding } | kill

To polecenie najpierw listuje wszystkie procesy w systemie (ps), następnie wybiera spośród nich te, które nie odpowiadają, aby w końcu posłać je do Krainy Wiecznych Zwisów :) Operujemy tutaj na obiektach reprezentujących procesy, a jedną z ich właściwości jest, jak widać, Responding, którą można użyć do filtrowania.
W pełnej wersji komenda wyglądałaby tak naprawdę następująco:

Get-Process | Where-Object -filterScript { -not $_.Responding } | Stop-Process

co być może wygląda wymowniej, lecz jest z pewnością bardziej rozwlekłe. Na szczęście PowerShell definiuje fabrycznie kilkanaście aliasów, które mapują się na polecenia odpowiadające z grubsza tym znanym z innych shelli - jak dir, ls, cd, rm, ps, man czy type. To pozwala w miarę szybko poznać podstawowe komendy i przyspiesza ich wpisywanie.

Na koniec trzeba stwierdzić, że przydatność PowerShella zależy prawdopodobnie przede wszystkim od tego, czy potrafimy wykorzystać całe to ogromne bogactwo klas .NET, COM i WMI, które powłoka ta udostępnia. Jeśli tak, to możemy znaleźć dla niej szerokie pole zastosowań. Polecam w każdym razie przyjrzenie się temu wynalazkowi (dostępnemu na każdą sensowną wersję Windows, tj. od XP SP2 wzwyż). Sam lubię używać go do zabijania procesów znacznie bardziej niż standardowego Menedżera zadań ;-)

Tags: ,
Author: Xion, posted under Applications » 3 comments

RAII-skie kwiatki

2008-04-11 19:43

Jako język nie posiadający słowa kluczowego finally, C++ preferuje nieco inną metodę na radzenie sobie z nieprzewidzianymi "wyskokami" z funkcji i związaną z tym możliwością wycieku zasobów (resource leak). Ten inny sposób jest znany skądinąd jako RAII: Resource Acquisition Is Initialization i polega na związaniu z każdym użyciem zasobu jakiegoś obiektu lokalnego, np.:

{
   ThreadLock lock;
   // (wykonywany tylko przez jeden wątek)
}

Proste i całkiem wygodne, jeśli tylko posiadamy już (lub zechcemy napisać) odpowiednią klasę, która w konstruktorze pozyskuje dany zasób - tutaj blokadę muteksa - a w destruktorze go oddaje.

Ale ten nieskomplikowany mechanizm daje możliwości popełnienia błędów, które są na swój interesujące, ale w realnym kodzie na pewno niezbyt przyjemne :) Pierwszy z nich związany jest z faktem, że lokalnych obiektów nie tworzy się znowu aż tak dużo i można popełnić w ich składni drobne, acz wielce znaczące faux pas z nawiasami:

ThreadLock lock();

Taki wiersz nie stworzy nam bowiem żadnego obiektu, ale zadeklaruje funkcję lock, zwracającą obiekt typu ThreadLock i niebiorącą żadnych argumentów. Zaskakujące? A to tylko prosta konsekwencja faktu, że cokolwiek, co można zinterpretować w C++ jako deklarację funkcji, zostanie tak właśnie zinterpretowane.

Można jednak ripostować, że nic takiego nie zdarzy się, jeśli do konstruktora naszego obiektu-blokady przekażemy chociaż jeden parametr. A zwykle tak właśnie będzie; tutaj np. byłoby nim odwołanie do obiektu typu mutex lub semafora, który chcemy zająć. Jednak nie zmienia to faktu, że w większości przypadków obiekt realizujący RAII wystarcza nam przez samo swoje istnienie, co z kolei sprawia, że w dalszym kodzie w ogóle się do niego nie odwołujemy. To zaś może spowodować, że pominiemy i tak nieużywany składnik jego deklaracji - czyli nazwę:

ThreadLock (&mutex);

Takie zagranie również nie powinno wywołać protestów kompilatora, ale prawie na pewno nie jest tym, o co nam chodzi. Tworzony obiekt jest teraz bowiem nie lokalny, ale tymczasowy: jego zasięg ogranicza się do wyrażenia, w którym został wprowadzony. Czyli do... średnika kończącego powyższą instrukcję! Taki też zakres ma opakowana przez ów obiekt blokada międzywątkowa.

Jak zatem widać, jest tu kilka okazji do popełnienia błędów, które mogą być trudne do wykrycia. Powinniśmy więc zwrócić na nie uwagę tym bardziej, że wobec braku w C++ instrukcji finally technika RAII jest jedynym sensownym wyjściem dla lokalnego pozyskiwania i zwalniania zasobów.

Tags: , ,
Author: Xion, posted under Programming » 13 comments
 


© 2012 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.