Привет!
Интернет полнится вопросами: "Как работать с OpenGL из потоков?". Невооруженным глазом видно, что проблема серьезная, касающаяся многих пользователей этой замечательной графической библиотеки. Коснулась она и меня. В моей программе графическими вопросами занимается, естественно, отдельная DLL-ка. ( В скобках следует отметить, что сейчас у нас графический движек реализован в DirectX, но я с удовольствием двигаюсь в сторону OpenGL :-) ) Вот пример тестовой программы, которая иллюстрирует, что мне нужно получить:
Здесь в левой части отображается ультразвуковое изображение с датчика (не реальное, конечно, это тестовый сигнал). Эти кадры формируются с периодом следования порядка 20-50 fps. В правой части - лента, которая едет справа налево и показывает выбранный срез (или луч) из левого кадра (этот сигнал тоже не реальный, берется из тестового массива). Таким образом можно выбрать интересующее место на левом кадре (с помощью маркера-линии; сейчас он не показан) и наблюдать изменение по времени в этом месте, например, работу сердечного клапана и т.п. Частота поступления этих лучей составляет от 4 до более сотни миллисекунд (это зависит от того, какое время на всю ленту выбрал доктор).
Исходная ультразвуковая информация поступает из железа и совершенно не синхронна между собой. Ультразвуковые кадры поступают целиком и помещаются в свою текстуру. Отдельные M-лучи (что в ленте) - в свою. Причем, если лучи следуют слишком быстро, то они собираются в пачки по несколько штук, чтобы результирующий fps был порядка 30-50.
Режимов работы прибора - дофига :-) Отображательных потоков - тоже. Получается такая петрушка: при старте программы инициализируется OpenGL со всеми нужными объектами. Затем, в зависимости от выбранного режима работы, запускаются один или несколько потоков отображения ультразвука конкретного режима работы.
Сложность тут в том, что контекст рендеринга привязан к потоку. Поэтому просто так залезать, например, в текстуру с новыми данными из некоторого потока отображения - не катит. В Интернете дают советы, что де мол, нужно как инициализацию OpenGL, так и работу с ним организовать в одном потоке. Но в моем случае это невозможно! На момент инициализации приборной программы потоков отображения вообще не существует. Это раз. Второе, таких потоков может быть более одного. Городить какой-то один единственный поток, который был бы "на все руки мастер" - можно, но как-то это некошерно...
Что же делать? Гугл подсказывает нам, что есть такая замечательная функция:
wglShareLists. Она действительно работает, но радости мне она не принесла... Если однопотоковый рендер этого кадра на моей видеокарте занимает порядка 200 микросекунд, то с этой функцией - примерно 3.7 миллисекунд. Это в
15-18 раз медленнее! Причем, такое время возникает даже в том случае, когда второй поток вообще никакого рендеринга не производит! ( Понятно, ведь драйвер - не Господь Бог, он не знает, будет-ли производиться рендеринг в следующую секунду. Но мне-то от этого не легче! :-) )
В этот момент стала страдать моя репа... От усиленного почесывания... :-) Я подумал, что можно ведь захватить контекст и в другом потоке, только он должен быть свободен, т.е. не должно быть активного контекста. Ну-ка, ну-ка! Я измерил время, которое занимает пара вызовов
wglMakeCurrent(m_hDC, m_hRC)/wglMakeCurrent(NULL, NULL). Оказалось, что это время исчезающе мал
о: всего лишь около 40 микросекунд! Прекрасно!
Тогда я сделал следующее. Я сотворил две функции
RC_open()/RC_close(), которые включают мой контекст и выключают его. И "обернул" ими все те внешние функции моей графической DLL-ки, которые имеют дело с OpenGL-ресурсами. Получилось что-то вроде такого кода:
GLENGINE_API void GLEngine_PaneM_SetViewp(int x,int y,int width,int height,int num_of_beams_to_plot)
{
GLEngine_Locker.Lock();
if( RC_open() ) { // RC_open checks Plotter.m_Ready
Plotter.m_PaneM->SetViewport(x, y, width, height, num_of_beams_to_plot);
RC_close();
}
GLEngine_Locker.Release();
}
Это образец функции для задания вьюпорта той самой ленты (правая часть скриншота). Locker - это примитивнейший класс, инкапсулирующий функционал критической секции, как самого быстрого механизма упорядочивания доступа к разделяемым ресурсам Винды (мьютексы заметно медленнее).
Испытывая радостное возбуждение, я запустил свой тестовый пример и обнаружил, что рендеринг из отдельного потока практически
нисколько не замедлился! Ура!! Эти 40 микросекунд - пустяки, в пределах погрешности измерения (Винда ведь, чай, не RTOS). Как было 200 с небольшим микросекунд, так и осталось!
Вот таков мой опыт в этом вопросе. Какие будут замечания, вопросы, критика?