文档列表见:Rust 移动端跨平台复杂图形渲染项目开发系列总结(目录)c++
2019.5.10 修改:渲染到视图git
gfx-hal接口以1:1模仿Vulkan,下面改用Vulkan接口做说明。因为Vulkan接口粒度过细,比OpenGL / ES难学。根据我的经验,对于移动端图形开发者,照着OpenGL ES的接口讲解Vulkan可下降学习难度。从逐帧渲染部分开始学习,跳过这些数据结构的初始化过程,有利于把握Vulkan的核心流程。github
// 准备渲染目标环境
glBindFramebuffer();
glFramebufferTexture2D(); glCheckFramebufferStatus(); // 假如渲染到纹理
glViewport(x, y, width, height);
// 准备Shader须要读取的数据
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函数,
// 渲染到纹理则无此操做。
// 为了避免干扰后续绘制,恢复刚才设置的Framebuffer操做为默认值。
eglSwapbuffers()/[EAGLContext presentRenderbuffer];
复制代码
可见,OpenGL / ES的接口屏蔽了绝大部分细节,总体代码量显得不多,但初学时也很差理解,用久了就成套路,以为就该这样,以至于第一次接触Vulkan发现不少细节以前彻底不了解,有点懵。编程
OpenGL / ES造成套路后的缺点是,出错的第一时间很难定位出是项目代码的问题,好比状态机没设置好,仍是驱动的问题,在iOS上还好,Android真是眼黑。我猜你会说有各厂家的Profile工具和Google gapid,都特么很差用,高通的技术支持建议咱们用Android 8.0 + Root设备。可是,每每出问题的都是Android 4.x。api
gfx-hal(Vulkan)逐帧渲染到视图的核心调用流程以下所示:安全
-> CommandPool -> ComanndBuffer
-> Submit -> Submission-> QueueGroup -> CommandQueue -> GraphicsHardware
复制代码
说明:bash
具体流程代码:数据结构
device.reset_fence(&frame_fence);
复制代码
command_pool.reset();
复制代码
let frame = swap_chain.acquire_image(!0, FrameSync::Semaphore(&mut frame_semaphore));
复制代码
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()
复制代码
let submission = Submission::new()
.wait_on(&[(&frame_semaphore, PipelineStage::BOTTOM_OF_PIPE)])
.submit(Some(submit));
复制代码
queue.submit(submission, Some(&mut frame_fence));
复制代码
device.wait_for_fence(&frame_fence, !0);
复制代码
swap_chain.present(&mut queue_group.queues[0], frame, &[])
复制代码
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操做。app
整体流程:函数
draw
/draw_indexed
/draw_indirect
等等代码示例以下:
let submit = {
// 从缓冲区中取出一个实际为RawCommandBuffer的实例,
// 加上线程安全对象,组装成线程安全的CommandBuffer实例,
// 这是HAL的编程“套路”,还有不少这类数据结构
let mut cmd_buffer = command_pool.acquire_command_buffer(false);
cmd_buffer.begin()
cmd_buffer.set_viewports(0, &[viewport]);
cmd_buffer.set_scissors(0, &[viewport.rect]);
cmd_buffer.bind_graphics_pipeline(&pipeline);
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
// 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]))],
).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的数据。
渲染到纹理(Render to Texture, RTT)场景没Swapchain,需配置RenderPass.Attachment.format
为纹理的像素格式。接着Submmit到Queue,流程就结束了,无需且没法调用swap_chain.present()
。若是要获取该CommandBuffer的GPU操做结束事件或耗时,添加相应的回调函数给CommandBuffer便可。
RenderPass.Attachment.format
let render_pass = {
let attachment = Attachment {
format: Some(format), /// 纹理的像素格式
samples: 1,
ops: AttachmentOps::new(AttachmentLoadOp::Clear, AttachmentStoreOp::Store),
stencil_ops: AttachmentOps::DONT_CARE,
layouts: Layout::Undefined..Layout::Present,
};
let subpass = SubpassDesc {
colors: &[(0, Layout::ColorAttachmentOptimal)], /// 匹配了 Vulkan 要求
depth_stencil: None, /// 同上应该使用 Optimal
inputs: &[],
resolves: &[],
preserves: &[],
};
let dependency = SubpassDependency {
passes: SubpassRef::External..SubpassRef::Pass(0),
stages: PipelineStage::COLOR_ATTACHMENT_OUTPUT..PipelineStage::COLOR_ATTACHMENT_OUTPUT,
accesses: Access::empty()..(Access::COLOR_ATTACHMENT_READ | Access::COLOR_ATTACHMENT_WRITE),
};
device.create_render_pass(&[attachment], &[subpass], &[dependency])
.expect("Can't create render pass")
};
复制代码
和渲染到视图同样提交便可,少一步swap_chain.present()
。如何验证到这步就够了呢?看源码是一种方案。若是是Metal,用Xcode Capture GPU Frame也是一种方案。如何对Cargo项目进行Xcode Capture GPU Frame?参考我另外一个文档:Xcode External Build System 失败的 Capture GPU Frame 经历、解决方案与复盘。
// ... lots of previous stuff
queue.submit(submission, Some(&mut frame_fence));
device.wait_for_fence(&frame_fence, !0);
复制代码
好比,DrawCall 1输出纹理为 DrawCall 2的一个输入纹理,DrawCall 2输出纹理为 DrawCall 3的一个输入纹理,这类场景在直播、短视频业务中很常见。
// **************** 准备 RenderPass ***************
// 建立新 RenderPass 前先释放老的 RenderPass
vkDestroyRenderPass(vkDevice, mVkRenderPass, nullptr);
VkSubpassDescription subpass;
subpass.pipelineBindPoint = mVkPipelineBindPoint; // 初始化为 VK_PIPELINE_BIND_POINT_GRAPHICS
subpass.flags = 0;
subpass.colorAttachmentCount = colorFormat != VK_FORMAT_UNDEFINED ? 1 : 0;
subpass.pColorAttachments = colorFormat != VK_FORMAT_UNDEFINED ? &color : nullptr;
subpass.pDepthStencilAttachment = depthstencilFormat != VK_FORMAT_UNDEFINED ? &depthstencil : nullptr;
subpass.pResolveAttachments = nullptr;
subpass.inputAttachmentCount = 0;
subpass.pInputAttachments = nullptr;
subpass.preserveAttachmentCount = 0;
subpass.pPreserveAttachments = nullptr;
VkRenderPassCreateInfo info;
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
info.pNext = nullptr;
info.flags = 0;
info.attachmentCount = static_cast<uint32_t>(attachments.size());
info.pAttachments = attachments.data();
info.subpassCount = 1;
info.pSubpasses = &subpass;
info.dependencyCount = 0;
info.pDependencies = nullptr;
VkResult err = vkCreateRenderPass(vkDevice, &info, nullptr, &mVkRenderPass);
// 错误处理 (err != VK_ERROR_OUT_OF_HOST_MEMORY && err != VK_ERROR_OUT_OF_DEVICE_MEMORY);
// **************** 准备 Framebuffer ***************
// 建立新 Framebuffer 前先释放老的 Framebuffer
vkDestroyFramebuffer(vkDevice, mVkFramebuffer, nullptr);
VkFramebufferCreateInfo info;
info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
info.pNext = nullptr;
info.flags = 0;
info.renderPass = *renderpass;
info.width = width;
info.height = height;
info.layers = 1;
info.attachmentCount = static_cast<uint32_t>(imageViews->size());
info.pAttachments = imageViews->data();
VkResult err = vkCreateFramebuffer(vkDevice, &info, nullptr, &mVkFramebuffer);
// 错误处理 (err != VK_ERROR_OUT_OF_HOST_MEMORY && err != VK_ERROR_OUT_OF_DEVICE_MEMORY);
// **************** 开始 CommandBuffer ***************
VkCommandBufferBeginInfo info;
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
info.pNext = nullptr;
info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; // glDraw* 只使用一次?
info.pInheritanceInfo = nullptr;
VkResult err = vkBeginCommandBuffer(mVkCommandBuffers.commandBuffer[mActiveCmdBuffer], &info);
// 错误处理 (err != VK_SUCCESS)
mVkCommandBuffers.commandBufferState[mActiveCmdBuffer] = CMD_BUFFER_RECORDING_STATE;
// **************** 开始 RenderPass ***************
VkRenderPassBeginInfo info;
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
info.pNext = nullptr;
info.framebuffer = *framebuffer;
info.renderPass = mVkRenderPass;
info.renderArea = mVkRenderArea;
info.clearValueCount = 2;
info.pClearValues = mVkClearValues;
const VkSubpassContents subpassContents = VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS;
vkCmdBeginRenderPass(*activeCmdBuffer, &info, subpassContents);
// **************** 分配 Secondary CommandBuffers ***************
VkCommandBuffer *commandBuffers = new VkCommandBuffer;
VkCommandBufferAllocateInfo cmdAllocInfo;
cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAllocInfo.pNext = nullptr;
cmdAllocInfo.commandPool = mVkCmdPool;
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY;
cmdAllocInfo.commandBufferCount = numOfBuffers; /// 值为 1
VkResult err = vkAllocateCommandBuffers(vkDevice, &cmdAllocInfo, commandBuffers);
// 错误处理 (err != VK_SUCCESS)
// **************** 开始 Secondary CommandBuffers ***************
VkCommandBufferInheritanceInfo inheritanceInfo;
inheritanceInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO;
inheritanceInfo.pNext = nullptr;
inheritanceInfo.renderPass = renderPass;
inheritanceInfo.subpass = 0;
inheritanceInfo.framebuffer = framebuffer;
inheritanceInfo.occlusionQueryEnable = VK_FALSE;
inheritanceInfo.queryFlags = 0;
inheritanceInfo.pipelineStatistics = 0;
VkCommandBufferBeginInfo cmdBeginInfo;
cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
cmdBeginInfo.pNext = nullptr;
cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT;
cmdBeginInfo.pInheritanceInfo = &inheritanceInfo;
VkResult err = vkBeginCommandBuffer(*cmdBuffer, &cmdBeginInfo);
// 错误处理 (err != VK_SUCCESS)
复制代码
/// Synchronization primitives which will be signalled once a frame got retrieved.
///
/// The semaphore or fence _must_ be unsignalled.
pub enum FrameSync<'a, B: Backend> {
/// Semaphore used for synchronization.
///
/// Will be signaled once the frame backbuffer is available.
Semaphore(&'a B::Semaphore),
/// Fence used for synchronization.
///
/// Will be signaled once the frame backbuffer is available.
Fence(&'a B::Fence),
}
复制代码
/// A strongly-typed command buffer that will only implement methods that are valid for the operations
/// it supports.
pub struct CommandBuffer<'a, B: Backend, C, S: Shot = OneShot, L: Level = Primary> {
pub(crate) raw: &'a mut B::CommandBuffer,
pub(crate) _marker: PhantomData<(C, S, L)>
}
复制代码
/// Thread-safe finished command buffer for submission.
pub struct Submit<B: Backend, C, S, L>(pub(crate) B::CommandBuffer, pub(crate) PhantomData<(C, S, L)>);
impl<B: Backend, C, S, L> Submit<B, C, S, L> {
fn new(buffer: B::CommandBuffer) -> Self {
Submit(buffer, PhantomData)
}
}
unsafe impl<B: Backend, C, S, L> Send for Submit<B, C, S, L> {}
复制代码
/// Submission information for a command queue, generic over a particular
/// backend and a particular queue type.
pub struct Submission<'a, B: Backend, C> {
cmd_buffers: SmallVec<[Cow<'a, B::CommandBuffer>; 16]>,
wait_semaphores: SmallVec<[(&'a B::Semaphore, pso::PipelineStage); 16]>,
signal_semaphores: SmallVec<[&'a B::Semaphore; 16]>,
marker: PhantomData<C>,
}
/////////////////////////////// submit接口 /////////////////////////////////
/// Append a new list of finished command buffers to this submission.
///
/// All submits for this call must be of the same type.
/// Submission will be automatically promoted to to the minimum required capability
/// to hold all passed submits.
pub fn submit<I, K>(mut self, submits: I) -> Submission<'a, B, <(C, K) as Upper>::Result>
where
I: IntoIterator,
I::Item: Submittable<'a, B, K, Primary>,
(C, K): Upper
{
self.cmd_buffers.extend(submits.into_iter().map(
|s| { unsafe { s.into_buffer() } }
));
Submission {
cmd_buffers: self.cmd_buffers,
wait_semaphores: self.wait_semaphores,
signal_semaphores: self.signal_semaphores,
marker: PhantomData,
}
}
复制代码
根据实践,持续更新。
Metal的CommandBuffer一旦Commit到Queue,则不能再次使用。Vulkan可屡次提交。
After a command buffer has been committed for execution, the only valid operations on the command buffer are to wait for it to be scheduled or completed (using synchronous calls or handler blocks) and to check the status of the command buffer execution. When used, scheduled and completed handlers are blocks that are invoked in execution order. These handlers should perform quickly; if expensive or blocking work needs to be scheduled, defer that work to another thread.
In a multithreaded app, it’s advisable to break your overall task into subtasks that can be encoded separately. Create a command buffer for each chunk of work, then call the enqueue() method on these command buffer objects to establish the order of execution. Fill each buffer object (using multiple threads) and commit them. The command queue automatically schedules and executes these command buffers as they become available.
提交CommandBuffer到Queue,Metal和Vulkan用了不一样的单词。Metal = commit()
,Vulkan = submit()
。