gfx-hal 逐幀渲染流程介紹
文件列表見: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的介面遮蔽了絕大部分細節,整體程式碼量顯得很少,但初學時也不好理解
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的資料。