2011-09-28 117 views
4

現在,我正在建模某種小型的OpenGL庫,以圖形化編程等方式矇混過關。因此,我使用類來包裝特定的OpenGL函數調用,如紋理創建,着色器創建等等, 到現在爲止還挺好。OpenGL對象創建

我的問題:

所有OpenGL調用必須由擁有創建OpenGL上下文的線程來完成(至少在Windows下,所有其他線程將什麼也不做,創建一個OpenGL錯誤)。所以,爲了獲得OpenGL上下文,我首先創建一個窗口類的實例(只是Win API調用的另一個包裝),最後爲該窗口創建一個OpenGL上下文。這聽起來對我來說很合邏輯。 (如果在我的設計中已經有一個讓你尖叫的缺陷,請告訴我...)

如果我想創建一個紋理,或任何其他需要OpenGL調用創建的對象,我基本上這樣做( OpenGL的對象調用的構造函數,例如):

opengl_object() 
{ 
    //do necessary stuff for object initialisation 
    //pass object to the OpenGL thread for final contruction 
    //wait until object is constructed by the OpenGL thread 
} 

所以,在的話,我創建一個對象,就像使用任何其他對象

opengl_object obj; 

,然後在其構造器,把自己變成一個隊列的OpenGL對象將由OpenGL上下文線程創建。然後,OpenGL上下文線程調用一個虛擬函數,該函數在所有OpenGL對象中實現,幷包含必要的OpenGL調用以最終創建該對象。

我真的認爲,這種處理這個問題的方式會很好。但是,現在,我認爲我非常錯誤。

雖然上述方法迄今爲止工作得非常好,但在類層次更深時,我會遇到麻煩。例如(這不是完美的,但它表明我的問題):

比方說,我有一個名爲sprite的類,顯然表示一個Sprite。它對OpenGL線程有自己的創建函數,其中頂點和紋理座標被加載到圖形卡存儲器中等等。到目前爲止,這沒有問題。讓我們進一步說,我想有2種渲染精靈的方式。一個實例,一個通過另一種方式。所以,我最終會得到2個類,sprite_instanced和sprite_not_instanced。兩者都來自精靈類,因爲它們都是精靈,它們的渲染方式不同。但是,sprite_instanced和sprite_not_instanced在其create函數中需要進一步的OpenGL調用。

我的解決方案至今(我覺得真的很可怕呢!)

我有某種C++中工作以及它如何影響虛擬功能的理解如何對象生成。所以我決定只使用類精靈的虛擬創建函數來將頂點數據等加載到圖形內存中。然後,sprite_instanced的虛擬創建方法將進行準備以呈現該精靈實例。 所以,如果我想要寫

sprite_instanced s; 

首先,精靈調用構造函數和一些初始化後,將構建線程傳遞對象到OpenGL的線程。在這一點上,傳遞的對象只是一個普通的精靈,所以sprite :: create將被調用,OpenGL線程將創建一個普通的精靈。之後,構造線程將調用sprite_instanced的構造函數,再次進行一些初始化並將對象傳遞給OpenGL線程。但是,這次是sprite_instanced,因此將調用sprite_instanced :: create。因此,如果我對上述假設是正確的,那麼至少在我的情況下,所有事情都應該發生,就像它應該發生的一樣。我花了最後一小時閱讀關於從構造函數調用虛函數以及v-table如何構建等等。我已經運行了一些測試來檢查我的假設,但這可能是編譯器特定的,所以我不會依賴它們100% 。另外,它感覺很糟糕,並且像一個可怕的黑客。

另一種解決方案

另一種可能性是在實施了OpenGL線程類工廠方法來採取照顧。所以我可以在這些對象的構造函數中完成所有的OpenGL調用。然而,在這種情況下,我需要很多功能(或者一種基於模板的方法),並且感覺像OpenGL線程需要做的事情可能會導致渲染時間的損失...

我的問題

可以按照我上面描述的方式處理它嗎?還是應該把這些東西丟掉,然後做點別的?

+1

這對於所謂的「小型OpenGL庫」來說是一個巨大的複雜性。它甚至是不必要的多線程。您列出的數據組織不會特別有效。您希望「精靈」的概念比「紋理」和「網格」等概念更高級別。 _更高一級。 –

+0

是的,它變得非常大......我想我只是在很多不同的方式好奇。但是,我沒有看到我所概述的數據組織將不會特別有效。你是用什麼方式表示的?性能?可維護性?還有其他什麼?也許這只是我在這裏簡化的一部分,但我想清除它... – Shelling

+0

我發現(VS編譯器),如果構造函數調用虛函數,總是調用基實現。但是如果我從構造函數調用的虛函數中調用虛函數,這可以正常工作。 – Luca

回答

2
  1. 在構造函數中調用任何虛函數總是很糟糕的形式。虛擬呼叫將不會正常完成。

  2. 您的數據結構非常混亂。您應該調查Factory對象的概念。這些是用來構造其他對象的對象。你應該有一個SpriteFactory,它被推入某種類型的隊列或其他類型。那個SpriteFactory應該是創建Sprite對象本身的東西。這樣,你就不會有這樣一個部分構建的對象的概念,在這個對象的創建中它將自己推入隊列等等。事實上,只要你開始寫「Objectname :: Create」,就停下來思考一下,「我真的應該使用一個Factory對象。」

+0

我想用工廠來創建對象。然而,在這種情況下,我沒有找到一個很好的方法來完成所需的OpenGL調用,或者如何將創建的對象傳遞給調用工廠的線程。我還沒有想出同時解決這兩個問題的想法。但我仍在考慮這個問題。 (我真的不喜歡在構造函數方法中的虛擬調用,但它碰巧工作,但正如我所說,它不知何故感覺很糟糕) – Shelling

1

OpenGL是爲C設計的,而不是C++。我學到的最好的作品是編寫函數而不是類來包裝OpenGL函數,因爲OpenGL在內部管理自己的對象。使用類加載數據,然後將它傳遞給處理OpenGL的C風格函數。 你應該非常小心在構造函數/析構函數中生成/釋放OpenGL緩衝區!

+2

爲什麼不呢?我沒有看到RAII不應該應用於OpenGL對象的理由。只要確保在上下文產生之前沒有創建那些對象,並且在上下文死亡之前所有對象都被銷燬,那麼問題是什麼? –

+2

這裏有一個關於OOP的部分:http://www.opengl.org/wiki/Common_Mistakes – Pubby

+0

實際上,使用RAII是背後的想法。當然,我確定在創建時有一個OpenGL上下文,並且對象不會再以任何方式用於銷燬。此外,上下文刪除破壞對象的OpenGL數據(這不是問題,反正只有一個上下文)。 – Shelling

1

我會避免讓你的對象插入GL線程隊列的建設。這應該是一個明確的步驟,例如

gfxObj_t thing(arg) // read a file or something in constructor 
mWindow.addGfxObj(thing) // put the thing in mWindow's queue 

這可以讓你做這樣的事情構建一組對象,然後把它們都在隊列一次,並保證構造函數結束被稱爲虛函數之前。請注意,將排隊放置在構造函數的末尾確實是而不是,因爲構造函數總是從最頂層的類中調用。這意味着如果你排隊一個對象來調用一個虛擬函數,派生類將在它們自己的構造函數開始執行之前被排隊。這意味着你有一個競爭條件可能會導致未初始化的對象的行爲!如果你沒有意識到你已經做了什麼,那麼這是一場噩夢。

+0

我在開始時就這樣做了。然而,在構造函數被調用之後,它給我留下了一個部分構造的對象(根據缺失的OpenGL部分)。在當前的解決方案中,我一直在等待基類完全構建(包括OpenGL部分),然後開始構建派生類。儘管迄今爲止我跑過的每個測試都按預期工作,但我正在研究像Nicol Bolas提到的解決方案 – Shelling

5

你已經給了一些很好的建議。所以我只是將它調高一點:

瞭解OpenGL的一個重要的事情是,它是一個狀態機,它不需要一些精心設計的「初始化」。你只是用它,就是這樣。緩衝區對象(紋理,頂點緩衝區對象,像素緩衝區對象)可能會使它看起來不同,大多數教程和真實世界的應用程序確實在應用程序啓動時填充緩衝區對象。

但是,在正常程序執行過程中創建它們是完全正確的。在我的3D引擎中,我使用雙緩衝區交換期間的空閒CPU時間將異步上傳到緩衝區對象(for(b in buffers){glMapBuffer(b.target, GL_WRITE_ONLY);} start_buffer_filling_thread(); SwapBuffers(); wait_for_buffer_filling_thread(); for(b in buffers){glUnmapBuffer(b.target);})中。

重要的是要明白,像精靈這樣簡單的東西不應該爲每個精靈賦予它自己的VBO。一個通常在一個VBO中分組大量的精靈。您不必一起繪製它們,因爲您可以抵消VBO並進行部分繪圖調用。但是這種常見的OpenGL模式(共享緩衝區對象的幾何對象)完全違背了你的類的原則。所以你需要一些緩衝區對象管理器,它向消費者提供地址空間片。

在OpenGL中使用類層次本身並不是一個壞主意,但它應該比OpenGL高一些。如果您只是將OpenGL 1:1映射到類,則您只會獲得複雜性和膨脹。如果我直接或通過課堂調用OpenGL函數,我仍然必須做所有的咕嚕工作。所以紋理類不應該只映射紋理對象的概念,而應該考慮與像素緩衝對象(如果使用的話)進行交互。

如果你真的想要在類中封裝OpenGL,我強烈建議不要使用虛函數,而是使用靜態(在編譯單元級別上的方法)內聯類,以便它們變成語法糖,編譯器不會膨脹太多。

+0

這是一個很好的答案! – Pubby

+0

感謝您的回答。我會重新考慮我的設計,已經有了一些改進,我對此感到很滿意。有一件事似乎導致了混亂:我只是選擇了精靈作爲例子。這是我首先想到的,顯然,這是一個壞主意。下次我會考慮一個更好的例子。 – Shelling

2

這個問題被簡化爲單個上下文被假定爲單線程上的當前事實;實際上可以有多個OpenGL上下文,也可以在不同的線程上(當我們處於時,我們考慮上下文名稱空間共享)。


首先,我想你應該將OpenGL調用與對象構造函數分開。這樣做可以讓你設置一個對象而不需要攜帶OpenGL上下文的貨幣;該對象可以在主渲染線程中被排隊創建。

一個例子。假設我們有兩個隊列:一個保存紋理用於從文件系統加載紋理數據的對象,一個保存紋理用於將紋理數據上傳到GPU內存(當然是加載數據之後)的對象。

線程1:質地裝載機

{ 
    for (;;) { 
     while (textureLoadQueue.Size() > 0) { 
      Texture obj = textureLoadQueue.Dequeue(); 

      obj.Load(); 
      textureUploadQueue.Enqueue(obj); 
     } 
    } 
} 

線程2:質地上傳代碼段,基本上是主渲染線程

{ 
    while (textureUploadQueue.Size() > 0) { 
     Texture obj = textureUploadQueue.Dequeue(); 

     obj.Upload(ctx); 
    } 
} 

紋理對象的構造應該是這樣的:

Texture::Texture(const char *path) 
{ 
    mImagePath = path; 
    textureLoadQueue.Enqueue(this); 
} 

This is onl例如。當然,每個對象都有不同的要求,但這個解決方案是最具可擴展性的。


我的解決方法基本上是由接口IRenderObject(該文件是從當前實現完全不同的,因爲我在這一刻很多重構和發展正處在一個非常阿爾法級)描述。這個解決方案適用於C#語言,由於垃圾收集管理而引入額外的複雜性,但是這個概念完全適用於C++語言。

本質上,接口IRenderObject定義一個基OpenGL的對象:

  • 它有一個名稱(那些由根返回例程)
  • 它可以使用當前OpenGL上下文是created
  • 它可以是deleted使用當前的OpenGL上下文
  • 它可以是released異步使用「OpenGL垃圾收集器」

創建/刪除操作非常直觀。取RenderContext抽象當前上下文;使用該對象,也能夠執行檢查,可以是有用的,以找到缺陷在對象創建/刪除:

  • Create方法檢查上下文是否是當前的,如果上下文可以創建該類型的一個對象,等等...上下文
  • Delete方法檢查是否是最新的,更重要的是,作爲檢查參數傳遞的上下文是否是共享的上下文的同一個對象的名稱空間創造了基本IRenderObject

下面是Delete方法的示例。這裏的代碼的工作,但如預期它不工作:

RenderContext ctx1 = new RenderContext(), ctx2 = new RenderContext(); 
Texture tex1, tex2; 

ctx1.MakeCurrent(true); 
tex1 = new Texture2D(); 
tex1.Load("example.bmp"); 
tex1.Create(ctx1);   // In this case, we have texture object name = 1 

ctx2.MakeCurrent(true); 
tex2 = new Texture2D(); 
tex2.Load("example.bmp"); 
tex2.Create(ctx2);   // In this case, we have texture object name = 1, the same has before since the two contexts are not sharing the object name space 

// Somewhere in the code 
ctx1.MakeCurrent(true); 

tex2.Delete(ctx1);   // Works, but it actually delete the texture represented by tex1!!! 

異步釋放操作的目的是刪除對象,但不具有當前上下文(INFACT該方法不採用任何的RenderContext參數)。可能發生的情況是該對象被放置在一個單獨的線程中,該線程沒有當前上下文;還有,我不能依靠行李分類器(C++沒有),因爲它是在一個線程中執行的,我無法控制。而且,可以實現IDisposable接口,所以應用程序代碼可以控制OpenGL對象的生命週期。

OpenGL GarbageCollector,在具有正確上下文當前的線程上執行。

+0

這個答案肯定太久了。 – Luca

1

我認爲這裏的問題不是RAII,或者OpenGL是c風格的界面。這是你假設精靈和sprite_instanced都應該從一個共同的基礎派生。這些問題始終伴隨着類的層次結構發生,而我學習的關於面向對象的第一課,主要是通過很多錯誤,其中最重要的是封裝而不是派生。除了如果你要派生,通過抽象接口來完成。

換句話說,不要被這兩個類中都有名稱「sprite」的事實所愚弄。他們的行爲完全不同。對於它們共享的任何常見功能,實現封裝該功能的抽象基礎。

+0

感謝您的回答。我很抱歉通過選擇sprite的例子在這個問題上引起了一些混淆。我沒有這樣實施,我也不會。我只是需要一個例子讓我的想法變得清晰,這是我想到的第一個(也是顯然不好的)想法。 – Shelling

+0

即便如此,我認爲這個原則是成立的。你需要一個工廠來創建東西(正如其他人已經注意到的),它最好返回抽象接口指針(不是強制性的,但我傾向於使用shared_ptr幾乎所有東西)。工廠本身可以是抽象的,然後你可以創建一個OpenGL工廠或一個D3D工廠,每個工廠實現都可以爲OpenGL或D3D創建對象。 – Robinson