关于vulkan多窗口系统设计思考1

先抽象出一个渲染表面接口RenderSurfaceInterface类,其中声明了例如表面尺寸、表面格式、表面数量(用于多重缓冲)等函数。
渲染表面接口分别由Window和RenderSurfece两个类实现,Window类用于直接把渲染结果呈现到窗口,而RenderSurfece是渲染到特定缓冲区。
Window和RenderSurfece类中都包含了LogicDevice类指针,LogicDevice其中有对vulkan逻辑设备的创建销毁和一些池和队列等对象的管理。

当调用RenderSurfaceInterface的Draw()进行渲染先要等待获取下一个可用的图像索引(因为多重缓冲),例如如果现在是Window类调用vkAcquireNextImageKHR(..)获取可用图像索引,如果是RenderSurfece类每次循环到下一个图像索引并等待获取到它。

在单窗口系统中,拿到当前帧的图像索引之后,就可以将其传给Material类的Draw(uint32_t imageIndex)函数进行绘制了。每个Material为了符合vulkan的渲染流程(RenderPass)的多子流程(Subpass)的定义中包含多个管线Pipeline,pipeline和Subpass是一一对应的,在我的实践中是通过renderpass的定义文件自动生成material的材质文件,材质文件中包含了多个管线和每个管线阶段的shader,只需要修改每个对应的shader文件即可,运行时Material类会自动生成好所有的管线和相应的数据。

Material类的Draw(uint32_t imageIndex)函数之所以要传入imageIndex,是因为在多缓冲情况下,每个管线的动态描述符不能每个只有一份。这样理解:假设现在是3缓冲,可能一个帧缓冲区渲染结束然后正在呈现给显示器,但是另外正在GPU中渲染后面两帧的缓冲,那么势必这两帧所用到的动态描述符不能是同一批。如果是同一批,假设前面一帧给描述符的内存传递是5后面一帧传递是10,因为两帧都在渲染中,后面为描述符内存的写入的10可能会覆盖掉前一帧的5,导致结果错误。也可能会引发未知状况。所以所有动态资源的描述符必须和多缓冲区的数量相同,每次材质渲染通过传入imageIndex来决定渲染哪一批的动态资源。在我现在的实践中描述符不同的集(set)用于决定资源的范围和更新频次,set 0 为全局数据,存放一些例如全局灯光所有材质公用的数据,set 1 为每个材质的静态数据也就是说材质在初始化的时候数据就已经被传到显存中之后不能更改,set 2 为动态数据之后可以每帧频繁的更改,所以这里set 0 和set 2 需要多份因为每帧数据都在变动,只是set 0是全局所有材质公用的几个显存,而set 2是每个材质都有几份。至于每个材质独有的MVP矩阵是通过推式常量PushConstant推过去的。

在我的实践中只需要在shader文件中写好对应的set和绑定点,Material类在初始化时会预先通过自己写的shader分析器生成好的反射文件自动生成对应的管线组、描述符集等资源。之后只需要通过shader文件中的对象名称就可以设置set 2的对象。有点类似unity 在c#中设置shader的变量的值。但不一样的是需要先指定子流程索引,后面接对象名称和值。类似SetUniformVector2(uint32_t subpassIndex, const char* name, vec2 data);  因为毕竟一个Material是由多个子流程构成的,可能之后会改为所有子流程中的变量名称不能重复,在编译时进行一遍检测,这样就不需要在设置时写subpassIndex了。其中带来的好处是我只要保证shader的对象名称相同,这样我可以切换渲染流renderpass而不用改变游戏逻辑代码。unity、ue4、cryengine都不能切换渲染流,好吧其实硬要说是可以切换的只是你要新建一个工程,然后之前的shader都作废了,然后从新搞一套shader按照新的规则写,这样两个工程并不能通用。比如unity的LWRP和HDRP就是这样。unity中把这东西叫做渲染管线。我不大喜欢unity用的这个词,因为在vulkan和opengl的角度上来说管线是指从顶部阶段到底部阶段的一个周期的过程。unity渲染管线其实意思是整体流程很是费解。之后我会把这个叫做整体渲染流程。

上面所说的单窗口系统相对来说还是比较容易的,但是多窗口情况下问题就会变得相对复杂。


关于多窗口间的逻辑设备的共享方案:

1.每个窗口有自己独立的逻辑设备
2.窗口间可以共享同一个逻辑设备

1的情况在我目前看来没有什么好处,因为一个应用程序是一个整体通常每个窗口所需的功能都是一样的没有必要抽象出不同的逻辑设备,而且抽象出不同逻辑设备之后每个窗口之间的很多资源也不能共享,因为很多资源是通过逻辑设备申请的。例如命令池、队列池。

所以采用2的方案更好,这样的话Window的构造函数有两个
Window(Instance *instance);
Window(Instance *instance, std::shared_ptr<LogicalDevice>& logicalDeviceSharedPtr);
如果使用第一个当前Window就会创建一个新的逻辑设备,如果使用第二个就会引用一个已有的逻辑设备。所以应用程序刚开始会先使用第一个构造函数构构造Window再调用error Init()生成一个带有逻辑设备的窗口(之所以使用error Init()是因为创建逻辑设备可能会失败,而直接在构造函数中做再使用try是不可取的),然后在获取第一个逻辑设备的logicalDeviceSharedPtr用来创建第二个窗口。这样他们就公用同一个LogicalDevice,当所有Window都释放以后才会释放这个LogicalDevice。

现在所有窗口的已经共享同一个逻辑设备,下面我们要看下材质的共享

在多窗口系统中,渲染同样的物体会产生共用材质资源的问题,比如在游戏编辑器里会有多个窗口用不同的摄像机角度渲染同一个场景,多个窗口可能使用相同的材质和模型图像资源,对于窗口和材质的共享会产生下列3种设计:

1.共享相同材质不共享材质动态资源:渲染时窗口间动态资源独立。
2.共享相同材质与材质动态资源:渲染时窗口间动态资源同步。
3.不共享材质:渲染时窗口间动态资源独立。

第三种和第二种做对比没有优势所以直接出局。

第一种方案每个Window共享相同的Material,但是虽然材质是共享的,但是材质的动态资源并不共享,在Material类中要为每个窗口创建单独的动态资源和描述符,当Window渲染Material时需要指定当前Window在Material中的窗口索引和帧索引类似material->Draw(uint32_t windowIdex, uint32_t imageIndex)这样material会根据窗口索引和帧索引找到对应的资源并渲染,这样做的好处是,假设两个窗口的多缓冲策略不同,其中一个是单缓冲策略,另一个是3重缓冲,这样两个窗口的刷新率势必会不相同,如果材质资源提交到彼此不相关的显存中,那么两个窗口可以独立的按照自己的刷新率去渲染,虽然他们用的是相同的材质但是不需要和另一个窗口刷新同步,缺点是需要更大的显存,而且假设现在只有一个窗口在运行着一个游戏,然后玩家又开了一个窗口,那么material的所有资源都要从新整理提交一份到GPU并和新窗口绑定,这过程可能异常缓慢。那么看来让两个窗口形成某种同步,共享资源可能是更好的选择。我们看一下方案2。

第二种方案还是Window共享相同的Material,但是资源也是共享的。这样在两个窗口渲染时必须保证在渲染过程中显存内容不能发生更改,假设有两个窗口,一个窗口是双缓冲,另一个窗口是三缓冲,那么渲染缓冲的同步过程如下:

双缓冲:1 2 1 2 1 2
三缓冲:1 2 3 1 2 3

从上面关系可以看出一种交替同步机制,但矛盾的是如果我们只有一个窗口3缓冲,显然我们的动态资源缓存和描述符应该创建三份,这样好同时进行渲染而不冲突,但因为我们有了第二个窗口他是双缓冲窗口,我们第一个窗口虽然是三缓冲,但因为要和双缓冲同步所以三缓冲其实实际效果变成了双缓冲,这并不是一个好的主意。让我们看看另外一种同步机制

双缓冲:1   2 1 2 1   1 2 1
三缓冲:1 2 3 1 2   3 1 2 3

这种同步机制在当前帧要渲染时获取哪个缓冲区可以渲染如果不能渲染等待下一帧,问题是如果我没赶上上一帧的那班车,可能就要等一段时间才能赶上下一班车,这会造成画面卡顿。但通常只会卡缓冲少的那个。这样的设计看似是可行的。

如果按上面的方案每个Material应该有多份动态资源和描述符,其中可能有两种创建它们的方式,1是在材质初始化时就决定了材质拥有多少动态资源,2是在之后可以动态增加减少数量。动态资源数量至少不能小于拥有最大多缓冲的Window的多缓冲数量。可根据当前Window的最大多缓冲数量手动设置。但所有Material的动态资源份数应该相同。所以Window要根据自己的多缓冲数量从新设置所有的Material的动态资源份数,那最好设计一个新的类名为MaterialManager其中用链表保存了所有所有已经创建的Material,Material中也包含了他的父MaterialManager对象。每一帧渲染时通过MaterialManager的NextDescriptorSet()函数用于渲染时切换到下一个动态资源的描述符集的索引。

每帧渲染要先调用NextDescriptorSet()函数切换到下一个资源的描述符集,渲染每个Material时Material会根据自身MaterialManager的descriptorSetIndex()函数获取当前的资源的索引,然后调用vkCmdBindDescriptorSets(window->currentFrame(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[materialManager->escriptorSetIndex()], 0, nullptr); 来绑定资源渲染。

所以每次只需要

...
materialManager->NextDescriptorSet();
material1->Draw(windowPtr);
material2->Draw(windowPtr);
...

实际上不只可以传入Window指针也可以传入RenderSurfece指针渲染到特定缓冲区中,所以Draw函数应该是Draw(RenderSurfaceInterface* rsi)。

 

可能在LogicDevice类中包含一个RenderPassManager类来管理所有当前逻辑设别的RenderPass,可以如以下创建RenderPass:
RenderPass* renderPass1 = logicDivce->renderPassManager()->AdddRenderPass("renderpass1.rp");
RenderPass* renderPass2 = logicDivce->renderPassManager()->AddRenderPass("renderpass2.rp");
需要传入renderpass定义文件的路径返回RenderPass类指针

如果现在有两个窗口需要渲染,创建和渲染过程可能如下

// 创建窗口
Window *window1 = new Window();
Window *window2 = new Window();

Window* window1 = app.instances()->AddWindow(view, width, height, framebufferSize);
LogicDivce *logicDevice = window1.logicDevice();
Window* window2 = app.instances()->AddWindow(new Window(logicDevice), width, height, framebufferSize);


// 创建RenderPass
RenderPassManager* renderPassManager = window1->logicDvice()->renderPassManager();
RenderPass *renderPass1 = renderPassManager.AddRenderPass("renderpass1.rp");
RenderPass *renderPass2 = renderPassManager.AddRenderPass("renderpass2.rp");
RenderPass *renderPass3 = renderPassManager.AddRenderPass("renderpass2.rp");


MaterialManager materialManager1 = renderPass1->materialManager();
materialManager1.AddManager("m1.mat");
materialManager1.AddManager("m2.mat");
materialManager1.AddManager("m3.mat");

MaterialManager materialManager2 = renderPass2->materialManager();
materialManager2.AddManager("m1.mat");
materialManager2.AddManager("m2.mat");

MaterialManager materialManager3 = renderPass3->materialManager();
materialManager3.AddManager("m1.mat");
materialManager3.AddManager("m2.mat");

// 更新材质数据
for (Material& material : materialManager.materials()) {
    material->SetPushConstantMVP(mvp);
    material->SetPushConstantPostion("posion", {0.0f, 1.0f, 2.0f});
    material->SetUniformVector3("test", {0.0f, 1.0f, 2.0f});
}

// 开始渲染window1

window1->BeginDraw();
materialManager1.Draw(camara->mask());
window1->EndDraw();

// 开始渲染window2

window2->BeginDraw();
materialManager1.Draw(camara->mask());
materialManager2.Draw(camara->mask());
materialManager3.Draw(camara->mask());
window2->EndDraw();

logicDevice->SubmitCommandBuffer();

logicDivce->renderPassManager()->Next();  // 切换下一帧记录的动态资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值