文档列表见:Rust 移动端跨平台复杂图形渲染项目开发系列总结(目录)
gfx-hal接口以1:1模仿Vulkan,下面改用Vulkan接口作说明。由于Vulkan接口粒度过细,比OpenGL / ES难学数倍。根据个人经验,对于移动端图形开发者,照着OpenGL ES的接口讲解Vulkan可降低学习难度。从逐帧渲染部分开始学习,跳过这些数据结构的初始化过程,可以更明显地感受到Vulkan的核心流程。
OpenGL / ES 逐帧渲染流程示例
// 准备渲染目标环境
glBindFramebuffer();
glFramebufferTexture2D(); glCheckFramebufferStatus(); // 假如渲染到纹理
glViewport(x, y, width, height);
// 准备渲染目标环境
glUseProgram(x);
glBindBuffer(i)
loop i in 0..VertexVarCount {
glEnableVertexAttribArray(i);
glVertexAttribPointer(i, ...);
}
loop i in 0..UniformVarCount {
switch UniformType {
case NoTexture: glUniformX(i, data); break;
case Texture: {
glActiveTexture(j);
glBindTexture(type, texture_name);
glUniform1i(location, j);
break;
}
default:ERROR();
}
}
// 配置其他Fragment操作,比如glBlend, glStencil
glDrawArrays/Elements/ArraysInstanced...
// 到此完成Draw Call,视情况调用EGL函数交换前后帧缓冲区,非GL函数,
// 渲染到纹理则无此操作。
// 为了不干扰后续绘制,恢复刚才设置的Fragment操作为默认值。
eglSwapbuffers()/[EAGLContext presentRenderbuffer];
复制代码
可见,OpenGL / ES的接口屏蔽了绝大部分细节,整体代码量显得很少,但初学时也不好理解,用久了就成套路,觉得就该这样,以致于第一次接触Vulkan发现很多细节之前完全不了解,有点懵。
gfx-hal逐帧渲染到视图的调用流程介绍
gfx-hal(Vulkan)逐帧渲染到视图的核心调用流程如下所示:
EventSource ->[CommandPool -> ComanndBuffer
-> Submit -> Submission
-> QueueGroup -> CommandQueue]
-> GraphicsHardware
复制代码
说明:
- EventSource:表示信号源,比如相机回调一帧图像、屏幕的vsync信号、用户输入等。
- CommandQueue:用于执行不同类型任务的队列,比如渲染任务、计算任务。
- QueueGroup:CommandQueue集合
- GraphicsHardware:图形硬件
具体流程代码:
-
重置Fence,给后面提交Submission到队列使用。
device.reset_fence(&frame_fence); 复制代码
-
重置CommandPool
command_pool.reset(); 复制代码
-
从SwapChain获取Image索引
let frame = swap_chain.acquire_image(!0, FrameSync::Semaphore(&mut frame_semaphore)); 复制代码
-
通过CommandPool创建、配置CommandBuffer,命令录制结束后得到有效的Submit对象
let mut cmd_buffer = command_pool.acquire_command_buffer(false); // 一系列类似OpenGL / ES的Fragment操作、绑定数据到Program的配置 // 两个值得注意的Pipeline操作 cmd_buffer.bind_graphics_pipeline(&pipeline); cmd_buffer.bind_graphics_descriptor_sets(&pipeline_layout, 0, Some(&desc_set), &[]); // 联合RenderPass的操作 let mut encoder = cmd_buffer.begin_render_pass_inline(&render_pass,...); let submit = cmd_buffer.finish() 复制代码
-
通过Submit创建Submission
let submission = Submission::new() .wait_on(&[(&frame_semaphore, PipelineStage::BOTTOM_OF_PIPE)]) .submit(Some(submit)); 复制代码
-
提交Submission到队列
queue.submit(submission, Some(&mut frame_fence)); 复制代码
-
等待CPU编码完成
device.wait_for_fence(&frame_fence, !0); 复制代码
-
交换前后帧缓冲区
swap_chain.present(&mut queue_group.queues[0], frame, &[]) 复制代码
配置CommandBuffer的进一步介绍
OpenGL / ES 2/3.x没CommandPool
与CommandBuffer
数据结构,除了最新的OpenGL小版本才加入了SPIR-V和Command,但OpenGL ES还没更新。Metal的CommandBuffer
接口定义不同于Vulkan。Metal创建MTLCommandBuffer
,由Buffer与RenderPassDescriptor
一起创建出 Enconder
,然后打包本次渲染相关的资源,最后提交Buffer到队列让GPU执行。Vulkan基本把Metal的Encoder操作放到CommandBuffer,只留了很薄的Encoder操作。
总体流程:
- 由Command Pool分配可用Command Buffer
- 配置viewport等信息
- 设置输出目标
- 设置绘制方式,
draw
/draw_indexed
/draw_indirect
等等 - 结束配置
代码示例如下:
let submit = {
// 从缓冲区中取出一个实际为RawCommandBuffer的实例,加上线程安全对象,组装成CommandBuffer实例,这是线程安全的
let mut cmd_buffer = command_pool.acquire_command_buffer(false);
cmd_buffer.set_viewports(0, &[viewport.clone()]);
cmd_buffer.set_scissors(0, &[viewport.rect]);
cmd_buffer.bind_graphics_pipeline(&pipeline.as_ref().unwrap());
cmd_buffer.bind_vertex_buffers(0, pso::VertexBufferSet(vec![(&vertex_buffer, 0)]));
cmd_buffer.bind_graphics_descriptor_sets(&pipeline_layout, 0, Some(&desc_set)); //TODO
{
let mut encoder = cmd_buffer.begin_render_pass_inline(
&render_pass,
&framebuffers[frame.id()],
viewport.rect,
&[command::ClearValue::Color(command::ClearColor::Float([0.8, 0.8, 0.8, 1.0]))],
);
encoder.draw(0..6, 0..1);
}
cmd_buffer.finish()
};
复制代码
前面代码显示了CommandBuffer两个很关键的操作:bind_graphics_pipeline(GraphicsPipeline)
和bind_graphics_descriptor_sets(PipelineLayout, DescriptorSet)
。GraphicsPipeline相当于OpenGL / ES的Program,PipelineLayout
和DescriptorSet
描述了Shader的Uniform变量如何读取Buffer的数据。