wtorek, 1 grudnia 2009

Tekstury NPOT w OpenGL

Ostatnio przyszło mi skorzystać z tekstur nie mających wymiarów będących potęgami liczby 2 (NonPowerOfTwo). Dawniej radziłem sobie z takimi teksturami rozciągając mniejszą teksturę na większą mającą rozmiar będący potęgą 2 lub wklejając ją na fragment takiej tekstury. Dostępne są jeszcze dwa rozszerzenia wspomagające pracę z teksturami NPOT: texture_non_power_of_two i texture_rectangle. Oba są w standardzie OpenGL, texture_rectangle jest troche starsze. Przyjrzyjmy się im bliżej.
cechy texture_rectangle:
  • koordynaty tekstur musimy podawać nieznormalizowane, tzn.: [0..szerokosc_tekstury],[0,wysokosc_tekstury]
  • nie można używać mipmapingu, filtrowanie 'zmienia sie' na liniowe (GL_LINEAR)
  • taka tekstura nie może mieć ramki
  • zawijanie tekstur musi być typu GL_CLAMP_*
  • zamiast np. GL_TEXTURE_2D używamy GL_TEXTURE_RECTANGLE, w shaderach GLSL zamiast sampler2d i texture2d: sampler2dRect, texture2DRect
cechy texture_non_power_of_two:
  • koordynaty podajemy 'normalnie' czyli [0..1],[0..1]
  • można używać dowolnego filtrowania
  • można używać mipmapingu
  • tekstura może mieć ramkę
  • dowolny format zawijania tekstur
Jak widać używanie texture_rectangle jest problematyczne, natomiast texture_non_power_of_two nie zmusza do wprowadzania zmian w programie: po prostu tworzymy teksturę podając 'dowolny' wymiar i używamy jak zwykłą teksturę. Wybieram ARB_texture_non_power_of_two.
W moim przypadku nie obyło się bez niespodzianek :) Oczywiście mój Radeon 9600 obsługuje oba rozszerzenia, ale nie do końca... Czasem działa, czasem nie - pełna losowość. A jak renderuję do tekstury NPOT to w ogóle nie działa. Szybka zmiana parametrów tekstury i wniosek: Po wyłączeniu filtrowania, ustawieniu zawijania na GL_CLAMP_TO_EDGE i nie generowaniu mipmap wszystko działa. Działa tak jak texture_rectangle, a obsługuje sie jak texture_non_power_of_two, achh te 'ogl-owskie' sterowniki ATI...

sobota, 28 listopada 2009

GUI

Niedawno skończyłem pisać system GUI do swojego silnika. Zawiera podstawowe kontrolki, obsługę skórek, rożych typów czcionek, prosty tryb 3d i takie tam :P Nie chcę sie rozpisywać, chyba wszystko widać na screenach i na filmikach. Teraz zabieram sie za ciekawsze rzeczy niż pisanie interfejsów :)






niedziela, 18 października 2009

Jak (nie) optymalizować kodu - Epizod 1

Pierwszą próbą optymalizacji bylo użycie FPU (czyli jednostka zmiennoprzecinkowa CPU) do zbudowania funkcji zastepujacych odpowiedniki z cmath. Pierwsze testy w trybie debug z domyslnymi ustawieniami wygladaly bardzo pozytywnie. Liczyłem w pętli kilka operacji, uzywajac odpowiednio funkcji z biblioteki standardowej cmath i z moich funkcji z uzyciem FPU. Czasy moich funkcji byly około 3 razy krótsze. Po włączeniu optymalizacji w kompilatorze - /fp:fast i /Oi (ten przełacznik włącza wstawianie instrinsic-ów w miejsce niektórych funkcji, w tym wiekszości tych z cmath) czasy moich funkcji byly juz tylko 2 razy krotsze. Po przełączeniu do trybu release stało sie to czego sie spodziewałem(?), cmath był szybszy... o wiele. Opcje /fp:precise i /Ox dały od kilku do kilkusetkrotne przyspieszenie. Nie pomogło też wstawienie do funkcji __forceinline. Przełączyłem więc się na podgląd instrukcji Asemblera. Hmm... kod dla funkcji z cmath nie byl wogóle generowany (nadal mowa o trybie Release). Stąd takie kosmiczne czasy ;) Rozwiązaniem tego 'problemu' jest wyłączenie optymalizacji (C++/Optimalization/Optimalization=Disabled (/Od)) lub zmiana sposobu mierzenia obliczeń. Dla przykładu w takim kodzie NIE zostanie wywolana funkcja sqrt:
float x=std::sqrt(xxx);
x=sth;

kompilator zapewne ufa że brak wywołania funkcji sqrt nie spowoduje jakiegoś błedu w dalszych obliczeniach, bo za chwile w miejsce zmiennej x zostanie zapisana nowa wartość. Więc na przykład taka metoda mierzenia wydajności jest chyba bez sensu:
TIMER_START;
for(int i=0;i<99999;++i){
xx3=std::sqrt(rrr);
rrr+=0.56f;
}
TIMER_END;

Zmiana ustawień kompilatora moze spowodować że wyniki będa troche niesprawiedliwe. Pozostaje zmiana metody mierzenia. Ustawiem spowrotem opcje (C++/Optimalization/Optimalization=Full Optimization (/Ox)). Trzeba zmusic kompilator do uruchomienia funkcji z cmath. Kod:
float sth=...;
TIMER_START;
for(int i=0;i<99999;++i){
xx3=std::sqrt(rrr);
sth+=xx3; // np. tak
rrr+=0.56f;
}
TIMER_END;

Dodałem zmienna sth ktora będzie zainteresowana wynikiem funkcji std::sqrt. Ale to za mało nadal funkcja sqrt nie bedzie wywołana, ba - wogóle cała pętla będzie 'wyrzucona'. Działanie programu nie zmieni sie czy będzie ta pętla czy nie. Wystarczy wypisac dalej printf("%f",sth) i juz działa... Swoją drogą spryciarz z tego kompilatora :) Ostateczne pomiary sa pozytywne:
fpu_math time= 0.004849s
cmath time= 0.005963s
fpu_math time= 0.004900s
cmath time= 0.010668s
fpu_math time= 0.001266s
cmath time= 0.002462s
...

Ostatecznie można powiedzieć że przyspieszenie jest, choć nie wiem czy to wszystko warte zachodu (dla satysfakcji można duzo zrobic) :P
Jeszcze dodam kilka przykładów funkcji których użyłem w teście jak kogoś to interesuje (w komentarzach bardziej złożonych funkcji opisałem zawartości stosu):
float Cosf(float x)
{
__asm {
fld x
fcos
}
}
float Sqrtf(float x)
{
__asm {
fld x
fsqrt
}
}
// oszczedze miejsce (wiekszosc wyglada jak powyzsze) :) i podam dalej tylko te trudniejsze do napisania:
float Powf(float x,float y)
{
__asm {
fld y
fld x
fyl2x ;log2(x)*y

fst st(1) ;log,log
frndint ;int(log),log
fxch st(1)
fsub st(0),st(1) ;float(log),int(log)

f2xm1 ;2^float(log)-1,int(log)
fld1
faddp st(1),st(0) ;2^float(log),int(log)

fscale ;2^int(log)*2^float(log)
}
}
float Expf(float x)
{
// exp(x) == e^x
__asm {
;y==e
fld x
fld GE_E
fyl2x ;log2(x)*y

fst st(1) ;log,log
frndint ;int(log),log
fxch st(1)
fsub st(0),st(1) ;float(log),int(log)

f2xm1 ;2^float(log)-1,int(log)
fld1
faddp st(1),st(0) ;2^float(log),int(log)

fscale ;2^int(log)*2^float(log)
}
}
float Logf(float x)
{
// ln(x) (naturalny!)
__asm {
fld1
fld x
fyl2x ;log2(x)

fldl2e ;log2(e)

fdivp st(1),st(0)
}
}
float Log10f(float x)
{
__asm {
fld1
fld x
fyl2x ;log2(x)

fldl2t ;log2(10)

fdivp st(1),st(0)
}
}

Dodatkowo funkcje których nie ma w standardowej bibliotece matematycznej, a mogą sie przydać jak ktoś sie interesuje tematem :) :
void Sincosf(float x,float *oSin,float *oCos)
{
// sinus i cosinus od x
__asm {
mov eax,oSin
mov edx,oCos

fld x
fsincos

fstp dword ptr[edx]
fstp dword ptr[eax]
}
}
float Log2f(float x)
{
// log2(x)
__asm {
fld1
fld x

fyl2x ; sciagnie sam st(1)
}
}
float Logxf(float x,float y)
{
/*
oblicza logarytm dowolnej podstawy: logx(y)
sposob liczenia:
logx(y)=log2(y)/log2(x)
*/
__asm {
fld1
fld y
fyl2x ;log2(y) zwija 1 ze stosu

fld1
fld x
fyl2x ;log2(x) zwija 1 ze stosu

fdivp st(1),st(0)
}
}

Jak tytuł posta wskazuje to jest 1 część z serii walk o szybszy kod. W miare czasu mogą pojawić sie 2 kolejne części traktujące odpowiednio o optymalizacjach z wykorzystaniem MMX i SSE...

poniedziałek, 21 września 2009

Tip C++

Niekiedy w kodzie, zwlaszcza przy jakims szybkim tescie zachodzi potrzeba wykonania jakiegos fragmentu kodu tylko raz, a ktory jest np. w jakiejs funkcji ktora sie wykonuje czesto. wtedy mozna zrobic cos takiego:
static bool jakas_flaga=true;
if(jakas_flaga){
    zrob_cos_tylko_raz();
    jakas_flaga=false;
}

Tak robilem zazwyczaj, poki nie zaczalem szukac 'lepszego' rozwiazania. Takiego ktore wyglada lepiej i jest 'szybsze' w uzyciu. Z pomoca przyszedl preprocesor:
// definiuje:
#define ONCE static int LOL=1;if(LOL>-1)if(LOL--)
// uzywam pozniej tak:
ONCE{
    zrob_cos_tylko_raz();
}

Wyglada duzo lepiej... Dzialanie jest proste, ale efekt zadowalajacy :)

poniedziałek, 24 sierpnia 2009

dynamiczne tablice wielowymiarowe 'w jednym kawalku pamieci'

Jak wiadomo tworzenie wielu obiektow na stercie (new) moze powodowac znaczna fragmentacje pamieci, wiec nalezy tego unikac. dla bardzo duzej ilosci obiektow, potrzebujacej bardzo duzej ilosci operacji zwalniania i alokowania mozna stosowac recznie 'placement new' lub bardziej zaawansowane 'upraszczacze' jak np. 'FreeList'. a co ze zwykla tablica? standardowa funkcja alokujaca tablice 2-wymiarowa moze wygladac tak:

template<typename T>T **alloc2d(int width,int height){
    T **mem=new T*[width];
    for(int x=0;x<width;++x)
        mem[x]=new T[height];
    return mem;
}

jak widac tablice mamy w pamieci 'w kawalkach' o rozmiarze odpowiadajacym zmiennej height. jak to zmienic? trzeba zastosowac placement new. nowa wersja wyglada tak (typ int8 odpowiada 1 bajtowi):

template<typename T>T **alloc2d(int iX,int iY){
    // alokuje wszystko w jednym bloku (dane i wskazniki na dane)
    int8 *raw=new int8[iX*iY*sizeof(T)+iX*sizeof(T*)];
    // przydziela pamiec z bloku dla wskaznikow
    // wskazniki sa przed danymi-stad offset o rozmiarze wskaznikow ponizej
    T **ret=new(raw) T*[iX];
    // przydziela pamiec z bloku dla danych (znajduja sie za wskaznikami)
    T *data=new(&raw[iX*sizeof(T*)]) T[iX*iY];
    // przypisuje do wskaznikow adresy danych
    for(int i=0;i<iX;++i,data+=iY)
        ret[i]=data;
    return ret;
}

co nam to daje (oprocz umiejscowienia w jednym kawalku pamieci)?
  • wskazniki do danych sa 'blisko' samych danych (co podobno przyspiesza dostep do danych tablicy)
  • dane mozemy pobrac 'w calosci' - maja one adres poczatku, co w 'zwyklej' wersji nie jest zapewnione. mozemy np. zaalokowac taka tablice dla tekstury (dwuwymiarowa), zaladowac ja bezposrednio z pliku, pozmieniac jakas niewinna grupke pikseli i przekazac jakiejs funkcji ktora ja przyjmie bez dodatkowych operacji
  • jest lepsza od wersji w ktorej alokujemy tylko dane, a dostajemy sie do poszczegolnej komorki przeliczajac indeksy. czemu lepsza? bo z moich pomiarow wynika ze predkosc dzialania przy dostepie do komorek w losowej kolejnosci jest drastycznie wieksza dla tablicy ze wskaznikami. dla przypadku gdy czytamy tablice 'po kolei' szybkosc dostepu jest taka sama jak sie dostac bezposrednio do danych takiej tablicy. mozna sie posluzyc taka prosta funkcja:

template<typename T>T *Get2d(T **iA,int iX){
    // mijamy wskazniki
    return (T*)(((int8*)iA)+iX*sizeof(T*));
}

na koniec trzeba usunac taka tablice:

template<typename T>void Free2d(T **iA){
    int8 *ptr=(int8*)iA;
    delete []ptr;
    iA=NULL;
}

jak mozna sie domyslic mozna sobie zrobic tez wersje 3,4,... wymiarowa. ps. zaznaczam zeby nie uzywac wszystkiego 'na sile' - wszystko zalezy od problemu, a idealnych rozwiazan nie ma(to juz mowie ogolnie)!

czwartek, 20 sierpnia 2009

operator [] (indeksu)

ostanio napotkalem taki problem: mam obiekt ktory cos przechowuje i to cos mozna modyfikowac operatorem indeksu. problem w tym ze nie moge przekazac w funkcji operatora referencji tego obiektu (konkretnie u mnie to nie byl obiekt). nie moglem tego zrobic w normalny sposob:
to_chce_zmodyfikowac &operator[](int jakis_id){
return dane[jakis_id];
}

rozwiazanie znalazlem w bibliotece standardowej c++, dokladniej w klasie bitset. dodam ze bardzo mi sie spodobalo to rozwiazanie... klasa ta (bitset) posiada 'w sobie' jakis obszar pamieci (typ parametryzowany przez szablon) zawierajacy zmienna w ktorej mozna modyfikowac poszczegolne bity. jesli np. tym typem bedzie int to jak operatorem indeksu podac referencje do ktoregos z bitow tego inta? zastosowano tam ciekawy hack: stworzono klase ktora beda zwracac jako wynik funkcji operatora indeksu, a ktora moze jako oddzielny obiekt modyfikowac swojego wlasciciela. w uproszczeniu cos takiego (nazwy i funkcje nie odpowiadaja oryginalnym odpowiednikom bo nie pamietam oryginalnych nazw funkcji i klas w tym momecie, chodzi tylko o ogolny zarys):
// bit_type - typ parametryzowany
class bitset{
class bitref{
friend class bitset; // chce zeby bitset mnie modyfikowal...
bitset *p; // ktory bitset modyfikuje
int bit_index; // ktory bit moge zmodyfikowac
public:
bitref &operator=(const bit_type nowa_wartosc){
// ta funkcja przyjmuje nowa wartosc dla 'mojego'
// bitu (wskazywanego przez p i bit_index)
// ktora zamierzam wpisac tam gdzie trzeba...
p->set_bit(bit_index,nowa_wartosc);
return *this;
}
operator bit_type(){
// jesli chce tylko pobrac wartosc indeksu to
// pobieram i oddaje - wszystko dziala...
return p->get_bit(bit_index);
}
};
bitref operator[](int bit_index){
// teraz ten nieszczesny operator indeksu
// tworzymy sobie obiekt bitref i ustawiamy mu wlasciciela...
bitref br;
// chcemy modyfikowac siebie - wiec ustawiamy...
br.p=&this;
// uzytkownik klasy chce modyfikowac konkretny bit - ok...
br.bit_index=bit_index;
// zwracamy uzytkownikowi 'bit do modyfikacji':
return br;
}
};

i teraz mozna tak modyfikowac bity:
jakis_bitset[3]=2; // modyfikuje 3 bit
int jakas_flaga=jakis_bitset[31]; // pobiera 31 bit

i o to chodzilo...

poniedziałek, 16 marca 2009

opengl 3.1 - impresje

przeczytalem dokumentacje opengl 3.1. naczytalem sie przed tym wiele newsow - negatywnych opiniach o tym wlasnie wydaniu opengl, wiec tym bardziej wydalo mi sie to ciekawe. co do wersji 3.0 na poczatku troche przerazila mnie lista funkcji do usuniecia (ktora na 'pierwszy rzut oka' wygladala mi na liste dostepnych funkcji :>. podam ciekawsze) np.:
  • listy wyswietlania - uzywam do wyswietlania czcionek, wiec troche tego szkoda
  • szerokosc linii (glLineWidth) - nie uzywam tego, ale co ciekawe odmienilo sie'ekipie' i przywrocili te funkcje juz w aktualnej wersji (3.1). poszlo spac tylko glLineStipple
  • glBegin/glEnd - jesli nie ma list wyswietlania to tym lepiej ze tego nie ma. w sumie nawet mi to odpowiada... no i oczywiscie 'polecialy' razem z tym inne smieci: glTexCoord, glIndex, glVertex,...
  • cala masa funkcji do przetwarzania wierzcholkow: gl*Pointer, glClientActiveTexture, funkcje do przetwarzania macierzy: glFrustum, glOrtho, glRotate, glTranslate, glScale, glLoadMatrix, gl[Push/Pop]Matrix, glLoadIdentity... caly machanizm operowania na macierzach... wyglada to dosc drastycznie :)
  • punkty tylko jako POINT_SPRITE - do narysowania pukntu jednokolorowego trzeba shader pisac... swoja droga glColor tez spac poszedl
  • wszystkie czworokaty - bedzie mozna wyswietlac z figur 'tylko' trojkaty. to mi sie bardzo podoba :D
  • glPolygonMode bedzie dzialac tak samo dla tylnych i przednich faces
  • alpha test - hmm, bardzo zdziwila mnie obecnosc tego na liscie :/
  • bufor akumulacji
w wersji 3.0 to wszystko jeszcze bylo dostepne, ale oznaczone jako 'deprecated', w 3.1 zostalo na dobre usuniete...

w takim razie jak rysowac cos na ekranie nie mogac ustawic odpowiednich macierzy (glMatrixMode nie ma) a co dopiero ich zaladowac (glLoadMatrix tez)? jak przekazac dane do rysowania: wierzcholki, koordynaty, normalne, ...? teraz nie ma chyba wyboru i trzeba uzyc samych vertex arrays (tylko glVertexAttrib*) lub wpakowanych do buffer object. co z macierzami modelowania, projekcji,..? nie ma informacji wprost jak to ma wygladac, ale trzeba bedzie recznie przekazywac swoje macierze do shaderow. malo tego - to wcale nie bedzie konieczne: mozemy zasymulowac dotychczasowy sposob i zrobic swoje macierze projekcji, modelowania, itd. w taki sam sposob jak to bylo w wersji gl z macierzami (<3.0), lub wymyslic swoj sposob transformacji, np. mozna bedzie doskonalej zasymulowac tryb 2d uzywajac macierzy 2x2, mozna zamienic macierze na cos kompletnie innego. czyli z tej strony nie mamy chyba zadnych ograniczen. to daje nam duza wieksza kontrole i duzo wieksza zlozonosc prostych aplikacji. widac ze wiekszosc tych usunietych funkcji da sie zastapic shaderami, wiec wiekszosc odpowiedzialnosci za proces renderowania spadnie wlasnie na shadery. z jednej strony mamy bardzo elastycznymechanizm shaderow, a z drugiej strony mamy api: chude, male ale zwinne i proste :) oczywiscie innej metody niz wykorzystanie shaderow nie ma (jak mozna sie domyslic). i bardzo dobrze. duze zmiany mozna domyslic sie zostaly wprowadzone w glsl, wiec to bedzie moja kolejna lektura...

na ten moment nie chce wydawac twardych opinii (jak debile, bo inaczej nie chce ich nazwac z pewnego wielkiego polskiego portalu informatycznego typu: "opengl ostatecznie przegral", czy "nie rowna sie z dx10" etc.), ale pierwsze wrazenie na temat nowego opengl mam bardzo pozytywne. podoba mi sie to odchudzenie (poza alpha testem i troche listami wyswietlania). teraz to api wydaje mi sie bardziej profesjonalne, 'ze tak powiem', bardziej uniwersalne. ja sam uzywam opengl w wersji (prawie) 2.1, bo moj sprzet wiecej nie potrafi wiec nie moge dokonac wyboru czy uzywac nowego api, ale jakbym mogl to napewno bym gruntowanie sobie trojke sprawdzil :)

niedziela, 15 marca 2009

{

blog.start();