Jef*_*aka -2 c++ opengl gpu mandelbrot
我是 OpenGL 的新手。我的第一个项目是渲染一个 mandelbrot 集(我觉得这很有趣)并且由于必须完成的计算的性质,我认为最好在 GPU 上进行(基本上我在每个复杂平面的一部分的点,很多时间,我根据输出为这个点着色:大量可并行计算,这对 GPU 来说似乎不错,对吧?)。
因此,当单个图像没有太多计算时一切正常,但是一旦像素*迭代超过大约 90 亿,程序就会崩溃(显示的图像显示只计算了其中的一部分,青色部分是初始背景):
事实上,如果计算总数低于这个限制但足够接近(比如 85 亿),它仍然会崩溃,但需要更多的时间。所以我猜有一些问题不会出现在足够少的计算中(在它到达那里之前它总是完美无缺地工作)。我真的不知道它可能是什么,因为我真的很陌生。当程序崩溃时,它说:“Mandelbrot Set.exe 中 0x000000005DA6DD38 (nvoglv64.dll) 处的未处理异常:请求致命程序退出。”。它也是在那里指定的相同地址(它仅在我退出 Visual Studio,我的 IDE 时才会更改)。
那么这里是整个代码,加上着色器文件(顶点着色器不做任何事情,所有计算都在片段着色器中):编辑:这是项目所有 .cpp 和 .h 文件的链接,代码太大而不能放在这里,无论如何都是正确的(虽然远非完美); https://github.com/JeffEkaka/Mandelbrot/tree/master
这是着色器:
NoChanges.vert(顶点着色器)
#version 400
// Inputs
in vec2 vertexPosition; // 2D vec.
in vec4 vertexColor;
out vec2 fragmentPosition;
out vec4 fragmentColor;
void main() {
gl_Position.xy = vertexPosition;
gl_Position.z = 0.0;
gl_Position.w = 1.0; // Default.
fragmentPosition = vertexPosition;
fragmentColor = vertexColor;
}
Run Code Online (Sandbox Code Playgroud)
CalculationAndColorShader.frag(片段着色器)
#version 400
uniform int WIDTH;
uniform int HEIGHT;
uniform int iter;
uniform double xmin;
uniform double xmax;
uniform double ymin;
uniform double ymax;
void main() {
dvec2 z, c;
c.x = xmin + (double(gl_FragCoord.x) * (xmax - xmin) / double(WIDTH));
c.y = ymin + (double(gl_FragCoord.y) * (ymax - ymin) / double(HEIGHT));
int i;
z = c;
for(i=0; i<iter; i++) {
double x = (z.x * z.x - z.y * z.y) + c.x;
double y = (z.y * z.x + z.x * z.y) + c.y;
if((x * x + y * y) > 4.0) break;
z.x = x;
z.y = y;
}
float t = float(i) / float(iter);
float r = 9*(1-t)*t*t*t;
float g = 15*(1-t)*(1-t)*t*t;
float b = 8.5*(1-t)*(1-t)*(1-t)*t;
gl_FragColor = vec4(r, g, b, 1.0);
}
Run Code Online (Sandbox Code Playgroud)
我正在使用 SDL 2.0.5 和 glew 2.0.0,我相信最后一个版本的 OpenGL。代码已在 Visual Studio(我相信是 MSVC 编译器)上编译,并启用了一些优化。此外,即使在我的 gpu 计算中我也使用双打(我知道它们超慢,但我需要它们的精度)。
您需要了解的第一件事是 GPU(以及一般而言,大多数异构架构)上的“上下文切换”与 CPU/主机架构上的不同。当您向 GPU 提交任务时(在本例中为“渲染我的图像”),GPU 将单独处理该任务直至完成。
我自然地抽象了一些细节:Nvidia 硬件将尝试在未使用的内核上安排较小的任务,并且所有三个主要供应商(AMD、Intel、NVidia)都有一些微调的行为,这使我的上述概括复杂化,但作为一个原则上,您应该假设提交给 GPU 的任何任务都将消耗 GPU 的全部资源,直到完成。
就其本身而言,这不是一个大问题。
但是在 Windows(和大多数消费者操作系统)上,如果 GPU 在单个任务上花费太多时间,操作系统将假定 GPU 没有响应,并且会执行几种不同的操作之一(或者可能是多个操作的一个子集)其中):
确切的时间会有所不同,但您通常应该假设,如果单个任务花费的时间超过 2 秒,程序就会崩溃。
那么你如何解决这个问题呢?好吧,如果这是基于 OpenCL 的渲染,那就很简单了:
std::vector<cl_event> events;
for(int32_t x = 0; x < WIDTH; x += KERNEL_SIZE) {
for(int32_t y = 0; y < HEIGHT; y += KERNEL_SIZE) {
int32_t render_start[2] = {x, y};
int32_t render_end[2] = {std::min(WIDTH, x + KERNEL_SIZE), std::min(HEIGHT, y + KERNEL_SIZE)};
events.emplace_back();
//I'm abstracting the clSubmitNDKernel call
submit_task(queue, kernel, render_start, render_end, &events.back(), /*...*/);
}
}
clWaitForEvents(queue, events.data(), events.size());
Run Code Online (Sandbox Code Playgroud)
在 OpenGL 中,您可以使用相同的基本原理,但由于 OpenGL 模型的抽象程度非常荒谬,因此事情变得更加复杂。因为驱动程序想要将多个绘制调用捆绑到一个底层硬件的单个命令中,所以您需要明确地让它们表现自己,否则驱动程序会将它们捆绑在一起,即使您会遇到完全相同的问题你写它是为了专门分解任务。
for(int32_t x = 0; x < WIDTH; x += KERNEL_SIZE) {
for(int32_t y = 0; y < HEIGHT; y += KERNEL_SIZE) {
int32_t render_start[2] = {x, y};
int32_t render_end[2] = {std::min(WIDTH, x + KERNEL_SIZE), std::min(HEIGHT, y + KERNEL_SIZE)};
render_portion_of_image(render_start, render_end);
//The call to glFinish is the important part: otherwise, even breaking up
//the task like this, the driver might still try to bundle everything together!
glFinish();
}
}
Run Code Online (Sandbox Code Playgroud)
的确切外观render_portion_of_image需要您自己设计,但基本思想是向程序指定仅渲染render_start和之间的像素render_end。
您可能想知道 的值KERNEL_SIZE应该是多少。这是你必须自己试验的东西,因为它完全取决于你的显卡有多强大。值应该是
根据我个人的经验,最好的确定方法是在程序开始之前进行一堆“测试”渲染,在 Mandelbrot 集中央灯泡的 32x32 图像上,以逃逸算法的 10,000 次迭代渲染图像(一次渲染,不会破坏算法),并查看需要多长时间。我使用的算法基本上是这样的:
int32_t KERNEL_SIZE = 32;
std::chrono::nanoseconds duration = 0;
while(KERNEL_SIZE < 2048 && duration < std::chrono::milliseconds(50)) {
//duration_of is some code I've written to time the task. It's best to use GPU-based
//profiling, as it'll be more accurate than host-profiling.
duration = duration_of([&]{render_whole_image(KERNEL_SIZE)});
if(duration < std::chrono::milliseconds(50)) {
if(is_power_of_2(KERNEL_SIZE)) KERNEL_SIZE += KERNEL_SIZE / 2;
else KERNEL_SIZE += KERNEL_SIZE / 3;
}
}
final_kernel_size = KERNEL_SIZE;
Run Code Online (Sandbox Code Playgroud)
我建议的最后一件事是使用OpenCL来完成渲染 mandelbrot 集本身的繁重工作,并使用 OpenGL(包括 OpenGL??OpenCL Interop API!)在屏幕上实际显示图像。在技术层面上,OpenCL 既不会比 OpenGL 快也不会慢,但它让您可以对您执行的操作进行很多控制,并且更容易推理 GPU 正在做什么(以及您需要做什么)改变其行为)当您使用比 OpenGL 更明确的 API 时。如果您想坚持使用单个 API,您可以改用 Vulkan,但由于 Vulkan 的级别非常低,因此使用起来非常复杂,除非您能够应对挑战,否则我不建议您这样做。
编辑:其他一些事情:
floats 渲染,另一个用doubles渲染。在我的版本这个节目的,其实我有一个使用两个版本的float数值模拟double,描述在这里。在大多数硬件上,这可能会更慢,但在某些架构(特别是 NVidia 的 Maxwell 架构)上,如果处理floats的速度足够快,它实际上可以仅以double绝对数量级来超越:在某些 GPU 架构上,floats 比doubles快 32 倍。| 归档时间: |
|
| 查看次数: |
1307 次 |
| 最近记录: |