我是OpenGL的新手。我的第一个项目是渲染一个mandelbrot集(我觉得非常有趣),并且由于计算的性质必须要完成,我认为在GPU上做它们会更好(基本上我在每个上应用一个复杂的函数)复杂平面的一部分的点,很多时间,我根据输出着色这一点:许多可并行化的计算,这对GPU来说似乎不错,对吗?)。
所以当单个图像的计算量不是太多时,一切都运行良好但是一旦像素*迭代超过大约9亿,程序崩溃(显示的图像显示只有部分图像被计算出来,青色部分是初始背景):
Dark Part of the Mandelbrot Set not Fully Calculated
事实上,如果计算总数低于此限制但足够接近(比如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;
}
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);
}
我正在使用SDL 2.0.5和glew 2.0.0,我相信OpenGL的最新版本。代码已在Visual Studio(我相信的MSVC编译器)上编译,并启用了一些优化。另外,即使在我的gpu计算中我也在使用双打(我知道它们超慢,但我需要它们的精度)。
答案 0 :(得分:4)
您需要了解的第一件事是&#34;上下文切换&#34; GPU(主要是大多数异构架构)与CPU /主机架构不同。当您向GPU提交任务时 - 在这种情况下,&#34;渲染我的图像&#34; - 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());
在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();
}
}
render_portion_of_image
的确切外观是您自己需要设计的内容,但基本的想法是为程序指定render_start
和render_end
之间的像素将被渲染。
您可能想知道KERNEL_SIZE
应该是什么价值。这是你必须自己试验的东西,因为它完全取决于你的显卡有多强大。值应为
根据我的个人经验,确定的最佳方法是进行一系列测试&#34;在程序开始之前渲染,在Mandelbrot Set的中心灯泡的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;
我建议的最后一件事是使用OpenCL来重复提升渲染mandelbrot设置本身,并使用OpenGL(包括OpenGL←→OpenCL Interop API!)来实际显示屏幕上的图像。从技术层面来看,OpenCL既不比OpenGL更快也不慢,但是它可以让你对你执行的操作有很多控制,并且更容易推断出GPU正在做什么(以及当你使用比OpenGL更明确的API时,你需要做的就是改变它的行为。你可以,如果你想坚持使用单一的API,可以使用Vulkan,但由于Vulkan非常低级,因此使用非常复杂,除非你接受挑战,否则我不建议这样做
编辑:其他一些事情:
float
呈现,另一个用double
呈现。在我的此程序版本中,我实际上有一个版本使用两个float
值来模拟double
,如here所述。在大多数硬件上,这可能会更慢,但在某些架构(特别是NVidia的Maxwell架构)上,如果处理速度float
足够快,它实际上可以仅仅通过纯粹的表现胜过double
幅度:在某些GPU架构上,float
比double
快32倍。