趁热打铁,实现无头渲染!
由于博客实在不是一个贴代码的好地方,接下来的文章里就只能讨论总体的设计,仅在比较关键的地方列出大体代码结构了。
刚刚上期我们完成了将 RGB 图像写入视频流的基本架构,现在我们需要想办法渲染出 RGB 图像了。而说到 2D 渲染,我脑海里出现的最合适的库是 SDL. 当然,如果后面需要支持渲染 3D 模型的话,使用 GLFW 也可以是一个选择。
SDL 可以在不创建(可见)窗体的情况下创建渲染器,并且支持渲染到材质的操作。这正好符合我们的需要。
不过,虽然说名义上是「无头」渲染,通过 SDL 渲染图形仍然要创建窗口出来。这意味着哪怕未来会需要在服务器上运行这个程序,服务器上也需要装好 X11 的一套东西才行。
SDL 目前也在经历 v2/v3 变更。为了这个项目能用得久一点,我选择了使用 SDL3.
初始化无窗口 SDL
我们只需要用到 SDL 的图形能力,所以只需要初始化图形子系统:
if (!SDL_Init(SDL_INIT_VIDEO)) {
spdlog::error("failed to initialize SDL: {}", SDL_GetError());
return -20;
}
然后就是创建渲染器:
SDL_Window *window = nullptr;
SDL_Renderer *renderer = nullptr;
if (!SDL_CreateWindowAndRenderer(
"Preview", width, height, 0, &window, &renderer)) {
spdlog::error("failed to create window: {}", SDL_GetError());
return -21;
}
这之后,我们就可以创建一个用于做为渲染目标的材质了:
texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
width,
height);
if (!texture) {
spdlog::error("failed to create texture: {}", SDL_GetError());
return -22;
}
绘制一些图形
和有窗口的 SDL 不同,无头 SDL 模式下完全不需要去实现 SDL_AppIterate 那一坨东西。拿到渲染器之后就可以直接使用了:
SDL_SetRenderTarget(renderer, texture);
SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
SDL_RenderClear(renderer);
同样的,也不需要调用 SDL_RenderPresent 函数。渲染到材质是立即执行的。
拿到渲染结果
SDL 的渲染器 (Renderer) 会自动选择最合适的图形后端,这也意味着在有显卡的机器上,这个后端十有八九是 GPU. 要拿到材质的原始 RGB 值,则需要将它「下载」到 CPU 端:
auto* surface = SDL_RenderReadPixels(renderer, nullptr);
if (!surface) {
spdlog::error("failed to read pixels: {}", SDL_GetError());
return -23;
}
我们指定的材质格式是 RGBA8888, 这是为了渲染的时候可以保留透明度信息,以便之后实现多个图层的叠加。当然,现在读取了材质内容,自然是为了将它提交到渲染循环当中,那么就需要将它转换成 RGB24 的格式:
auto* converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGB24);
if (!converted) {
spdlog::error("failed to convert surface: {}", SDL_GetError());
SDL_DestroySurface(surface);
return -24;
}
然后就可以将 RGB24 数据提交到编码循环里了:
memcpy(rgbBuffer, converted->pixels, surface->w * surface->h * 3);
完成使用后的资源记得及时释放:
SDL_DestroySurface(converted);
SDL_DestroySurface(surface);
性能的担忧
提早优化是万恶之源。
我在写这一段实现的时候,我的直觉是这段代码的效率太低了:它需要做两次复制。如果我选择使用 CPU 渲染,我就可以节省从 GPU 复制到 CPU 的操作;如果我选择直接使用 RGB24 作为绘制格式的话,我就可以再省下转换的操作。
这两次「贵」的操作可能一开始看起来很浪费,但是仔细思考一下:之后的各个操作,如果用 CPU 硬拉渲染可能会比用 GPU 渲染要慢许多。而且更重要的是,在图层逐渐增多的情况下,大部分操作都是渲染操作,让 GPU 进行大量的渲染显然比 CPU 渲染更为高效。
并且,从 GPU 复制到 CPU 这个「贵」的操作只有最终的图层合并完成之后才会需要做,而转换操作也只是在最终提交一帧的时候进行。所以整体来看,这并不是什么性能问题。
以及,如果真的认为有性能问题,那么必须通过性能剖析来验证。任何「直觉」在现代计算机的超高并发+分支预测+乱序执行下都会失效。
下一步
现在有了 SDL, 我们便可以做一些简单的图形绘制工作了。当然,仅局限于绘制一些色块未免有些无聊。(除非你是真正的大角虫,可以硬用色块砌出动画。)PreFound 当初设计的目标之一是渲染一些编程相关的视频短文 (video essay), 所以想办法绘制一些文字进去是绝对必要的。
正在加载评论……