2013-06-18 38 views
12

Chcę napisać proste opakowanie RAII dla obiektów OpenGL (tekstury, bufory ramek itp.) Zauważyłem, że wszystkie funkcje glGen* i glDelete* mają tę samą sygnaturę, więc moja pierwsza próba była taka :Otoczka RAII dla obiektów OpenGL

typedef void (__stdcall *GLGenFunction)(GLsizei, GLuint *); 
typedef void (__stdcall *GLDelFunction)(GLsizei, const GLuint *); 

template <GLGenFunction glGenFunction, GLDelFunction glDelFunction> 
class GLObject 
{ 
    GLuint m_name; 
public: 
    GLObject() 
    { 
     glGenFunction(1, &m_name); 
    } 

    ~GLObject() 
    { 
     glDelFunction(1, &m_name); 
    } 

    GLuint getName() {return m_name;} 
}; 

typedef GLObject<glGenTextures, glDeleteTextures> GLTexture; 

To działa dobrze dla tekstur, ale nie dla buforów ramki: glGenFramebuffers i glDeleteFramebuffers adresy funkcyjne nie są znane w czasie kompilacji i nie mogą być używane jako argumenty szablonów. Więc zrobiłem drugą wersję:

class GLObjectBase 
{ 
    GLuint m_name; 
    GLDelFunction m_delFunction; 

public: 
    GLObjectBase(GLGenFunction genFunc, GLDelFunction delFunction) 
     : m_delFunction(delFunction) 
    { 
     genFunc(1, &m_name); 
    } 

    GLuint getName() 
    { 
     return m_name; 
    } 

protected: 
    ~GLObjectBase() 
    { 
     m_delFunction(1, &m_name); 
    } 
}; 

class GLFrameBuffer : public GLObjectBase 
{ 
public: 
    GLFrameBuffer() : GLObjectBase(glGenFramebuffers, glDeleteFramebuffers) {} 
}; 

Ale nie podoba mi się to, ponieważ mam do przechowywania del wskaźnik funkcji w każdym przypadku, że nie ulegnie zmianie w czasie wykonywania.

Jak utworzyć klasę opakowania, w której zapisuje się tylko nazwę obiektu w każdym wystąpieniu bez konieczności tworzenia grupy prawie skopiowanych przez kopiowanie klas?

mógłby zrobić coś takiego:

template <int N> 
class GLObject2 
{ 
    GLuint m_name; 
    static GLDelFunction glDelFunction; 
public: 
    GLObject2(GLGenFunction genFunction, GLDelFunction delFunc) 
    { 
     genFunction(1, &m_name); 
     if (glDelFunction == nullptr) 
      glDelFunction = delFunc; 
     ASSERT(glDelFunction == delFunc); 
    } 

    GLuint getName() {return m_name;} 

protected: 
    ~GLObject2() 
    { 
     glDelFunction(1, &m_name); 
    } 
}; 

template <int N> 
GLDelFunction GLObject2<N>::glDelFunction = nullptr; 

class GLTexture: public GLObject2<1> 
{ 
public: 
    GLTexture(): GLObject2<1>(glGenTextures, glDeleteTextures) {} 
}; 

class GLRenderBuffer: public GLObject2<2> 
{ 
public: 
    GLRenderBuffer(): GLObject2<2>(glGenRenderbuffers, glDeleteRenderbuffers) {} 
}; 

Czy ktoś może sugerować bardziej eleganckie rozwiązanie?

+0

IMHO, interfejsy API OpenGL prawdopodobnie nie są wystarczająco zunifikowane, aby tego rodzaju ogólne podejście było bardzo udane. Różne generacje funkcji wykorzystują różne podejścia do projektowania interfejsu API. – Trillian

+3

@Trillian: Nonsens. Z 12 typów obiektów OpenGL [tylko 3 używają niestandardowych metadologii tworzenia/niszczenia] (http://www.opengl.org/wiki/OpenGL_Object#Non-standardowe_obiekty). To zły pomysł z innych powodów, ale nie z tego powodu. –

+1

@NicolBolas: Z ciekawości: Co sprawia, że ​​ten pomysł jest zły? I czy odpowiadasz jako część tej złej decyzji, czy poleciłbyś ją? – Skalli

Odpowiedz

15

Naprawdę myślisz o tym jak programista C. Używasz C++, więc rozwiąż go tak, jak zrobiłby to programista C++. Z klasą cechy:

struct VertexArrayObjectTraits 
{ 
    typedef GLuint value_type; 
    static value_type Create(); 
    static void Destroy(value_type); 
}; 

jak prawdziwy klasa C++ cechami, mamy każdy obiekt zadeklarować swój własny value_type. Umożliwi to dostosowanie go do obiektów OpenGL, które nie używają GLuint s, jak sync objects (chociaż interfejs Create/Destroy i tak nie byłby dla nich dobry, więc prawdopodobnie nie powinieneś się tym przejmować).

Więc piszesz jedną klasę cech dla każdego typu obiektu OpenGL. Twoje funkcje Create i Destroy będą odpowiednio przekazywać połączenia do API C.

Po robi to, co potrzebne jest RAII-wrapper wokół te interfejsy:

template<typename T> 
class OpenGLObject 
{ 
public: 
    OpenGLObject() : m_obj(T::Create()) {} 
    ~OpenGLObject() {T::Destroy(m_obj);} 

    operator typename T::value_type() {return m_obj;} 

private: 
    typename T::value_type m_obj; 
}; 

OpenGLObject<VertexArrayObjectTraits> będzie posiadać VAO.

+4

Pośpiesznie oznaczyłem tę odpowiedź jako zaakceptowaną, ale po jej udzieleniu myślę, że nie podoba mi się to bardzo. Mógłbym stworzyć pół tuzina kopiowanych klas, które różnią się jedynie funkcjami budowania i niszczenia obiektów. Twoja propozycja, gdy jest [nieco] lepsza architektonicznie nadal pozostawia mnie z tą samą ilością skopiowanych do skopiowania teraz cech-klas oraz klasy obiektu. – n0rd

+3

@ n0rd: Taka sama liczba klas tak, ale klasy mają po 5 ŁATWEJ linii każda, z wyjątkiem jednej, która jest nadal względnie prosta i 20 linii. To wciąż wielka wygrana w złożoności. –

+0

Naiwna implementacja nie byłaby dużo większa ani bardziej złożona: zmienna do przechowywania nazwy obiektu, konstruktora jednokreskowego, destruktora jednowierszowego, pobierającego jedną linię. Porównaj z klasami cech: 'value_type' typedef,' Create' function, 'Destroy' function. – n0rd

9

Po co odnawiać koło? Jest schludny rozwiązanie wykorzystujące std::unique_ptr, który już zapewnia wymaganą funkcjonalność, więc trzeba tylko napisać cechy (!):

template<void (*func)(GLuint)> 
struct gl_object_deleter { 
    struct pointer { // I wish we could inherit from GLuint... 
     GLuint x; 
     pointer(std::nullptr_t = nullptr) : x(0) {} 
     pointer(GLuint x) : x(x) {} 
     operator GLuint() const { return x; } 
     friend bool operator == (pointer x, pointer y) { return x.x == y.x; } 
     friend bool operator != (pointer x, pointer y) { return x.x != y.x; } 
    }; 
    void operator()(GLuint p) const { func(p); } 
}; 

void delete_texture(GLuint p) { glDeleteTextures(1, &p); } 
void delete_shader(GLuint p) { glDeleteShader(p); } 
// ... 
typedef std::unique_ptr<void, gl_object_deleter<delete_texture>> GLtexture; 
typedef std::unique_ptr<void, gl_object_deleter<delete_shader>> GLshader; 
// ... 

Większość funkcje Create* zwraca tablicę poprzez ich argumentacji, która jest niewygodna, kiedy przydzielaj swoje obiekty jeden po drugim. Możliwe jest określenie zestawu procedur tworzenia pojedynczych przypadkach:

GLuint glCreateTextureSN(GLenum target) { GLuint ret; glCreateTextures(target, 1, &ret); return ret; } 
GLuint glCreateBufferSN() { GLuint ret; glCreateBuffers(1, &ret); return ret; } 
// ... 

Niektóre funkcje OpenGL jak glCreateShader może być stosowany bezpośrednio. Teraz możemy używać go w sposób następujący:

GLtexture tex(glCreateTextureSN(GL_TEXTURE_2D)); 
glTextureParameteri(tex.get(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 
// ... 
GLtexture tex2 = std::move(tex); // we can move 
tex2.reset(); // delete 
return tex2; // return... 

Jednym minusem jest to, że nie można zdefiniować niejawny oddanych do GLuint, więc musisz zadzwonić get() wyraźnie.Ale, po drugie, zapobieganie przypadkowemu rzutowi na GLuint nie jest takie złe.

+2

Obiekty podobne do OpenGL są również jednym z klasycznych przykładów, kiedy użycie shared_ptr jest naprawdę dobre. Tekstury i inne rzeczy są zwykle dzielone między różnymi miejscami w rendererze, a usuwanie go tylko wtedy, gdy nikt go już nie potrzebuje, jest dokładnie tym, co robi shared_ptr. – TravisG

+0

@TravisG: Jednym z problemów 'shared_ptr' w porównaniu do' unique_ptr' jest to, że jego funkcja 'get()' zawsze zwraca wskaźnik. Kończysz przydzielanie 'GLuint' na stercie lub pisanie jawnego' reinterpret_cast (tex.get()) '. Może jednak mógłbyś napisać jakieś opakowanie. – ybungalobill

+1

Możesz zajrzeć do posta JohannesD [z tego wątku] (http://stackoverflow.com/a/6272139). Typ GLuint może nie spełniać wymogu NullablePointer niestandardowego typu przechowywania unique_ptr. – Bovinedragon