本文特指使用CUDA在NVIDIA显卡上的编程。
现代计算机整个系统的运算核心、控制核心。处理依赖性非常高的事情。包括Control控制单元、ALU运算单元、Cache显存等。
可进行绘图运算工作的专用微处理器,是连接计算机和显示终端的纽带。处理依赖性非常低的事情,因为有非常多的计算单元高并发。拥有更多的计算单元:
计算机的渲染,是通过CPU去计算然后一行一行的去扫描显示到显示器上,这种一行一行扫描显示利用的是人的视觉停留来完成显示。
光栅扫描显示;
图像是由像素阵列组成, 显示⼀个图像时间(显示整个光栅所需的时间)和图像复杂度⽆关,因为屏幕上的像素点都要扫描到。
因为视觉有暂留,所以当1秒显示连贯的16帧及以上就可以认为没有卡顿。
而我们要显示在页面上的并不是我们常见的png、jpg格式的图片,这些是压缩图,要显示的是位图(位图的大小也就是图片的宽高4(RGBA,红黄蓝,透明度))。
如果说这个屏幕上半部分正在扫描显示上一帧的位图数据,到下半部分的时候,帧缓存去完成了计算后新的帧缓冲区数据。那么下半部分显示的是新的位图数据,这样上下是对不上的,这就是屏幕撕裂。
屏幕撕裂通过双缓存,三缓存,垂直同步等技术来避免。
其实是两个帧缓冲区,屏幕想去读取一个缓冲区上的内容,系统这个时候回去渲染下一个要显示的内容并存放到梁歪一个帧缓冲区中。然后屏幕切换去读取先渲染后存放的缓冲区内的内容,系统继续渲染下一个要显示的内容,同步存放到之前的那个缓冲区。
例如:GPU开辟A、B两个帧缓冲区。当GPU处理完一帧图像数据后存入A帧缓冲区,给A帧缓冲区加锁。屏幕开始读取A帧缓冲区数据,GPU处理完新的一帧数据,因为A有锁,就会存入B帧缓冲区并加锁。屏幕读取显示完A帧缓冲区后,A帧缓冲区解锁,去读B帧缓冲区。
为了解决屏幕撕裂的问题,引入了垂直信号的概念。也就是在CPU 和GPU 处理数据的时候,等待垂直信号,垂直信号到来的时候,CPU 和GPU 都处理完了,那么就去存入帧缓冲区,否者不会。
在显示器显示之前,必须完成从 数据 -> 位图 的渲染,最后才送交帧缓冲区等待显示。
顶点数据(存在内存中,CPU处理)-> 几何运算-> 光栅化-> 片元/像素 -> 位图 -> 显示。再详细的分解,就会如下如所示:
CPU 处理存储在内存中的顶点数据(数组)
GPU 几何操作:将顶点数据进行顶点转换、坐标转换、旋转平移缩放或者光照计算等处理
GPU 顶点着色器处理:将顶点转成图元
GPU图元装配:连接信息线条
GPU 光栅化处理转成栅格化数据
GPU 片元着色器会对栅格化数据的每一个像素进行位运算,决定每一个像素的颜色
OpenGL(Open Graphics Library)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来绘制从简单的图形到比较复杂的三维景象。而另一种程序接口系统是仅用于Microsoft Windows上的Direct3D。OpenGL常用于CAD、虚拟现实、科学可视化程序和电子游戏开发。
OpenGL的高效实现(利用了图形加速硬件)存在于Windows,部分UNIX平台和Mac OS。这些实现一般由显示设备厂商提供,而且非常依赖于该厂商提供的硬件。开放源代码库Mesa是一个纯基于软件的图形API,它的代码兼容于OpenGL。但是,由于许可证的原因,它只声称是一个“非常相似”的API。
当今,OpenGL是视频行业领域中用于处理2D/3D图形的最为广泛接纳的API,在此基础上,为了用于计算机视觉技术的研究,从而催生了各种计算机平台上的应用功能以及设备上的许多应用程序。其是独立于视窗操作系统以及操作系统平台,可以进行多种不同领域的开发和内容创作,简而言之,其帮助研发人员能够实现PC、工作站、超级计算机以及各种工控机等硬件设备上实现高性能、对于视觉要求极高的高视觉图形处理软件的开发。
OpenGL规范由1992年成立的OpenGL架构评审委员会(ARB)维护。ARB由一些对创建一个统一的、普遍可用的API特别感兴趣的公司组成。根据OpenGL官方网站,2002年6月的ARB投票成员包括3Dlabs、Apple Computer、ATI Technologies、Dell Computer、Evans & Sutherland、Hewlett-Packard、IBM、Intel、Matrox、NVIDIA、SGI和Sun Microsystems,Microsoft曾是创立成员之一,但已于2003年3月退出。
作为独立于操作系统的开放的三维图形的软件开发包,在其基础上开发的应用程序能够简单方便的移植于各种平台。其具有七大功能:
建立3D模型:OpenGL除了能够处理一般的2D图形,即点、线、面的绘制外,主要任务是集合了3D立体的物体绘制函数。
图形变换:OpenGL利用基本变换以及投影变换处理图形。所谓的基本变换就是在处理2D平面图形时的平移、旋转、变比、镜像变换。投影变换就是在处理3D立体图形时的平行投影以及透视投影。通过变换方式,可以将2D的平面图形清晰明了的变换成3D的立体图形,从而在减少计算的时间的同时就能够提高了图形显示的速度。
颜色模式:OpenGL库中的颜色模型:使用较为广泛的RGBA模式以及颜色索引模式(color index)。
光照、材质的设置:OpenGL库中包含了多种光照的类型。材质是用光反射率来表示的。其原理是基于人眼的原理,场景中的物体是由光的红绿蓝的分量以及材质的红绿蓝的反射率的乘积后所形成的颜色值。
纹理映射:纹理指的是物体表面的花纹。OpenGL库中集合了对于物体纹理的映射处理方式,能够十分完整的复现物体表面的真实纹理。
图像增强功能和位图显示的扩展功能:OpenGL的功能包括像素的读写、复制外,以及一些特殊的图像处理功能:比如,融合、反走样、雾的等等特殊的处理方式。对于图像的重现和处理,可以使得效果更有真实感,逼真。
双缓存功能:OpenGL创新性的运用了双缓存形式。计算场景、生成画面图像、显示画面图像分别将其由前台缓存和后台缓存分开处理,大大提高了计算机的运算能力以及画面的显示速度。
这几个库也创建在OpenGL之上,提供了OpenGL本身没有的功能:
GLFW,GLUT,GLEW、GLEE。对OpenGL进行扩展,或者创建OpenGL窗口。OpenGL Performer,由SGI开发并可以在IRIX、Linux和Microsoft Windows的一些版本上使用,构建于OpenGL,可以创建实时可视化仿真程序。
当开发者需要使用最新的OpenGL扩展时,他们往往需要使用GLEW库或者是GLEE库提供的功能,可以在程序的运行期判断当前硬件是否支持相关的扩展,防止程序崩溃甚至造成硬件损坏。这类库利用动态加载技术(dlsym、GetProcAddress等函数)搜索各种扩展的信息。
OpenGL 的窗口上下文(OpenGL context)的创建过程相当复杂,在不同的操作系统上也需要不同的做法。因此很多游戏开发和用户界面库都提供了自动创建 OpenGL 上下文的功能,其中包括SDL、Allegro、SFML、FLTK、Qt等。也有一些库是专门用来创建 OpenGL 窗口的,其中最早的便是GLUT,后被freeglut取代,比较新的也有GLFW可以使用。
以下包可以用来创建并管理 OpenGL 窗口,也可以管理输入,但几乎没有除此以外的其它功能:
纯窗口:
GLFW——跨平台窗口和键盘、鼠标、手柄处理;偏向游戏。开发首选。
freeglut——跨平台窗口和键盘、鼠标处理;API 是 GLUT API 的超集,同时也比 GLUT 更新、更稳定。
GLUT——早期的窗口处理库,已不再维护,支持创建 OpenGL 窗口的还有一些“多媒体库”,同时还支持输入、声音等类似游戏的程序所需要的功能。
多媒体库:
Allegro 5——跨平台多媒体库,提供针对游戏开发的 C API。
SDL——跨平台多媒体库,提供 C API。
SFML——跨平台多媒体库,提供 C++ API;同时也提供 C#、Java、Haskell、Go 等语言的绑定窗口包。
UI库:
FLTK——小型的跨平台 C++ 窗口组件库。
Qt——跨平台 C++ 窗口组件库,提供了许多 OpenGL 辅助对象,抽象掉了桌面版 OpenGL 与 OpenGL ES 之间的区别。
wxWidgets——跨平台 C++ 窗口组件库。
OpenGL相对于市面上主流的视频处理软件具有如下的优势:
具有强大的通用性和可移植性,可以将其轻松的移植在多个不同的平台上进行二次开发。因为OpenGL本身是一个与硬件无关的软件接口,所以,通用于市面上较为流行的平台:比如,Windows、Unix、Linux、MacOS等。
能够转换3D图形设计软件制作的模型文件。OpenGL仅仅作为一个图形的底层图形库,并没有提供直接描述某个场景地几何实体的圆元。但是,为了其转换的方便性,其内部集合了许多转换函数,可以快速方便的将3DS/3DSMAX、AutoCAD等3D绘制的图形设计制作出的DXF以及3DS模型的文件转换成数组的形式,从而将图像转换成数据进行编程处理。
配备了高级图形库:Open Inventor、Cosmo3D、Optimizer等。这些软件库针对于创建、编辑以及处理分析三维的立体场景提供了高级的应用程序单元;提高不同类型的图形格式的交换数据的能力
推荐安装glfw和glad。
Glfw提供了opengl窗口及上下文的创建。Glad提供了opengl接口的导出。
包含glad头文件,实现文件,glfw头文件和库。
新建控制台程序,建立main.cpp,拷贝如下代码:
#include
#include
int main()
{
// 初始化 GLFW
if (!glfwInit())
{
return -1;
}
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "Triangle Example", nullptr, nullptr);
if (!window)
{
glfwTerminate();
return -1;
}
// 将当前窗口设置为上下文
glfwMakeContextCurrent(window);
// 初始化 GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
glfwTerminate();
return -1;
}
// 主循环
while (!glfwWindowShouldClose(window)) {
// 渲染和事件处理
// 清除颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
// 绘制三角形
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f); // 设置顶点颜色
glVertex2f(-0.6f, -0.4f); // 第一个顶点
glColor3f(0.0f, 1.0f, 0.0f);
glVertex2f(0.6f, -0.4f); // 第二个顶点
glColor3f(0.0f, 0.0f, 1.0f);
glVertex2f(0.0f, 0.6f); // 第三个顶点
glEnd();
// 交换缓冲区和处理事件
glfwSwapBuffers(window);
glfwPollEvents();
}
// 清理和终止
glfwTerminate();
return 0;
}
如果一切配置成功,运行后显示:
对于显存中的图像,如何不拷贝到CPU,直接渲染到界面上?
CUDA提供了和OpenGL纹理对象绑定的接口,可以使用cuda核函数直接对OpenGL纹理进行修改。
int main()
{
int nDrawIndex = 0;
StopWatchInterface* timer = NULL;
sdkCreateTimer(&timer);
//glfw初始化
glfwInit();
//glfw创建窗口
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "CUDA DRAW", nullptr, nullptr);
if (window == nullptr)
{
printf("创建窗口失败");
//终止
glfwTerminate();
return -1;
}
//设置当前OpenGL上下文
glfwMakeContextCurrent(window);
// glad初始化
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
printf("加载失败");
return -1;
}
// 创建OpenGL纹理对象
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 设置纹理参数和格式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
//创建显存
int memSize = SCR_WIDTH * SCR_HEIGHT * 4 * sizeof(unsigned char);
// 分配CUDA显存用于图像数据
uint8_t* h_idata = nullptr;
cudaHostAlloc((void**)&h_idata, memSize, cudaHostAllocDefault);
//拷贝到显存
uint8_t* cudaImage = nullptr;
cudaMalloc((void**)&cudaImage, memSize);
cudaMemcpy(cudaImage, h_idata, memSize, cudaMemcpyHostToDevice);
//将纹理对象注册到CUDA,可供cuda的接口访问
cudaGraphicsResource* cudaResource;
cudaGraphicsGLRegisterImage(&cudaResource, textureID, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsReadOnly);
//循环
while (!glfwWindowShouldClose(window))
{
sdkResetTimer(&timer);
sdkStartTimer(&timer);
// 清空缓冲区
glClear(GL_COLOR_BUFFER_BIT);
// 映射注册的纹理对象到显存指针
cudaArray* cudaArrayPtr;
cudaGraphicsMapResources(1, &cudaResource, 0);
cudaGraphicsSubResourceGetMappedArray(&cudaArrayPtr, cudaResource, 0, 0);
//修改显存数据,并复制到纹理
offsetGridImage(cudaImage, SCR_WIDTH, SCR_HEIGHT, nDrawIndex);
nDrawIndex++;
// 将CUDA显存中的数据复制到OpenGL纹理cudaArrayPtr
cudaMemcpy2DToArray(cudaArrayPtr, 0, 0, cudaImage, SCR_WIDTH * sizeof(unsigned char) * 4,
SCR_WIDTH * sizeof(unsigned char) * 4, SCR_HEIGHT, cudaMemcpyDeviceToDevice);
// 解除映射
cudaGraphicsUnmapResources(1, &cudaResource, 0);
// 使用OpenGL绘制纹理
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, textureID);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex2f(-1.0f, -1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex2f(1.0f, -1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex2f(1.0f, 1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex2f(-1.0f, 1.0f);
glEnd();
glDisable(GL_TEXTURE_2D);
// 交换缓冲区和处理事件
glfwSwapBuffers(window); //这个函数会等待到刷新时机
glfwPollEvents();
sdkStopTimer(&timer);
float elapsedTimeInMsCPU = sdkGetTimerValue(&timer);
std::cout << "time:" << elapsedTimeInMsCPU << std::endl;
}
sdkDeleteTimer(&timer);
// 清理和释放资源
cudaGraphicsUnregisterResource(cudaResource);
glDeleteTextures(1, &textureID);
glfwTerminate();
//释放显存
cudaFreeHost(h_idata);
cudaFree(cudaImage);
return 0;
}
其中OffsetGrid核函数实现如下:
__global__ void offsetKernel(unsigned char* imageData, int width, int height, int time)
{
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.y;
if (x >= width && y >= height) return;
int index = (y * width + x) * 4;
//根据time,生成色块位置 色块大小为100*100
time %= width - 100;
if (y > height / 2 && y< height / 2 + 100 && x>time && x < time + 100)
{
// 白色
imageData[index] = 255;
imageData[index + 1] = 0;
imageData[index + 2] = 0;
imageData[index + 3] = 255;
}
else
{
// 白色
imageData[index] = 255;
imageData[index + 1] = 255;
imageData[index + 2] = 255;
imageData[index + 3] = 255;
}
}
void offsetGridImage(void* cudaImage, int imageWidth, int imageHeight,int time)
{
dim3 blockDims(16, 16);
dim3 gridDims((imageWidth + blockDims.x - 1) / blockDims.x, (imageHeight + blockDims.y - 1) / blockDims.y);
offsetKernel << <gridDims, blockDims >> > ((unsigned char*)cudaImage, imageWidth, imageHeight, time);
}
在opengl窗口中出现一个红色色块往右运动,运动的速度取决于屏幕的刷新频率。