最终会映射到./docs/TurboDesign:Render设计中
注:该文档有些图表是使用mermaid进行书写的,github并不支持复杂的mermaid语法,导致在线查看该文档会渲染不出图表,本人是使用VSCode安装Markdown Preview Mermaid Support插件进行查看和书写的。
-
2022/12/4
- 结束琐碎设计文档化
- 创建
Turbo驱动设计章节,用于开始Turbo引擎Engine层面与FrameGraph配合的设计
-
2022/12/5
- 创建
资源的创建与销毁章节 - 创建
Context上下文章节 - 创建
WorldRender/Render 渲染器章节
- 创建
-
2022/12/7
- 更新
资源的创建与销毁章节 - 更新
Context上下文章节
- 更新
-
2022/12/8
- 创建
Surface、Swapchain和Context章节 - 创建
Surface章节 - 创建
Swapchain章节
- 创建
-
2022/12/9
- 创建
离屏渲染流程章节 - 创建
将RenderTarget结果拷贝给用户章节 - 创建
用户自定义PassNode章节 Image资源派生中增加CubeImage资源类资源的创建与销毁章节中增加在使用Render::TContext创建Redner::TImage时与Turbo::Core::TImage资源类的对应说明
- 创建
-
2022/12/10
- 将原先的
TImage资源说明,创建成资源章节 - 更新
资源资源章节中的Descriptor与资源的创建与销毁章节对应上 - 创建
Format格式章节,用于规定创建Image和Buffer时的资源格式 - 创建
Turbo驱动初步章节,用于将琐碎设计和之后的Turbo驱动设计做区分
- 将原先的
-
2022/12/11
- 创建
资源的所有者端域章节 - 更新
资源章节,增加端域
- 创建
-
2022/12/12
- 创建
Usage和Domain章节
- 创建
-
2022/12/13
- 更新
Usage和Domain章节 - 创建
资源拷贝传输章节
- 更新
-
2022/12/14
- 更新
Usage和Domain章节 - 更新
资源拷贝传输章节
- 更新
-
2022/12/15
- 更新
Usage和Domain章节。回读内存位标应为HOST_ACCESS_RANDOM,而不是HOST_ACCESS_SEQUENTIAL_WRITE - 创建
Image的Format章节
- 更新
-
2022/12/16
- 更新
Usage和Domain章节。高频内存位标条件应为TRANSFER_DST,而不是TRANSFER_SRC - 更新
资源章节。增加Vulkan标准对于3D纹理资源的layer限值,由Turbo引擎维护 - 创建
Image的Format章节 - 更新
Image的Format章节
- 更新
-
2022/12/17
- 更新
Image的Format章节
- 更新
-
2022/12/18
- 更新
Image的Format章节 - 更新
资源章节,添加Depth图片说明
- 更新
-
2022/12/19
- 更新
资源章节,添加更新Texture和3DImage说明
- 更新
-
2022/12/24
- 更新
资源拷贝传输章节
- 更新
-
2022/12/25
- 更新
资源拷贝传输章节
- 更新
-
2022/12/26
- 更新
Context上下文章节
- 更新
-
2022/12/27
- 更新
Context上下文章节 - 更新
用户自定义PassNode章节 - 创建
Mesh,Material和Drawable章节
- 更新
-
2022/12/28
- 更新
用户自定义PassNode章节 - 创建
Pipeline章节 - 创建
RenderPass章节
- 更新
-
2022/12/29
- 更新
Pipeline章节 - 创建
Pipeline的VertexBinding章节
- 更新
-
2022/12/30
- 更新
RenderPass章节
- 更新
-
2023/1/3
- 更新
用户自定义PassNode章节
- 更新
-
2023/1/5
- 将
RenderPass章节更名为Context::CmdBeginRenderPass - 创建
RenderPass章节 - 创建
Subpass章节
- 将
-
2023/1/6
- 创建
Shader章节
- 创建
-
2023/1/9
- 创建
指令推送章节 - 创建
指令等待章节
- 创建
-
2023/1/10
- 更新
指令等待章节
- 更新
-
2023/1/11
- 创建
异步资源回收章节
- 创建
-
2023/1/18
- 更新
Context::CmdBeginRenderPass章节 - 更新
资源的创建与销毁章节 - 更新
异步资源回收章节
- 更新
-
2023/1/22
Context::CmdBeginRenderPass章节下创建RenderPass 创建章节Context::CmdBeginRenderPass章节下创建FrameBuffer 创建章节Context::CmdBeginRenderPass章节下FrameBuffer 创建章节下创建RenderPassPool章节
-
2023/1/23
Context::CmdBeginRenderPass章节下增加RenderPassProxy(RenderPass代理)章节
-
2023/1/26
- 创建
Turbo::Render::TRenderPass 转 Turbo::Core::TRenderPass章节
- 创建
-
2023/2/6
- 创建
Image的Layout章节
- 创建
-
2023/2/9
- 更新
FrameBuffer 创建章节 - 更新
RenderPassProxy(RenderPass代理)章节
- 更新
-
2023/2/10
- 更新
FrameBuffer 创建章节 - 创建
Image的ImageView章节
- 更新
-
2023/2/13
- 更新
FrameBuffer 创建章节
- 更新
-
2023/2/14
- 更新
Context::CmdBeginRenderPass章节
- 更新
-
2023/2/16
- 更新
Image的Layout章节
- 更新
-
2023/2/20
- 更新
Pipeline的VertexBinding章节
- 更新
-
2023/2/21
- 更新
Pipeline的VertexBinding章节
- 更新
-
2023/2/22
- 更新
Pipeline的VertexBinding章节 - 更新
资源章节 - 创建
Image章节 - 创建
Buffer章节
- 更新
-
2023/2/24
- 更新
Format格式章节 - 更新
Pipeline的VertexBinding章节
- 更新
-
2023/2/26
- 创建
绑定Pipeline章节 - 创建
绑定Pipeline的DescriptorSet章节
- 创建
-
2023/2/27
- 更新
绑定Pipeline的DescriptorSet章节 - 更新
绑定Pipeline章节 - 创建
Pipeline创建章节
- 更新
-
2023/2/28
- 创建
DrawCall章节 - 创建
Dispatch章节 - 更新
Buffer章节中的TIndexBuffer
- 创建
-
2023/3/1
Buffer章节中增加TUniformBuffer
-
2023/3/5
- 创建
Sampler章节
- 创建
-
2023/3/6
- 更新
DrawCall章节
- 更新
来源于docs/images下的一些平日琐碎设计,该文档是琐碎设计的整理
Unity中的一个材质就可以看成对应fg的一个PassNode,多个可渲染物体绑定同一个材质是,其实就是在当前的Material所对应的的那个PassNode内渲染绑定的Mesh,换句话说就是多个DrawableObject如果绑定了同一个Material,就在Material对应的PassNode下渲染多个DrawableObject
根据
Material中的渲染配置生成对应的RenderPass、Subpass和Pipeline
随即引发了一些问题:
-
这些材质应该放在
fg的何处?,换句话说就是,材质对应的应该是哪个PassNode?这些PassNode的前后关系是啥?
答:分两种情况:GBuffer的延迟渲染和Forward的前向渲染。还可能有特殊的自定义的Material可能会在后处理阶段等 -
后处理材质放在何处?(属于问题1的范畴)
-
比如
DepthPass会去渲染所有物体,ColorPass也会去渲染所有物体,也就是说DepthPass中会有Drawable对DepthMaterial的绑定,ColorPass中会有Drawable对ColorMaterial的绑定。
答:这时Drawable最好是引擎临时构建的,如果不是临时构建的话,在渲染前就要构建出很多Drawable对象,而很多对象只是材质不同,而Mesh都是相同的
Turbo的目的是灵活性并不是强制用户使用fg,也就是说fg只是Turbo中可有可无的架构,为了达到这个目的Turbo需要细化fg中的步骤,将其抽出来作为单独的内容或是类,而fg只是使用这些Turbo引擎提供的功能类来将渲染流程驱动起来。比如如下结构流程:
RenderPass
|_Pass0
| |_inputs(对应PassNode的输入)
| |_outputs(对应PassNode的输出)
| |...(渲染时绑定)pipeline
|_Pass1
| |_inputs(对应PassNode的输入)
| |_outputs(对应PassNode的输出)
| |...(渲染时绑定)pipeline
|_Pass...
Pipeline
|_vertex shader
|_...几何,细分等
|_fragment shader
|_各种pipeline配置如上对应示意的所有均为声明,Turbo引擎并不会构建真正的底层对象,只有运行时,渲染时会去构建相应的对象,以此来实现动态渲染
首先明确的是
Material的层级高于fg,也就是说Material用于最终声明对应的PassNode,fg不应该内嵌Material
Mesh some_mesh;
Material color_material;
Material pbr_material;
MeshRender mesh_render0;
mesh_render0.mesh=some_mesh0;
mesh_render0.material=pbr_material;
MeshRender mesh_render1;
mesh_render1.mesh=some_mesh1;
mesh_render1.material=color_material;转成对应的fg
Material color_material => [Color PassNode]
Material pbr_material => [PBR PassNode]现在解决两个问题:
- 某个
MeshRender何时进行真正的渲染绘制?
答:引擎会收集所有绑定同一个Material的MeshRender的网格顶点信息(也就是vertex buffer之类的),并将在对应的底层PassNode的Execute阶段渲染收集到的所有MeshRender
注:为了快速得到Material都绑定了哪些MeshRender,有两种方案:Material内部维护一个数组,用于存放对应的MeshRender对象MeshRender中的Material只是Material的句柄,这样就可以快随找到对应的Material
Engine与FrameGraph
Engine中该有的类
Shader
|_Vertex Shader
|_Geometry Shader
|_细分着色器
|_Fragment Shader
|_光追标准着色器
|_...
Pipeline
|_Graphic Pipeline
|_Compute Pipeline
|_Raytracing Pieline
Image
|_Color Image
|_Depth Image
|_...
Subpass
RenderPass对应代码结构示意:
ColorImage some_color_image;
DepthImage some_depth_image;
VertexShader vertex_shader;
FragmentShader fragment_shader;
GraphicPipeline some_graphic_pipeline;
some_graphic_pipeline.SetVertexShader(vertex_shader);
some_graphic_pipeline.SetFragmentShader(fragment_shader);
some_graphic_pipeline.Set几何拓扑配置(...);
some_graphic_pipeline.Set透明融合配置(...);
some_graphic_pipeline.Set...配置(...);
Subpass subpass0;
subpass0.BindingColorImage(some_color_image);
subpass0.BindingDepthImage(some_depth_image);
Subpass subpass1;
subpass1.Binding...Image(...);
RenderPass render_pass;
render_pass.AddSubpass(subpass0);
render_pass.AddSubpass(subpass1);Engine与FrameGraph对应示意
RenderPass············PassNode
|_Subpass0············|_pass0
|_Subpass1············|_pass1
|_Subpassn············|_passn
\||/
\/
Execute
执行阶段
\||/
\/
commandBuffer.BindingPipeline(...)
commandBuffer.BindingVertexBuffer(...)
commandBuffer.Draw(...)渲染流程
VertexShader vertex_shader;
FragmentShader fragment_shader;
ColorImage color_image;//或者叫RenderTarget更能体现其作用
GraphicPipeline some_graphic_pipeline;
some_graphic_pipeline.BindVertexShader(vertex_shader);
some_graphic_pipeline.BindFragmentShade(fragment_shader);
Mesh mesh;
//<对应FrameGraph::PassNode::Setup阶段>
Subpass subpass0;
subpass0.AddColorImage(color_image);
RenderPass render_pass;
render_pass.AddSubpass(subpass0);
//</对应FrameGraph::PassNode::Setup阶段>
//<对应FrameGraph::PassNode::Execute阶段>
CommandBuffer command_buffer;
command_buffer.BindRenderPass(render_pass);
command_buffer.BindPipeline(some_graphic_pipeline);
command_buffer.BindMesh(mesh);
command_buffer.Draw(...);
//</对应FrameGraph::PassNode::Execute阶段>
//注:PassNode的对应FrameBuffer需要引擎内部自动构建
//这就需要CommandBuffer也是Virtual虚的(因为按照Vulkan标准,CommandBuffer使用图形管线,必须提前构建好FrameBuffer)问:真正的Pipeline和RenderPass何时创建?
答:如果使用fg的话,每一帧都会根据配置重新构建
问:如果不使用fg呢?
FrameGraph示意
PassNode
Setup
//设置当前RenderPass的各种Subpass
Subpass subpass0;
subpass0.Read(someImage);
subpass0.Write(someImage);
Subpass subpass1;
subpass1.Read(someImage);
subpass1.Write(someImage);
Execute
1. 背后根据Setup阶段的配置创建RenderPass
2. 背后根据RenderPass创建FrameBuffer
3. 背后调用BeginRenderPass(图形管线)
4. 调用各种渲染指令,例如:(BBBD)
BindPipeline(...)
BindVertexBuffer/BindIndexBuffer(...)
BindPipelineData(...)
Draw/DrawIndex(...)
NextSubpass()
BBBD
...
NextSubpass()
BBBD
...问:Pipeline何时构建?
答:现用先构建,或其他更好的方案
VertexBuffer和IndexBuffer或许可以通过Mesh简化流程
问:目前RenderPass和Subpass基本上整理清楚了,但是Pipeline一直没有一个良好的架构,比如Pipeline何时构建? **答**:由于Vulkan标准,在创建Pipeline`是需要指定如下数据
- RenderPass
- Subpass
也就是说,
Pipeline是特定于某一个RenderPass的某一个Subpass下的,换一句话说就是,Pipeline应该在RenderPass::Begin后Subpass执行时,Pipeline绑定中创建
问:如果Pipeline应该在RenderPass::Begin后Subpass执行时,Pipeline绑定中创建的话,会有个问题:
- 当用户在同一个
RenderPass下同一个Subpass下绑定多个相同的Pipeline - 当用户在同一个
RenderPass下不同的Subpass下绑定多个相同的Pipeline - 当用户在不同
RenderPass下不同的Subpass下绑定多个相同的Pipeline
会如何?
答:首先Pipeline只对某一RenderPass下的某一Subpass创建,这是一个Pipeline的最小活动范围,所以是个树状图。
RenderPass
|_Subpass0
| |_Pipeline 0...n
|_Subpass1
| |_Pipeline 0...n
|_Subpass2
| |_Pipeline 0...n
|_Subpass...
| |_Pipeline 0...n所以在绑定Pipeline时,引擎会根据当前的RenderPass下的当前Subpass中的Pipeline数组中找是否已经创建此Pipeline,如果已经创建就不用创建了,如果没创建就创建。
问:如何判断已经构建该Pipeline0?换句话说,如何判断两个Pipeline相等?
答:目前能够想到的是主色器相同,配置相同,RenderPass相同,Subpass相同
注:Vulkan标准中Pipeline有兼容性特性
问:如何创建Pipeline并保存在何处?如何管理?
答:一个Pipeline属于某一个RenderPass下的某一个Subpass,一个Subpass可以绑定多个Pipeline,而对于Vulkan是基于Command驱动的,而不像OpenGL是基于Context。所以Pipeline正常来说生命周期应该和CommandBuffer相同,也就是说当CommandBuffer绑定Pipeline是引擎需要去查看当前RenderPass下的当前Subpass下有没有创建相应的Pipeline,如果
没有创建则创建,创建完直接使用。由于Pipeline每一帧都会重新创建,这样就可以满足动态改变Pipeline了。如果使用fg的话,fg,Pipeline,CommandBuffer的生命周期应该都是一样的,都是一帧的生命周期。
总结一下:
如何创建Pipeline?:
答:某一帧,某一RenderPass下某一Subpass下使用,CommandBuffer绑定时,引擎会使用当前RenderPass当前Subpass创建Pipeline并存入CommandBuffer中。
注:存入CommandBuffer可能不是最优解,可能搞一个类似一帧的Context之类会更加合理。
保存在何处?:
答:搞一个类似一帧的Context之类会更加合理
如何管理:
答:
- 增 :搞一个类似一帧的
Context之类会更加合理 - 删 :在一帧
Context结束时 - 改 :用户动态改变
Pipeline的配置,引擎根据用户配置生成真正的Pipeline - 查 :略 。目前没想到实际用途。
问:现在面临的问题是,如何判断某个被绑定的RenderPass的Pipeline已经被创建,而不需要重复创建。换句话说:如何判断两个RenderPass两Pipeline相同,相等?
用户端可能的用法(不使用fg)
//创建一些有用的Image
vector<Images> some_images;
//声明一个Subpass
Subpass sp0;
sp0.AddColorImage(some_images[color_index]);
sp0.AddColorImage(some_images[depth_index]);
//声明一个Subpass
Subpass sp1;
sp0.AddInput(some_images[input1_index]);
sp0.AddColorImage(some_images[input2_index]);
//声明一个RenderPass
RenderPass rp;
rp.AddSubpass(sp0);
rp.AddSubpass(sp1);
//声明一些Pipeline
Pipeline pipeline0(拓扑类型,混合,深度...);
Pipeline pipeline1(拓扑类型,混合,深度...);
Pipeline pipeline...(拓扑类型,混合,深度...);
//创建CommandBuffer
CommandBuffeer cb;
cb.RenderPassBegin(rp);
cb.BindVertexBuffer(...);
cb.BindPipeline(some_pipeline);
cb.Draw(...)
cb.NextSubpass();
cb.[...];
//推送执行
DeviceQueue.Submit(cb);现在要解决的问题是:
问:如何断定两个RenderPass相等?如何断定两个Pipeline相等
原因:因为用户绑定到RenderPass中的Pipeline只是个描述,引擎需要判断是否底层已经创建了符合此描述的对象。
答:一帧的对应资源会存入Context中(这里的Context确切的说应该是一帧内拥有的所有资源和环境,包括所有的创建的RenderPass和Pipeline)
每一帧的RenderPass,Subpass和绑定的Pipeline都会变,所以一帧的资源函数需要存入Context中
结构示意:
Context
|_RenderPass 0
| |_Subpass 0------BindPipeline(....)
| |_Subpass 1------BindPipeline(....)
|_RenderPass 1
| |_Subpass 0------BindPipeline(....)
| |_...
|_RenderPass ...- 判断两个
RenderPass相同。RenderPass A``RenderPass B
所有对应的Subpass要兼容
如果B的Subpass数量小于等于A的数量,再判断
如果B的Subpass对应的写入写出与A的Subpass的相等
否则两个RenderPass不相等
否则两个RenderPass不相等- 判断两个
Pipeline相同(发生在Pipeline绑定时)
如果两个Pipeline绑定到同一个RenderPass下的同一个Subpass,并且Pipeline配置相同,则两个Pipeline相同,反之则不相同
RenderPass与资源(Image)之间的关系
首先明确RenderPass并不会创建和销毁资源,RenderPass只绑定Subpass,Subpass中绑定Image资源,资源的创建和销毁应交由用户或FrameGraph驱动,资源的创建应该在CommandBufer绑定RenderPass之前完成,销毁需要在合适的时候(异步的),在CommandBuffer绑定RenderPass时会基于RenderPass的Subpass对资源的引用情况去创建FrameBuffer,换句话说,FrameBuffer的生命周期与RenderPass同在,FrameBuffer应该存在Context中与RenderPass同在。当调用CommandBuffer的BeginRenderPass()时引擎会去找是否已经创建该RenderPass,如果已经创建,再去看对应的FrameBuffer有没有创建,如果没有创建就去创建,反之则不创建。
没创建<------RenderPass创建了吗------>创建了
| |
RenderPass创建任务 创建了<------对应的FrameBuffer创建了吗?------>没创建
| |
返回 创建对应的FrameBuffer 首先像
Image,Buffer这样的资源是不会采用构造函数去创建真正的资源的,而是调用对应资源类的Create(...)和Destroy()成员函数创建和销毁,所以资源只能通过new和delete创建和销毁对应的资源对象(注:这里不是资源,而是资源对象),引擎架构中资源类中会存有真正的资源,而资源类的Create(...)和Destroy()成员函数则是真正去创建和销毁资源的函数。如果资源对象销毁了,而资源没有销毁,触发异常。
引擎架构中,资源类,
RenderPass类,Pipeline类全都是先声明配置,后引擎会进行相应的创建和销毁,这样就可以实现动态改变资源,RenderPass和Pipeline了。其中Shader资源比较特殊,由于Shader的创建是个性能影响的元素,所以Shader资源的构造就是创建了相应的资源。
VertexShader* vs=new ...(FilePath);
FragmentShader* fs=new ...(FilePath);
//Pipeline 不应该使用new创建,应该使用临时声明,并使用`Context`真正的在引擎内部进行管理
Pipeline pipeline(vs,fg);
ColorImage* color = new ColorImage();
color->Create(...);
DepthImage* depth = new DepthImage();
depth->Create(...);
Subpass sp0;
sp0.AddColor(color);
sp0.SetDepth(depth);
//RenderPass不应该使用new创建,同Pipeline一样用`Context`真正的在引擎内部进行管理
RenderPass rp;
rp.AddSubpass(sp0);
CommnadBuffer* cb=new ...;
cb->BeginRenderPass(rp);
cb->BindPipeline(pipeline);
cb->.../DrawCall(...);
cb->EndRenderPass();首先将CommandBuffer中的RenderPass和Pipeline转移出来,到一个Context中,Context中存有一帧中所有的数据,包括当前真的RenderPass,pipeline,CommandBuffer等
如果不使用
FrameGraph,那么Turbo的流程应该是
- 初始化:包括
Instance,Device,DeviceQueue的创建 - 创建各种资源:最常见的就是
Image,还有就是Shader等 - 使用创建的各种资源声明
Subpass - 使用声明的
Subpass组建声明RenderPass - 由
CommandBuffer去Begin()和End()绑定的RenderPass。在绑定时需要传入一帧的上下文Context,用于查看当前上下文是否已经创建了此RenderPass,如果已经创建了就不用创建了直接用,反之要创建后再用 - 声明
Pipeline(注意是声明,不是创建,创建Pipeline是引擎Context的任务) - 由
CommandBuffer去BindingPipeline绑定Pipeline,同绑定RenderPass一样需要传入Context用于判断是否创建了此Pipeline - 将
CommandBuffer提交并运行
接着不使用FrameGraph的说明,Turbo流程,现在整理一下那些任务时FrameGraph能够帮助我们完成的。
先强调几点:贴图纹理,Shader之类的资源还是需要手动管理的,Context将会帮助我们管理RenderPass和Pipeline。
FrameGraph能够帮助我们完成的:
- 帮助我们管理
FrameBuffer相关Image及其生命周期 - 帮助我们声明和管理
RenderPass及其内部的Subpass(这点很重要) - 帮助我们调用一些可以提前确定的指令,比如
RenderPassBegin(),RenderPassEnd()等 - 帮助我们将指令提交到
DeviceQueue中
资源回收同步点
方案示意如下:
假如Swapchain中有三张图,正常来说Swapchain中每一张图片都是一帧,都是一个FrameGraph。
Image 0;
Image 1;
Image 2;同步点:Fence0--->|0|Present(第1帧)
同步点:Fence1--->|1|Present(第2帧)
同步点:Fence2--->|2|Present(第3帧)
同步点:Fence0--->|0|Present(第4帧)
同步点:Fence1--->|1|Present(第5帧)
同步点:Fence2--->|2|Present(第6帧)
...以此类推
---------------------------------------------->time解释:每一帧都会等待上次用到自己(图片0,1,2)的那一帧结束后再使用自己,每一帧等待上次结束是回收资源的好时候,可能的行为:唤醒资源回收线程等。
主要有两种资源Image和Buffer,每个资源内部都有一个Descriptor的结构体,用于创建时描述该资源。
Image派生有:
- ColorImage
- DepthImage
- StencilImage
- DepthStencilImage
ColorImage派生有:
- ColorImage1D(这个可能用处不多)
- ColorImage2D
- ColorImage3D
DepthImage派生有:
- DepthImage2D (二维的Depth纹理比较常用)
StencilImage派生有:
- StencilImage2D
DepthStencilImage派生有:
- DepthStencilImage2D
ColorImage2D派生有:
- CubeImage
Buffer派生有:
- VertexBuffer
- IndexBuffer
注:按照Vulkan标准:If imageType is VK_IMAGE_TYPE_3D, arrayLayers must be 1(如果创建三维纹理资源,layer必须是1)
typedef uint32_t TFlags;
enum TImageType
{
1D,
2D,
3D
};
enum TFormat//这个枚举放到TFormat.h中作为通用枚举(Buffer也要用)
{...};
enum TUsageFlagsBits
{
TRANSFER,//考虑是否由Turbo管理该TUsageFlagsBits::TRANSFER用例
SAMPLED,
STORAGE,
COLOR_ATTACHMENT,
DEPTH_STENCIL,
INPUT_ATTACHMENT,
};
typedef TFlags TUsages;
enum TImageCreateFlagBits
{
CUBE//用于天空盒
};
using TImageCreateFlags = uint32_t;
class Image{
struct Image::Descriptor
{
TImageCreateFlags flags;//CUBE用于六面体纹理,多用于天空盒(详见CubeImage)
TFormat format;
uint32_t width;//1D轴,当(width≠0,height=0,depth=0)时,对应Turbo::Core::TImageType::1D
uint32_t height;//2D轴,当(width≠0,height≠0,depth=0)时,对应Turbo::Core::TImageType::2D
uint32_t depth;//3D轴,当(width≠0,height≠0,depth≠0)时,对应Turbo::Core::TImageType::3D
uint32_t mipLevels;
uint32_t layers;
TImageUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
void Create(const std::string &name, const Image::Descriptor &descriptor,void* allocator);
void Destroy();
};
class ColorImage: public Image
{
struct ColorImage::Descriptor
{
TImageCreateFlags flags;
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t mipLevels;
uint32_t layers;
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class ColorImage1D:public ColorImage
{
struct ColorImage1D::Descriptor
{
//TImageCreateFlags flags; //flags值为0,Cube纹理需要六个二维纹理,一维不满足该条件
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;//width不能为0
//uint32_t height;//该属性由Turbo维护,值为1
//uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
uint32_t layers; //默认值为1,TODO:考虑是否由Turbo维护
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class ColorImage2D: public ColorImage
{
struct ColorImage2D::Descriptor
{
TImageCreateFlags flags; //TODO:考虑是否由Turbo维护,由于有了CubeImage类,这个flags对于ColorImage2D有点多余
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
//uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
uint32_t layers; //默认值为1,TODO:考虑是否由Turbo维护
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class ColorImage3D: public ColorImage
{
struct ColorImage3D::Descriptor
{
//TImageCreateFlags flags;//flags值为0,Cube纹理需要六个二维纹理,三维不满足该条件
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
uint32_t depth;//depth不能为0
uint32_t mipLevels; //默认值为1
//uint32_t layers; //默认值为1,由Turbo维护(根据Vulkan标准:If imageType is VK_IMAGE_TYPE_3D, arrayLayers must be 1)
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class CubeImage: public ColorImage2D
{
struct CubeImage::Descriptor
{
//TImageCreateFlags flags;//该属性由Turbo维护,为CUBE
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;
uint32_t height;
//uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
//uint32_t layers; //该属性由Turbo维护,默认值为6
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class DepthStencilImage: public Image
{
struct DepthStencilImage::Descriptor
{
//TImageCreateFlags flags; //由Turbo维护,默认值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持深度的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
uint32_t layers; //默认值为1,TODO:考虑是否由Turbo维护
TUsages usages;//内部会自动附上TImageUsageBits::DEPTH_STENCIL_ATTACHMENT
TDomain domain;//详见[资源的所有者端域]章节
};
};
class DepthImage:public DepthStencilImage
{
struct DepthImage::Descriptor
{
//TImageCreateFlags flags; //由Turbo维护,默认值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持深度的格式)
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t mipLevels; //默认值为1
uint32_t layers; //默认值为1
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class DepthImage2D:public DepthImage
{
struct DepthImage2D::Descriptor
{
//TImageCreateFlags flags; //由Turbo维护,默认值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持深度的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
//uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
uint32_t layers; //默认值为1
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
}由于在开发中,二维/三维的单张颜色纹理和二维的单张深度纹理最为常用,所以Turbo应该提供如下Texture类
class Texture2D: public ColorImage2D
{
struct Texture2D::Descriptor
{
//TImageCreateFlags flags;//该属性由Turbo维护, flags值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
//uint32_t depth;//该属性由Turbo维护,默认值为1
uint32_t mipLevels;
//uint32_t layers;//该属性由Turbo维护,默认值为1
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class Texture3D: public ColorImage3D
{
struct Texture3D::Descriptor
{
//TImageCreateFlags flags;//该属性由Turbo维护, flags值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持颜色的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
uint32_t depth;//depth不能为0
uint32_t mipLevels;
//uint32_t layers;//该属性由Turbo维护,默认值为1
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};
class Cubemap: public CubeImage
{
struct Cubemap::Descriptor
{
...
};
};
class DepthTexture2D: public DepthImage2D
{
struct DepthImage2D::Descriptor
{
//TImageCreateFlags flags; //由Turbo维护,默认值为0
//TFormat format; //该属性由Turbo维护(Turbo会设置支持深度的格式)
uint32_t width;//width不能为0
uint32_t height;//height不能为0
//uint32_t depth; //该属性由Turbo维护,值为1
uint32_t mipLevels; //默认值为1
//uint32_t layers; //由Turbo维护,默认值为1
TUsages usages;
TDomain domain;//详见[资源的所有者端域]章节
};
};与Image类似
typedef enum TBufferUsageBits
{
TRANSFER_SRC = 0x00000001,//考虑是否由Turbo管理该TUsageFlagsBits::TRANSFER_SRC用例
TRANSFER_DST = 0x00000002,//考虑是否由Turbo管理该TUsageFlagsBits::TRANSFER_DST用例
UNIFORM_TEXEL = 0x00000004,
STORAGE_TEXEL = 0x00000008,
UNIFORM = 0x00000010,
STORAGE = 0x00000020,
INDEX = 0x00000040,
VERTEX = 0x00000080,
INDIRECT = 0x00000100,
} TBufferUsageBits;
using TBufferUsages = uint32_t;
class TBuffer
{
public:
struct Descriptor
{
TBufferUsages usages;
uint64_t size;
TDomain domain;
};
void Create(const std::string &name, const Descriptor &descriptor, void *allocator);
void Destroy(void *allocator);
}计划派生出如下子类:
enum TVertexRate
{
VERTEX,
INSTANCE
};
using AttributeID=uint32_t;
class TVertexBuffer: public TBuffer
{
private:
uint32_t stride;
TVertexRate rate;
std::vector<Attribute> attributes;
public:
struct Descriptor
{
//TBufferUsages usages;//由Turbo管理,将会默认包括TBufferUsageBits::VERTEX
uint64_t size;
TDomain domain;
uint32_t stride;
TVertexRate rate = TVertexRate::VERTEX;//默认值为TVertexRate::VERTEX
};
void Create(const std::string &name, const Descriptor &descriptor, void *allocator);
void Destroy(void *allocator);
AttributeID AddAttribute( TFormatType formatType, uint32_t offset);
};class TIndexBuffer: public TBuffer
{
public:
enum class IndexType
{
UINT16,
UINT32
};
public:
struct Descriptor
{
//TBufferUsages usages;//由Turbo管理,将会默认包括TBufferUsageBits::INDEX
uint64_t size;
TDomain domain;
IndexType indexType;
};
void Create(const std::string &name, const Descriptor &descriptor, void *allocator);
void Destroy(void *allocator);
};UniformBuffer常见于存储一个Struct中的数据
class TUniformBuffer: public TBuffer
{
public:
struct Descriptor
{
//TBufferUsages usages;//由Turbo管理,将会默认包括TBufferUsageBits::UNIFORM
uint64_t size;
TDomain domain;
};
void Create(const std::string &name, const Descriptor &descriptor, void *allocator);
};或者说提供一个模板类型的UniformBuffer(这样的弊端就是一个UniformBuffer只能对应一种struct,好处就是方便直接):
template<class T>
class TUniformBuffer: public TBuffer
{
public:
struct Descriptor
{
//TBufferUsages usages;//由Turbo管理,将会默认包括TBufferUsageBits::UNIFORM
//uint64_t size;//由模板 sizeof(T)推出
TDomain domain;
};
void Create(const std::string &name, const Descriptor &descriptor, void *allocator);
}namespace Turbo::Render
typedef enum class TFormat
{
R8G8B8A8_SRGB,
B8G8R8A8_SRGB,
R8G8B8_SRGB,
B8G8R8_SRGB,
R8G8B8A8_UNORM,
B8G8R8A8_UNORM,
R8G8B8_UNORM,
B8G8R8_UNORM,
D32_SFLOAT,
D16_UNORM,
R32_SFLOAT,
R32G32_SFLOAT,
R32G32B32_SFLOAT,
R32G32B32A32_SFLOAT,
}TFormat;资源的创建与销毁需要一个资源分配器,而该资源分配器因该由Context上下文来创建
资源分配器命名为TResourceAllocator,其构造函数参数:context。
资源分配器只要分配两种资源:
- Image
- Buffer
资源分配器只分配基础资源,比如class Image类的资源,而不会分配其派生的子类(因为子类都是派生自Image),class Buffer资源类似。
方案一(弃用)×
- 由于创建
Turbo::Render::Image需要返回Turbo::Core::TImage和Turbo::Core::TImageView两个类,所以TResourceAllocator在创建Image时需要返回std::pair<Turbo::Core::TImage*, Turbo::Core::TImageView*>。- 对于销毁
Turbo::Render::Image,需要传入Turbo::Core::TImage和Turbo::Core::TImageView两个类,其中Turbo在销毁时查看Turbo::Core::TImageView是否能与Turbo::Core::TImage对应上,能对上就删除,对不上直接返回异常。
方案二(采纳)√
- 由于
Turbo::Render::TContext来创建和销毁Turbo::Core::TImage并由Turbo::Render::TImage来创建和销毁Turbo::Core::TImageView(其原因是Turbo::Core::TImageView其实可以动态的改变,当将某Image解释成Color Image,对应生成支持Color的ImageView即可,当想解释成Depth时,重新建立支持Depth的ImageView即可,灵活管理,方便扩展)
class TResourceAllocator
{
public:
TResourceAllocator(TContext* context);
Turbo::Core::TImage* CreateImage(const Turbo::Render::TImage::Descriptor& des)
{
//返回使用context创建的图片资源
return context->CreateImage(des.width,des.height,des.depth,...);
}
void DestroyImage(Turbo::Core::TImage* image)
{
context->DestroyImage(image,imageView);
}
}Buffer同Image
- 对于
TContext::CreateImage(...)与Turbo::Core::TImage参数对应struct Turbo::Render::Image::Descriptor { Turbo::Render::TFlag flag;//用于CubeImage Turbo::Render::TFormat format; uint32_t width;//1D轴,当(width≠0,height=0,depth=0)时,对应Turbo::Core::TImageType::1D uint32_t height;//2D轴,当(width≠0,height≠0,depth=0)时,对应Turbo::Core::TImageType::2D uint32_t depth;//3D轴,当(width≠0,height≠0,depth≠0)时,对应Turbo::Core::TImageType::3D uitn32_t mipLevels; uint32_t layers; TUsages usages; }; Turbo::Render::TContext::CreateImage(const Turbo::Render::Image::Descriptor& descriptor); Turbo::Core::TImage::TImage( TDevice *device, //由TContext指定 VkImageCreateFlags imageFlags, //由Render层传入,一般Render::TCubeImage创建指定 TImageType type, //由Render层转换推出(根据Turbo::Render::Image::Descriptor的长宽高转换推出) TFormatInfo format, //由Render层传入 uint32_t width, //由Render层传入 uint32_t height, //由Render层传入 uint32_t depth, //由Render层传入 uint32_t mipLevels, //由Render层传入 uint32_t arrayLayers, //由Render层传入 TSampleCountBits samples, //由Turbo维护 TImageTiling tiling, //由Turbo维护 TImageUsages usages, //由Render层传入 TMemoryFlags memoryFlags, //由Turbo维护 TImageLayout layout//由Turbo维护,默认值TImageLayout::UNDEFINED )
目前TResourceAllocator分配和销毁Image和Buffer是直接分配,直接销毁的(这是短期目标),目前每一帧中用到就分配,结束就销毁,但是这并不是一种良构。正常来说分配的Image和Buffer最终应该在Context或者ResourceAllocator中存储并使用Cache或者Pool之类技术存储,这样也能与异步资源回收(见异步资源回收章节)互相配合。
注:异步资源回收主要是针对FrameGraph中的资源进行回收,用户自定义的资源,比如采样纹理之类的还是要自己管理
所谓的资源所有者端位,其实是指资源是在CPU端创建,还是在GPU端创建。对于CPU和GPU的资源之间可以互相拷贝传输,所以需要在创建资源时设置资源的端域
有多种情况:
CPU端到GPU端 Loadinggraph TD; CreateCPUDomainResource["创建CPU端资源"] CreateGPUDomainResource["创建GPU端资源"] SetDataIntoCPUDomainResource[将数据设置到CPU端资源中] CopyCPUDomainResourceIntoGPUDomainResource[将CPU端资源数据拷贝到GPU端资源中] ReleaseCPUDomainResource[释放CPU端资源] CreateCPUDomainResource-->CreateGPUDomainResource CreateGPUDomainResource-->SetDataIntoCPUDomainResource SetDataIntoCPUDomainResource-->CopyCPUDomainResourceIntoGPUDomainResource CopyCPUDomainResourceIntoGPUDomainResource-->ReleaseCPUDomainResource
GPU端到CPU端 Loadinggraph TD; CreateCPUDomainResource["创建CPU端资源"] CreateGPUDomainResource["创建GPU端资源"] SetDataIntoGPUDomainResource["通过运行CommandBuffer(Draw/Dispatch等),将数据设置到GPU端资源中"] CopyGPUDomainResourceIntoCPUDomainResource[将GPU端资源数据拷贝到CPU端资源中] ReleaseGPUDomainResource[释放GPU端资源] CreateCPUDomainResource-->CreateGPUDomainResource CreateGPUDomainResource-->SetDataIntoGPUDomainResource SetDataIntoGPUDomainResource-->CopyGPUDomainResourceIntoCPUDomainResource CopyGPUDomainResourceIntoCPUDomainResource-->ReleaseGPUDomainResource
CPU端到CPU端 Loadinggraph TD; CreateCPUDomainResourceCPUa["创建CPU端资源CPUa"] CreateCPUDomainResourceCPUb["创建CPU端资源CPUb"] Copy["使用CommandBuffer或者memcpy在CPUa和CPUb之间拷贝数据"] CreateCPUDomainResourceCPUa-->CreateCPUDomainResourceCPUb CreateCPUDomainResourceCPUb-->Copy
GPU端到GPU端 Loadinggraph TD; CreateGPUDomainResourceGPUa["创建GPU端资源GPUa"] CreateGPUDomainResourceGPUb["创建GPU端资源GPUb"] Copy["使用CommandBuffer在GPUa和GPUb之间拷贝数据"] CreateGPUDomainResourceGPUa-->CreateGPUDomainResourceGPUb CreateGPUDomainResourceGPUb-->Copy
CPU端与GPU端兼容
有时可以创建CPU端和GPU端共享的资源
namespace Turbo::Render
typedef enum TDomainBits
{
CPU=0x00000001,
GPU=0x00000002
}TDomainBits;
using TDomain = uint32_t;考虑是否向用户开放
TRANSFER_SRC和TRANSFER_DST?如果Turbo负责维护该属性应该会更符合设计思想,用户只需要考虑使用Domain[CPU,GPU]即可,会比较简单
不同的Usage和Domain会影响Turbo底层对于具体资源的创建策略。
常见的资源创建策略有一下几种:
- GPU独占资源(GPU)
表示该资源只有GPU可以访问。 Loadinggraph TD; direction TB subgraph GPU subgraph GPUImage["Image"] direction TB subgraph GPUImageDescriptor[Descriptor] GPUImageDescriptorArgs["Usages:不限\nDomain:GPU"] end subgraph GPUImageCreateTImage["创建Core::TImage"] GPUImageCreateTImageArgs["Tiling:OPTIMAL\nMemoryFlags:DEDICATED_MEMORY"] end GPUImageDescriptor--对应底层-->GPUImageCreateTImage end subgraph GPUBuffer["Buffer"] direction TB subgraph GPUBufferDescriptor[Descriptor] GPUBufferDescriptorArgs["Usages:不限\nDomain:GPU"] end subgraph GPUBufferCreateTImage["创建Core::TBuffer"] GPUBufferCreateTImageArgs["MemoryFlags:DEDICATED_MEMORY"] end GPUBufferDescriptor--对应底层-->GPUBufferCreateTImage end end满足以下条件即为
GPU独占资源:
Domain只有GPU位标
- 对应
Turbo::Core底层资源内存分配为:Turbo::Core::TMemoryFlagsBits::DEDICATED_MEMORYTurbo::Core::Image对应的构造参数:
- Turbo::Core::TImageTiling::OPTIMAL
Turbo::Core::TBuffer对应的构造参数:
- 略
- 用于传输拷贝的暂存副本(CPU→GPU)
注:暂存副本多为Buffer资源
由于GPU独占资源只能使用GPU进行访问,有时需要将CPU端的数据赋值给GPU端,所以需要使用一个CPU端可写入并且可以拷贝到GPU端的资源,此种资源叫做暂存副本(Staging)。 Loadinggraph TD; direction TB subgraph CPU[CPU端资源] subgraph CPUImage["Image"] direction TB subgraph CPUImageDescriptor[Descriptor] CPUImageDescriptorArgs["Usages:TRANSFER_SRC+除了TRANSFER_DST所有\nDomain:CPU"] end subgraph CPUImageCreateTImage["创建Core::TImage"] CPUImageCreateTImageArgs["Tiling:LINEAR(注意:Vulkan标准限值)\nMemoryFlags:HOST_ACCESS_SEQUENTIAL_WRITE"] end CPUImageDescriptor--对应底层-->CPUImageCreateTImage end subgraph CPUBuffer["Buffer"] direction TB subgraph CPUBufferDescriptor[Descriptor] CPUBufferDescriptorArgs["Usages:TRANSFER_SRC+除了TRANSFER_DST所有\nDomain:CPU"] end subgraph CPUBufferCreateTImage["创建Core::TBuffer"] CPUBufferCreateTImageArgs["MemoryFlags:HOST_ACCESS_SEQUENTIAL_WRITE"] end CPUBufferDescriptor--对应底层-->CPUBufferCreateTImage end end CPUImage--"使用Map/Copy将数据赋值给CPU端Image"-->UseMapCopyDataIntoImage[刷新CPU端Image数据] CPUBuffer--"使用Map/Copy将数据赋值给CPU端Image"-->UseMapCopyDataIntoBuffer[刷新CPU端Buffer数据] subgraph CreateGPUOnlyResource["GPU独占资源"] GPUOnlyImage["Image"] GPUOnlyBuffer["Buffer"] end UseMapCopyDataIntoImage--"使用CopyCommand将CPU端数据拷贝至GPU端"-->GPUOnlyImage UseMapCopyDataIntoImage--"使用CopyCommand将CPU端数据拷贝至GPU端"-->GPUOnlyBuffer UseMapCopyDataIntoBuffer--"使用CopyCommand将CPU端数据拷贝至GPU端"-->GPUOnlyImage UseMapCopyDataIntoBuffer--"使用CopyCommand将CPU端数据拷贝至GPU端"-->GPUOnlyBuffer
满足以下条件即为
暂存副本:
Usage包含TRANSFER_SRC位标,不包括TRANSFER_DST位标,并且Domain只包含CPU位标对应
Turbo::Core底层资源内存分配为:Turbo::Core::TMemoryFlagsBits::HOST_ACCESS_SEQUENTIAL_WRITE
Turbo::Core::Image对应的构造参数:
- (一般都是创建暂存
Buffer,之后Command拷贝到OPTIMAL和DEDICATED_MEMORY的Image中)- Turbo::Core::TImageTiling::
OPTIMAL(目前有问题VMA:OPTIMAL with HOST_ACCESS_SEQUENTIAL_WRITE)
- 作为传输的数据源
usage应该为TRANSFER_SRCTurbo::Core::TBuffer对应的构造参数:
- ...
- 回读(CPU←GPU)
回读是将GPU独占资源回读拷贝至CPU端,简单来说是暂存副本(CPU→GPU)的逆向。 Loadinggraph TD; direction TB subgraph CPU[CPU端资源] subgraph CPUImage["Image"] direction TB subgraph CPUImageDescriptor[Descriptor] CPUImageDescriptorArgs["Usages:TRANSFER_DST+除了TRANSFER_SRC所有\nDomain:CPU"] end subgraph CPUImageCreateTImage["创建Core::TImage"] CPUImageCreateTImageArgs["Tiling:LINEAR(注意:Vulkan标准限值)\nMemoryFlags:HOST_ACCESS_RANDOM"] end CPUImageDescriptor--对应底层-->CPUImageCreateTImage end subgraph CPUBuffer["Buffer"] direction TB subgraph CPUBufferDescriptor[Descriptor] CPUBufferDescriptorArgs["Usages:TRANSFER_DST+除了TRANSFER_SRC所有\nDomain:CPU"] end subgraph CPUBufferCreateTImage["创建Core::TBuffer"] CPUBufferCreateTImageArgs["MemoryFlags:HOST_ACCESS_RANDOM"] end CPUBufferDescriptor--对应底层-->CPUBufferCreateTImage end end CPUImage--"使用Map获取数据指针"-->UseMapCopyDataIntoImage[CPU端获取数据] CPUBuffer--"使用Map获取数据指针"-->UseMapCopyDataIntoBuffer[CPU端获取数据] subgraph CreateGPUOnlyResource["GPU独占资源"] GPUOnlyImage["Image"] GPUOnlyBuffer["Buffer"] end GPUOnlyImage--"使用CopyCommand将GPU端数据拷贝至CPU端"-->CPUImage GPUOnlyBuffer--"使用CopyCommand将GPU端数据拷贝至CPU端"-->CPUImage GPUOnlyImage--"使用CopyCommand将GPU端数据拷贝至CPU端"-->CPUBuffer GPUOnlyBuffer--"使用CopyCommand将GPU端数据拷贝至CPU端"-->CPUBuffer满足以下条件即为
回读:
Usage包含TRANSFER_DST位标,不包含TRANSFER_SRC位标,并且Domain只包含CPU位标对应
Turbo::Core底层资源内存分配为:Turbo::Core::TMemoryFlagsBits::HOST_ACCESS_RANDOM
Turbo::Core::Image对应的构造参数:
一种是是创建
CPU端的Buffer,之后Command将Image拷贝到Buffer中,之后读Buffer。另一种是创建
CPU端的Image,之后将GPU端的Image拷贝到CPU端Image中Turbo::Core::TImageTiling::LINEAR(如果满足以下条件)
in Vulkan 标准 : VkImageCreateInfo:
Images created withtilingequal toVK_IMAGE_TILING_LINEARhave further restrictions on their limits and capabilities compared to images created withtilingequal toVK_IMAGE_TILING_OPTIMAL. Creation of images with tilingVK_IMAGE_TILING_LINEARmay not be supported unless other parameters meet all of the constraints:
imageTypeisVK_IMAGE_TYPE_2Dformatis not a depth/stencil formatmipLevelsis 1arrayLayersis 1samplesisVK_SAMPLE_COUNT_1_BITusageonly includesVK_IMAGE_USAGE_TRANSFER_SRC_BITand/orVK_IMAGE_USAGE_TRANSFER_DST_BITTurbo::Core::TImageTiling::OPTIMAL(如果使用
Khronos KTX标准进行纹理回读的话,满足此种情况)Turbo::Core::TBuffer对应的构造参数:
- ...
综上:(不同一个是暂存副本和回读两者的底层Core::TImage创建相同TRANSFER_SRC一个是TRANSFER_DST),可以按照Domain::CPU进行创建区分
- 高频传输(CPU⇄GPU)(多为CPU与GPU端共享资源)
高频传输一般用于CPU频繁的更改资源数据,GPU之后频繁的读此数据(多见于uniform buffer)
- 注:高频传输多为
Buffer资源*- 大体上和
暂存副本区别不大,Domain为CPU+GPU,并且在分配MemoryFlags时多了一个HOST_ACCESS_ALLOW_TRANSFER_INSTEAD位标,并在Copy时,由Turbo负责正确拷贝 Loadinggraph TD; direction TB subgraph CPUAndGPU[CPU与GPU端共享资源] subgraph CPUImage["Image"] direction TB subgraph CPUImageDescriptor[Descriptor] CPUImageDescriptorArgs["Usages:TRANSFER_DST\nDomain:CPU+GPU"] end subgraph CPUImageCreateTImage["创建Core::TImage"] CPUImageCreateTImageArgs["Tiling:LINEAR(注意:Vulkan标准限值)\nMemoryFlags:HOST_ACCESS_SEQUENTIAL_WRITE+HOST_ACCESS_ALLOW_TRANSFER_INSTEAD"] end CPUImageDescriptor--对应底层-->CPUImageCreateTImage end subgraph CPUBuffer["Buffer"] direction TB subgraph CPUBufferDescriptor[Descriptor] CPUBufferDescriptorArgs["Usages:TRANSFER_DST\nDomain:CPU+GPU"] end subgraph CPUBufferCreateTImage["创建Core::TBuffer"] CPUBufferCreateTImageArgs["MemoryFlags:HOST_ACCESS_SEQUENTIAL_WRITE+HOST_ACCESS_ALLOW_TRANSFER_INSTEAD"] end CPUBufferDescriptor--对应底层-->CPUBufferCreateTImage end end subgraph TurboCopy["Copy()"] direction TB IsSupportHOST_VISIBLE{{是否支持HOST_VISIBLE}} Memcpy[memcpy] UseStagingCase[使用暂存副本流程] IsSupportHOST_VISIBLE--支持-->Memcpy IsSupportHOST_VISIBLE--不支持-->UseStagingCase end CPUImage--"使用Copy将数据赋值给CPU端Image(由Turbo负责正确拷贝)"-->IsSupportHOST_VISIBLE CPUBuffer--"使用Copy将数据赋值给CPU端Image(由Turbo负责正确拷贝)"-->IsSupportHOST_VISIBLE Memcpy-->FinishCopy[赋值结束] Memcpy-->FinishCopy UseStagingCase-->FinishCopy UseStagingCase-->FinishCopy满足以下条件即为
高频传输:
Usage包含TRANSFER_DST位标,并且Domain包含CPU和GPU位标对应
Turbo::Core底层资源内存分配为:Turbo::Core::TMemoryFlagsBits::HOST_ACCESS_SEQUENTIAL_WRITE和Turbo::Core::TMemoryFlagsBits::HOST_ACCESS_ALLOW_TRANSFER_INSTEAD此种情况在
Copy时需要Turbo底层查看分配的内存是否支持VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT如果支持直接memcpy,如果不支持需要调用暂存副本流程(详情参考VulkanMemoryAllocator::Advanced data uploading)(这需要提供Copy接口函数统一调配), 如果简化的话可以直击使用Turbo::Core::TMemoryFlagsBits::HOST_ACCESS_SEQUENTIAL_WRITE(有待测试)
Turbo::Core::Image对应的构造参数(同暂存副本情况):
- ...
Turbo::Core::TBuffer对应的构造参数:
- ...
资源的拷贝传输基本上有两种方式:
- 创建
CPU端资源,之后使用memcpy(...)直接将数据拷贝至资源中 - 创建
GPU端资源,之后使用Commandbuffer将数据拷贝至资源中
资源的拷贝传输包括:
-
CPU⇄GPU
1.1CPU→GPU- 使用
暂存副本
void* some_data=...; Image/Buffer* cpu_resource=new Image/Buffer(TRANSFER_SRC, Domain::CPU);//暂存副本 cpu_resource->Copy(some_data);//将数据存入暂存副本 /* void Copy(some_data){ memcpy(src: some_data, dst: this cpu resource); } */ delete some_data;//可以删除原始数据了 Image/Buffer* gpu_resource=new Image/Buffer(TRANSFER_DST, Domain::GPU);//GPU独占资源,内部位标 gpu_resource->Copy(cpu_resource);//将暂存资源中的数据存入GPU独占资源中 /* void Copy(cpu_resource){ command_buffer->Copy(src: cpu_resource, dst: this gpu resource); device_queue->submit(command_buffer, fence); fence->WaitUntil(); } */
1.2
CPU←GPU- 使用
回读
Image/Buffer* gpu_resource = ...;//某些已经有数据的GPU独占资源 Image/Buffer* cpu_resource = new Image/Buffer(TRANSFER_DST, Domain::CPU);//回读副本 cpu_resource->Copy(gpu_resource);//将GPU独占资源数据存入回读副本 /* void Copy(gpu_resource){ command_buffer->Copy(src: gpu_resource, dst: this cpu resource); device_queue->submit(command_buffer, fence); fence->WaitUntil(); } */ cpu_resource->Open();//开启资源 void* data = cpu_resource->Data();//获取资源指针 void* read_some_data = new ...(data[n...n+i]);//读数据 cpu_resource->Close();//关闭资源
1.3
CPU↔GPU- 使用
高频传输
void* some_data =...; Image/Buffer* cpu_gpu_share_resource = new Image/Buffer(TRANSFER_DST, Domain::CPU + Domain::GPU);//CPU端和GPU端共享资源 cpu_gpu_share_resource->Copy(some_data);//将数据存入共享资源 /* void Copy(some_data){ if(is_support_host_visible) { memcpy(src:some_data, dst:this cpu resource); } else { //TODO: 使用暂存副本流程 } } */
- 使用
-
CPU⇄CPU- 使用
memcpy(...)
Image/Buffer* cpu_resource_a = ...;//某个已经存有数据的CPU端资源a Image/Buffer* cpu_resource_b = new Image/Buffer(TRANSFER_SRC, Domain::CPU);//CPU端资源b cpu_resource_b->Copy(cpu_resource_a);//将数据存入共享资源 /* void Copy(cpu_resource_a){ { memcpy(src:cpu_resource_a, dst:this cpu resource); } } */
- 使用
-
GPU⇄GPU- 使用
CommandBuffer
void* some_data=...; Image/Buffer* gpu_resource_a = ...;//某个已经存有数据的GPU端资源a Image/Buffer* gpu_resource_b = new Image/Buffer(TRANSFER_DST, Domain::CPU);//GPU端资源b gpu_resource_b->Copy(gpu_resource_a);//将数据存入暂存副本 /* void Copy(gpu_resource_a){ command_buffer->Copy(src: gpu_resource_a, dst: this gpu resource); device_queue->submit(command_buffer, fence); fence->WaitUntil(); } */
- 使用
资源拷贝传输大致可分为3种情况:
- 以void*为代表的的数据资源
- 以Buffer为代表的的数据资源
- 以Image为代表的的数据资源
所以正常来说void*,Buffer,Image三者之间应该两两互相可拷贝传输
graph LR;
VoidPtrSrc["void*"]
BufferSrc["Buffer"]
ImageSrc["Image"]
VoidPtrDst["void*"]
BufferDst["Buffer"]
ImageDst["Image"]
VoidPtrSrc<-..->VoidPtrDst
VoidPtrSrc<---->BufferDst
VoidPtrSrc<---->ImageDst
BufferSrc<---->VoidPtrDst
BufferSrc<---->BufferDst
BufferSrc<---->ImageDst
ImageSrc<---->VoidPtrDst
ImageSrc<---->BufferDst
ImageSrc<---->ImageDst
-
void*↔void*
此种情况属于程序自身内存拷贝,不属于Turbo负责的范畴 -
void*→Buffervoid* some_data; Buffer buffer; buffer.Copy(some_data,size);void Buffer::Copy(void* src,uint32_t size)Loadinggraph TD; IsCPUVisible{{资源能否在CPU端被访问}} UseMemcpy[使用memcpy进行数据拷贝] UseTempResource[使用临时资源进行数据拷贝] IsCPUVisible--能-->UseMemcpy IsCPUVisible--不能-->UseTempResource -
void*→Image同
void*→Buffervoid TImage::Copy(void* src,uint32_t size) -
Buffer→void*有个前提是
Buffer必须是CPU域的,也就是CPU可访问资源void* Buffer::Open()void Buffer::Close()Buffer buffer; void* data_ptr = buffer.Open(); //使用data_ptr将数据拷贝出来 //... buffer.Close();
-
Buffer→BufferLoadinggraph TD; IsSrcAndDstCPUVisible{{目标Buffer和源Buffer能否都能在CPU端被访问}} UseMemcpy[使用memcpy进行数据拷贝] UseCommandBuffer[使用CommandBuffer进行数据拷贝] IsSrcAndDstCPUVisible--能-->UseMemcpy IsSrcAndDstCPUVisible--不能-->UseCommandBuffervoid Buffer::Copy(const Buffer& buffer) -
Buffer→Image使用
CommandBuffer::CmdCopyBufferToImage(...)void Image::Copy(const Buffer& buffer) -
Image→void*同Buffer→void*void* Image::Open()void Image::Close() -
Image→Buffer使用
CommandBuffer::CmdCopyImageToBuffer(...)void Buffer::Copy(const Image& image) -
Image→Image使用
CommandBuffer::CmdCopyImage(...)或CommandBuffer::CmdBlitImage(...)void Image::Copy(const Image& image)
图片(纹理)主要有一下几种数据:
- 颜色(有时存储的颜色值并不是“颜色”信息,也可以是法线等信息)
- 深度
- 模板(用的不多)
由于不同设备对于不同格式的支持程度不大相同,所以在使用某种格式时需要先查看是否支持该格式。
- 对于
颜色数据,目前有如下格式
R8G8B8A8_SRGB
B8G8R8A8_SRGB
R8G8B8_SRGB
B8G8R8_SRGB
R8G8B8A8_UNORM
B8G8R8A8_UNORM
R8G8B8_UNORM
B8G8R8_UNORM分配策略如下
graph TD;
IsContainColorAttachmentOrInputAttahcmentUsage{{"Usage是否包含COLOR_ATTACHMENT || Usage是否包含INPUT_ATTACHMENT "}}
IsSupportR8G8B8A8_SRGB{{"是否支持R8G8B8A8_SRGB(检测是否满足Usage标志位)"}}
UseR8G8B8A8_SRGB[使用R8G8B8A8_SRGB]
IsSupportB8G8R8A8_SRGB{{"是否支持B8G8R8A8_SRGB(检测是否满足Usage标志位)"}}
UseB8G8R8A8_SRGB[使用B8G8R8A8_SRGB]
IsSupportR8G8B8_SRGB{{"是否支持R8G8B8_SRGB(检测是否满足Usage标志位)"}}
UseR8G8B8_SRGB[使用R8G8B8A8_SRGB]
IsSupportB8G8R8_SRGB{{"是否支持B8G8R8_SRGB(检测是否满足Usage标志位)"}}
UseB8G8R8_SRGB[使用B8G8R8_SRGB]
IsSupportR8G8B8A8_UNORM{{"是否支持R8G8B8A8_UNORM(检测是否满足Usage标志位)"}}
UseR8G8B8A8_UNORM[使用R8G8B8A8_UNORM]
IsSupportB8G8R8A8_UNORM{{"是否支持B8G8R8A8_UNORM(检测是否满足Usage标志位)"}}
UseB8G8R8A8_UNORM[使用B8G8R8A8_UNORM]
IsSupportR8G8B8_UNORM{{"是否支持R8G8B8_UNORM(检测是否满足Usage标志位)"}}
UseR8G8B8_UNORM[使用R8G8B8_UNORM]
IsSupportB8G8R8_UNORM{{"是否支持B8G8R8_UNORM(检测是否满足Usage标志位)"}}
UseB8G8R8_UNORM[使用B8G8R8_UNORM]
InitTargetFormat[初始化目标格式为UNDEFINED]
InitTargetFormat-->IsContainColorAttachmentOrInputAttahcmentUsage
IsContainColorAttachmentOrInputAttahcmentUsage--是-->IsSupportR8G8B8A8_SRGB
IsContainColorAttachmentOrInputAttahcmentUsage--否-->IsContainSampledUsage{{"Usage是否包含Sampled || Usage是否包含Storage"}}
IsSupportR8G8B8A8_SRGB--是-->UseR8G8B8A8_SRGB
IsSupportR8G8B8A8_SRGB--否-->IsSupportB8G8R8A8_SRGB
IsSupportB8G8R8A8_SRGB--是-->UseB8G8R8A8_SRGB
IsSupportB8G8R8A8_SRGB--否-->IsSupportR8G8B8_SRGB
IsSupportR8G8B8_SRGB--是-->UseR8G8B8_SRGB
IsSupportR8G8B8_SRGB--否-->IsSupportB8G8R8_SRGB
IsSupportB8G8R8_SRGB--是-->UseB8G8R8_SRGB
IsSupportB8G8R8_SRGB--否-->IsSupportR8G8B8A8_UNORM
IsSupportR8G8B8A8_UNORM--是-->UseR8G8B8A8_UNORM
IsSupportR8G8B8A8_UNORM--否-->IsSupportB8G8R8A8_UNORM
IsSupportB8G8R8A8_UNORM--是-->UseB8G8R8A8_UNORM
IsSupportB8G8R8A8_UNORM--否-->IsSupportR8G8B8_UNORM
IsSupportR8G8B8_UNORM--是-->UseR8G8B8_UNORM
IsSupportR8G8B8_UNORM--否-->IsSupportB8G8R8_UNORM
IsSupportB8G8R8_UNORM--是-->UseB8G8R8_UNORM
IsSupportB8G8R8_UNORM--否-->error["抛出不支持异常,没找到支持的格式"]
IsContainSampledUsage--是-->IsSupportR8G8B8A8_UNORM
IsContainSampledUsage--否-->TargetFormatStillUndefined[此时目标格式仍为UNDEFINED]
TargetFormatStillUndefined--从头筛选-->IsSupportR8G8B8A8_SRGB
- 对于
深度数据,目前有如下格式
D32_SFLOAT,
D16_UNORM分配策略如下
graph TD;
InitTargetFormat[初始化目标格式为UNDEFINED]
IsSupportD32_SFLOAT{{"是否支持D32_SFLOAT(检测是否满足Usage标志位)"}}
UseD32_SFLOAT[使用D32_SFLOAT]
IsSupportD16_UNORM{{"是否支持D16_UNORM(检测是否满足Usage标志位)"}}
UseD16_UNORM[使用D16_UNORM]
error["抛出不支持异常,没找到支持的格式"]
InitTargetFormat-->IsSupportD32_SFLOAT
IsSupportD32_SFLOAT--是-->UseD32_SFLOAT
IsSupportD32_SFLOAT--否-->IsSupportD16_UNORM
IsSupportD16_UNORM--是-->UseD16_UNORM
IsSupportD16_UNORM--否-->error
- 对于
模板数据,目前暂时不考虑
根据Vulkan标准,在创建Image时其对应的initialLayout,也就是初始化的布局必须是VK_IMAGE_LAYOUT_UNDEFINED或者VK_IMAGE_LAYOUT_PREINITIALIZED,在此Turbo是采用VK_IMAGE_LAYOUT_UNDEFINED,但是在之后的使用中(特别是CommandBuffer中)需要转换Image的Layout。在Vulkan标准12.4 Image Layout章节中,有如下描述:
After initialization, applications need not use any layout other than the general layout, though this may produce suboptimal performance on some implementations.
对应译文:
在
Image初始化之后(创建之后),应用需要使用通用布局(VK_IMAGE_LAYOUT_GENERAL)而不是其他布局,尽管使用通用布局在某些平台实现中会导致次优化(性能没有专用布局那么优化)
也就是说按照Vulkan标准来说,使用通用布局是必然。根据Filament的源码,其内部也确实使用的是VK_IMAGE_LAYOUT_GENERAL布局,但在作为着色器采样纹理时使用了专用布局VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。
//in {filament_dir}/filament/backend/src/vulkan/VulkanUtility.cpp
VkImageLayout getDefaultImageLayout(TextureUsage usage)
{
//由于Filement有时需要采样深度纹理获得深度数据(比如SSAO),所以简单使用了VK_IMAGE_LAYOUT_GENERAL布局
if (any(usage & TextureUsage::DEPTH_ATTACHMENT)) {
return VK_IMAGE_LAYOUT_GENERAL;
}
//由于Filement有时需要将同一个纹理不同miplevel之间互相拷贝(比如Bloom),并且想要避免昂贵的布局转换操作,所以简单使用VK_IMAGE_LAYOUT_GENERAL布局
if (any(usage & TextureUsage::COLOR_ATTACHMENT)) {
return VK_IMAGE_LAYOUT_GENERAL;
}
// Finally, the layout for an immutable texture is optimal read-only.
return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}对于Filament一些额外说明:
- 对于将
CPU资源拷贝到GPU时会将纹理布局转换到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL布局,之后拷贝操作完再将布局转换回来 - 对于渲染最后的
Present显示之前,会将图片的当前布局转换成VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
注:在ImageView中也有Layout,多个不同ImageView对于同一个Image其ImageView的Layout可以是多种多样的。这也就是在Turbo的设计中,一个Image可以对应多个ImageView
如果是Turbo管理Image的Layout的话,最好是在CommandBuffer中记录指令时进行纹理布局管理(因为Vulkan是以CommandBuffer为核心的架构)。比如:
只往纹理中写数据 Loadinggraph LR; Pass--写-->ColorTexture(["ColorTexture"]) Pass--写-->DepthTexture(["DepthTexture"])对应的
Layout可能为:
ColorTexture:VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMALDepthTexture:VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
只读纹理 Loadinggraph LR; ColorTexture(["ColorTexture"])--读-->Pass DepthTexture(["DepthTexture"])--读-->Pass对应的
Layout可能为:
ColorTexture:VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMALDepthTexture:VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
等等使用情况
注:纹理的Layput有使用指令前的布局转换,有使用中的布局转换,也有使用后的布局转换
理想的情况:
Texture tex;//VK_IMAGE_LAYOUT_UNDEFINED
command_buffer.BeginRenderPass(render_pass);//此时知道RenderPass对应的FrameBuffer要用到的所有纹理,进行指令前的布局转换
command_buffer.BindPipelineDescriptorSet(..., tex);//进行指令中的布局转换
command_buffer.ClearColorImage(..., tex);//进行指令中的布局转换
command_buffer.ClearDepthStencilImage(..., tex);//进行指令中的布局转换
command_buffer.CopyBufferToImage(..., tex);//进行指令中的布局转换
command_buffer.CopyImageToBuffer(..., tex);//进行指令中的布局转换
command_buffer.CopyImage(..., tex);//进行指令中的布局转换
command_buffer.BlitImage(..., tex);//进行指令中的布局转换
...//等等
command_buffer.EndRenderPass();//进行指令后的布局转换这需要每个纹理在记录特定CommandBuffer的指令前后进行layout转换(此种方法太麻烦,容易出问题):
- 每个
CommandBuffer都有一个表保存所有会用到的Image所对应的当前Layout,这个表用于记录Image的各种Layout转换 - 每个
CommandBuffer有两个同步函数:Flash,Wait。在Wait时当CommandBuffer执行完成后将Image的Layout同步到Image中
还有一种相对简单的方式:在创建Image时Context中已经存在CommandBuffer了,在Image创建后会顺便将Layout转换指令加入CommandBuffer中(此种方法可能有局限性,比如:创建完并且转换指令已经推送到了CommandBuffer但是没提交就销毁了该Image就会导致CommandBuffer在提交时找不到Image,虽然这种情况出现是没有意义的)。
在Image::Create(...)创建完Turbo::Core::TImage之后还没有结束,对于Vulkan渲染来说,ImageView是必须的。而一个Image可以对应多个ImageView,并且每个ImageView可以对应不同的Subresource(相当于Image的一个子集)
在每一个Render::Image中提供一个GetImageView(...)函数:
graph TD;
GetImageView["GetImageView(...)"]
FindImageView["在当前创建完成的多个ImageView集合中找兼容的ImageView,防止重复创建"]
IsFind{"是否找到了"}
CreateNewImageView["创建新的ImageView"]
ReturnImageView["返回已有的ImageView"]
GetImageView-->FindImageView
FindImageView-->IsFind
IsFind--否-->CreateNewImageView
IsFind--是-->ReturnImageView
创建一个ImageView需要如下参数:
enum ImageViewType
{
1D,
2D,
3D,
CUBE,
1D_ARRAY,
2D_ARRAY,
CUBE_ARRAY
};
enum
{
COLOR_BIT,
DEPTH_BIT,
STENCIL_BIT,
};
//需要的参数如下:
//Turbo::Core::TImage* image;//该参数通过Turbo::Render::TImage中可获取
ImageViewType imageViewType;
//Format format;//该参数通过Turbo::Render::TImage中可获取
ImageViewAspect aspect;//该参数虽然对外公开,但是可以通过Turbo::Render::TImage和其子类推出来
uint32_t baseMipLevel;
uint32_t levelCount;
uint32_t baseArrayLayer;
uint32_t layerCount;考虑:这样设计是否为良构?Turbo::Render::Image的出现以及其子类Turbo::Render::TTexture2D、Turbo::Render::TTexture3D和Turbo::Render::TDepthTexture2D出现就是为了将繁杂的Turbo::Core::TImage和Turbo::Core::TImageView进行封装的,所以提供GetImageView(...)函数并不是一个好的选择。
所以在此Turbo选择在Turbo::Render::TImage中将Turbo::Core::TImage和Turbo::Core::TImageView统一管理。
创建ImageView最好留给Turbo::Render::TImage的子类进行构建,Turbo::Render::TImage作为基类提供类似protected: virtual Turbo::Core::TImageView * CreateImageView(....)的接口函数,该接口函数会在Turbo::Render::TImage::Create(...)函数中创建完Turbo::Core::TImage之后调用,用于创建Turbo::Core::TImageView
对于着色器Shader,想要采样纹理,需要一个采样器(Sampler),而Sampler也与Image和Buffer一样,当成一种资源创建
class TSampler
{
struct Descriptor
{
TFilter minFilter = TFilter::LINEAR;
TFilter magFilter = TFilter::LINEAR;
TMipmapMode mipmapMode = TMipmapMode::LINEAR;
TAddressMode addressModeU = TAddressMode::REPEAT;
TAddressMode addressModeV = TAddressMode::REPEAT;
TAddressMode addressModeW = TAddressMode::REPEAT;
TBorderColor borderColor = TBorderColor::FLOAT_OPAQUE_WHITE;
float mipLodBias = 0.0f;
float minLod = 0.0f;
float maxLod = 0.0f;
};
void Create(const std::string &name, const Image::Descriptor &descriptor,void* allocator);
void Destroy();
};Context上下文中有整个Turbo的Vulkan环境,包括Core::TInstance、Core::TPhysicalDevice、Core::TDevice、Core::TDeviceQueue和各种CommandBuffer环境等
用户在构建上下文对象时,上下文的构造函数会去初始化环境。
在构造完Context之后,使用Context去构造WorldRender/Render进行后面渲染
Context需要提供CreateImage(...),DestroyImage(...),CreateBuffer(...),DestroyBuffer(...)函数,用于创建和销毁资源
Context中应该有一个默认的CommandBufferPool,并提供CommandBuffer* AllocateCommandBuffer()和void FreeCommandBuffer(CommandBuffer*)函数
Context中应该有一个默认的CommandBuffer
用户在使用Context创建完WorldRender/Render后调用WorldRender/Render::DrawFrame(...),其中DrawFrame(...)函数会去构建一帧的FrameGraph并进行一帧的渲染
在创建完Context后Turbo的基本环境已近构建,如果想要将渲染画面展示在屏幕上,还需要用户传入从窗口获得的Surface。
目前有以下几种情况:
- 用户没有指定
Surface(离屏渲染) - 用户指定了
Surface - 用户更改了(重新指定了)
Surface
- 对于用户没有指定
Surface
如果用户没有指定Surface,Turbo则会在内部创建一个虚的Surface,并使用虚Surface创建虚Swapchain,最终创建ColorImage用于存储最终的渲染结果(RenderTarget)
- 随即带来个问题:创建多大的
Surface呢?
- 对于用户指定了
Surface
如果用户指定了Surface,Turbo则会使用该Surface创建Swapchain,最终创建ColorImage用于存储最终的渲染结果(RenderTarget)
- 对于用户更改了(重新指定了)
Surface
如果用户之前已经指定了Surface,并再次指定Surface,如果当前Surface和指定的Surface不相同需要等待之前多有相关工作结束,并重新构建相关资源。
Turbo::Render核心将使用离屏渲染,将渲染结果写入RenderTarget,如果用户绑定了Surface则将RenderTarget的渲染结果拷贝到Surface所对应的Swapchain所对应的Image中。
优点:
- 灵活,非常容易实现
GBufferPass和Post-ProcessPass之类的功能 - 不依赖某一窗口,甚至是可以没有窗口
- 架构统一
缺点:
- 会多一次纹理图片颜色拷贝(无伤大雅,噗哈哈)
使用CommandBuffer::CmdBlitImage(...)可以很好的支持该工作
现在有个问题:如果采用方案二,离屏渲染的图片(
RenderTarget)大小是多少呢?
- 解决方案:需要用户自定义创建
Surface(此Surface可以使虚的也可以是实的),并将创建好的Surface绑定给Context,之后Context根据Surface进行操作。 如此会有两种情况:
用户没有指定
Surface
如果用户没有绑定任何Surface,Context将不会做任何事情,应为没有目标输出用户指定了
Surface Loadinggraph TD; IsSurfaceSame{{当前Surface与用户指定的Surface是否相同}}--相同--->DoNothing[什么也不做]; IsSurfaceSame--不相同-->WaitAll[等待之前所有工作结束并回收资源]; WaitAll-->Refresh[使用用户指定的Surface进行新的构建]; DoNothing-->Frame[继续下一帧工作] Refresh-->Frame[继续下一帧工作]
用户创建的Surface有两种
- 虚拟
Surface:对应着离屏渲染。所谓虚拟Surface是不跟任何窗口系统相关的虚拟表面(大白话是:不能显示在屏幕上,但可以获取渲染结果) - 真实
Surface: 对应着与窗口系统相关的Surface(底层为VkSurface与Turbo::Core::TSurface对应)(大白话是:能显示在屏幕上,同时可以获取渲染结果)
这两种Surface都对应着Turbo::Render::TSurface,只不过是对应得构造函数不同罢了。
- 如果用户使用的是虚拟
Surface,Turbo引擎将会在内部构建一套ColorImage:RenderTarget并在渲染结束后将渲染结果交给用户做后续工作 - 如果用户使用的是真实
Surface,Turbo引擎将会在内部构建Swapchain等一系列工作,并将渲染结果展现在屏幕窗口上,同时用户也可以获取相应的渲染结果(同离屏渲染) 注:现在Turbo并没有Turbo::Windows跨平台窗口层,而是交由用户自己制定需求,而大多数跨平台窗口层都提供返回VkSurface的接口,这也是Turbo支持跨平台窗口的原因,在未来也许会推出Turbo::Windows跨平台窗口层吧~?
namespace Turbo
{
namespace Render
{
class TSurface
{
private:
VkSurface vkSurface = VK_NULL_HANDLE;
public:
TSurface(uint32_t width,uint32_t height,uint32_t layer,uint32_t imageCount,...);//对应着[虚拟Surface]
TSurface(VkSurface vkSurface);//对应着[真实Surface]
//TSurface(const Turbo::TWindows& windows);//for未来~?
}
}
}为了满足离屏渲染的需求,
离屏渲染并不需要Turbo::Core::TSurface需要支持虚拟SurfaceTurbo::Core::TSurface支持虚拟Surface,而是需要RenderTarget纹理
注:以下代码已被遗弃,但可以做一个虚拟Surface内部数据参考
//虚拟Turbo::Render::TSurface对应的Turbo::Core::TSurface
Turbo::Core::TSurface属性
{
private:
T_VULKAN_HANDLE_PARENT Turbo::Core::TDevice *device = Context.device;//上下文中创建的设备
T_VULKAN_HANDLE_HANDLE VkSurfaceKHR vkSurfaceKHR = VK_NULL_HANDLE;//vkSurfaceKHR为空是虚Surface的标志
bool isExternalHandle = true;//为true,单独为属性赋值
std::vector<Turbo::Core::TQueueFamilyInfo> supportQueueFamilys;//空数组,size()为0
uint32_t minImageCount;//同uint32_t Turbo::Render::TSurface::imageCount
uint32_t maxImageCount;//同uint32_t Turbo::Render::TSurface::imageCount
Turbo::Core::TExtent2D currentExtent;//同uint32_t Turbo::Render::TSurface::width 和 ~::height
Turbo::Core::TExtent2D minImageExtent;//同uint32_t Turbo::Render::TSurface::width 和 ~::height
Turbo::Core::TExtent2D maxImageExtent;//同uint32_t Turbo::Render::TSurface::width 和 ~::height
uint32_t maxImageArrayLayers;//同uint32_t Turbo::Render::TSurface::layer
Turbo::Extension::TSurfaceTransforms supportedTransforms;//TSurfaceTransformBits::TRANSFORM_IDENTITY_BIT
Turbo::Extension::TSurfaceTransformBits currentTransform;//TSurfaceTransformBits::TRANSFORM_IDENTITY_BIT
Turbo::Extension::TCompositeAlphas supportedCompositeAlpha;//TCompositeAlphaBits::ALPHA_OPAQUE_BIT
Turbo::Core::TImageUsages supportedUsageFlags;//IMAGE_TRANSFER_SRC+IMAGE_TRANSFER_DST+IMAGE_COLOR_ATTACHMENT
std::vector<Turbo::Extension::TSurfaceFormat> surfaceFormats;//{format=Turbo选择一个颜色格式R8G8B8A8,TColorSpace::colorSpaceType=TColorSpaceType::SRGB_NONLINEAR}
std::vector<Turbo::Extension::TPresentMode> presentModes;//FIFO
}该类型由Turbo管理。对用户透明
为了满足离屏渲染的需求,
同Turbo::Core::TSwapchain需要支持虚拟SurfaceSurface情况
Turbo::Render层的任务核心,其中RenderTarget是离屏渲染的目标纹理图片(内部对应一个Image)
graph TD;
UserCreateContext[用户创建Context上下文]
UserCreateContext-->BeginFrame[开始当前帧]
BeginFrame-->IsBindSurface{{是否已经绑定Surface}}
IsBindSurface--未绑定-->DoNothingWithoutSurface[什么也不做]
IsBindSurface--"已绑定(通过调用Context.BindSurface(...))"-->IsSurfaceSame[什么也不做]
IsSurfaceSame{{当前Surface与用户指定的Surface是否相同}}--相同--->DoNothingForSameSurface[什么也不做]
IsSurfaceSame--不相同-->WaitAll[等待之前所有工作结束并回收资源]
subgraph RefreshSurface[使用用户指定的Surface进行新的构建]
direction TB
IsVirtualSurface{{是否是虚Surface}}
subgraph CreateRenderTargetAccordingVirtualSurface[根据虚拟Surface创建RenderTarget]
direction TB
GetVirtualSurfaceConfig[获取用户的虚拟Surface的配置]
end
IsVirtualSurface--是-->CreateRenderTargetAccordingVirtualSurface
subgraph CreateRenderTargetAccordingActualSurface[根据真实Surface创建RenderTarget]
direction TB
GetVkSurface[获取VkSurface] -->CreateSwapchain[根据VkSurface创建Swapchain]
CreateSwapchain-->GetImage[获取Swapchain中的Image]
end
IsVirtualSurface--否-->CreateRenderTargetAccordingActualSurface
end
subgraph FrameGraph["FrameGraph"]
direction TB
CreateRenderTargetForFG[创建RenderTarget]-->UseRenderTarget
subgraph UseRenderTarget["使用RenderTarget渲染(此处为FrameGraph的核心)"]
direction TB
end
UseRenderTarget-->IsVirtualSurface1{{是否是虚Surface}}
IsVirtualSurface1--是-->CopyResultReturntoUser[将RenderTarget结果拷贝给用户]
IsVirtualSurface1--否-->CopyRenderTargetToSwapchainImage[将RenderTarget渲染结果拷贝 Swapchain的Image中]
CopyRenderTargetToSwapchainImage-->ToUser[将渲染结果拷贝给用户]
ToUser-->PresentSwapchainImage["显示到Surface上(PresentPass)"]
end
CreateRenderTargetAccordingVirtualSurface-->CreateRenderTargetForFG
CreateRenderTargetAccordingActualSurface-->CreateRenderTargetForFG
WaitAll-->IsVirtualSurface
DoNothingForSameSurface-->CreateRenderTargetForFG
CopyResultReturntoUser-->EndFrame[当前帧结束]
PresentSwapchainImage-->EndFrame
%%EndFrame-->BeginFrame
用户如何获取渲染的结果呢?
用户通过Surface获取渲染结果
用户通过自定义PassNode获取渲染结果
一个PassNode代表一个GPU过程,GPU过程主要有两个过程:
- RenderPass:绘制过程
- ComputePass:计算过程
而在RenderPass绘制过程中是可以绑定调用ComputePass的,所以PassNode主要是用于描述绘制过程。
而PassNode有两个阶段:Setup初始化阶段和Execute执行阶段
Setup初始化阶段
该阶段主要是描述各Subpass,其中Subpass用于描述对各Image的读写情况,之后Turbo会根据相应配置创建相应的RenderPass,FrameBuffer等
graph TD;
PassNode>"PassNode::Setup"]
subgraph Subpass0[Subpass0]
direction LR
Depth0-.读.->Subpass
Color0-.读.->Subpass
Subpass--写-->Color1
Subpass--写-->Color2
Subpass--写-->Depth2
end
Subpass1["Subpass1"]
Subpass2["Subpass2"]
Subpassn["Subpass..."]
PassNode-->Subpass0
Subpass0-->Subpass1
Subpass1-->Subpass2
Subpass2-->Subpassn
struct CustomPassData
{
Resource colorTex;
Resource normalTex;
Resource depthTex;
}
//FrameGraph::PassNode::Setup
[&](TFrameGraph::TBuilder &builder, CustomPassData &data)
{
data.colorTex = builder.Create<Texture2D>("color",{512,512,Usage::Color})
data.normalTex = builder.Create<Texture2D>("normal",{512,512,Usage::Normal})
data.depthTex = builder.Create<DepthTexture2D>("depth",{512,512,Usage::Depth})
Subpass subpass0 = builder.CreateSubpass();
subpass0.Write(data.colorTex);
subpass0.Write(data.depthTex);
Subpass subpass1 = builder.CreateSubpass();
subpass1.Read(data.colorTex);
subpass1.Read(data.depthTex);
subpass1.Write(data.normalTex);
}Execute执行阶段
执行阶段就是运行Setup阶段设置的各种Subpass,执行阶段会去创建Pipeline,绑定CommandBuffer等
graph TD;
PassNode>"PassNode::Execute"]
BeginRenderPass["BeginRenderPass"]
subgraph Execute
direction TB
CmdBindPipeline[CmdBindPipeline]
CmdBindVertexBuffer[CmdBindVertexBuffer]
CmdDraw[CmdDraw]
CmdNextSubpass[CmdNextSubpass]
EtcExecute["..."]
end
subgraph MeshMaterial["Mesh/Material"]
direction TB
CmdBindMesh[CmdBindMesh]
CmdBindMaterial[CmdBindMaterial]
CmdMeshDraw[CmdMeshDraw]
EtcMeshMaterial["..."]
end
EndRenderPass["EndRenderPass"]
PassNode-->BeginRenderPass
BeginRenderPass--底层-->CmdBindPipeline
CmdBindPipeline-->CmdBindVertexBuffer
CmdBindVertexBuffer-->CmdDraw
CmdDraw-->CmdNextSubpass
CmdNextSubpass-->EtcExecute
EtcExecute-->EndRenderPass
BeginRenderPass-.高层.->CmdBindMesh
CmdBindMesh-.->CmdBindMaterial
CmdBindMaterial-.->CmdMeshDraw
CmdMeshDraw-.->EtcMeshMaterial
EtcMeshMaterial-.->EndRenderPass
//FrameGraph::PassNode::Execute
//方案一(倾向于此方案)
[=](const CustomPassData &data, const TResources &resources, void *context) {
Texture2D& color = resources.Get<Texture2D>(data.colorTex);
Texture2D& normal = resources.Get<Texture2D>(data.normalTex);
DepthTexture2D& depth = resources.Get<DepthTexture2D>(data.depth);
//注:以下context->*仅为示意,大概率会随着设计的改变而变化
Context* context = static_cast<Context*>(context);
context->CmdBeginRenderPass(???TODO: 如何将RenderPass从PassNode中提出来???);
context->CmdBindPipeline(pipeline);//来自Material:Pipeline需要现用,现创建
context->CmdPushConstants(0, sizeof(alpha), &alpha);//来自Material
context->CmdBindPipelineDescriptorSet(pipeline_descriptor_set);//来自Material
context->CmdBindVertexBuffers(vertex_buffers);//来自Mesh
context->CmdSetViewport(frame_viewports);//来自Camera和Surface
context->CmdSetScissor(frame_scissors);//来自Material
context->CmdBindIndexBuffer(index_buffer);//来自Mesh
context->CmdDrawIndexed(indices_count, 1, 0, 0, 0);//来自Drawable
context->CmdNextSubpass();
...
context->CmdEndRenderPass();
}
//方案二(需要高级特性)
[=](const CustomPassData &data, const TResources &resources, void *context) {
Texture2D& color = resources.Get<Texture2D>(data.colorTex);
Texture2D& normal = resources.Get<Texture2D>(data.normalTex);
DepthTexture2D& depth = resources.Get<DepthTexture2D>(data.depth);
Context* context = static_cast<Context*>(context);
context->CmdBeginRenderPass(render_pass, swpachain_framebuffers[current_image_index]);
context->CmdSetViewport(frame_viewports);
context->BindMaterial(material);
//context->CmdBindPipeline(pipeline);//来自Material:Pipeline需要现用,现创建
//context->CmdPushConstants(0, sizeof(alpha), &alpha);//来自Material
//context->CmdBindPipelineDescriptorSet(pipeline_descriptor_set);//来自Material
//context->CmdSetScissor(frame_scissors);//来自Material
context->BindMesh(mesh);
//context->CmdBindVertexBuffers(vertex_buffers);//来自Mesh
//context->CmdBindIndexBuffer(index_buffer);//来自Mesh
context->DrawMesh();
//context->CmdDrawIndexed(indices_count, 1, 0, 0, 0);//来自Drawable
context->CmdNextSubpass();
...
context->CmdEndRenderPass();
}由于Pipeline是在绑定时由Turbo动态管理创建,所以需要一个Pipeline描述,用于告诉Context绑定什么样的Pipeline
Pipeline主要有两类
- Graphic图形管线
- Compute计算管线
其中计算管线比较简单,只需要指定计算着色器即可。
class ComputePipeline
{
ComputeShader* computeShader;
};
ComputeShader* compute_shader = new ...;
ComputePipeline compute_pipeline(compute_shader);
//FrameGraph::PassNode::Execute
context->BindPipeine(compute_pipeline);
context->Dispatch(...);而对于图形管线,需要的相对较多了(大部分有默认值):
class GraphicsPipeline
{
VertexShader* vertexShader;
FragmentShader* fragmentShader;
Topology topology;//POINT_LIST,LINE_LIST,LINE_STRIP,TRIANGLE_LIST等
Polygon polygon;//FILL,LINE,POINT等
Cull cull;//Front|Back
FrontFace front;//COUNTER_CLOCKWISE,CLOCKWISE
float lineWidth;
bool depthTestEnable;
bool depthWriteEnable;
CompareOp depthCompareOp;//LESS_OR_EQUAL
bool blendEnable;
BlendFactor srcColorBlendFactor;
BlendFactor dstColorBlendFactor;
BlendOp colorBlendOp;
BlendFactor srcAlphaBlendFactor;
BlendFactor dstAlphaBlendFactor;
BlendOp alphaBlendOp;
...等
/*bool stencilTestEnable;
StencilOp frontFailOp;
StencilOp frontPassOp;
StencilOp frontDepthFailOp;
CompareOp frontCompareOp;*/
};
VertexShader* vertex_shader = new ...;
FragmentShader* fragment_shader = new ...;
GraphicsPipeline graphics_pipeline(vertex_shader, fragment_shader);
//FrameGraph::PassNode::Execute
context->BindPipeine(graphics_pipeline);
context->BindVertexBuffer(vertex_buffer);
context->BindIndexBuffer(index_buffer);
context->Draw(...);context在绑定pipeline时并不会真的去创建pipeline,而只是更新context中当前pipeline状态,真正的的Pipeline创建在调用DrawCall绘制指令时,Turbo会根据当前状态进行相应对象的创建,归根结底,在DrawCall时创建Pipeline是因为:Turbo::Core中创建TGraphicsPipeline需要指定std::vector<Turbo::Core::TVertexBinding>,也就是顶点属性,而顶点属性正常来说应该在Mesh中,或者声明在VertexBuffer中(声明在VertexBuffer中,这是一个好主意),并在绑定VertexBuffer时更新相关状态,总而言之:需要在创建Pipeline之前告诉Turbo绑定的顶点数据的属性,这样Turbo才能根据顶点属性创建Pipeline。由于其复杂性,新建一个Pipeline的VertexBinding章节单独讨论
在
Vulkan标准中对于VkPipelineVertexInputStateCreateInfo::pVertexAttributeDescriptions有这样的规定:
All elements of pVertexAttributeDescriptions must describe distinct attribute locations来源翻译过来就是:绑定的
VkVertexInputAttributeDescription::location不能有重复的
如前文所说,在创建Pipeline时需要指定std::vector<Turbo::Core::TVertexBinding>,而顶点属性现在存在于绑定的VertexBuffer中,其实还有一个地方:那就是Shader中,当用户创建完VertexShader后,Turbo其实在VertexShader中存有有着色器的in属性变量,也就是说Turbo知道该Shader需要什么样的TVertexBinding
按照Shader中的in变量声明来构建相应的TVertexBinding,应该算是一个好主意,但是会有一个问题:就是顶点着色器中的in声明需要与VertexBuffer对应上才行,而这种对应,Turbo并不能进行干预,只能用户自己写Shader时将in声明的变量与绑定的VertexBuffer相对应。换句话就是Turbo并不能干预用户如何实现Shader代码
VertexBinding主要用于描述如下:
-
Bindinguint32_t binding:绑定索引uint32_t stride:单个数据长度TVertexRate rate:相对VERTEX顶点(这个用的比较多),还是相对INSTANCE实例
-
Attribute(每个Binding所对应的的)uint32_t location:对应着色器的接口locationTFormatType formatType:单个数据格式(相对于Binding::stride的长度)uint32_t offset:偏移(相对于Binding::stride的长度)
而这些属性主要位于VertexBuffer中,这也就是为什么VertexBinding与VertexBuffer一一对应
Vulkan标准有如下规定:// Provided by VK_VERSION_1_0 typedef struct VkPipelineVertexInputStateCreateInfo { VkStructureType sType; const void* pNext; VkPipelineVertexInputStateCreateFlags flags; uint32_t vertexBindingDescriptionCount; const VkVertexInputBindingDescription* pVertexBindingDescriptions; uint32_t vertexAttributeDescriptionCount; const VkVertexInputAttributeDescription* pVertexAttributeDescriptions; } VkPipelineVertexInputStateCreateInfo;
- All elements of
pVertexBindingDescriptionsmust describe distinct binding numbers
译:pVertexBindingDescriptions中所有成员不能有重复的uint32_t VkVertexInputBindingDescription::binding值- All elements of
pVertexAttributeDescriptionsmust describe distinct attribute locations
译:pVertexAttributeDescriptions中所有成员不能有重复的uint32_t VkVertexInputAttributeDescription::location值
其中对应参数的确认阶段来源:
uint32_t Binding::binding:在调用绑定顶点缓冲集时(CmdBindVertexBuffers),可以推出该值uint32_t Binding::stride:有两种方式- 在创建
VertexBuffer时指定当前顶点缓冲的stride值(采用此方式,作为VertexBuffer的一种属性,比较符合直觉) 在调用绑定顶点缓冲集时,设置该(麻烦,在绑定时可能都不知道绑定的stride值VertexBuffer中的数据格式)
- 在创建
TVertexRate Binding::rate: 在创建VertexBuffer时指定(默认为VERTEX,如果是实例数据将会设置为INSTANCE)uint32_t Attribute::location: 在声明Pipeline时指定(也就是动态指定)TFormatType Attribute::formatType: 在创建VertexBuffer时指定(采用此方式,作为VertexBuffer的一种属性,比较符合直觉)uint32_t Attribute::offset: 在创建VertexBuffer时指定(采用此方式,作为VertexBuffer的一种属性,比较符合直觉)
综上则在创建VertexBuffer时,需要设置VertexBuffer的属性:
- 一个
uint32_t Binding::stride - 一个
TVertexRate Binding::rate:(注:由Vulkan标准可推出:一个VertexBuffer对于VERTEX和INSTANCE是一元的,也就是说一个VertexBuffer要么全是VERTEX,要么全是INSTANCE,不可能既是VERTEX也是INSTANCE) - 多个
Attribute,每个Attribute包括:TFormatType Attribute::formatTypeuint32_t Attribute::offset
声明成伪代码:
class Attribute
{
TFormatType formatType;
uint32_t offset;
};
class VertexBuffer: public Turbo::Render::TBuffer/*也许会作为Turbo::Render::TBuffer的一个子类*/
{
private:
uint32_t stride;
TVertexRate rate;
std::map<std::string, Attribute> attributes;
public:
VertexBuffer(uint32_t stride, TVertexRate rate = VERTEX);
void AddAttribute(const std::string& attribute, TFormatType formatType, uint32_t offset);
};需要在设置GPU指令时动态设置的的属性:
uint32_t Binding::bindinguint32_t Attribute::location:考虑如何对应:对应到哪个VertexBuffer的哪个Attribute上?参考如下伪代码
声明成伪代码:
//假如Vertex Shader对于in属性声明如下
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec3 color;
layout (location = 3) in vec2 uv;
layout (location = 4) in float weight;
layout (location = 5) in vec3 tangent;
//Turbo引擎端
typedef struct vec3
{
float x;
float y;
float z;
} vec3;
typedef struct vec2
{
float x;
float y;
} vec2;
typedef struct weight_tangent
{
float weight;
vec3 tangent;
} uv_tangent;
VertexBuffer position_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer normal_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer color_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer uv_buffer(/*stride*/sizeof(vec2));//rate默认为VERTEX
VertexBuffer weight_and_tangent_buffer(/*stride*/sizeof(weight_tangent));//rate默认为VERTEX
position_buffer.AddAttribute(/*attribute*/"POSITION", /*formatType*/R32G32B32, /*offset*/0);
normal_buffer.AddAttribute(/*attribute*/"NORMAL", /*formatType*/R32G32B32, /*offset*/0);
color_buffer.AddAttribute(/*attribute*/"COLOR", /*formatType*/R32G32B32, /*offset*/0);
uv_buffer.AddAttribute(/*attribute*/"UV", /*formatType*/R32G32, /*offset*/0);
weight_and_tangent_buffer.AddAttribute(/*attribute*/"WEIGHT", /*formatType*/R32, /*offset*/offsetof(weight_tangent, weight));
weight_and_tangent_buffer.AddAttribute(/*attribute*/"TANGENT", /*formatType*/R32G32B32, /*offset*/offsetof(weight_tangent, tangent));
//Turbo将根据绑定的顶点属性进行解析
command_buffer->BindVeretxAttribute(position_buffer, "POSITION", /*location*/0);
command_buffer->BindVeretxAttribute(normal_buffer, "NORMAL", /*location*/1);
command_buffer->BindVeretxAttribute(color_buffer, "COLOR", /*location*/2);
command_buffer->BindVeretxAttribute(uv_buffer, "UV",/*location*/3);
command_buffer->BindVeretxAttribute(weight_and_tangent_buffer, "WEIGHT",/*location*/4);
command_buffer->BindVeretxAttribute(weight_and_tangent_buffer, "TANGENT", /*location*/5);如上的设计有弊端:
- 当场景中有大量需要绘制的模型时,光用于描述每一个模型的
VertexBuffer中的Attribute那个字符串(POSITION、NORMAL等)就有很多,这会占据大量的没有必要的内存,所以将字符串去掉是很有必要的
优化的方式有两种:
- 将用于
attribute的字符串改为使用数字标时的ID号或索引号,这会大大减少内存使用
VertexBuffer position_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer normal_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer color_buffer(/*stride*/sizeof(vec3));//rate默认为VERTEX
VertexBuffer uv_buffer(/*stride*/sizeof(vec2));//rate默认为VERTEX
VertexBuffer weight_and_tangent_buffer(/*stride*/sizeof(weight_tangent));//rate默认为VERTEX
uint32_t position_id = position_buffer.AddAttribute(/*formatType*/R32G32B32, /*offset*/0);
uint32_t normal_id = normal_buffer.AddAttribute(/*formatType*/R32G32B32, /*offset*/0);
uint32_t color_id = color_buffer.AddAttribute(/*formatType*/R32G32B32, /*offset*/0);
uint32_t uv_id = uv_buffer.AddAttribute(/*formatType*/R32G32, /*offset*/0);
uint32_t weight_id = weight_and_tangent_buffer.AddAttribute(/*formatType*/R32, /*offset*/offsetof(weight_tangent, weight));
uint32_t tangent_id = weight_and_tangent_buffer.AddAttribute(/*formatType*/R32G32B32, /*offset*/offsetof(weight_tangent, tangent));
//Turbo将根据绑定的顶点属性进行解析
command_buffer->BindVeretxAttribute(position_buffer, position_id, /*location*/0);
command_buffer->BindVeretxAttribute(normal_buffer, normal_id, /*location*/1);
command_buffer->BindVeretxAttribute(color_buffer,color_id, /*location*/2);
command_buffer->BindVeretxAttribute(uv_buffer, uv_id,/*location*/3);
command_buffer->BindVeretxAttribute(weight_and_tangent_buffer, weight_id,/*location*/4);
command_buffer->BindVeretxAttribute(weight_and_tangent_buffer, tangent_id, /*location*/5);- 完全抛弃
attribute的字符串,改为完全动态绑定(此种方式类似OpenGL的glVertexAttribPointer函数)
//OpenGL
void glVertexAttribPointer( GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);VertexBuffer position_buffer();
VertexBuffer normal_buffer();//rate默认为VERTEX
VertexBuffer color_buffer();//rate默认为VERTEX
VertexBuffer uv_buffer();//rate默认为VERTEX
VertexBuffer weight_and_tangent_buffer();//rate默认为VERTEX
command_buffer->BindVeretxAttribute(position_buffer, /*stride*/sizeof(vec3), /*formatType*/R32G32B32, /*offset*/0, /*rate*/VERTEX, /*location*/0);//for position
command_buffer->BindVeretxAttribute(normal_buffer, /*stride*/sizeof(vec3), /*formatType*/R32G32B32, /*offset*/0, /*rate*/VERTEX, /*location*/1);//for normal
command_buffer->BindVeretxAttribute(color_buffer, /*stride*/sizeof(vec3),/*formatType*/R32G32B32, /*offset*/0, /*rate*/VERTEX, /*location*/2);//for color
command_buffer->BindVeretxAttribute(uv_buffer, /*stride*/sizeof(vec2), /*formatType*/R32G32, /*offset*/0, /*rate*/VERTEX, /*location*/3);//for uv
command_buffer->BindVeretxAttribute(weight_and_tangent_buffer, /*stride*/sizeof(weight_tangent), /*formatType*/R32, /*offset*/offsetof(weight_tangent, weight), /*rate*/VERTEX, /*location*/4);//for weight
command_buffer->BindVeretxAttribute(weight_and_tangent_buffe, /*stride*/sizeof(weight_tangent), /*formatType*/R32G32B32,/*offset*/offsetof(weight_tangent, tangent), /*rate*/VERTEX, /*location*/5);//for tangentTurbo考虑优化方式1和2都提供相应的接口
在调用BindVeretxAttribute时Turbo不会真的将VertexBuffer塞入CommandBuffer,只有在绑定了pipeline并且调用了绘制指令时才会去真正的构建Pipeline和将VertexBuffer塞入CommandBuffer中。
注:根据Vulkan标准来看在DrawCall指令之前需要绑定所有绘制相关的数据(包括渲染管线和顶点缓存数组),此时DrawCall便是是收集统计的好时机
对于Pipeline能够正确执行,还需要将对应的各种layout (set = M, binding = N) uniform UNIFORM_TYPE UNIFORM_NAME[T]属性进行绑定,Turbo将会提供BindDescriptor(uint32_t set, uint32_t binding, std::vector<各种uinform资源类型>)接口进行对应资源绑定
在Turbo中将所有绑定的资源都视为可一次性绑定多个资源,也就是
//shader
layout (set = M, binding = N) uniform UNIFORM_TYPE UNIFORM_NAME[T]
//host
context.BindDescriptor(uint32_t set, uint32_t binding, std::vector<各种uinform资源类型>)就算绑定的资源只有一个,也视为size为1的数组进行设置(这与Turbo的Core层相一致)
在Turbo的Core层,需要创建完Pipeline才能进行Descriptor相关资源数据的绑定,所以在调用BindDescriptor(...)时,内部是使用了一个树状结构先将用户绑定的Descriptor进行缓存。
目前Turbo应该提供如下基本Descriptor绑定
Buffer: 对应GLSL中uniform Buffer{...}Buffer;。最终存储为std::vector<TBuffer *>std::pair<TImage, TSampler>: 对应GLSL中uniform sampler2D xxx;。最终存储为std::vector<std::pair<TImageView *, TSampler *>>TImage: 对应GLSL中uniform texture2D xxx;。最终存储为std::vector<TImageView *>Sampler: 对应GLSL中uniform sampler xxx;。最终存储为std::vector<TSampler *>
问题来了,对于不同的类型Turbo如何进行不同类型的Descriptor缓存呢?(泛型编程可能是一个解决方法,但是有点麻烦,不直观)
一种比较直接的方式就是使用Map直接记录对应Descriptor数组:
//Turbo的内部维护结构
struct DescriptorID
{
uint32_t set = MAX;
uint32_t binding = MAX;
}
std::map<DescriptorID, std::vector<TBuffer *>> bufferMap;
std::map<DescriptorID, std::vector<std::pair<TImageView *, TSampler *>>> imageCombineWithSamplerMap;
std::map<DescriptorID, std::vector<TImageView *>> sampledImageMap;
std::map<DescriptorID, std::vector<TSampler *>> samplerMap;
//对应的Context对外接口
BindDescriptor(uint32_t set, uint32_t binding, std::vector<TBuffer>);//在bufferMap中进行缓存
BindDescriptor(uint32_t set, uint32_t binding, std::vector<std::pair<TImage, TSampler>>);//在bimageCombineWithSamplerMap中进行缓存
BindDescriptor(uint32_t set, uint32_t binding, std::vector<TImage>);//在sampledImageMap中进行缓存
BindDescriptor(uint32_t set, uint32_t binding, std::vector<Sampler>);//在samplerMap中进行缓存这有一个问题:用户在一次DrawCall时重复性绑定,如下
BindDescriptor(0, 0, some_buffers);//在bufferMap中进行缓存
BindDescriptor(0, 0, some_images);//在sampledImageMap中进行缓存用户在同一个set和binding下绑定了不同类型的Descriptor,虽然此行为在Vulkan标准中不被允许,但在Turbo中将会进行覆盖操作,这就需要Turbo记录某某set和binding下的Descriptor是否已经被绑在了哪个类型的缓存里,如果已经绑定了,将原来的绑定解除,进行新的绑定,反之则直接绑定。这需要Turbo内部提供一个数据结构用于记录某某set和binding被绑定到了哪个缓冲中。
可能的结构为:
enum DescriptorType
{
UNIFROM_BUFFER,//表示相应数据缓存在bufferMap中
COMBINED_IMAGE_SAMPLER,//表示相应数据缓存在imageCombineWithSamplerMap中
SAMPLED_IMAGE,//表示相应数据缓存在sampledImageMap中
SAMPLER,//表示相应数据缓存在samplerMap中
};
class BindingMap
{
std::map<uint32_t binding, DescriptorType> bindings;
};
class SetMap
{
std::map<uint32_t set, BindingMap> sets;
};这样Turbo的判断流程大致如下:
graph TD;
Input["传入set号, binding号, std::vector<各种uinform资源类型>"]
IsSetMapHasSet{"SetMap中是否存有传入的Set号"}
IsBindingMapHasBinding{"BindingMap中是否存有传入的Binding号"}
AddNewSetAndBinding["增加新的Set,Binding映射(SetMap中增加新项)。并将std::vector<各种uinform资源类型>存入相应缓存"]
subgraph RefreshSetAndBindgMap["更新Set和Binding对应的缓存"]
RemoveDescriptorBinding["将对应的描述符数据从相应缓存中移除"]
AddDescriptorBinding["将对应的描述符数据添加到相应缓存中"]
UpdateSetBindingMap["更新Set和Binding的相应记录"]
RemoveDescriptorBinding-->AddDescriptorBinding
AddDescriptorBinding-->UpdateSetBindingMap
end
AddNewBinding["增加新的Binding(BindingMap增加新项目)。并将std::vector<各种uinform资源类型>存入相应缓存 "]
Input-->IsSetMapHasSet
IsSetMapHasSet--是-->IsBindingMapHasBinding
IsSetMapHasSet--否-->AddNewSetAndBinding
IsBindingMapHasBinding--是-->RefreshSetAndBindgMap
IsBindingMapHasBinding--否-->AddNewBinding
Turbo的Context中分别存有用于计算管线和图形管线的管线缓存
class Context
{
TComputePipeline compute_pipeline;
TGraphicsPipeline graphics_pipeline;
std::vector<Turbo::Core::TPipeline*> pipeline;//所有创建的管线
}当用户绑定管线,其实就是更新相应的TComputePipeline compute_pipeline和TGraphicsPipeline graphics_pipeline
ComputePipeline创建需要如下数据:
ComputerShader:来源于绑定的ComputePipeline中
GraphicsPipeline创建需要如下数据:
RenderPass:来源于BeginRenderPass(...)中subpass:来源于NextSubpass()中std::vector<TVertexBinding>:来源于绑定的BindVeretxAttribute中std::vector<TShader *> &shaders:来源于绑定的GraphicsPipeline中各种渲染配置(包括Topology,PolygonMode,CullModes等等):来源于绑定的GraphicsPipeline中
Subpass实际上是多个Image的集合,包括如下三种Image集
InputColorDepthStencil(确切说DepthStencil不是Image集,应该是单一的,只能有一张DepthStencil纹理)
//in Turbo::Render
class Subpass
{
private:
std::vector<ColorImage> colors;
std::vector<Image> inputs;
DepthStencilImage depthStencil;
public:
Subpass& AddColorAttachment(const ColorImage& colorImage);
Subpass& AddInputAttachment(const Image& image);
Subpass& SetDepthStencilAttachment(const DepthStencilImage& depthStencilImage);
const std::vector<ColorImage>& GetColorAttachments();
const std::vector<Image>& GetInputAttachments();
const DepthStencilImage GetDepthStencilAttachment();
};RenderPass实际上是多个Subpass的集合
//in Turbo::Render
class RenderPass
{
private:
std::vector<Subpass> subpasses;
public:
RenderPass& AddSubpass(const Subpass& subpass);
const std::vector<Subpass>& GetSubpasses();
};在FrameGraph::PassNode::Execute阶段绑定RenderPass时
context->CmdBeginRenderPass(render_pass)Turbo主要做两件事:
- 创建相应的
RenderPass - 创建相应的
FrameBuffer
FrameBuffer中可以很轻松的得到其中的Attachment集合(可以获取对应的ImageView),而RenderPass需要从各个Subpass中统计出来,所以为了方便,RenderPass需要提供std::vector<Turbo::Render::TImage> GetAttachemts()函数。
对于RenderPass和FrameBuffer的创建,需要根据FrameGraph::PassNode::Setup阶段中声明的各种Subpass来创建。
现在有个问题:如何从PassNode中将RenderPass配置提取出来,并传给Context,说白了就是传给CmdBeginRenderPass(RenderPass)函数的RenderPass参数的实参如何构建?接口如何设计?
可能的方式:
[=](const PresentPassData &data, const TResources &resources, void *context)
{
FrameGraph::RenderPass fg_render_pass = resources.GetRenderPass();
//std::vector<FrameGraph::Subpass> subpasses = fg_render_pass.GetSubpasses();
//FrameGraph::Attachment attachment = subpasses[x].GetAttachment();
//Texture2D &color_texture = resources.Get<Texture2D>(data.colorTexture);
//Texture2D &color_texture = attachment.Get<Texture2D>(data.colorTexture);
//...
Render::RenderPass render_pass = Fg_RenderPass_To_Render_RnederPass(fg_render_pass);
context->CmdBeginRenderPass(render_pass);
...
context->CmdEndRenderPass();
}FrameGraph::PassNode中增加FrameGraph::RenderPass FrameGraph::PassNode::GetRenderPass(),这样就可以获得FrameGraph::PassNode::Setup阶段所配置的RenderPass,并拿着该配置创建Context需要的RenderPass。缺点就是有点麻烦,需要定义两头差不多的RenderPass数据结构。优化的方式就是直接将FrameGraph::RenderPass作为Context::CmdBeginRenderPass的实参。
//Turbo::Render
class Context
{
//void BeginRenderPass(Turbo::FrameGraph::TRenderPass &renderPass);//会有问题,见下文
void BeginRenderPass(Turbo::Render::TRenderPass &renderPass);
}对于void Context::BeginRenderPass(Turbo::FrameGraph::TRenderPass &renderPass),由于Turbo::FrameGraph::TRenderPass下Turbo::FrameGraph::TSubpass下绑定的各个资源都是资源句柄id,对于该资源句柄对应得资源究竟是什么,只有开发用户知道,而这对于Turbo来说并不知道,所以更不用说在void Context::BeginRenderPass(Turbo::FrameGraph::TRenderPass &renderPass)中解析出对应资源句柄id对应得究竟是什么类型的资源。
Turbo对于RenderPass中的各种Subpass所绑定的Attachment,Turbo会进行统计,目的是创建与之对应的FrameBuffer,所以在此规定一下统计顺序,创建的FrameBuffer顺序也与之对应:
Color附件集Input附件集DepthStencil附件集
当调用Context::BeginRenderPass(...)后,Turbo会去查找是否有已经创建完的RenderPass,尝试重复利用已有的RenderPass,如果没有找到的话,则创建相应的RenderPass。
由于需要查询当前Context是否存在相互兼容的RenderPass已做到重复利用已有的RenderPass,此时需要Context中存在一个用于存储RenderPass的集合(Set)或池子(Pool,这个不错)
Context下有多个RenderPass,存储在RenderPassPool中。
RenderPassPool应该有如下功能:
bool Find(RenderPass& renderPass):查询相应的兼容的RenderPass,如果存在则返回true,并将结果写入到RenderPass& renderPass中,如果没找到则返回false,在内部会去调用std::find(...)函数,所以class RenderPass需要实现对于==符号重载RenderPass& Allocate(RenderPass& renderPass):先使用Find(renderPass)查询一下是否有该RenderPass,如果有直接返回相应的RenderPass,反之则分配一个RenderPass并存入池子中,之后返回创建的RenderPassvoid Free(RenderPass& renderPass):销毁相应的RenderPass
对于RenderPassPool中存储的RenderPass应该关心如下事物(核心思路是适配Turbo::Core::TRenderPass):
- 对应的
Turbo::Core::TRenderPass - 从
Turbo::Render::TRenderPass中的各种Subpass - 从
Turbo::Render::TRenderPass中的各种Subpass中指定的attachment相关的
由于需要管理RenderPass和FrameBuffer,所以提供了RenderPassProxy类,用于整合管理RenderPass和FrameBuffer,并由RenderPassPool创建和销毁
新的想法最近想到一种比较符合直觉的模式:刷新模式(我自己起的名字)
由于之前使用
RenderPassPool来创建RenderPassProxy类,RenderPassProxy类下面有真正的RenderPass,中间多一个RenderPassProxy来进行转换总感觉怪怪的,所以想到了如下方式:/* Turbo::Render::TRenderPass { friend class TRenderPassPool;//使得TRenderPassPool可以去刷新renderPass成员变量 private: Turbo::Core::TRenderPass* renderPass=nullptr;//注意:此成员变量为private,为待刷新的数据 } */ Turbo::Render::TRenderPass render_pass;//此时其成员变量renderPass(真正的RenderPass现在为空) Turbo::Render::TRenderPassPool render_pass_pool; bool result = render_pass_pool.Allocate(render_pass);//此时进行Turbo::Render::TRenderPass::renderPass成员变量的刷新 if(result) { //创建RenerPass成功 //render_pass的成员变量renderPass将会被TRenderPassPool附上有效值 { //伪代码 Turbo::Core::TRenderPass* core_render_pass = render_pass.renderPass; if(core_render_pass != nullptr) { ... } } } else { //创建RenerPass失败 //render_pass的成员变量renderPass仍然为空 }
在创建完Turbo::Core::TRenderPass之后,可以通过const std::vector<TAttachment> & Turbo::Core::TRenderPass::GetAttachments()函数获取相应的
通过用户指定的Turbo::Render::TRenderPass在RenderPassPool中创建Turbo::Core::TRenderPass,对于Turbo来说其主要任务有两个
- 收集各个
Subpass中的Attachment,集成到统一的一个Attachment数组中 - 计算各个
Subpass中的Attachment相对于Attachment数组中的偏移等(AttachmentReference)
由于会有重复的Attachment(如何判断两个Attachment相等:通过查看Attachment底层Image是否为同一个),需要剔除重复的Attachment项目
一个RenderPass下可以绑定多个FrameBuffer,在使用RenderPassPool得到了一个有效的RenderPass后,Turbo需要根据用于传入的RenderPass配置来查看当前的RenderPassPool返回的RenderPass是否有支持当前的FrameBuffer。对应得流程如下:
graph TD;
InputRenderPass["外部输入的RenderPass(配置)"]
AnValidRenderPass["RenderPassPool返回的一个有效的RenderPass(RenderPassProxy)"]
FindFrameBuffer{"寻找是否有兼容的FrameBuffer"}
CreateFrameBuffer["创建FrameBuffer"]
ReturnFrameBuffer["返回FrameBuffer"]
InputRenderPass-->AnValidRenderPass
AnValidRenderPass-->FindFrameBuffer
FindFrameBuffer--没找到-->CreateFrameBuffer
FindFrameBuffer--找到了-->ReturnFrameBuffer
CreateFrameBuffer-->ReturnFrameBuffer
现在有一个问题:
FrameBuffer中对应的Image是每一帧都在做变化的,而且一次渲染只能绑定一个FrameBuffer,所以FrameBuffer的生命周期只在一帧中。
也就是说在创建完RenderPass之后,需要去类似FrameBufferPool结构中去找是否有兼容的FrameBuffer,如果有的话直接返回,没有的话新创建一个,之后渲染完一帧后回收销毁FrameBufferPool中的所有的FrameBuffer
注:根据Vulkan标准,创建FrameBuffer时,必须指定RenderPass(标准规定,是与之兼容的RenderPass)。所以在创建FrameBuffer前必须创建RenderPass
同RenderPassPool创建RenderPass类似,FrameBufferPool也需要传入RenderPass,并将创建结果刷新到RenderPass中,这需要RenderPass有一个存储刷新FrameBuffer的成员变量
最经典的DrawCall就是vkCmdDraw和vkCmdDrawIndexed,对于Turbo为Draw(...)和DrawIndexed(...)
此时对于调用DrawCall,Turbo会收集之前用户设置的数据,并对相关资源进行创建,记录相关指令,最后清空相关资源。
对于创建:
-
创建
GraphicsPipeline-
收集
RenderPass:来源于BeginRenderPass(...)中 -
收集
subpass:来源于NextSubpass() -
收集
std::vector<TVertexBinding>:来源于BindVeretxAttribute(...) -
收集
std::vector<TShader *> &shaders:来源于BindPipeine(...)所绑定的GraphicsPipeline中 -
收集
Pipeline属性(Topology,Polygon,CullModes等):来源于BindPipeine(...)所绑定的GraphicsPipeline中注:一帧结束后销毁
-
-
创建
TPipelineDescriptorSet-
通过
TDescriptorPool的Allocate(TPipelineLayout*)函数 -
传入的
TPipelineLayout从之前创建的Pipeline获得 -
通过
TPipelineDescriptorSet的BindData将数据绑定进去,数据来源于BindDescriptor(...)注:一帧结束后回收
-
对于记录:
- 记录
CmdBindPipeline(...)指令,来源于之前创建的GraphicsPipeline - 记录
CmdBindPipelineDescriptorSet(...)指令,来源于之前创建的TPipelineDescriptorSet - 记录
CmdBindVertexBuffers(...)指令,来源于之前调用BindVeretxAttribute中的绑定 - 记录
DrawCall指令(CmdDraw和CmdDrawIndexed),来源于用户对于DrawCall的调用
对于清空:
- 清空由于
BindVeretxAttribute(...)记录的VertexBuffer和std::vector<TVertexBinding> - 清空由于
BindDescriptor(...)记录的Descriptor和相关记录结构
注:如果用户在绑定完VeretxAttribute和Descriptor之后没有再调用相关绑定函数接口,Turbo调用DrawCall将不会再收集更新相关指令,因为相关指令没有更新的必要,默认使用之前的绑定结果。(如果新的渲染与之前的绑定的数据不兼容的话将会因不符合Vulkan标准而出问题)
由于创建Pipeline和记录渲染指令都位于DrawCall中,所以需要在DrawCall中提供防止重复创建Pipeline的功能,比如:
...
context.Draw(....);
context.Draw(....);
context.Draw(....);
context.NextSubpass();
...在如上的三次渲染中,如果不防止重复创建Pipeline的话将会创建三次Pipeline。这就需要在绘制时判断,当前绑定的Pipeline是否已被创建。
在Vulkan标准中,一个Pipeline内部属性对应如下:
RenderPass:当前Pipeline属于哪个RenderPassSubpass:当前Pipeline属于当前RenderPass下的哪个SubpassShaders:当前Pipeline对应的着色器
每当绘制时,DrawCall是知道当前位于哪个RenderPass下的哪个Subpass下使用哪个Pipeline的,所以每次DrawCall需要判断当前渲染指令下,在当前RenderPass下的当前Subpass下是否有与绑定的Pipeline兼容的已创建的Pipeline,如果有直接使用,如果没有新建一个
graph TD;
DrawCall["DrawCal(...)"]
NavigateCurrentRenderPass["定位到当前RenderPass"]
NavigateCurrentSubpass["定位到当前Subpass"]
IsHadCompatiblePipeline{"是否有兼容的Pipeline"}
UseCompatiblePipeline["使用兼容性的Pipeline"]
CreateNewPipeline["创建新的Pipeline"]
ReturnUse["返回使用"]
DrawCall-->NavigateCurrentRenderPass
NavigateCurrentRenderPass-->NavigateCurrentSubpass
NavigateCurrentSubpass-->IsHadCompatiblePipeline
IsHadCompatiblePipeline--是-->UseCompatiblePipeline
IsHadCompatiblePipeline--否-->CreateNewPipeline
UseCompatiblePipeline-->ReturnUse
CreateNewPipeline-->ReturnUse
提供一个PipelinePool也许是一个好方法
Shader(着色器),是组成Pipeline的重要组件,常见的Shader如下:
VertexShader(顶点着色器)GeometryShader(几何着色器)TessellationShader(细分着色器)FragmentShader(片元着色器)
光追标准着色器(NVIDIA标准或Khronos标准):
RayGenerationShaderIntersectionShaderAnyHitShaderClosetHitShaderMissShader
NVIDIA标准:
TaskShaderMeshShader
指令推送就是将记录了一堆指令的CommandBuffer推送到GPU进行执行,所以Context中需要提供Flush()函数
graph TD;
Flush["Context::Flush(...)函数调用"]
CreateFence["创建Fence"]
SubmmitCurrentCommandBuffer["推送当前CommandBuffer"]
NoWaitAndPushIntoFenceSet["将Fence加入到【待同步Fence集合】中(将来的某一时刻进行同步等待)"]
CurrentCommandBufferPushIntoCommandBufferSet["将当前CommandBuffer加入到【待同步CommandBuffer集合】中"]
ReCreateCommandBuffer["重新创建一个CommandBuffer,并作为当前CommandBuffer使用"]
Return["返回"]
Flush-->CreateFence
CreateFence-->SubmmitCurrentCommandBuffer
SubmmitCurrentCommandBuffer-->NoWaitAndPushIntoFenceSet
NoWaitAndPushIntoFenceSet-->CurrentCommandBufferPushIntoCommandBufferSet
CurrentCommandBufferPushIntoCommandBufferSet-->ReCreateCommandBuffer
ReCreateCommandBuffer-->Return
注:Context::Flush(...)函数调用的最佳时机多位于Renderer::BeginFrame()或Renderer::EndFrame()时(目前Turbo还没有Renderer的相关设计)
指令推送到GPU执行后,需要在未来的某一时刻进行等待GPU指令执行完成,所以Context需要提供bool Wait(uint64_t timeout)函数
graph TD;
Wait["Context::Wait(...)函数调用"]
WaitAllFenceTimeoutFenceSet["等待【待同步Fence集合】中的Fence,timeout时长"]
IsHasTimeout{"是否有超时"}
YesHasTimeout["说明【待同步CommandBuffer集合】中有的CommandBuffer在timeout时间段内没有完成"]
NoHasTimeout["说明所有的CommandBuffer都完成了"]
RemoveAndDestroyCommandBuffers["销毁并移除所有【待同步CommandBuffer集合】中的CommandBuffer"]
RemoveAndDestroyFences["销毁并移除所有【待同步Fence集合】中的Fence"]
RemoveFinishedCommandBufferAndDestroy["移除【待同步CommandBuffer集合】中已完成的CommandBuffer并销毁"]
RemoveFinishedFenceAndDestroy["移除【待同步Fence集合】中已完成的Fence并销毁"]
ReturnFalse["返回False,标时超时,规定时间内有任务没完成"]
ReturnTrue["返回True,规定时间内所有任务完成"]
Wait-->WaitAllFenceTimeoutFenceSet
WaitAllFenceTimeoutFenceSet-->IsHasTimeout
IsHasTimeout--有超时-->YesHasTimeout
IsHasTimeout--无超时-->NoHasTimeout
NoHasTimeout-->RemoveAndDestroyCommandBuffers
RemoveAndDestroyCommandBuffers-->RemoveAndDestroyFences
RemoveAndDestroyFences-->ReturnTrue
YesHasTimeout-->RemoveFinishedCommandBufferAndDestroy
RemoveFinishedCommandBufferAndDestroy-->RemoveFinishedFenceAndDestroy
RemoveFinishedFenceAndDestroy-->ReturnFalse
注:判断是否有超时时需要Turbo::Core层提供同时等待多个Fence的接口,目前未提供该接口,只能一个Fence一个Fence的等,需要扩展Turbo::Core接口,比如提供class Turbo::Core::TFences
注:Context::Wait(...)函数调用的最佳时机多为获取Swapchain的Image时(当Swapchain的Image上次被使用后,这次又再一次被使用,这时需要等待上一次使用结束,才能进行这一次的使用)
由于FrameGraph在void Execute(...)时的流程如下
graph TD;
CreateResource["创建PassNode要用的资源数据"]
PassNodeExecute["运行PassNode的Execute回调"]
DestroyResource["销毁不需要的资源数据"]
CreateResource-->PassNodeExecute
PassNodeExecute-->DestroyResource
DestroyResource--下一个PassNode-->CreateResource
这里在销毁不需要的资源数据时,此时可能用户并没有调用Flush,换句话说就是,用户此时可能并没有将指令推送到GPU,如果此时销毁了相应的资源,之后再推送指令,由于资源已经销毁了,所以在销毁不需要的资源数据时并不能真的去销毁资源,而需要将资源保存下来,等待未来的某一合适的时候销毁。
Filament引擎在销毁不需要的资源数据时,是将资源保存到了一个Cache中,如下:
//Filament engine code
void ResourceAllocator::destroyTexture(TextureHandle h) noexcept {
if constexpr (mEnabled) {//目前mEnabled永远为true
// find the texture in the in-use list (it must be there!)
auto it = mInUseTextures.find(h);
assert_invariant(it != mInUseTextures.end());
// move it to the cache
const TextureKey key = it->second;
uint32_t size = key.getSize();
//将资源转移到缓存中
mTextureCache.emplace(key, TextureCachePayload{ h, mAge, size });
mCacheSize += size;
// remove it from the in-use list
mInUseTextures.erase(it);
} else {
mBackend.destroyTexture(h);
}
}在Filament中mTextureCache是作为ResourceAllocator成员变量来缓存资源文件的,这是一个不错的选择。
目前能想到的资源回收有两个方面:
- 每个资源有个最长生命周期,超过生命周期将自动回收(这个可能需要想一下
Turbo::Core::Image、Turbo::Core::TImageView和Turbo::Render::TImage之间如何配合,配合不好可能会出问题) ResourceAllocator提供一个Gc()函数,用于定期回收资源
Turbo上层想要抽象出Mesh,Material和Drawable,其中:
-
Mesh代表三维模型的各种顶点信息,包括:- 顶点位置数据
- 顶点法线数据
- 顶点索引数据
- 顶点UV数据
- 等等
-
Material代表渲染流程,也就是代表着:RenderPass中的配置和渲染指令Command -
(非必要)
Drawable或者叫MeshDrawable,是Mesh和Material的配对
mermaid图测试
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
