【问题标题】:OpenGL object creationOpenGL对象创建
【发布时间】:2011-09-28 22:59:23
【问题描述】:

现在,我正在对某种小 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 个类,sprite_instanced 和 sprite_not_instanced。两者都是从 sprite 类派生的,因为它们都是 sprite,只是呈现方式不同。然而, sprite_instanced 和 sprite_not_instanced 需要在其创建函数中进一步调用 OpenGL。

到目前为止我的解决方案(我对此感到非常糟糕!)

我对 c++ 中的对象生成如何工作以及它如何影响虚函数有一定的了解。所以我决定只使用类精灵的虚拟创建功能将顶点数据等加载到图形内存中。然后 sprite_instanced 的虚拟创建方法将准备渲染该 sprite 实例。 所以,如果我想写

sprite_instanced s;

首先,调用 sprite 构造函数,经过一些初始化,构造线程将对象传递给 OpenGL 线程。此时,传递的对象只是一个普通的精灵,所以 sprite::create 将被调用,OpenGL 线程将创建一个普通的精灵。之后,构造线程将调用 sprite_instanced 的构造函数,再次进行一些初始化并将对象传递给 OpenGL 线程。然而这一次,它是一个 sprite_instanced,因此 sprite_instanced::create 将被调用。

所以,如果我对上述假设是正确的,那么至少在我的情况下,一切都会完全按照它应该发生的那样发生。我花了最后一个小时阅读关于从构造函数调用虚函数以及如何构建 v-table 等。我已经运行了一些测试来检查我的假设,但这可能是编译器特定的,所以我不依赖它们 100% .此外,它只是感觉很糟糕,就像一个可怕的黑客。

另一种解决方案

另一种可能性是在 OpenGL 线程类中实现工厂方法来处理这个问题。所以我可以在这些对象的构造函数中进行所有的 OpenGL 调用。但是,在这种情况下,我需要很多函数(或一种基于模板的方法),并且当 OpenGL 线程要做的事情超出其需要时,感觉可能会损失潜在的渲染时间......

我的问题

可以按照我上面描述的方式处理吗?还是我应该把这些东西扔掉去做别的事情?

【问题讨论】:

  • 这对于一个所谓的“小 OpenGL 库”来说是一个巨大的复杂性。它甚至是不必要的多线程。您概述的数据组织不会特别有效。您希望“精灵”的概念比“纹理”和“网格”等概念更高层次。 更多更高级别。
  • 是的,不知怎的,它变得相当大了......我想我只是对太多不同的方式感到好奇。但是,我看不出我所概述的数据组织方式不会特别有效。你是什​​么意思?表现?可维护性?还有什么吗?也许这只是我在这里简化的一部分,但我想清除它......
  • 我发现(VS 编译器)如果构造函数调用虚函数,则始终调用基本实现。但是如果我从构造函数调用的虚函数中调用虚函数,就可以了。
  • 多线程实际渲染过程本身不会给您带来任何好处。只有一个 GPU,所以驱动程序最后仍然必须自己序列化调用。 D3D 11 提供了一种批处理调用(命令列表)的方法。我不确定 GL 4 是否也可以。当我读到它时,我扬起了眉毛,除非在某些非常特殊的情况下,否则它会对性能产生很大影响。无论如何,我认为您的解决方案可能有点过度设计。

标签: c++ inheritance opengl


【解决方案1】:

你已经得到了一些很好的建议。所以我会稍微调味:

了解 OpenGL 的重要一点是,它是一个状态机,不需要一些复杂的“初始化”。您只需使用它,仅此而已。 Buffer Objects(Textures、Vertex Buffer Objects、Pixel Buffer Objects)可能会让它看起来不一样,而且大多数教程和现实世界的应用程序确实在应用程序启动时填充了Buffer Objects。

但是,在常规程序执行期间创建它们是完全可以的。在我的 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 封装在类中,我强烈建议不要使用虚函数,而是使用静态(在编译单元级别)内联类,这样它们就会变成语法糖,编译器不会膨胀太多。

【讨论】:

  • 感谢您的回答。我会重新考虑一下我的设计,已经有了一些改进,对此我很满意。不过,似乎会导致混乱的一件事是:我只是选择了精灵作为示例。这是我想到的第一件事,显然,这是一个坏主意。下次我会想一个更好的例子。
【解决方案2】:

假设单个上下文在单个线程上是当前的,这一事实简化了问题;实际上可以有多个 OpenGL 上下文,也可以在不同的线程上(当我们在这里时,我们会考虑上下文名称空间共享)。


首先,我认为您应该将 OpenGL 调用与对象构造函数分开。这样做可以让您在不携带 OpenGL 上下文货币的情况下设置对象;随后,对象可以在主渲染线程中排队以进行创建。

一个例子。假设我们有 2 个队列:一个包含 Texture 对象,用于从文件系统加载纹理数据,一个包含 Texture 对象,用于将纹理数据上传到 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::Texture(const char *path)
{
    mImagePath = path;
    textureLoadQueue.Enqueue(this);
}

这只是一个例子。当然每个对象都有不同的要求,但是这个解决方案是最具可扩展性的。


我的解决方案本质上是由接口IRenderObject 描述的(文档与当前的实现有很大不同,因为我现在正在重构很多并且开发处于非常alpha 级别)。此解决方案适用于 C# 语言,由于垃圾收集管理而引入了额外的复杂性,但该概念完全适用于 C++ 语言。

本质上,IRenderObject 接口定义了一个基础 OpenGL 对象:

  • 它有一个名称(由 Gen 例程返回的名称)
  • 可以是 created 使用当前的 OpenGL 上下文
  • 可以是deleted,使用当前的 OpenGL 上下文
  • 可以使用“OpenGL 垃圾收集器”异步released

创建/删除操作非常直观。提取当前上下文的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!!!

异步释放操作旨在删除对象,但没有当前上下文(实际上该方法不带任何 RenderContext 参数)。对象可能被放置在没有当前上下文的单独线程中;而且,我不能依赖垃圾收集器(C++ 没有),因为它是在我无法控制的线程中执行的。此外,最好实现 IDisposable 接口,以便应用程序代码可以控制 OpenGL 对象的生命周期。

OpenGL GarbageCollector,在当前具有正确上下文的线程上执行。

【讨论】:

  • 这个答案肯定太长了。
【解决方案3】:
  1. 在构造函数中调用任何虚函数总是不好的形式。虚拟通话不会正常完成。

  2. 您的数据结构非常混乱。您应该研究工厂对象的概念。这些是您用来构造其他对象的对象。你应该有一个 SpriteFactory,它被推入某种队列或其他任何东西。那个 SpriteFactory 应该是创建 Sprite 对象本身的东西。这样一来,您就不会有这种部分构造对象的概念,即创建它会将自身推入队列等等。

    确实,每当您开始编写“Objectname::Create”时,请停下来想一想,“我真的应该使用 Factory 对象。”

【讨论】:

  • 我想到了使用工厂来创建对象。但是,在这种情况下,我没有找到执行所需 OpenGL 调用的好方法,或者如何将创建的对象传递给调用工厂的线程。我还没有想出同时解决这两个问题的想法。但我还在考虑。 (我也不太喜欢构造方法中的虚拟调用,但它碰巧起作用了。但正如我所说,不知何故感觉很糟糕)
【解决方案4】:

OpenGL 是为 C 而不是 C++ 设计的。我学到的最好的方法是编写函数而不是类来包装 OpenGL 函数,因为 OpenGL 在内部管理自己的对象。使用类来加载数据,然后将其传递给处理 OpenGL 的 C 风格函数。 您应该非常小心地在构造函数/析构函数中生成/释放 OpenGL 缓冲区!

【讨论】:

  • 为什么不呢?我认为没有理由不将 RAII 应用于 OpenGL 对象。只要您确保在上下文出现之前没有创建任何对象,并且在上下文结束之前将所有对象都销毁,那么有什么问题?
  • 实际上,使用 RAII 是其背后的想法。当然,我要确保在创建时有可用的 OpenGL 上下文,并且在销毁时不再以任何方式使用对象。此外,上下文会在销毁时删除对象的 OpenGL 数据(这不是问题,反正只有一个上下文)。
  • @Pubby8 确切地说,违反 RAII 并不是最好的做法。
  • @pmr “所以这不是最好的做法”只是一种观点,而不是教条。事实上,OpenGL 对象在参考系统上的工作效果更好,而不是 RAII。有人在堆栈上创建了纹理吗?
  • @Nicol Bolas 原因是分配给堆的对象需要一个 delete 调用,该调用不会自动调用。 RAII 未涵盖此问题。
【解决方案5】:

我会避免让您的对象在构造时将自己插入到 GL 线程的队列中。这应该是一个明确的步骤,例如

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

这让你可以做一些事情,比如构造一组对象,然后一次将它们全部放入队列中,并保证构造函数在任何虚函数被调用之前结束。请注意,将入队放在构造函数的末尾并保证这一点,因为构造函数总是从最顶层的类向下调用。这意味着,如果您将一个对象排队以在其上调用一个虚函数,则派生类将在其自己的构造函数开始执行之前入队。这意味着您有可能导致对未初始化对象执行操作的竞争条件!如果您没有意识到自己做了什么,那将是调试的噩梦。

【讨论】:

  • 我一开始就是这么做的。但是,在调用构造函数后,它给我留下了一个部分构造的对象(就缺少 OpenGL 部分而言)。在当前的解决方案中,我处理了竞争条件,等待基类完全构造(包括 OpenGL 部分),然后开始构造派生类。尽管到目前为止我运行的每个测试都按预期工作,但我正在研究像 Nicol Bolas 提到的解决方案
【解决方案6】:

我认为这里的问题不在于 RAII,也不是 OpenGL 是 c 风格的界面这一事实。这是您假设 sprite 和 sprite_instanced 都应该从一个共同的基础派生。这些问题一直存在于类层次结构中,我学到的关于面向对象的第一个教训,主要是通过许多错误,就是封装几乎总是比派生更好。除了如果要派生,请通过抽象接口进行。

换句话说,不要被这两个类都具有“sprite”名称这一事实所迷惑。他们在其他方面的行为完全不同。对于它们共享的任何通用功能,实现一个封装该功能的抽象基础。

【讨论】:

  • 感谢您的回答。我很抱歉通过选择那个精灵示例在这个问题上造成了一些混乱。我没有那样实现它,我也不会。我只需要一个例子来说明我的想法,这是我想到的第一个(而且显然很糟糕)的例子。
  • 即便如此,我认为原则仍然成立。您需要一个工厂来创建东西(正如其他人所指出的那样),它最好返回抽象接口指针(不是强制性的,但我倾向于使用 shared_ptr 来处理几乎所有事情)。工厂本身可以是抽象的,然后您可以创建 OpenGL 工厂或 D3D 工厂,每个工厂实现都为 OpenGL 或 D3D 创建对象。
猜你喜欢
  • 1970-01-01
  • 2021-12-30
  • 1970-01-01
  • 2014-05-23
  • 1970-01-01
  • 1970-01-01
  • 2015-02-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多