我的光线追踪器目前是多线程的,我基本上是将图像分成系统拥有的尽可能多的块并将它们并行渲染。但是,并非所有块都具有相同的渲染时间,因此大多数时间的一半运行时只有 50% 的 CPU 使用率。
法典
std::shared_ptr<bitmap_image> image = std::make_shared<bitmap_image>(WIDTH, HEIGHT);
auto nThreads = std::thread::hardware_concurrency();
std::cout << "Resolution: " << WIDTH << "x" << HEIGHT << std::endl;
std::cout << "Supersampling: " << SUPERSAMPLING << std::endl;
std::cout << "Ray depth: " << DEPTH << std::endl;
std::cout << "Threads: " << nThreads << std::endl;
std::vector<RenderThread> renderThreads(nThreads);
std::vector<std::thread> tt;
auto size = WIDTH*HEIGHT;
auto chunk = size / nThreads;
auto rem = size % nThreads;
//launch threads
for (unsigned i = 0; i < nThreads - 1; i++)
{
tt.emplace_back(std::thread(&RenderThread::LaunchThread, &renderThreads[i], i * chunk, (i + 1) * chunk, image));
}
tt.emplace_back(std::thread(&RenderThread::LaunchThread, &renderThreads[nThreads-1], (nThreads - 1)*chunk, nThreads*chunk + rem, image));
for (auto& t : tt)
t.join();
我想将图像分成 16x16 块或类似的东西并平行渲染它们,因此在每个块渲染后,线程切换到下一个,依此类推......这将大大增加 CPU 使用率和运行时间。
如何设置光线追踪器以多线程方式渲染这些 16x16 块?
我假设问题是"如何将块分发到各个线程?
在当前解决方案中,需要提前确定区域并将其分配给线程。 诀窍是颠覆这个想法。 让线程询问每当他们完成一大块工作时下一步该做什么。
以下是线程将执行的操作的概述:
void WorkerThread(Manager *manager) {
while (auto task = manager->GetTask()) {
task->Execute();
}
}
因此,您创建一个管理器对象,该对象在线程每次调用其 GetTask 方法时返回一个工作块(以任务的形式)。 由于该方法将从多个线程调用,因此必须确保它使用适当的同步。
std::unique_ptr<Task> Manager::GetTask() {
std::lock_guard guard(mutex);
std::unique_ptr<Task> t;
if (next_row < HEIGHT) {
t = std::make_unique<Task>(next_row);
++next_row;
}
return t;
}
在此示例中,经理创建一个新任务来光线跟踪下一行。 (如果您愿意,可以使用 16x16 块而不是行。 当所有任务都发出后,它只返回一个空指针,这实质上是告诉调用线程没有什么可做的了,然后调用线程将退出。
如果您提前完成了所有任务,并让经理按照要求分发它们,这将是一个典型的"工作队列"解决方案。 (常规工作队列还允许动态添加新任务,但对于此特定问题,您不需要该功能。
我的做法有点不同:
-
获取 CPU 和/或内核数
您没有指定操作系统,因此您需要为此使用操作系统API。 搜索系统关联掩码。
-
将屏幕划分为多个线程
我用线而不是 16x16 块划分屏幕,所以我不需要有 que 或其他东西。只需为每个CPU/内核创建线程,该线程将仅处理其水平线射线。这很简单,因此每个线程的
ID
数应从零开始计数,CPU/内核数n
因此属于每个进程的行是:y = ID + i*n
如果
i={0,1,2,3,... }
一次y
变大或相等,则屏幕分辨率停止。这种类型的访问有其优点,例如通过 ScanLine 访问屏幕缓冲区不会在线程之间发生冲突,因为每个线程只访问其行......我还为每个线程设置了亲和掩码,因此它使用自己的CPU/内核,只是它给了我一个小的提升,所以没有那么多的进程切换(但这在较旧的操作系统版本上很难说它现在做什么)。
-
同步线程
基本上你应该等到所有线程都完成。 如果是,则在屏幕上呈现结果。您的线程可以停止,您将在下一帧创建新线程,或者跳转到
Sleep
循环,直到再次强制渲染......我正在使用后一种方法,所以我不需要一遍又一遍地创建和配置线程,但要注意
Sleep(1)
可以睡得更多,然后只是1 ms
.