Войти
IrrlichtСтатьи

Рендер сцен Irrlicht в отдельном потоке

Автор:

Статья рассказывает как уменьшить загрузку процессора, разделив работу программы на два потока. Производительность программы, при этом повышается даже на одно ядерных процессорах.

Статья рассказывает о том, как выполнять просчёт сцен Irrlicht в отдельном потоке.
Исходный код для статьи находится тут

    Какие преимуществах даёт вынесение рендера в отдельный поток?

1. Загрузка процессора может снижаться до 0%
2. Значительно снижается время реакции на ввод
3. Время реакции на ввод не зависит от ограничения FPS
4. Как следствие из пунктов 1 - 3: максимально эффективное использование времени процессора

    Каким образом это достигается?

1. Процессор обрабатывает события ввода только тогда, когда они происходят
2. Рендер работает только тогда, когда надо обновить кадр

      Irrlicht и много-поточность
    Irrlicht не использует потоки, и не знает о том, какой поток в данный момент использует какой либо объект irrlicht, или объект пользователя. При использовании нескольких потоков, пользователь библиотеки должен сам позаботиться о том, чтобы обеспечить корректный доступ разными потоками к объектам, которые они (потоки) используют вместе. Под корректным доступом, здесь понимается такой доступ, при котором один поток обращается к общему объекту, тогда когда другой поток его не использует. Такой подход называется синхронизацией. Для обеспечения синхронизации в Win32 API предусмотрено несколько способов, в этой статье мы будем использовать один, самый простой, основанный на мьютексах. Мьютесксы выбраны, не только по причине простоты, а из за того что этот механизм синхронизации возможно реализовать на платформах, отличных от Windows.
   
    Код программы
    При подготовке этой статьи за основу был взят пример из Irrlicht SDK 1.4.1 под названием 02.Quake3Map.
    Мьютекс - это специальный объект системы, характеризуемый целым числом, хранимым в типе HANDLE. Доступ к нему должен быть возможен из любого потока программы, использующих общие объекты, поэтому он объявлен глобально:

HANDLE mutex;      // Объект синхронизации
Инициализируется mutex в main, следующим образом:
mutex = CreateMutex (NULL, FALSE, NULL);
   
    Для прозрачности понимания примера, обработка событий Irrlicht была вынесена в глобальную функцию по средством своего класса приёмника сообщений:
// Вызывает глобальную функцию для обработки сообщений irrlicht
class EventDispatcher : public IEventReceiver
{
  virtual bool OnEvent (const SEvent& irrEvent)
  {
    return eventHandler(irrEvent);
  }
  public:
  
  // Указатель на глобальную функцию-обработчик сообщений irrlicht
  bool (*eventHandler) (const SEvent& irrEvent);
};

    Работа программы разбивается на два потока: основной поток программы выполняющийся в функции main, и дополнительный, выполняющийся в своей глобальной функции renderWorker. Основной поток программы создаёт дополнительный,

// Создать поток для рендеринга
DWORD threadId; // Идентификатор потока
HANDLE thread = CreateThread(NULL, 0, renderWorker, NULL, 0, &threadId);
после чего обеспечивает обработку сообщений окна:
// Главный цикл обработки сообщений окна
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
  TranslateMessage(&msg);
  DispatchMessage(&msg);
}
    Обратите внимание, на то что используется вызов GetMessage, вместо PeekMessage. Это позволяет не выполнять цикл while, пока не придёт сообщение окна. Выполнение основного потока приостанавливается на вызове GetMessage, до наступления события ввода, таким образом процессор освобождается от бесполезного повторения цикла обработки сообщений.

Рендер и основной поток будут использовать общие данные (глобальные объекты):

// Глобальные объекты
IrrlichtDevice* device;  // Видео устр. глобально по отношению к программе
ISceneManager* smgr;  // Для примера, ISceneManager будет выступать как общие данные

    Для синхронизации доступа к этим объектам из разных потоков, каждый поток должен знать, когда другой поток не использует общий объект, для этого в Win32 API мы будем использовать две функции WaitForSingleObject и ReleaseMutex. В качестве аргумента в нашей программе они принимают глобальный объект mutex.
   
    Краткая справка, подробности смотри в MSDN. Функция WaitForSingleObject переводит mutex  в состояние "занято", если он был в состоянии "свободно", и далее возвращает выполнение в вызвавший поток. Если mutex находился в состоянии "занято",  то выполнение вызвавшего её потока останавливается, до тех пор пока mutex не перейдёт в состояние "свободно". Функция ReleaseMutex, переводит mutex в состояние "свободно"

    Таким образом, функция потока рендера примет следующий вид:

DWORD WINAPI renderWorker (void* arg)
{
  IVideoDriver* driver = device->getVideoDriver();
  while(device->run())
  {  
    // Переводим объект синхронизации в состояние "занято".
    // Теперь другой поток, вызвав WaitForSingleObject(mutex, INFINITE), будет
    // ждать, пока объект синхронизации не перейдёт в состояние "свободно"
    WaitForSingleObject(mutex, INFINITE);
    
    // Обращаемся к общим данным здесь. Например к ISceneManager и IVideoDriver
    driver->beginScene(true, true, video::SColor(0, 0, 0, 0));
    smgr->drawAll();
    driver->endScene();
    
    // Переводим объект синхронизации в состояние "свободно".
    // Теперь нельзя обращаться к общим данным
    ReleaseMutex(mutex);
    
    // Остонавить работу рендера, что бы уменьшить использование
    // времени текущим потоком
    device->yield();
  }
  return NULL;
}
    Вызов device->yield() в случае с загруженной картой из примера, позволяет снизить загрузку процессора до 0%.
   
    Функция обработки событий примет вид:
bool eventHandler (const SEvent& irrEvent)
{
  // Переводим объект синхронизации в состяние "занято".
  // Теперь другой поток, вызвав WaitForSingleObject(mutex, INFINITE), будет
  // ждать, пока объект синхронизации не перейдёт в состояние "свободно"
  WaitForSingleObject(mutex, INFINITE);
  
  // Обращаемся к общим данным здесь. Например к ISceneManager
  smgr->getActiveCamera()->OnEvent(irrEvent);
  
  // Переводим объект синхронизации в состяние "свободно".
  // Теперь нельзя обращаться к общим данным
  ReleaseMutex(mutex);
  return true;
}

    Обратите внимание, что перед тем как использовать общий объект, мы вызываем WaitForSingleObject, а после использования ReleaseMutex. Ход выполнения программы должен строиться таким образом, чтобы после каждого вызова WaitForSingleObject, обязательно шёл вызов ReleaseMutex. Если это не так, то выполнение программы остановится на вызове WaitForSingleObject, что приведёт к зависанию "на всегда" (deadlock) в потоке, вызвавшем WaitForSingleObject.

    Вызов smgr->getActiveCamera()->OnEvent(irrEvent), необходим, т.к. мы назначили своего обработчика событий:

// Этот объект будет вызывать глобальную функцию, при возникновении события Irrlicht
EventDispatcher dispatcher;

// Передать указатель на глобальную функцию-обработчик событий irrlicht
dispatcher.eventHandler = eventHandler;
  
// Передать в IrrlichtDevice указатель на приёмник событий irrlicht
device->setEventReceiver(&dispatcher);

  Заключение
    Мне хочется отметить, что переделав стандартный пример irrlicht SDK, вынеся задачу рендера в отдельный поток, я получил существенное снижение загрузки CPU при ограничении FPS на уровне 64 кадров/с, снизив её с 50 до 0 % (!), при том что появилось субъективное ощущение более быстрой реакции картинки на экране при движении мышью.

#render, #многопоточность

10 августа 2008 (Обновление: 26 дек. 2008)

Комментарии [9]