AutomaticHandle ciąg dalszy

Jak się okazuje nic nie jest na początku idealne. Dzięki uwagom Revo udało mi się usprawnić szablon automatycznych uchwytów. Revo poradził mi dodanie dodatkowego wskaźnika na licznik odwołań do danego zasobu. Dzięki temu możliwe było usunięcie jednej funkcji wirtualnej, a druga została sprowadzona do roli sprzątaczki, która zostaje wywołana tylko w momencie, gdy licznik osiąga wartość 0. Dodatkowo dodałem sprawdzenie czy uchwyt nie jest zerowy przed odwołaniem się do wskaźników.

Poniżej znajduję się kod poprawionej klasy szablonowej AutomaticHandle:

typedef unsigned int Dword;
 
template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(NLib::Dword handle, MHandleMgr* manager, NLib::Dword* resCounter) : m_handle(handle), m_manager(manager), m_resCounter(resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle), m_resCounter(src.m_resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Destruktor
	~AutomaticHandle()
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
	}
	// Operator przypisania
	const AutomaticHandle& operator =(const AutomaticHandle& src)
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
		m_handle = src.m_handle;
		if(m_handle) ++(*(m_resCounter = src.m_resCounter));
		return src;
	}
	// Operator równości
	bool operator ==(const AutomaticHandle& src)
	{ return m_handle == src.m_handle; }
	// Operator różności
	bool operator !=(const AutomaticHandle& src)
	{ return m_handle != src.m_handle; }
	// Operator Dword
	operator NLib::Dword() const
	{ return m_handle; }
	// Operator bool
	operator bool() const
	{ return !!m_handle; }
 
private:
	// Uchwyt
	NLib::Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
	// Wskaźnik do licznika zasobu
	NLib::Dword* m_resCounter;
};

Kolejną klasą jest AutoHandleMgr, czyli klasa implementująca obsługę automatycznych uchwytów. Aktualnie tylko trzy funkcje są dostępne klasie pochodnej, w tym jedna dla uchwytów. Klasa ta posiada jedną funkcję abstrakcyjną, w której po zdefiniowaniu w klasie pochodnej należy usuwać odpowiednie zasoby zgodnie z przekazanym indeksem.

Oto jej definicja:

typedef unsigned int Dword;
 
class AutoHandleMgr
{
	// Unia do wyciągania informacji z uchwytu
	enum
	{
		// Rozmiary pól
		MAX_BITS_INDEX = sizeof(NLib::Dword) * 4,
		MAX_BITS_MAGIC = sizeof(NLib::Dword) * 4,
		// Maksymalne wartości pól
		MAX_INDEX = (1 << MAX_BITS_INDEX) - 1,
		MAX_MAGIC = (1 << MAX_BITS_MAGIC) - 1
	};
 
public:
	// Funkcja do zwracania uchwytu
	void ReleaseResource(NLib::Dword handle)
	{
		NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		NAssert(CheckMagic(handle), "Wrong handle");
		DeleteHandleAt(GetIndex(handle));
		ReleaseResourceByIndex(GetIndex(handle));
	}
 
protected:
	// Funkcja tworzy nowy uchwyt i go zwraca
	// Index zawarty w uchwycie odpowiada indeksowi w tablicy z danymi
	// w odziedziczonej klasie
	NLib::Dword CreateNewHandle()
	{
		NLib::Dword handle;
		static NLib::Dword s_autoMagic = 0;
		if(++s_autoMagic > MAX_MAGIC) s_autoMagic = 1;	// 0 oznacza pusty uchwyt
		// Jeżeli nie ma wolnych miejsc to tworze nowy index
		if(m_freeSlots.empty())
		{
			handle = m_magicValues.size() << (MAX_BITS_INDEX - 1);
			m_magicValues.push_back(s_autoMagic);
		}
		else
		{
			handle = m_freeSlots.back() << (MAX_BITS_INDEX - 1);
			m_magicValues[m_freeSlots.back()] = s_autoMagic;
			m_freeSlots.pop_back();
		}
		handle |= s_autoMagic;
		return handle;
	}
	// Funkcja zwraca index
	NLib::Dword GetIndex(NLib::Dword handle)
	{ return (handle >> (MAX_BITS_INDEX - 1)) & MAX_INDEX; }
 
private:
	// Funkcja kasuje wskazany uchwyt
	void DeleteHandleAt(NLib::Dword index)
	{	m_freeSlots.push_back(index);
		m_magicValues[index] = 0; }
	// Funkcja zwraca część magiczną
	NLib::Dword GetMagic(NLib::Dword handle)
	{ return handle & MAX_MAGIC; }
	// Funkcja sprawdzająca wartośc magic
	bool CheckMagic(NLib::Dword handle)
	{	NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		return GetMagic(handle) == m_magicValues[GetIndex(handle)]; }
 
private:
	// Funkcja dekrementująca licznik w zasobach
	virtual void ReleaseResourceByIndex(NLib::Dword index) = 0;
 
private:
	// Vector zawierający magiczne wartości
	std::vector m_magicValues;
	// Vector zawierający indeksy z wolnymi miejscami
	std::vector m_freeSlots;
};

W przypadku tego menadżera uchwyty muszą być usunięte przed nim, inaczej pojawią się błędy z dostępem do pamięci.

Refactor silnika + automatyczne uchwyty

Właśnie mija tydzień od zakończenia dwutygodniowego pobytu w domu w czasie końcówki sesji i przerwy egzaminacyjnej. W tym czasie rozpocząłem przepisywania mojego “silnika” od nowa, tym razem w oparciu o interfejsy. Są one bardzo poręczne, gdy chce się ukryć implementację klas wewnątrz statycznych, bądź dynamicznych bibliotek. Głównym założeniem przy projektowaniu drugiej wersji NIne było właśnie ukrycie implementacji wewnątrz statycznej biblioteki .lib. Dzięki takiemu postępowaniu biblioteka wygląda ładnie i pozwala ładnie rozszerzać własną funkcjonalność.

Drugim zagadnieniem poruszanym w tej notce są uchwyty, które same zarządzają licznikiem danego zasobu.
Z początku implementację uchwytów opierałem na artykule znajdującym się w książce “Game Programming Gems”, jednak problemem okazało się zmuszenie tychże uchwytów do sprzątania po sobie zasobów. Na szczęście rozwiązanie okazało się proste, ponieważ wystarczyło umieścić w klasie uchwytu wskaźnik do menadżera zasobów.

Poniżej znajduje się moja implementacja takiego uchwytu:

template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(Dword handle, MHandleMgr* manager) : m_handle(handle), m_manager(manager)
	{}
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle)
	{ m_manager->Copy(src.m_handle); }
	// Destruktor
	~AutomaticHandle()
	{ m_manager->Release(m_handle); }
	// Operator przypisania
	const AutomaticHandle& operator =(const ManagedHandle& src)
	{ m_manager->Release(m_handle);
	  m_handle = src.m_handle;
	  m_manager->Copy(src.m_handle);
	  return src; }
	// Operator Dword
	operator Dword() const
	{ return m_handle; }
	/*
	...
	*/
 
private:
	// Uchwyt
	Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
};

Jak widać klasa zawiera dodatkową zmienną – wskaźnik na klasę menadżera ustawiany przy konstrukcji. Klasa menadżera musi zawierać używane metody, które wewnątrz zarządzają licznikiem odwołań na dany zasób, a w momencie jego spadku do 0, likwidować dany zasób.

Fenomen SSE

Na warsztacie pojawił się ostatnio bardzo ciekawy temat dotyczący wykorzystania instrukcji procesora przetwarzających dane potokowo. Jak wiadomo każdemu programiście, każdy procesor wspiera różny zestaw funkcji (ponieważ są one dołączane do poprzednich). O to lista tych rozszerzeń:

  • MMX (operacje na liczbach całkowitych)
  • SSE (operacje zmiennoprzecinkowe pojedynczej precyzji)
  • SSE2 (operacje zmiennoprzecinkowe podwójnej precyzji)
  • SSE3 (konwersje i dodawanie w poziomie)
  • SSSE3 (dodatkowe działania na liczbach całkowitych)
  • SSE4 (dodatkowe instrukcje wektorowe, w tym upragniony iloczyn skalarny dwóch wektorów)

O ile rozszerzenie MMX korzysta z rejestrów koprocesora (co czyni go niewydajnym, ponieważ przełączanie między MMX a koprocesorem trwa trochę czasu), o tyle pozostałe rozszerzenia wprowadzają dodatkowe rejestry xmm (8 dla procesorów 32-bitowych oraz 16 dla procesorów 64-bitowych) po 128 bitów każdy. Rejestry te są traktowane tak jak jest to wymagane do danej funkcji, zatem dla liczb całkowitych jest całe 128-bitowe słowo lub można dzielić na pół, aż do uzyskania 16 osobnych bajtów. W przypadku liczb zmiennoprzecinkowych są to 4 liczby typu float lub 2 liczby typu double.

Dostępne funkcje pozwalają wykonywać obliczenia zarówno skalarnie jak i wektorowo. Oczywiście najlepiej jest gdy przeprowadzane operacje są głównie wektorowe, ponieważ pozwala to zaoszczędzić mnóstwo cennych cykli procesora. Największy problem to oczywiście pisanie operacji przy użyciu tych funkcji, ponieważ wymagana jest podstawowa znajomość Assemblera, chociaż dzięki tzw. funkcjom intrinsics wcale nie trzeba robić w kodzie wstawek asemblerowych, gdyż funkcje robią to zamiast programisty.

Przyznam szczerze, mnie również  bardzo interesują te funkcje, dlatego postanowiłem z ich pomocą napisać własną implementacje wektorów i macierzy (również dlatego, że te w D3DX, nie mają tego wsparcia). Jest to dość żmudna robota, ponieważ trzeba jakoś zagwarantować to, żeby moje wektory ruszyły na starszych komputerach (a nuż trzeba będzie), ale mimo wszystko uważam, że warta zachodu. Mogę już powiedzieć, że funkcja obliczająca długość wektora, dzięki instrukcją z rozszerzenia SSE, jest szybsza od wersji z D3DX.

NLib

Postanowiłem udostępnić moją “bibliotekę”, którą posługuję się od jakiegoś czasu. Nie jest ona jakoś super wypasiona, ale zawiera wszystkie elementy, które są mi aktualnie potrzebne, czyli:

  • Moduł podstawowy – konwersje, typedefy, jakieś funkcje liczące w czasie kompilacji
  • Profile
  • Logger
  • FStream – strumień dla plików w oparciu o funkcje WinApi
  • StructReader – funkcja do obsługi bardzo prostych plików konfiguracyjnych
  • Timer – obsługa zegara, licznik FPS w oparciu o QPC lub GetTickCount()
  • Window – prosta klasa do obsługi okna

Update’y pojawiają się wraz z pomysłami i zapotrzebowaniem na nowe wynalazki, więc nie wiem kiedy nowa wersja.

Pobierz

Assert

Ostatnio dowiedziałem się o bardzo fajnej funkcji jaką udostępnia biblioteka standardowa, czyli o funkcji Assert. Funkcja ta ma tę zaletę, że warunek, który jej przekazujemy jest sprawdzany tylko, gdy jest umieścimy definicje NDEBUG. Wtedy, gdy warunek zwróci fałsz, zostaje wyświetlone okienko, gdzie i kiedy miało zdarzenie. W przeciwnym wypadku, gdy nie zdefiniujemy NDEBUG, sprawdzanie warunku po prostu znika, dzięki czemu osiągamy większą wydajność.

Oczywiście standardowa makrodefinicja nie jest idealna, ponieważ wyświetla różne rodzaje okienka w zależności od tego, czy używamy main czy WinMain. Ale napisanie swojego odpowiednika tej funkcji nie nastręcza problemów, wystarczy się przyjrzeć makrodefinicji zawartej w pliku assert.h. Polecam również artykuł w książce Perełki Programowania Gier część pierwsza, rozdział 1.12. Są tam świetne porady na ten temat.

Zmiany na lepsze?

Postanowiłem, że należałoby wreszcie zrobić porządek z biblioteką NLib oraz z silnikiem NIne. Niestety jak się okazało przy projektowaniu gry (myślę nad klonem Tetrisa w 3D), aktualny stan silnika nie pozwala na jego rozbudowę i pisanie w oparciu o niego jakiejkolwiek gry. Dlatego stwierdziłem, że lepszym pomysłem byłoby zaprojektować grę, a następnie do niej odpowiedni interfejs, mając na uwadze ponowne użycie kodu do tworzenia innych gier.

Na początek zająłem się biblioteką NLib. Zamierzam kompletnie uniezależnić się od wyjątków, ponieważ uważam, że są one dobre do zwykłych programów, w których występuje wiele sytuacji wyjątkowych. W grze są niepotrzebne, gdyż jeżeli nastąpi sytuacja wyjątkowa, to grę i tak należy zamknąć, a poza tym podobno obsługa wyjątków spowalnia kod. Zamiast tego błędami będzie sie zajmował Logger, do którego będą trafiać wszystkie komunikaty (nawet te najmniej ważne), dzięki temu będzie wiadomo co i kiedy poszło nie tak. Dlatego będzie wymagany do używania jakiegokolwiek modułu z biblioteki.
Ważną zmianą jest też wprowadzenie obsługi Unicode. Niestety standardowe strumienie zapisujące do plików, nie zapisują ich w tym kodowaniu, dlatego zacząłem pisać własny strumień oparty na funkcjach WinApi. Z początku będzie obsługiwał tylko Little Endian, ale myślę nad dodaniem obsługi plików zapisanych jako Big Endian i ANSI (w trybie Unicode projektu). Z tego strumienia będzie korzystał również Logger. Oczywiście Unicode będzie obsługiwany tylko wtedy, gdy w ustawieniach projektu kodowanie będzie w tym trybie. W trybie ANSI, strumień będzie zapisywał tylko w ANSI.

Uważam, że należy iść z duchem czasu, a nie pozostawać w epoce kamienia łupanego :D.

StructReader

Wpadłem na pomysł prostego serializer’a, który by potrafił czytać dane z pliku podzielonego na sekcje i zapisywać je do prostej struktury – agregata. Miał być to szablon funkcji, który pobierałby jako jeden z argumentów wskaźnik do tej struktury, następnie wyliczał miejsce zmiennej w strukturze, na podstawie tablicy z typami (enum) i w końcu zapisywał przeczytane dane odpowiednio, sformatowane przez bibliotekę standardową. Niestety jak się okazało (czego do tej pory nie wiedziałem) miedzy składnikami klasy, struktury są przerwy, więc nie można skorzystać z wskaźnika, którego chciałem przesuwać o rozmiar przeczytanego typu, gdyż spowoduje to błędy zapisu.
Na szczęście rozwiązanie tego problemu okazało się bardzo proste i jest o wiele bardziej uniwersalne, otóż zamiast wskaźnika na obiekt struktury, przekazuję funkcji (ponieważ szablon jest już niepotrzebny) tablicę wskaźników do zmiennych i jej rozmiar. Dzięki niej wiem gdzie w pamięci znajduje się dana zmienna. Rozwiązanie to ma też inna zaletę, ponieważ do funkcji mogę przekazać nie tylko obiekty struktury, ale również dowolne obiekty nienależące do struktury.

Oto przykładowy plik cfg:

1
2
3
4
5
6
7
8
Sekcja1{
   // Komentarz
}
// Komentarz 2
Sekcja2
{
   zmienna1 = 3.14;     // Komentarz np. opisuje jaki to typ, tu float
}

Jak widać schemat jest prosty:

  1. Sekcje oznaczamy nazwą i klamerkami
  2. Komentarze tylko po //, są ważne do końca linii
  3. Nazwy zmiennych nie są brane pod uwagę, liczy się znak ‘=’ i ‘;’, pierwszy rozpoczyna czytanie wartości, drugi kończy.

PS. Schemat pliku zmyślony z Regedit’a :).

W kodzie wygląda to tak:

1
2
3
4
5
6
7
8
9
10
11
// Tworzymy tablicę z typami
int types[] = { TYPE_FLOAT };        // Przykładowo float
 
// Zmienna
float temp;
 
// Tablica ze wskaźnikami
void* pointers = { &amp;temp };
 
// Odczyt z pliku
FillUpStruct("nazwa_pliku_jako_string", types, sizeof(types),  "Sekcja2", pointers, sizeof(pointers));

Według mnie wygląda to ładnie, i jest proste w użyciu, szczególnie przydatne przy zapisie ustawień np. urządzenia DX.