Raytracer

Jak zwykle po długiej nieobecności w końcu pojawia się kolejna notka. Tym razem chciałbym zaprezentować Raytracer, który napisałem w ramach laboratorium z Zaawansowanej grafiki komputerowej. Program oprócz standardowego śledzenia promieni potrafi również generować teksturę proceduralną cegły dla podanego atrybutu (tę część napisałem z kolegą Adamem Jordankiem) oraz prowadzić obliczenia na wielu komputerach wykorzystując model klient-serwer.

Aplikacja do renderowania grafiki wykorzystuje między innymi moją własną biblioteczkę matematyczną, proste kontenery, a oprócz tego – do obsługi klienta i serwera –  sockety i wątki (w zależności od systemu ich odpowiednie implementacje).  Implementacja Raytracera wykorzystuje algorytm przecięcia promienia z trójkątem oraz implementację kd-tree znalezioną na tej stronie, które według mojego przekonania są na tyle wydajne i stabilne, że nie było sensu tego modyfikować. Wykorzystywany model oświetlenia jest efektem prób i błędów, ale myślę że wygląda całkiem znośnie. Zaimplementowane są wszystkie podstawowe elementy takie jak cienie, odbicia czy refrakcja.

Z ciekawszych rzeczy, które można znaleźć w kodzie to:

  • obsługa linii poleceń, która opiera się przede wszystkim na liście poleceń, gdzie każde polecenie zawiera nazwę i opis parametru oraz wskaźnik do funkcji, która implementuje jego obsługę.
  • implementacja klienta i serwera w oparciu o stany oraz przydział zadań do poszczególnych klientów
  • wczytywanie plików oparte o stany (łatwość dodawania nowych elementów)

Więcej informacji i paczkę można znaleźć tutaj.

Optymalizacja aplikacji C++ z wykorzystaniem AMD CodeAnalyst

W tej notce opiszę moją pierwszą porządną styczność z programem CodeAnalyst i przy okazji pokażę, w jaki sposób udało mi się nieco zoptymalizować napisaną przeze mnie aplikację implementującą rozwiązywanie problemu kolorowania grafu za pomocą algorytmu genetycznego.

CodeAnalyst - Okno główne programuCodeAnalyst jest programem służącym do profilowania aplikacji. Jego zadaniem jest zbieranie różnego rodzaju próbek w trakcie działania testowanej aplikacji, które są następnie przedstawiane w formie wykresu lub tabeli. Dzięki temu widać od razu, które miejsca aplikacji mają największy wpływ na badany parametr z dokładnością do wykonywanych instrukcji Assemblera. Program ten można znaleźć na stronie producenta. Jego główne okienko jest przedstawione na obrazku obok.

Jak już wspomniałem badaną aplikacją jest moja implementacja algorytmu ewolucyjnego użytego do rozwiązania problemu kolorowania grafu. Kod tej aplikacji wraz z przykładowym grafem znajduje się tutaj. Jest to kod, od którego wychodzę w tej notce, zatem nie uwzględnia on żadnych poprawek. Poniższa tabela zawiera średnie wyniki pomiaru cykli i czasu jakie zabiera główna pętla programu na dwóch maszynach:

Procesor System Kompilator Częstotliwość zegara Liczba cykli Czas
AMD Turion64 2 GHz MS Windows 7 msvc 25 MHz (QPF) 4111926149 cykli (QPC) 164,48 s
Intel Pentium 4 2,66 GHz Linux g++ 2659,98 MHz (cpuinfo) 268359191768 cykli (__rdtsc()) 100,89 s

Optymalizacja będzie dotyczyć głównie tej pierwszej platformy, ale postanowiłem z ciekawości sprawdzić jak zmiany będą wpływać na czas wykonywania się kodu również na drugiej platformie.

Analiza programu będzie polegać na szukaniu miejsc, w których spędza on najwięcej czasu. Zatem na początek należy uruchomić program CodeAnalyst, a następnie utworzyć projekt wybierając profil Time-based profile. Według mnie dobrze jest również zaznaczyć opcje Stop data collection when app exits oraz Profile the duration of the app execution. Dzięki temu CodeAnalyst poczeka z analizą wyników aż badana aplikacja skończy działanie. Po utworzeniu projektu można przystąpić do testów. Zatem po wyciszeniu/wyłączeniu niepotrzebnych aplikacji można wybrać z menu Profile -> Start lub kliknąć zieloną strzałkę.

Po zakończeniu działania aplikacji zostanie wyświetlone podsumowanie, które wygląda mniej więcej tak:
CodeAnalyst - System dataCodeAnalyst - System graph

Obie te zakładki przedstawiają dokładnie to samo, czyli procentowy udział próbek w każdej aplikacji/bibliotece/module, różnica występuje tylko w formie przedstawienia danych. Co w tym podsumowaniu jest interesującego? W tym widoku jeszcze nic, ale CodeAnalyst umożliwia podgląd szczegółów każdego z wymienionych wcześniej bytów, nawet dowolnego procesu wybranego z zakładki Processes. Jednak ponieważ ta notka obejmuje tylko jedną aplikacje, w dodatku tę, która zebrała najwięcej próbek, zatem przejdę teraz do niej. Oto co pokazuje się po dwukrotnym kliknięciu na jej proces:
CodeAnalyst - Testowana aplikacja

W tym widoku widać już nieco więcej. Są tu pokazane funkcje, które zabierały najwięcej czasu. Na samej górze widać jedną, która zabiera aż 40% próbek, jest to funkcja EvaluateConflicts(), która zlicza konflikty dla każdego osobnika w całej populacji. Warto się jej przyjrzeć z bliska:
CodeAnalysy - Podgląd funkcji EvaluateConflicts()

CodeAnalyst, wykorzystując dane zawarte w pliku .pdb, który powinien znajdować się obok pliku wykonywalnego, potrafi wyświetlić i dopasować próbki do odpowiedniego miejsca w kodzie. Można również podejrzeć wykonywany w tym miejscu kod Assemblera, co jest bardzo przydatne (żeby nie powiedzieć kluczowe) przy profilowaniu aplikacji. Co zatem widać na powyższym screenie? Jest tam coś co tak naprawdę powinno znajdować się tylko i wyłącznie w aplikacji kompilowanej w trybie Debug. To sprawdzanie poprawności odwołania się do elementu tablicy w klasie std::vector. Jest to nieco dziwne, ponieważ jest tam używany operator[], a standardowo ta wersja odwołania nie powinna być sprawdzana, w przeciwieństwie do funkcji at().
Okazuje się jednak, że implementacja STL dostarczana z kompilatorem msvc ma zaimplementowane takie sprawdzenie, które można co prawda wyłączyć flagą _SECURE_SCL ustawioną na 0, ale domyślnie ta opcja jest włączona. Więcej informacji można znaleźć tu lub bezpośrednio tutaj.

Poniższa tabela zawiera wyniki pomiarów cykli i czasu z wyłączeniem tego sprawdzania:

Procesor System Kompilator Częstotliwość zegara Liczba cykli Czas Zysk
AMD Turion64 2 GHz MS Windows 7 msvc 25 MHz (QPF) 3191994350 cykli (QPC) 127,68 s 22,37%
Intel Pentium 4 2,66 GHz Linux g++ 2659,98 MHz (cpuinfo) 269220460226 cykli (__rdtsc()) 101,21 s -0.32%

W przypadku msvc widać, że jest już lepiej, a przypadek gcc został praktycznie nietknięty, zatem nie posiada on domyślnie tego typu sprawdzania. Na poniższych screenach widnieją wyniki ponownego próbkowania aplikacji.
CodeAnalysy - Próbki w programie po pierwszej poprawceCodeAnalyst - Funkcja EvaluateConflicts po wprowadzeniu pierwszych poprawek

Jak można zauważyć, ta drobna poprawka pozwoliła kompilatorowi zinline’ować całą funkcję EvaluateConflicts(). Dzięki temu aplikacja wykonuje się szybciej o ok. 22 %. Kod tej wersji znajduje się pod tym linkiem.

Jednak na tym nie koniec poprawek, ponieważ jest jeszcze jedno miejsce, z którym da się coś zrobić. Otóż jeśli się przyjrzeć podsumowaniu całej aplikacji jeszcze raz, można zauważyć, że spędza ona dużo czasu w funkcji insert() drzewa std::_Tree, które jest implementacją zbioru (std::set) w bibliotece standardowej. Klasa ta jest używana w operatorze selekcji w celu wybrania losowych i niepowtarzających się osobników do turnieju. Co za tym idzie, funkcja insert() jest wywoływana minimum raz dla każdego wolnego miejsca w turnieju (w przypadku testowym jest 50 miejsc), aż do wypełnienia całej nowej populacji (tutaj 5000), czyli krótko mówiąc – dość często.

Rozwiązaniem, które przyjąłem w tym miejscu było całkowite pozbycie się klasy std::set na rzecz własnego sposobu wybierania losowych i różnych osobników do turnieju. Wynikiem tego jest taka oto klasa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<uint RANGE>
class RandomSet    // singleton
{
public:
    RandomSet()
    { 
        for(uint i = 0; i < RANGE; ++i)
        { m_aSet[i] = i; }
    }
 
    const uint* GetRandomSet(uint uCount, uint uInnerRange)
    {
        uint uTemp, uRand;
        for(uint i = 0; i < uCount; ++i)
        {
            uRand = rand() % (uInnerRange - i);
            uTemp = m_aSet[i];
            m_aSet[i] = m_aSet[i + uRand];
            m_aSet[i + uRand] = uTemp;
        }
        return m_aSet;
    }
 
private:
    uint m_aSet[RANGE];
};

Klasa ta wykorzystuje tablicę, w której zapisane są kolejno indeksy wszystkich osobników. Przy każdym wywołaniu funkcji GetRandomSet() tablica ta jest ponownie mieszana tak, by na jej początku znalazły się w miarę losowe wartości. Dzięki temu, że wybierane wartości są zamieniane z wartościami z początku tablicy, zbiór pozostaje pełny ale jest on stopniowo mieszany, przy czym największa różnorodność występuje zawsze na początku tablicy. To ostatnie nie ma tak naprawdę większego znaczenia, ponieważ najważniejsze jest to, żeby funkcja zwracała za każdym razem różne wartości.

Kolejna tabela zawiera wyniki testów po wprowadzeniu tych zmian:

Procesor System Kompilator Częstotliwość zegara Liczba cykli Czas Zysk
AMD Turion64 2 GHz MS Windows 7 msvc 25 MHz (QPF) 1144922576 cykli (QPC) 45,8 s 64,13 %
Intel Pentium 4 2,66 GHz Linux g++ 2659,98 MHz (cpuinfo) 105596054178 cykli (__rdtsc()) 39,7 s 60,78 %

W przypadku tej poprawki można już zaobserwować znaczny zysk i to na obu platformach (Windows – 64%, Linux – 61 %). Na poniższych obrazkach kolejne zrzuty z CodeAnalyst.
CodeAnalyst - Poprawka GetRandomSet - podsumowanie aplikacjiCodeAnalyst - Poprawka GetRandomSet - funkcja

Widać tutaj, że ilość próbkowanego kodu znacznie zmalała, a proporcje między funkcjami znów się zróżnicowały. Dzięki temu można zobaczyć, że funkcja GetRandomSet() zabiera znacznie mniej próbek niż EvaluateGeneration(). Kod tej wersji można pobrać stąd.

Pozostaje teraz ponownie zapytać, czy można coś tu jeszcze zoptymalizować? Może i można, ale prawdopodobnie zajmie to już trochę więcej czasu niż poprzednie poprawki. Jednym z oczywistych kroków jakie można tutaj wykonać to zastąpienie std::vector zwykłymi tablicami dynamicznymi, ale w tym przypadku zysk jest osiągalny tylko na Linuksie co pokazuje kolejna tabela:

Procesor System Kompilator Częstotliwość zegara Liczba cykli Czas Zysk
AMD Turion64 2 GHz MS Windows 7 msvc 25 MHz (QPF) 1144922576 cykli (QPC) 46,2 s -0.89 %
Intel Pentium 4 2,66 GHz Linux g++ 2659,98 MHz (cpuinfo) 105596054178 cykli (__rdtsc()) 37 s 6,78 %

Kod tego przypadku znajduje się tutaj.

I to właściwie tyle, wnioski? CodeAnalyst jest bardzo dobrym narzędziem do profilowania aplikacji, jest łatwy w obsłudze, a co najważniejsze jest za darmo. Oprócz tego warto zwracać uwagę na implementacje biblioteki standardowej w używanym kompilatorze, ponieważ można się nieco zdziwić, tak jak to miało miejsce w przypadku std::vector w tym przykładzie. Dodatkowo warto czasami poszukać innych rozwiązań dla niektórych problemów, ponieważ wyniki mogą okazać się całkiem zaskakujące.

Na koniec jeszcze taka małą dygresja odnośnie algorytmu ewolucyjnego – operator mutacji powinien testować prawdodpobieństwo dla każdego genu osobnika (czyli koloru węzła), a nie tak jak w tej implementacji, dla całego osobnika :).

Operacje na systemie plików w systemach Windows i Linux w C++

Tym razem notka o tym, w jaki sposób można wykonywać różne operacje w systemie plików w wymienionych w temacie systemach. Oczywiście każdy programista C/C++ zna funkcje wykonujące podstawowe operacje na plikach, takie jak:

  • odczyt, modyfikacja, zapis (funkcja fopen i poboczne oraz klasy fstream)
  • zmiana nazwy pliku lub katalogu (funkcja rename())
  • usunięcie pliku (funkcja remove())

Nieco więcej problemów sprawiają pozostałe operacje, takie jak np. pobranie pełnej ścieżki do pliku czy przeglądanie listy plików w katalogu, ponieważ w bibliotece standardowej nie ma do tego celu odpowiednich funkcji, zatem należy ich szukać w API systemu. Niestety co API to różne funkcje, ale zdarzają się i takie, które istnieją w obu systemach.

Na początek pobieranie pełnej ścieżki do bieżącego katalogu, czyli funkcja getcwd. Jej prototyp w systemie Windows jest następujący:

#include <direct.h>
char* _getcwd(char* _DstBuf, int _SizeInBytes);

a w systemie Linux:

#include <unistd.h>
char* getcwd(char* _DstBuf, int _SizeInBytes);

Jak widać prototyp obu funkcji jest identyczny, więc nic nie stoi na przeszkodzie utworzenia sobie odpowiedniego aliasu nazwy za pomocą typedef.

Trochę lepiej jest z funkcją stat() i jej strukturą o tej samej nazwie (thx C…, typedef needed), która istnieje w obu systemach. Funkcja ta służy do pobierania pełnych informacji o pliku, czyli np. daty modyfikacji, typu czy rozmiaru. Jej przykładowe użycie wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
#include <sys/stat.h>
typedef struct stat FileStatus; // thx C
 
/*...*/
FileStatus fileStatus;
if(stat(szFullPath.c_str(), &fileStatus) < 0)
{
    return;
}
 
unsigned int uFileSize = fileStatus.st_size;	// Pobranie rozmiaru pliku

Nieco gorzej jest z wyciąganiem listy plików w katalogu, ponieważ w tym przypadku oba systemy mają do tego różne funkcje. W przypadku systemu Windows wygląda to tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <windows.h>
/*...*/
 
WIN32_FIND_DATA info;
HANDLE hFind = FindFirstFile(szDirPath, &info);
 
if(INVALID_HANDLE_VALUE == hFind)
{
    printf("Error");
    return;
}
 
do
{
    if((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
    {
        printf("Directory: %s\n", info.cFileName);
    }
    else
    {
        printf("File: %s\n", info.cFileName);
    }
}
while(FindNextFile(hFind, &info) != 0);
 
FindClose(hFind);

A w systemie Linux tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <dirent.h>
#include <string.h> // c-string operations
typedef struct dirent DirEntry;
typedef struct stat DirEntryStat; // thx C again
 
DIR* pDir;
pDir = opendir(szDirPath);
if(pDir == null)
{
    printf("Error");
    return;
}
 
DirEntry* pDirent;
DirEntryStat dirEntryStat;
while((pDirent = readdir(pDir)) != null)
{
    char buffer[255];
    strcpy(buffer, szDirPath);
    strcat(buffer, pDirent->d_name);	// Do ponizszej funkcji potrzebna jest sciezka z biezacego katalogu
 
    if(stat((buffer, &dirEntryStat) < 0)
    {
        printf("Error");
        continue;	// file corrupted... NEXT!
    }
 
    if((dirEntryStat.st_mode & S_IFDIR) != 0)	// Katalog
    {
        printf("Directory: %s\n", pDirent->d_name);
    }
    else if((dirEntryStat.st_mode & S_IFREG) != 0)	// Plik
    {
        printf("File: %s\n", info.pDirent->d_name);
    }
}
 
closedir(pDir);

Można zauważyć, że funkcja z WinAPI jest nieco przyjaźniejsza, ponieważ operuje na uchwycie do żądanego katalogu, w przeciwieństwie do funkcji Linuksowej, która po prostu zwraca nazwę tego co siedzi w katalogu i nic więcej.

NIne 3 SceneGraph – paczka

Tym razem krótki wpis wraz z aplikacją przedstawiającą zaimplementowany SceneGraph. Różnica między tą a poprzednią wersją jest taka, że do obracającego modelu Tiny (postać) został podpięty model LandShark (statek) dzięki czemu zaczął on latać nad planszą zgodnie z obrotem Tiny. Dodatkowo pod ten ostatni można podpiąć kamerę, którą nie traci swobodnego ruchu, co pozwala poruszać się po nowym lokalnym układzie współrzędnych. Hierarchię modeli można zobaczyć na poniższym obrazku:

SceneGraphTree

Odnośnie sterowania odsyłam do ReadMe. Aplikację można pobrać stąd.

Data-Oriented-Design – SceneGraph proof of concept

Jakiś czas temu postanowiłem do swojego silnika dołączyć SceneGraph, którego zadaniem byłaby hierarchizacja modeli (w relacji rodzic-dzieci) występujących na scenie oraz aktualizacja ich macierzy świata. Generalnie taki graf zawiera nieco więcej informacji, takich jak na przykład bryły otaczające wraz z drzewem ósemkowym, na podstawie których można odrzucić modele niebędące w zasięgu kamery, jeszcze przed renderowaniem całej sceny. Jednak aktualnie zależało mi na zaimplementowaniu samej podstawy, czyli macierzy świata, ale w sposób zorientowany na dane, jak ma to miejsce w prezentacji Pitfalls of Object-Oriented Programming.

Hierarchizacja sceny polega na zebraniu wszystkich elementów sceny i połączeniu ich w drzewo, które składa się z korzenia oraz węzłów potomnych. W takim drzewie każdy rodzic oddziałuje bezpośrednio na swoje dziecko, czyli w tym przypadku transformacja, którą przechowuje rodzic ma wpływ na transformację przechowywaną przez dziecko, dla D3DX wygląda to tak:

D3DXMatrixMultiply(&childWorldMatrix, &childLocalMatrix, &parentWorldMatrix);

Istnieje kilka sposobów na powiązanie węzłów SceneGraph z elementami sceny. Najprostszym z nich jest struktura SGNode, której instancję można umieścić w klasie reprezentującej jakiś element sceny. Taka struktura wygląda następująco:

1
2
3
4
5
struct SGNode
{
   SGNode* pSibling, pChild;
   D3DXMATRIX localMatrix, worldMatrix;
};

Ponieważ sceny zwykle nie są statyczne, każdy taki węzeł powinien być aktualizowany co klatkę. Do tego celu powinna służyć odpowiednia funkcja klasy zarządzającej drzewem, której zadaniem będzie przejście rekursywnie po całym drzewie i zaktualizowanie danych. Oto przykład takiej funkcji:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void updateNodes(SGNode* pRoot)
{
   SGNode* pNode = pRoot->pChild;
 
   while(pNode != null)
   {
      D3DXMatrixMultiply(&pNode->worldMatrix, &pNode->localMatrix, &pRoot->worldMatrix);
      updateNodes(pNode);
      pNode->pSibling;
   }
}
 
// Wywołanie na korzeniu
updateNodes(m_pRoot);

Ten prosty przykład wystarczy by zauważyć problem, który został poruszony w wymienionej prezentacji. Chodzi przede wszystkim o skakanie po pamięci wskaźnikami, które są dość obficie używane w takiej implementacji. Powoduje to cache-missy, zwłaszcza w przypadku, gdy każdy węzeł jest alokowany osobno.

Rozwiązaniem tego, jak można się domyślić, jest umieszczenie tych danych w sposób liniowy, tak jak ma to miejsce w wymienionej prezentacji. Ważne jest również, aby dane były rozdzielone i uszeregowane w kolejności ich późniejszej aktualizacji.

W swoim silniku rozwiązałem to wykorzystując uchwyty, a węzły traktując jak każdy inny zasób, który można tworzyć i niszczyć. Wszystko to znajduje się w jednej klasie, która wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class NSceneGraph
{
   struct NSceneGraphNode
   {
      NSize_t   uChild,
            uSibling;
      NSize_t uIndex;            // Indeks do tablic danych
      NSize_t uNext;             // Indeks następnego węzła
   };
 
public:
   NRESULT InitSceneGraph();
 
   void Release();
 
   NRESULT CreateNode(NHSceneGraphNode& hNode);
 
   void ReleaseNode(NHSceneGraphNode hNode);
 
   void ChangeParent(NHSceneGraphNode hNode);
 
   void ChangeParent(NHSceneGraphNode hNode, NHSceneGraphNode hParent);
 
   NMMatrixPP GetLocalMatrix(NHSceneGraphNode hNode);
 
   void SetLocalMatrix(NHSceneGraphNode hNode, NMMatrixPP matrix);
 
   NMMatrixPP GetWorldMatrix(NHSceneGraphNode hNode);
 
   const NMMatrix* GetWorldMatrixPtr(NHSceneGraphNode hNode);
 
   // Funkcja aktualizuje macierze world i inne dane
   void Update();
 
   // Funkcja porządkuje drzewo tak, by przechodzenie wszerz było liniowe i cache-friendly
   NRESULT ApplyChanges();
 
private:
   NRESULT ResizeArrays(NSize_t uSize);
 
   void AddSubtree(NSize_t uNode, NSize_t uNewParent);
 
   void RemoveSubtree(NSize_t uNode);
 
   void SetChildIndex(NSize_t uNode, NSize_t uChild)
   {
      m_aSceneNodes[uNode].uChild = uChild;
   }
 
   NSize_t GetChildIndex(NSize_t uNode)
   {
      return m_aSceneNodes[uNode].uChild;
   }
 
   void SetSiblingIndex(NSize_t uNode, NSize_t uSibling);
 
   NSize_t GetSiblingIndex(NSize_t uNode);
 
   void SetDataIndex(NSize_t uNode, NUint32 uIndex);
 
   NSize_t GetDataIndex(NSize_t uNode);
 
   void SetNextIndex(NSize_t uNode, NSize_t uNext);
 
   NSize_t GetNextIndex(NSize_t uNode);
 
   bool RemoveSubtree(NSize_t uRoot, NSize_t uNode);
 
private:
   NStableDataVector<NSceneGraphNode> m_aSceneNodes;
 
   bool m_bDirty;
   NSize_t m_uArraysSize;
   NAlignedDynamicTable<NMMatrix> m_aLocalMatrices;
   NAlignedDynamicTable<NMMatrix> m_aWorldMatrices;
};

Teraz przydałoby się wyjaśnienie co do tego kodu. Po pierwsze nie ma tutaj wskaźników, ponieważ jednym z założeń silnika jest wykorzystywanie większych obszarów pamięci zamiast pojedynczych alokacji. Z tego wynika założenie o uchwytach, co implikuje wykorzystanie w przedstawionym kodzie indeksów. NStableDataVector reprezentuje kontener, którego kluczowym założeniem jest niezmienność pozycji przechowywanych danych, dzięki czemu uchwyty pozostają ważne przez cały czas działania aplikacji. NAlignedDynamicTable jest natomiast klasą, która odpowiada za tworzenie i obsługę tablicy przechowującej różne dane (w tym przypadku macierze), które wymagają wyrównanego adresu.

Wracając jednak do samego drzewa sceny – ponieważ jego głównym założeniem jest implementacja rozwiązań z wymienionej prezentacji, dane zostały podzielone na kilka odpowiednich tablic, a indeksy do tych tablic zostały dodane do węzła. Wszystkie nowo utworzone węzły są potomkami węzła głównego, a zmiana rodzica jest transakcją, która usuwa podany węzeł i od razu dodaje go jako dziecko nowego rodzica (funkcja ChangeParent()).

Kluczową funkcją tej klasy jest ApplyChanges(), jej zadaniem ułożenie danych w taki sposób, aby zredukować liczbę cache-missów do minimum, wygląda ona tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
NRESULT NSceneGraph::ApplyChanges()
{
   NAssert(m_bDirty != false, "Needless call of this function");
 
   NRESULT result;
   // Temp arrays
   NDataQueue<NSize_t> tempQueue;
   NAlignedDynamicTable<NMMatrix> tempLocalMatrices;
   NAlignedDynamicTable<NMMatrix> tempWorldMatrices;
 
   if(NFAILED(result = tempLocalMatrices.Create(m_aLocalMatrices.GetSize(), MATRIX_ALIGNMENT))
   || NFAILED(result = tempWorldMatrices.Create(m_aWorldMatrices.GetSize(), MATRIX_ALIGNMENT))
   || NFAILED(result = tempQueue.Create(m_uArraysSize / 2, 0)))
   {
      tempLocalMatrices.Release();
      tempWorldMatrices.Release();
      tempQueue.Release();
      return result;
   }
 
   tempLocalMatrices[0] = m_aLocalMatrices[0];
   tempWorldMatrices[0] = m_aWorldMatrices[0];
 
   // Główny algorytm
   NSize_t uRootNode = 0;
   tempQueue.PushElement(GetChildIndex(uRootNode));
 
   SetNextIndex(uRootNode, GetChildIndex(uRootNode));
 
   NSize_t uNext = uRootNode;
   for(NUint32 i = 1; !tempQueue.IsEmpty(); )
   {
      NSize_t uNode = *tempQueue.GetFront();
      tempQueue.PopElement();
 
      for(; uNode != NMAX_SIZE_T; ++i)
      {
         SetNextIndex(uNext, uNode);
 
         tempLocalMatrices[i] = m_aLocalMatrices[GetDataIndex(uNode)];
         tempWorldMatrices[i] = m_aWorldMatrices[GetDataIndex(uNode)];
         SetDataIndex(uNode, i);
 
         if(GetChildIndex(uNode) != NMAX_SIZE_T)
         {
            tempQueue.PushElement(GetChildIndex(uNode));
         }
 
         uNext = GetNextIndex(uNext);
         uNode = GetSiblingIndex(uNode);
      }
   }
   SetNextIndex(uNext, NMAX_SIZE_T);
 
   m_aLocalMatrices.Release();
   m_aWorldMatrices.Release();
   tempQueue.Release();
 
   m_aLocalMatrices = tempLocalMatrices;
   m_aWorldMatrices = tempWorldMatrices;
   m_bDirty = false;
 
   Update();
 
   return NRV_SUCCESS;
}

Powyższa funkcja została wyrwana prosto z pliku źródłowego. Jak widać przechodzi ona całe drzewo wszerz porządkując dane (macierze) oraz aktualizując indeks danych i następnego węzła. Najważniejszym faktem dotyczącym tej funkcji jest to, że powinna być wywołana po wszystkich czynnościach związanych z dodawaniem, usuwaniem lub przenoszeniem węzłów. Dane uporządkowane można później wielokrotnie uaktualniać za pomocą funkcji Update(), która nie będzie posiadać narzutu związanego ze skakaniem po danych (pozostaje tylko skakanie po węzłach, które same w sobie nie są duże).

Funkcja ta wygląda w tym przypadku tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void NSceneGraph::Update()
{
   NAssert(m_bDirty == false, "Call this function before running updates");
 
   NSize_t uParentNode = 0;
 
   while(uParentNode != NMAX_SIZE_T)
   {
      NSize_t uNode = GetChildIndex(uParentNode);
 
      while(uNode != NMAX_SIZE_T)
      {
         NSize_t uDataIndex = GetDataIndex(uNode);
         // Macierze globalne
         m_aWorldMatrices[uDataIndex] = NMMatrixMul(m_aLocalMatrices[GetDataIndex(uNode)], m_aWorldMatrices[GetDataIndex(uParentNode)]);
 
         uNode = GetSiblingIndex(uNode);
      }
 
      uParentNode = GetNextIndex(uParentNode);
   }
}

Cały algorytm przechodzenia po tym drzewie polega na aktualizacji każdego dziecka dla bieżącego rodzica. Dzięki informacji o następnym węźle przechodzenie po drzewie odbywa się poziom po poziomie.

Jak widać cały problem podejścia zorientowanego na dane udało się umieścić w jednej klasie, co więcej funkcja porządkująca dane nie wymaga częstego wywoływania, ponieważ najprawdopodobniej będzie to miało miejsce w przypadku ładowania/usuwania sceny. Narzut związany z korzystaniem z uchwytów i indeksacją również można zredukować przed dodanie funkcji zwracających wskaźnik na żądane dane, które po każdym wywołaniu ApplyChanges() należy zaktualizować.

NIne3 – stan aktualny

W tej notce prezentuję aktualny stan mojego frameworka NIne3, którego strukturę opisałem jakiś czas temu. Jest to swego rodzaju ‘proof of concept’, czyli że to jednak działa. Z głównych feature’ów chciałbym wymienić:

  • shadowmapy (nowość :))
  • oświetlenie per-pixel
  • pliki wykonywalne dla platfrom x64 i x86 z obsługą SSE i bez
  • stały krok update’u logiki (tego akurat można nie zauważyć)
  • obsługa heightmap

I to chyba tyle, a tutaj link do aplikacji (wymaga redistów DX Feb 2010).

Struktura katalogów w projekcie

Czasami istnieje potrzeba pisania jednego projektu w różnych środowiskach programistycznych, tym bardziej jeśli korzystamy z systemu kontroli wersji (np. SVN,  GIT) i chcemy skompilować projekt z założenia multiplatformowy na innym systemie operacyjnym. W takim przypadku fajnie by było, aby projekt miał strukturę katalogów niezależną od IDE, ale jak powinna ona wyglądać? Według mnie tak:

  • include
  • src
  • workdir
  • projects
  • libs

Katalog include powinien zawierać tylko pliki nagłówkowe, które będą dołączane w innych projektach. Tak naprawdę jest on tutaj tylko ze względu na projekt biblioteki (np. lib, dll, itp.), więc w przypadku kompilacji do pliku wykonywalnego, można ten katalog pominąć, wtedy wszystkie pliki nagłówkowe powinny znaleźć się w katalogu src. Katalog src jest przeznaczony tylko dla plików źródłowych bieżącego projektu, które mogą być uporządkowane w różnych podkatalogach. W workdir powinna się znaleźć cała struktura katalogów docelowego programu, czyli wszystkie pliki i katalogi, które będą dostarczane ze skompilowanym plikiem wykonywalnym. Katalog libs służy do przechowywania wszystkich dołączanych bibliotek używanych w aplikacji. Dla każdej z nich powinien znaleźć się odpowiedni katalog a w nim jeszcze dwa – include i lib. Ostatnim katalogiem jest projects, który jest przeznaczony do przechowywania plików projektów dla każdego IDE (oczywiście każdy w osobnym katalogu).

Taka struktura jest przejrzysta i niezależna od stosowanego IDE. Zresztą nie jest to mój pomysł, ponieważ jest on od dawna stosowany w środowiskach linuksowych, ale w nieco innej postaci. Kluczowe jest po prostu trzymanie plików źródłowych w jednym katalogu niezależnym od stosowanego środowiska.

NIne3 – założenia i struktura silnika

Witam ponownie. Tym razem napiszę nieco o założeniach i strukturze mojego silnika, którym ma docelowo być NIne3. Numerek ’3′ świadczy o tym, że jest to już trzecia iteracja, zatem jest to pewna zmiana w stosunku do wcześniejszych postów. Kod został przepisany na nowo, zmieniły się również założenia, ale część z nich oraz różne fragmenty kodu zostały wykorzystane ponownie. Muszę przyznać, że jestem zadowolony z aktualnego stanu kodu i myślę, że ta iteracja będzie jedną z tych dłuższych i dotrwa do czegoś większego :).

Założenia

Podstawowe założenia to:

  • brak wyjątków
  • brak STL (strumienie, kontenery, std::string)
  • brak funkcji wirtualnych (przynajmniej na niskim poziomie)
  • obiektowość
  • modułowość
  • całkowite ukrycie wykorzystywanego API graficznego
  • oddzielenie klas implementacyjnych od interfejsu programistycznego
  • minimalizacja alokacji pamięci
  • bezpieczny kod

Dlaczego takie założenia? Otóż, po pierwsze, wyjątki, mimo że jest to bardzo wygodny feature języka C++, są dość kosztowne, ponieważ wymagają dużo pracy włożonej w napisanie kodu, który zwolni wszystkie zasoby i sam przy tym nie wygeneruje błędów. Z drugiej strony różne kompilatory różnie implementują obsługę wyjątków, więc nie jest do końca pewne czy nie pojawi się dodatkowy narzut związany z obsługą powrotu z funkcji wewnątrz bloku try/catch.

Natomiast STL mimo, że jest biblioteką napisaną w najwydajniejszy możliwy sposób (a przynajmniej powinna być) i jednocześnie jest dość elastyczna, jest dość obszerna i na start dorzuca niecałe pół megabajta danych do pliku .exe, z czego z większości i tak się nie korzysta bezpośrednio. Ostatecznie nie spełnia ona również podstawowego założenia jaką jest brak obsługi wyjątków, którą można co prawda wyłączyć, ale nie jest to łatwe. Dlatego strumienie zostały u mnie zastąpione starą, dobrą funkcją printf, kontener std::vector własną implementacją uwzględniającą jeden z punktów o minimalnej ilości alokacji, a z std::string zrezygnowałem całkowicie, ponieważ nie jest ona konieczna, a poza tym dodatkowo nie spełniają kilku z wymienionych punktów.

Brak funkcji wirtualnych na niskim poziomie wziął się z oczywistego narzutu w postaci vtable, zatem wykorzystywany interfejs został napisany z użyciem PIMPL, dzięki czemu możliwa jest optymalizacja i inline’owanie funkcji (na poziomie konsolidacji). Nie wykluczam natomiast użycia ich w przyszłości na wyższym poziomie kodu, ale póki co staram się je omijać.

Obiektowość wiąże się przede wszystkim z enkapsulacją kodu, co pozwala oddzielić kod obsługi tekstur od kodu obsługi okna, efektów etc. w sposób wyraźny. Dzięki temu kod jest przejrzysty i łatwy w modyfikowaniu. Ten punkt powiązany jest bezpośrednio z punktem następnym czyli modułowością. Chciałbym, żeby kod był łatwo rozszerzalny i umożliwiał wymianę różnych klas oraz ich dodawanie, bez modyfikowania pozostałych. Pokażę to niżej na przykładzie hierarchii modułów.

W tej iteracji postanowiłem również ukryć całkowicie wykorzystywane API graficzne, którym póki co nadal jest Direct3D 9. API jest ukryte nie tylko na zewnątrz biblioteki, ale również wewnątrz dla modułów wyższego rzędu, które nie korzystają bezpośrednio z klas implementacyjnych, ale z interfejsu użytkownika, co zapewnia niezależność.

Docelowy użytkownik biblioteki nie widzi bezpośrednio klas implementacyjnych tylko klasy interfejsowe napisane przy użyciu PIMPL, które są uporządkowane w odpowiedniej hierarchii, którą pokażę nieco niżej. Daje to swobodę działania wewnątrz silnika i uporządkowanie na zewnątrz, jest również mniej błędogenne.

Ostatnie dwa punkty są niejako powiązane, ponieważ zakładam uruchamianie silnika na maszynach z ograniczoną ilością pamięci i możliwość wystąpienia błędu alokacji pamięci, więc obsługa takich błędów jest wymagana. Natomiast w celu minimalizacji tego typu błędów postanowiłem zmniejszyć ilość alokacji do minimum stosując kilka własnych klas do tego celu (bez menadżera pamięci póki co). Dodatkowo zakładam, że wszelkie możliwe błędy wystąpią w przypadku inicjalizacji i ładowania zasobów, więc każdy możliwy błąd zostaje przekazany do użytkownika biblioteki i zapisany w logu. Natomiast w czasie działania aplikacji błędy nie powinny wystąpić  albo będą występować bardzo rzadko, w tym przypadku ich sprawdzenie, o ile to możliwe, będzie występować w jednym ustalonym miejscu.

Hierarchia interfejsów

Jak już wspomniałem wcześniej klasy implementacyjne są oddzielone od użytkownika warstwą interfejsu, który ma za zadanie utworzyć odpowiednią hierarchię po to, aby klasy specjalizowane, zależne od ogólnych, nie mogły być pobrane i użyte przed zainicjalizowaniem tamtych. Dodatkowo daje to możliwość zmian wewnątrz silnika, bez zmiany samego interfejsu. Sama hierarchia wygląda następująco:

  • ICore:
    • IWindowManager:
      • INInput
    • ITimer
    • IRenderer:
      • IMeshManager
      • ITextureManager
      • IEffectManager:
        • IEffect
      • IModelLoader:
        • IModel:
          • IMaterial
      • IMeshLoaderX
      • IMeshLoaderHeightmap

Jak widać podział jest dość wyraźny. Wyszczególniłem całkowicie niezależne od siebie moduły: Timer, WindowManager i Renderer. Każdą z nich można inicjalizować osobno lub zrobić to za pomocą klasy Core, która zainicjalizuje wszystkie moduły i udostępni kilka podstawowych funkcji takich jak: Update(), Present(), ChangeDisplayMode(), które na nich operują.

Największym modułem jest Renderer, który również został wyraźnie podzielony na kilka podmodułów. Podstawowymi są tak naprawdę MeshManager, TextureManager oraz EffectManager (shadery) i to te klasy są odpowiedzialne za obsługę podstawowych zasobów niezbędnych do wyrenderowania czegokolwiek: siatek, tekstur oraz shaderów. Drugą grupą podmodułów są te odpowiedzialne za ładowanie wymienionych zasobów z plików w różnych formatach – aktualnie obsługuję tylko dwa – pliki .x oraz heightmapy. Ostatnią grupą są podmoduły zarządzające, których przedstawicielem jest ModelManager. Jego zadaniem jest zebranie zasobów w jednym miejscu i dodanie im odpowiedniej funkcjonalności tak, aby było mniej roboty przy renderowaniu.

Hierarchia modułów

Sercem silnika jest hierarchia modułów, która składa się z klas bibliotecznych, struktur oraz klas silnika:

  • Klasy biblioteczne:
    • NMath:
      • NMVector
      • NMMatrix
      • NMQuaternion
    • NAssert
    • NLogger
    • NDynamicTable
    • NDataVector
    • NStableDataVector
    • Inne (mniejsze klasy implementujące różną funkcjonalność)
  • Struktury
  • Engine:
    • Core
    • WindowManager:
      • Input
    • Timer
    • Renderer:
      • SubRenderer (Direct3D_9)
        • BuiltInLoader
          • MeshLoaderX
        • EffectManager
        • MeshManager
        • TextureManager
      • BuiltInLoaders
        • MeshLoaderHeightmap
      • ModelManager

W klasach bibliotecznych można znaleźć definicje typów podstawowych, bibliotekę matematyczną, kontenery oraz klasy pomocne w debugowaniu (logger i asercja). Biblioteka matematyczna jest napisana z myślą o wykorzystaniu funkcji intrinsic procesora (SSE) i jej wygląd bazuje w bardzo dużej mierze na XNAMath. Aktualnie obsługiwane są tylko wektory i macierze, ale nie wszystkie operacje zostały zaimplementowane, są tylko te aktualnie potrzebne. Między kontenerami można wyróżnić opakowaną w klasę zwykła tablicę dynamiczną (NDynamicTable), odpowiednik std::vector (NDataVector) oraz ten sam kontener, ale zachowujący wewnętrzne ułożenie danych (NStableDataVector - pomocne przy korzystaniu z uchwytów).

Hierarchia silnika różni się to trochę od hierarchii interfejsów, którą implementuje. Przede wszystkim wyróżnia się tutaj grupa SubRenderer. Jej celem jest zebranie wszystkich klas zależnych od API graficznego w jednym miejscu. Dzięki temu możliwa jest łatwa wymiana tego API przy zachowaniu funkcjonalności silnika. Wszystkie klasy, które znajdują się poza tą grupą korzystają albo z klasy pośredniej SubRenderer, albo z interfejsów użytkownika. Zmiana API będzie możliwa tylko statycznie, za pomocą odpowiedniej dyrektywy preprocesora. Klasy znajdujące się w tej grupie odpowiedzialne są za zarządzanie podstawowymi zasobami oraz samym urządzeniem i nie robią nic poza podstawowymi operacjami.

Wszystkie zasoby są udostępniane za pomocą uchwytów, dzięki którym można łatwo manipulować zasobami wewnątrz silnika bez obawy, że coś zostanie zgubione na zewnątrz. Niektóre z nich udostępniają dodatkowy interfejs (Effect, Model), który umożliwia bardziej szczegółową manipulację danym zasobem, ale klasy wewnętrzne Renderera manipulują wyłącznie uchwytami.

Oprócz tego istnieje cały zestaw pomocnych struktur, w tym właśnie szablon uchwytów, które bazują na odpowiednich akcesorach. Dzięki temu wszystko pozostaje hermetycznie zamknięte wewnątrz silnika.

Co dalej?

Aktualnie zakończyłem restrukturyzację hierarchii modułów do postaci takiej jaką przedstawiłem. Kolejnym moim celem jest dodanie SceneGraph, który będzie zarządzał drzewem modeli oraz przechowywał ich macierze świata i je aktualizował. Chcę przy tym wykorzystać poprawkę z tej prezentacji , czyli zawrzeć całe drzewo macierzy w jednej tablicy, która będzie cache-friendly.
Oprócz tego w związku z wydzieleniem SubRenderera chciałbym dodać drugi, oparty o OpenGL, ale to już będzie większy orzech do zgryzienia. Muszę przyznać, że sam jestem ciekaw wyniku :).

RAW Input

Tym razem będzie znów coś związanego z systemem Windows i programowaniem gier, czyli obsługa myszy za pomocą RAW Input. Jest to jeden z trzech popularnych sposobów obsługi tych urządzeń. Pozostałymi są komunikaty procedury okna oraz DirectInput, z tym że ten ostatni jest niezalecany i już nie jest wspierany przez Microsoft na rzecz właśnie RAW Input. Jednak dlaczego nie te dwa?

Komunikaty procedury okna nie są tak naprawdę takie złe, co więcej bardzo dobrze nadają się do małych gier casualowych, najlepiej tych odpalanych w oknie, ponieważ wartościami otrzymywanymi od systemu (w przypadku myszy) jest pozycja kursora, a pozycja jest poprawiana tak, aby znalazł się on w miejscu w którym użytkownik się tego spodziewa. Niestety dane otrzymywane w ten sposób nie są dokładne więc w bardziej wymagających grach mogą okazać się nieprzydatne.
DirectInput jest natomiast sposobem na uzyskanie dokładniejszych danych, jednak zasada jego działania opiera się na utworzeniu dodatkowego wątku, podpięcia się do wskazanego okna i odczytywaniu danych bezpośrednio ze sterownika, czyli surowych danych (RAW). Najprawdopodobniej jest to przyczyną braku wsparcia dla tej metody, dodatkowo niektóre antywirusy mogą ostrzegać że aplikacja którą napisaliśmy jest keyloggerem.

RAW Input umożliwia aplikacji uzyskanie surowych danych bezpośrednio ze sterownika. Zasada działania opiera się tutaj, tak samo jak w pierwszej metodzie, na komunikatach procedury okna. Różnica polega na tym, że chęć otrzymywania tych danych należy wcześniej w systemie zarejestrować. Jest to póki co najlepsza metoda uzyskiwania dokładnych danych z myszy, ponieważ dostarcza informacje na temat przesunięć, a nie pozycji kursora. Otrzymywanie danych z klawiatury również jest możliwe, ale w tym przypadku dokładność nie ma sensu.

Do rejestracji urządzeń, których dane chcielibyśmy otrzymywać tą metodą, służy funkcja:

<a href="http://msdn.microsoft.com/en-us/library/ms645600%28VS.85%29.aspx">BOOL WINAPI RegisterRawInputDevices(
  __in  PCRAWINPUTDEVICE pRawInputDevices,
  __in  UINT uiNumDevices,
  __in  UINT cbSize
);
</a>

Pierwszym parametrem tej funkcji jest tablica obiektów struktury RAWINPUTDEVICE, która opisuje rejestrowane urządzenie. Pozostałe parametry to ilość urządzeń i rozmiar struktury, czyli sizeof(RAWINPUTDEVICE).

Poniższy przykład pokazuje rejestrację myszy:

RAWINPUTDEVICE rawStructure;
rawStructure.usUsagePage = 1;                            // Typ urządzenia (w tym przypadku GENERIC)
rawStructure.hwndTarget = m_hWnd;                    // Uchwyt okna
rawStructure.usUsage = 2;                                   // Rodzaj urządzenia (2 - mysz)
rawStructure.dwFlags = RIDEV_NOLEGACY;        // Więcej info w opisie struktury
 
if(!RegisterRawInputDevices(&amp;rawStructure, 1, sizeof(RAWINPUTDEVICE)))
{
    // Obsługa błędu
}

Wartości usUsage i usUsagePage można znaleźć w tym dokumencie.

Jeśli rejestracja urządzenia się powiedzie, to aplikacja przestanie otrzymywać standardowe komunikaty związane z tym urządzeniem, a zacznie otrzymywać komunikaty WM_INPUT z danymi RAW tego urządzenia.
Dane można otrzymywać na dwa sposoby: pojedynczo, bezpośrednio w trakcie obsługi komunikatu lub zbuforowane. Poniżej podam przykład na standardową obsługę komunikatu WM_INPUT, więcej info na temat tej i drugiej metody można znaleźć na tej stronie.

LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
     switch(msg)
     {
     case WM_INPUT:
          {
               RAWINPUT riStruct;
               DWORD dwSize = sizeof(RAWINPUT);
 
               if(GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &amp;riStruct, &amp;dwSize, sizeof(RAWINPUTHEADER)) == (UINT)-1)
               {
                    // Obsługa błędu
               }
 
               if(riStruct.header.dwType == RIM_TYPEMOUSE)
               {
                    riStruct.data.mouse.lLastX;     // Przesuniecie X
                    riStruct.data.mouse.lLastY;     // Przesuniecie Y
 
                    riStruct.data.mouse.ulButtons;     // Przyciski myszy
 
                    riStruct.data.mouse.usButtonData;     // Rolka myszy
               }
          }
          break;
     }
}

Dziwić może to, że funkcja przyjmuje wskaźnik na zmienną do rozmiaru struktury RAWINPUT. Jest to spowodowane tym, że różne urządzenia mają różne struktury i podanie niewłaściwego rozmiaru skutkuje błędem oraz wpisaniem wymaganego rozmiaru pod podanym adresem.

Wyrejestrowanie urządzenia następuje po wywołaniu funkcji RegisterRawInputDevices podając jako w flagę w przekazywanej strukturze RIDEV_REMOVE.

Ogólnie rzecz biorąc RAW Input jest najlepszym sposobem na otrzymanie dokładnych danych z urządzeń wejściowych. Obsługa klawiatury jest trochę bardziej skomplikowana, a szczerze mówiąc nie widzę sensu korzystania z tego sposobu. Lepiej pozostać przy komunikatach. Osobiście uważam, że odpytywanie o stan inputu jest lepsze niż odpowiadanie na komunikaty, ponieważ aplikacja jest wtedy bardziej przewidywalna.

Pobieranie kodu HTML stron w C++ za pomocą WinSock2

Dzisiejsza notka będzie o tym jak pobrać kod HTML strony WWW w aplikacji C++ korzystając z WinSock2.

Pierwszą rzeczą, którą trzeba wiedzieć jest to w jaki sposób robi to przeglądarka internetowa. Zacznę od tego jak wygląda typowy adres strony WWW na przykładzie adresu do działu artykułów na stronie http://www.gamedev.pl/:

http://www.gamedev.pl/articles.php

Powyższy adres składa się z kilku części:

  • „http://” – protokół reprezentujący sposób transmisji danych, dzięki niemu przeglądarka wie w jaki sposób komunikować się z serwerem oraz na jaki port wysyłać żądania
  • „www.gamedev.pl” – domena na którą będzie wysłane zapytanie – podany adres jest tłumaczony na adres IP przez serwer DNS
  • „/articles.php” – adres żądanego plik lub żądanie dla serwera WWW, które aplikacja wykorzystuje do stworzenia nagłówka

Przeglądarka mając adres strony tworzy odpowiedni nagłówek, który zostaje wysłany do serwera WWW. Jak już wspomniałem adresem tego serwera jest domena zawarta w adresie WWW natomiast port jest określany na podstawie protokołu, czyli w przypadku „http://” jest to port 80. Nagłówek HTTP powinien zawierać następujące elementy:

  • rodzaj zapytania – w przypadku pobrania strony jest to „GET”
  • adres żądanego pliku lub żądanie – to co chcemy od serwera otrzymać, najczęściej jest to adres pliku na serwerze, czyli „/articles.php” w tym przykładzie
  • wersja protokołu – typowo HTTP/1.1
  • domena hosta

Dodatkowo, jeśli jest to konieczne, można wysłać informacje o tym, jakiej przeglądarki używamy (UserAgent), informacje o akceptowanych plikach, kodowaniu, ciasteczkach oraz czasie trwania połączenia, więcej można dowiedzieć się z tej strony wiki oraz samych nagłówków, które wysyła przeglądarka. Jako ciekawostkę mogę dodać, że istnieje fajna wtyczka dla Firefoxa, która pokazuje nagłówki wysyłane przez przeglądarkę – Live HTTP headers.

Oto nagłówek uzyskany z pomocą tej wtyczki podczas łączenia się do przykładowej strony:

1
2
3
4
5
6
7
8
9
GET /articles.php HTTP/1.1
Host: www.gamedev.pl
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

Jak widać jest on trochę długi (usunąłem informacje o ciasteczkach, nie są tutaj potrzebne). Aby pobrać kod strony wystarczą tak naprawdę dwie pierwsze linijki. Jednak ważną rzeczą jest, aby po każdej linijce występowała para znaków \r\n a koniec nagłówka reprezentowany był przez dwie pary tych znaków. W przypadku, gdy nagłówek nie będzie zawierał tak skonstruowanego nagłówka, serwer po prostu udrzuci zapytanie.

Teraz pora na kod C++ z WinSock2, oto on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Usuwa zbędne definicje z nagłówka
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
 
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
 
#define BUFFER_SIZE 2048
#pragma comment(lib, "ws2_32.lib")	// Niezbędna biblioteka
using namespace std;
 
int main()
{
	//************************************************
	// Inicjalizacja WinSock2
	//************************************************
	WSADATA wsaData;
	int error;
	string answer;
	ZeroMemory(&wsaData, sizeof(wsaData));
 
	if(FAILED(WSAStartup(MAKEWORD(2,2), &wsaData)))	// MAKEWORD(2,2) - Wersja WinSock
	{
		return 1;
	}
 
	char recvBuffer[BUFFER_SIZE];
	addrinfo hint;					// Struktura przechowująca dane o połączeniu
	addrinfo* wsResult;				// Wskaźnik na rezultat
	SOCKET pSocket;					// Właściwy pSocket
 
	ZeroMemory(recvBuffer, sizeof(recvBuffer));
	ZeroMemory(&hint, sizeof(hint));
	hint.ai_family = AF_UNSPEC;		// Rodzaj transmisji - nieokreślony
	hint.ai_socktype = SOCK_STREAM;	// Typ gniazda - strumień
	wsResult = NULL;
	pSocket = INVALID_SOCKET;
 
	//************************************************
	// Tworzenie zapytania	
	//************************************************
 
	// Wyciąganie informacji z adresu
	string httpAddress = "http://www.gamedev.pl/articles.php";
	string temp = httpAddress.substr(httpAddress.find("http://") + sizeof("http://") - 1);	// Tylko http:// więc można wyciąć
	string domain = temp.substr(0, temp.find_first_of('/'));								// Domena
	string addressTail = temp.substr(temp.find_first_of('/'));								// Żądanie pliku
 
	// Tworzenie nagłówka
	temp = string("GET ") + addressTail + " HTTP/1.1\r\n"
			   + "Host: " + domain + "\r\n"
			   //+ "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5\r\n"
			   //+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
			   //+ "Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"
			   //+ "Accept-Encoding: gzip,deflate\r\n"
			   //+ "Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7\r\n"
			   //+ "Keep-Alive: 300\r\n"
			   //+ "Connection: keep-alive\r\n"
			   + "\r\n\r\n";
 
	// Pobieranie IP serwera z serwera DNS
	if(FAILED(getaddrinfo(domain.c_str(), TEXT("80"), &hint, &wsResult)))
	{
		printf("Nie udalo sie pobrac IP\n");
		return 1;
	}
 
	// Wskaźnik na zwrócone adresy
	addrinfo* ptr = wsResult;
 
	// Tworzenie socketu
	if(FAILED(pSocket = socket(wsResult->ai_family, wsResult->ai_socktype, wsResult->ai_protocol))) 
	{
		freeaddrinfo(wsResult);
		printf("Nie udało się utworzyc socketu\n");
		return 1;
	}
 
	//************************************************
	// Wysyłanie zapytania	
	//************************************************
 
	// Łączenie się do serwera
	if(FAILED(connect(pSocket, ptr->ai_addr, (int)ptr->ai_addrlen))) 
	{
		printf("Nie udalo sie polaczyc do serwera\n");
		goto Error;
	}
 
	// Wysyłanie nagłówka
	if(FAILED(send(pSocket, temp.c_str(), temp.size(), 0)))
	{
		printf("Nie udalo sie wyslac naglowka\n");
		goto Error;
	}
 
	// Kończenie wysyłania
	if(FAILED(shutdown(pSocket, SD_SEND)))
	{
		printf("Nie udalo sie zamknac polaczenia\n");
		goto Error;
	}
 
	//************************************************
	// Odbieranie danych
	//************************************************
 
	// Odbieranie danych
	do
	{
		ZeroMemory(recvBuffer, sizeof(recvBuffer));
		error = recv(pSocket, recvBuffer, sizeof(recvBuffer), 0);
 
		if(error > 0)
		{
			answer += string(recvBuffer);
		}
	}
	while(error > 0);
 
	closesocket(pSocket);		// Zamknięcie socketu
	freeaddrinfo(wsResult);		// oraz zwolnienie struktury addrinfo
 
	printf("Kod HTML strony \"http://www.gamedev.pl/articles.php\":\n\n%s", answer.c_str());
	return 0;
 
Error:
	closesocket(pSocket);
	freeaddrinfo(wsResult);
	return 1;
}

Wynikiem tego kodu jest wyświetlony kod HTML przykładowej strony.