Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Frustum Culling (3 стр)

Frustum Culling (3 стр)

Автор:

Многопоточность (Multithreading).

Современные процессоры имеют несколько ядер. Вычисления могут выполняться одновременно, параллельно над данными.

Архитектуру современных игр нужно выстраивать с учетом многопоточности, т.е. разбивать работу на независимые части и выполнять параллельно, нагружая равномерно все ядра процессора. В проектировании стоит быть гибким. Слишком большое количество мелких задач ведет к увеличению оверхеда, связанным с синхронизацией между процессорами и переключение между задачами. Слишком маленькое количество но больших задач ведет к неравномерной загрузке ядер. Тут нужен баланс. В современных играх от нескольких сотен до тысячи задач/работ на фрейм.

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

Разумеется не стоит запрашивать результат сразу же после начала выполнения работ. Плюс стоит быть гибким и разбивать работу на оптимальное количество частей. Но это уже вопросы конкретной реализации / проекта.

Worker::Worker() : first_processing_oject(0), num_processing_ojects(0)
{
  //создаем 2 события: 1.сигнализирующее что у нас есть работа 2.сигнализирующее что работа выполнена
  has_jobs_event = CreateEvent(NULL, false, false, NULL); //есть новая работа ?
  jobs_finished_event = CreateEvent(NULL, false, true, NULL); //работа выполнена ?
}

void Worker::doJob()
{
  //собственно, выполнить свою часть работы
  cull_objects(first_processing_oject, num_processing_ojects);
}


//основная функция потока
unsigned __stdcall thread_func(void* arguments)
{
  printf("In thread...\n");
  Worker *worker = static_cast<Worker*>(arguments);

  //каждый рабочий поток имеет бесконечный цикл.
  //Работа прекратится только когда мы получим сигнал от программы (флаг stop_work)
  while (true)
  {
    //ждем начала работы
    //если работы нет, просто ждем (ждем события has_jobs_event).
    //Ожидая, мы не тратим время ЦПУ. Так устроены события.
    WaitForSingleObject(worker->has_jobs_event, INFINITE);

    //если получили сигнал о прекращении работ - выходим из бесконечного цикла
    if (worker->stop_work)
      break;

    //собственно - выполнить работу
    worker->doJob();

    //просигналим, что работа выполнена и можно забирать результат
    SetEvent(worker->jobs_finished_event);
  }

  _endthreadex(0);
  return 0;
}


void create_threads()
{
  //создаем потоки
  //разбиваем задачу на части, распределяем работу между потоками
  int worker_num_processing_ojects = MAX_SCENE_OBJECTS / num_workers;
  int first_processing_oject = 0;

  int i;
  for (i = 0; i < num_workers; i++)
  {
    //создаем поток в который передаем параметр &workers[i]
    workers[i].thread_handle =
      (HANDLE)_beginthreadex(NULL, 0, &thread_func, &workers[i], CREATE_SUSPENDED, &workers[i].thread_id);
    thread_handles[i] = workers[i].thread_handle;

    //установим параметры нашего рабочего, какую часть работы он выполняет
    workers[i].first_processing_oject = first_processing_oject;
    workers[i].num_processing_ojects = worker_num_processing_ojects;
    first_processing_oject += worker_num_processing_ojects;
  }

  //даем указания потокам начать выполнение, ожидать задач
  for (int i = 0; i < num_workers; i++)
    ResumeThread(workers[i].thread_handle);
}

void process_multithreading_culling()
{
  //говорим (сигналим) что у рабочих есть новая работа
  for (int i = 0; i < num_workers; i++)
    SetEvent(workers[i].has_jobs_event);
}

void wate_multithreading_culling_done()
{
  //ожидаем пока рабочие выполнят работу и просигналят об этом
  HANDLE wait_events[num_workers];
  for (int i = 0; i < num_workers; i++)
    wait_events[i] = workers[i].jobs_finished_event;

  //собственно здесь ждем выполнения всех работ.
  //Главный поток остановит свое выполнение и будет ждать рабочих.
  WaitForMultipleObjects(num_workers, &wait_events[0], true, INFINITE);
}

void do_cpu_culling()
{
  if (use_multithreading)
  {
    process_multithreading_culling(); //начать работу
    wate_multithreading_culling_done(); //подождать пока она выполнится в параллельных потоках
  } else
    cull_objects(0, MAX_SCENE_OBJECTS); //выполнить всю работу в главном потоке программы

  //собираем результаты видимых инстансов в 1 массив и пересылаем на GPU...
}

Таблица 3. Результаты: кулинг 100к объектов. Intel Core i5-4460 (4 полноценных ядра, без Hyper-threading). Многопоточность (4 потока). Только кулинг. Время в ms. В скобках — ускорение относительно обычной c++ реализации.

Метод Sphere AABB OBB
Simple c++ 0,92 (1) 1,42 (1) 9,14 (1)
SSE 0,26 (3,54) 0,46 (3,08) 3,48 (2,62)
Simple c++, Mulithreaded 0,25 (3,68) 0,4 (3,55) 2,5 (3,65)
SSE, Multithreaded 0,1 (9,2) 0,18 (7,89) 1 (9,14)

Многопоточная версия быстрее одно поточной в среднем в 3,6 раза если смотреть на обычную с++ реализацию.
Использовании SSE дает ускорение в 3 раза, относительно обычной реализации на с++.
Если использовать многопоточность и SSE вместе, то ускорение относительно обычной с++  реализации составляет 8,7 раз !
Т.е. в сумме мы соптимизировали вычисления почти в 9 раз, в зависимости от используемого примитива для кулинга.

Страницы: 1 2 3 4 5 Следующая »

8 февраля 2017

#Frustum Culling, #multithreading, #SSE


Обновление: 3 мая 2017

2001—2018 © GameDev.ru — Разработка игр