Многопоточная работа с OpenGL
Автор: /A\
В этой статье я расскажу об особенностях работы с OpenGL в нескольких потоках.
Введение
Создание общего контекста
Поведение OpenGL объектов в общих контекстах
Написание враппера
Операции
Глобальные переменные
Окно
Ядро
Менеджер VAO
Шейдеры
Загрузка ресурсов в параллельном потоке
Рендеринг в параллельном потоке
Тесты
Время выполнения функций OpenGL
Параллельный рендеринг
Заключение
Исходники
Введение
Во многих современных движках используется асинхронная загрузка ресурсов, это позволяет экономить видеопамять, делать карты неограниченных размеров и с большей детализацией. Рендеринг в параллельном потоке может использоваться для генерации процедурных текстур и геометрии или для рендеринга текстур отражений, изображений из камер и других операций, пока в основном потоке идет рендеринг сцены. Для всего этого необходимо использовать графическое API в двух и более потоках о чем и пойдет речь в статье.
Создание общего контекста
Для ОС Windows есть два способа:
HDC dc = ...; HGLRC share_rc = ...; // ранее созданный контекст HGLRC rc = wglCreateContext(dc ); wglShareLists( share_rc, rc );
HDC dc = ...; HGLRC share_rc = ...; // ранее созданный контекст int attribs[] = ...; // атрибуты контекста HGLRC rc = wglCreateContextAttribsARB(dc, share_rc, attribs );
Желательно чтобы у контекстов были одинаковые атрибуты и пиксельный формат.
Для Unix подобных ОС есть функции glXCreateContext и glXCreateContextAttribsARB.
Поведение OpenGL объектов в общих контекстах
Буферы, текстуры, шейдеры
Они без проблем работают на нескольких контекстах если не делать одновременное чтение и запись. После изменения данных (заливка вершин/изображения/исходника, генерация мип уровней, установка параметров, компиляция) и перед использованием в другом потоке нужно вызывать glFinish, чтобы драйвер завершил все изменения данных.
Framebuffer
Создавать FBO можно в любом контексте, но присоединять текстуры и проверять статус нужно в том контексте в котором он будет использоваться.
VertexArray
Вот здесь и начинаются проблемы: VAO не делается общим для контекстов. При использовании идентификатора VAO созданного в другом контексте генерируется ошибка GL_INVALID_OPERATION или, если совпали идентификаторы с VAO, созданным в текущем контексте, то будут использованы его атрибуты.
Все это проверялось на нескольких видеокартах от nVidia и ATI/AMD.
Написание враппера
Исходник враппера и примеров можно скачать по ссылке в конце статьи.
Враппер предназначен для работы с OpenGL версии 3.3 и выше.
Задачи:
Операции
Операции предназначены для выполнения различных действий в потоках.
class IOperation { public: virtual void Do() = 0; virtual void Free( ) = 0; }; class IOperationC : public IOperation { public: virtual void SetCounter( int) = 0; };
Интерфейс IOperation предназначен для выполнения операции в одном потоке, интерфейс IOperationC может выполняться в нескольких потоках.
Функция Do() выполняет операцию, после чего обращений к этой операции не будет, поэтому в функции необходимо освободить все данные.
Функция Free() предназначена для освобождения данных, если не удалось добавить операцию в очередь потока.
Функция SetCounter(int) передает количество потоков в которые добавлена эта операция, при вызове функции Do() необходимо уменьшать счетчик на 1, а когда счетчик будет равен 0 - освободить данные.
Пример простой реализации операций:
class COperation : public IOperation { public: void Do() { // здесь можно что-нибудь выполнить delete this; } void Free( ) { delete this; } }; class COperationC : public IOperationC { private: int counter; public: void Do( ) { // здесь можно что-нибудь выполнить if ( --counter == 0 ) delete this; } void Free( ) { delete this; } void SetCounter( int c) { counter = c; } };
Так как изменение счетчика происходят в разных потоках, то их необходимо выполнять атомарно.
Глобальные переменные
Храниться глобальные переменные будут в статичных полях базового класса, от которого будут наследоваться все классы враппера.
Такие переменные как контекст OpenGL и окно являются уникальными для каждого потока, поэтому их объявим как локальные переменные потока.
Для объявление локальной переменной потока есть различные спецификаторы:
__thread // для GCC __declspec(thread ) // для Microsoft Visual C++ thread_local // если поддерживается стандарт C++11
Объявим макрос THREAD_LOCAL который в зависимости от компилятора будет подставлять нужный спецификатор.
Базовый класс
class CBaseObject { protected: static THREAD_LOCAL GLContext const * _s_pContext; static THREAD_LOCAL CWindow * _s_pWindow; static CVertexArraysManager * _s_pVAOMan; static CCore * _s_pCore; public: static GLContext const * s_GetGLContext() { return _s_pContext; } static CVertexArraysManager * s_GetVAOManager( ) { return _s_pVAOMan; } static CWindow * s_GetWindow( ) { return _s_pWindow; } static CCore * s_GetCore( ) { return _s_pCore; } };
Окно
Окна (или потоки) работают с очередью операций.
Каждое окно содержит свой OpenGL контекст. При вызове функции из OpenGL версии 1.1 они автоматически направляются тому контексту в котором были вызваны, но при использовании расширений перенаправление не работает и его приходится делать вручную. Для этого создадим структуру хранящую указатели на функции:
struct GLContext { // функции PFNGLACTIVETEXTUREPROC __glActiveTexture; ... // константы bool __GL_ARB_texture_non_power_of_two; ... GLContext(); // получить адреса функций и проверить поддерживаемые расширения bool Init( ); };
Для вызова функций и обращений к константам служат макросы:
// вызывает статичный метод класса CBaseObject // для получения указателя на текущий контекст #define GL_CALL(_func ) s_GetGLContext( )->_func // функции #define glActiveTexture GL_CALL( __glActiveTexture ) ... // константы #define GLCONST_ARB_texture_non_power_of_two GL_CALL( __GL_ARB_texture_non_power_of_two )
Для перенаправления вызовов служит локальная переменная потока _s_pContext объявленная в класса CBaseObject.
Указатель на структуру GLContext устанавливается тогда же когда происходит установка контекста рендеринга:
// сделать контекст текущим void CWindow::MakeContextCurrent() const { _s_pContext = &_sGLContext; if ( ::wglGetCurrentContext( ) != _hRC ) ::wglMakeCurrent( _hDC, _hRC ); } // сбросить контекст рендеринга void CWindow::ResetContext( ) const { ::wglMakeCurrent( _hDC, NULL ); _s_pContext = NULL; }
Используется два класса окна: CMainWindow и CSharedWindow.
CMainWindow - главное окно в которое идет рендеринг.
CSharedWindow - невидимое окно для общего контекста OpenGL, работает в отдельном потоке.
Типы окон:
MAIN - главное окно
LOAD - поток загрузки ресурсов
RENDER - поток рендеринга
Типы окон нужны для правильного распределения операций - в загрузочном потоке операции выполняются достаточно долго из-за низкой скорости чтения с диска, а более быстрые операции рендеринга будут дожидаться своей очереди.
Ядро
Ядро инициализирует окно и потоки, управляет добавлением операций в потоки.
Класс ядра выглядит так: