在一个次要线程中进行OpenGL渲染

27
我正在编写一个3D模型查看器应用作为业余项目,也是尝试不同渲染技术的测试平台。我使用SDL来处理窗口管理和事件,并使用OpenGL进行3D渲染。我的程序的第一版是单线程的,运行得足够好。但是,我发现单线程程序会导致系统变得非常缓慢/卡顿。我的解决方案是将所有渲染代码移动到另一个线程中,从而释放主线程处理事件并防止应用程序变得无响应。
这个解决方案间歇性地有效,该程序经常崩溃,由于一组错误(主要来自X窗口系统)的变化令我感到困惑。这使我对我最初的假设产生了疑问,即只要在上下文创建的线程中执行所有OpenGL调用,一切都应该还能正常工作。在花费了一整天在互联网上搜索答案后,我已经被彻底难住了。
更简洁地说:是否可能使用OpenGL在除主线程之外的线程中执行3D渲染? 我可以继续使用诸如SDL或GLFW等跨平台窗口库进行此配置吗? 是否有更好的方法来实现我想做的事情?
到目前为止,我一直在使用C++在Linux(Ubuntu 11.04)上进行开发,尽管我还可以使用Java和Python,如果有更适合这些语言的解决方案。
  • 我认为“X错误”是由终端显示的错误信息引起的服务器错误。以下是更多详细信息。
  • 多线程版本的应用程序出现的错误:

    XIO:在 X 服务器 ":0.0" 上发生致命 IO 错误 11(资源暂时不可用),在 73 个请求(已知处理的 73 个请求)后,剩余 0 个事件。

    失败请求的 X 错误:BadColor(无效的颜色映射参数) 失败请求的主要操作码:79(X_FreeColormap) 失败请求中的资源 ID:0x4600001 失败请求序列号:72 输出流中的当前序列号:73

    游戏:../../src/xcb_io.c:140:dequeue_pending_request:断言“req == dpy->xcb->pending_requests”失败。 中止

    我总是会得到以上三个错误之一,我得到的是随机变化的,这似乎证实了我的问题确实源于我的线程使用。请记住,我是边学边做,所以非常有可能在我无知的情况下犯了某些愚蠢的错误。

    解决方案: 对于任何遇到类似问题的人,我通过将调用SDL_Init(SDL_INIT_VIDEO)的操作移动到渲染线程,并使用互斥锁锁定上下文初始化,解决了我的问题。这确保了上下文在将要使用它的线程中创建,防止主循环在初始化任务完成之前开始。启动过程的简化概述如下:

    1)主线程初始化struct,该结构将在两个线程之间共享,其中包含互斥锁。
    2)主线程生成渲染线程并休眠一段短时间(1-5毫秒),以便渲染线程有时间锁定互斥锁。在此暂停后,主线程在尝试锁定互斥锁时阻塞。
    3)渲染线程锁定互斥锁,初始化 SDL 的视频子系统并创建 OpenGL 上下文。
    4)渲染线程解锁互斥锁并进入其“渲染循环”。

    5) 主线程不再被阻塞,因此在完成初始化步骤之前锁定和解锁互斥量。

    务必阅读答案和评论,那里有很多有用的信息。


    1
    不确定在X上是否允许,但在Win32上是可以的。该上下文只能在一个线程上处于活动状态,但听起来你已经遵守了这个规则。 - Ben Voigt
    这是我从研究中得到的印象。理想情况下,我真的很想让我的应用程序跨平台...即使没有其他原因,我也希望尽可能少地处理Windows API =P - rjacks
    不能怪你这样想...虽然我发现Win32 API的这些方面(事件分派)要优于Linux。我希望Linux有一个MsgWaitForMultipleObjectsEx等效函数(也许名字更短)。就像poll一样,但能够等待文件、子进程、定时器、线程、互斥体和UI消息。 - Ben Voigt
    1
    在解决方案的第一步中,您应该小心。有可能“短暂时间”不够长,为了确保,您应该使用一个标志来指示渲染线程是否已初始化。在主线程中,检查此标志,如果未设置,则线程应等待来自渲染线程的信号/广播。 - DrYap
    @BenVoigt 我知道这是一个旧帖子,但你不应该比较Linux和Win32 API。你应该比较Win32 API和X11。 - RamblingMad
    @CoffeeandCode:我已经做了,可以看到第一条评论。 - Ben Voigt
    6个回答

    8
    只要OpenGL上下文每次只被一个线程访问,就不应该遇到任何问题。您说即使是单线程程序也会使系统变得缓慢。这是否意味着整个系统还是只有您自己的应用程序?在单线程OpenGL程序中最糟糕的情况是,处理该程序的用户输入变得缓慢,但系统的其余部分不受影响。
    如果您使用一些合成窗口管理器(Compiz,KDE4 kwin),请尝试禁用所有合成效果,看看会发生什么。
    当您说“X错误”时,您是指客户端错误还是在X服务器日志中报告的错误?后一种情况不应该发生,因为X命令流的任何形式的格式不正确,X服务器必须能够处理并最多发出警告。如果它(X服务器)崩溃,则这是一个错误,应该向X.org报告。
    如果您的程序崩溃,则其与X的交互存在问题;在这种情况下,请提供其各种错误输出。

    @rjacks:这些是客户端错误,X服务器错误会显示在/var/log/Xorg.<n>.log上。此外,如果可用并检测到兼容配置,GNOME3 shell将使用合成。通过某些晦涩的gconf设置可能可以禁用它。但是我建议安装一个轻量级窗口管理器(如Fluxbox)并使用它进行测试。 - datenwolf
    1
    据我所知,SDL仍在使用Xlib,在你的情况下是在xcb之上的Xlib。已知Xlib存在几个无法修复的问题,看起来你确实遇到了其中的一些。不幸的是,GLX,特别是有关直接渲染部分是针对Xlib编写的。如果您可以在没有直接渲染的情况下生存 - 如果您因此使用缓冲区对象,那么纯间接渲染不会带来任何缺点,并且可以仅使用xcb完成。不幸的是,目前没有易于使用的库;这是我TODO管道中的一个项目:https://github.com/datenwolf/XcbIGL - datenwolf

    3
    在类似的情况下,我所做的是将OpenGL调用保留在主线程中,但将顶点数组准备工作移至单独的线程(或多个线程)。
    基本上,如果你能够将CPU密集型任务与OpenGL调用分离开来,你就不必担心OpenGL多线程的问题。
    这对我非常有效。

    3

    提醒一下 - X-Server有自己的同步子系统。 在绘制时尝试以下操作: man XInitThreads - 进行初始化
    man XLockDisplay/XUnlockDisplay -- 用于绘图(不确定是否用于事件处理);


    1
    1. C++, SDL, OpenGl:::
      1. 在主线程上:SDL_CreateWindow( );
      2. SDL_CreateSemaphore( );
      3. SDL_SemWait( );
      4. 在渲染线程上:SDL_CreateThread( run, "rendererThread", (void*)this )
      5. SDL_GL_CreateContext( )
      6. "初始化其余的OpenGl和glew"
      7. SDL_SemPost( ) //释放之前创建的信号量
      8. P.S:SDL_CreateThread( )只接受函数作为其第一个参数,而不是方法。如果想要一个方法,可以通过将其定义为友元函数来模拟类中的方法/函数。这样它将具有方法特征,同时仍然能够用作SDL_CreateThread( )的可调用对象。
      9. P.S.S:在为线程创建的"run(void* data)"中,"(void*)"是重要的。为了在函数内重新获取"this",需要使用此行代码:"ClassName* me = (ClassName*)data;"

    1
    这是半个答案,半个问题。
    在单独的线程中使用SDL进行渲染是可能的。它通常适用于任何操作系统。你需要做的是,在渲染线程接管时确保GL上下文当前。同时,在这样做之前,你需要从主线程释放它,例如:
    从主线程调用:
    void Renderer::Init()
    {
    #ifdef _WIN32
        m_CurrentContext = wglGetCurrentContext();
        m_CurrentDC      = wglGetCurrentDC();
        // release current context
        wglMakeCurrent( nullptr, nullptr );
    #endif
    #ifdef __linux__
        if (!XInitThreads())
        {
            THROW( "XLib is not thread safe." );
        }
        SDL_SysWMinfo wm_info;
        SDL_VERSION( &wm_info.version );
        if ( SDL_GetWMInfo( &wm_info ) ) {
            Display *display = wm_info.info.x11.gfxdisplay;
            m_CurrentContext = glXGetCurrentContext();
            ASSERT( m_CurrentContext, "Error! No current GL context!" );
            glXMakeCurrent( display, None, nullptr );
            XSync( display, false );
        }
    #endif
    }
    

    从渲染线程调用:

    void Renderer::InitGL()
    {
        // This is important! Our renderer runs its own render thread
        // All
    #ifdef _WIN32
        wglMakeCurrent(m_CurrentDC,m_CurrentContext);
    #endif
    #ifdef __linux__
        SDL_SysWMinfo wm_info;
        SDL_VERSION( &wm_info.version );
        if ( SDL_GetWMInfo( &wm_info ) ) {
            Display *display = wm_info.info.x11.gfxdisplay;
            Window   window  = wm_info.info.x11.window;
            glXMakeCurrent( display, window, m_CurrentContext );
            XSync( display, false );
        }
    #endif
        // Init GLEW - we need this to use OGL extensions (e.g. for VBOs)
        GLenum err = glewInit();
        ASSERT( GLEW_OK == err, "Error: %s\n", glewGetErrorString(err) );
    

    这里的风险是,SDL没有本地的MakeCurrent()函数,不幸的是。因此,我们必须在SDL内部进行一些探索(1.2, 1.3现在可能已经解决了这个问题)。
    还有一个问题,当SDL关闭时我遇到了一个问题,也许有人可以告诉我如何在线程终止时安全地释放上下文。

    1

    我遇到了你们的一个错误:

    ../../src/xcb_io.c:140: dequeue_pending_request: Assertion `req == 
        dpy->xcb->pending_requests' failed. Aborted
    

    以及许多其他的不同的内容。结果发现,SDL_PollEvent需要一个初始化内存的指针。所以这样会失败:

    SDL_Event *event;
    SDL_PollEvent(event);
    

    虽然这个可以工作:

    SDL_Event event;
    SDL_PollEvent(&event);
    

    如果有其他人从谷歌搜索到这个问题。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接