*写在前面
- 只针对renderdoc opengl es 场景进行说明, vukan的没有,不过其实应该也差不多啦,若有必要后面再考虑补上。
- 本文使用 文字+图+代码 的方式叙述,若只想了解原理过程,可忽略代码部分,
由于renderdoc代码还算复杂和乱,代码部分主要是辅助有兴趣阅读源码的同学去抓住代码主要逻辑。 - renderdoc 相关名词
- 对一帧抓流时,
- 开始时机:前一帧swapbuffer时,具体逻辑做在StartFrameCapture()接口;
- 结束时机:当前这一帧swapbuffer时,具体逻辑做在EndFrameCapture()接口;
即,前一帧的结束就是这一帧的开始嘛, 如图:*
what
- resource manager, 顾名思义就是renderdoc 用来管理gl资源的,在opengles之上自己做了个状态机, 用来记录gl 资源的使用情况。
- gl资源在renderdoc resource manager里边主要由3个结构体来描述,
GLResource:记录该资源 类型、在opengl es状态机里边的id,如glGenTextures生成的texture值;
ResourceId:记录该资源在resource manager 里边的id,
GLResourceRecord:以chunks形式,记录操作该资源的相关gl接口调用,尤其是创建、绑定、属性设置、数据upload,如glGenTextures,glGenBuffers等,一条glXXX用一块chunks记录。 - gl resource:
以下这些在renderdoc都可以描述为资源,基本涵盖所有gl资源了,有点类似于 linux里一切皆文件思想哈哈哈,
enum GLNamespace
{
eResUnknown = 0,
eResSpecial,
eResTexture,
eResSampler,
eResFramebuffer,
eResRenderbuffer,
eResBuffer,
eResVertexArray,
eResShader,
eResProgram,
eResProgramPipe,
eResFeedback,
eResQuery,
eResSync,
eResExternalMemory,
eResExternalSemaphore,
};
why
- 在opengles 之上做了个状态机, 能起到如下作用,
抓流场景:
(1)性能、内存优化: BackgroundCapturing时 对于每一块gl resource,记录其数据更新时间(postpone机制)、当前帧是否使用(frame reference机制)、是否dirty(dirty机制),
当进行真正的抓流ActiveCapturing时,就可以只序列化保存在当前帧使用的资源相关chunks;
重放场景:
(1)新旧资源映射: 对于来自opengl状态机的资源id, 抓流序列化时保存下来,
重放是,需要去创建一个对应的资源,由于id不保证一次,所以需要做一个映射,
即:origing GLResource <------> live GLResource
(2) 性能优化:
抓一帧时,有的资源是在该帧开始前就upload数据到gpu侧的,
那么需要在每次重放帧开始前,先对该资源gpu侧显存进行初始化,若每次都从磁盘把数据找出来再disk–>cpu–>gpu,太耗性能,
所以可以在resource manager里边做一个记录: live resource/origing resource <–> initialContents,
initialContents放在cpu侧内存, 每次重放前做cpu—data—> gpu就ok了。
无论是resource manager本身,还是其里边实现的下面那些frame reference、dirty、persistent、postpone等各种机制,都不是必须的,是对性能和内存的优化,
抓流时也可以都不用,hook后简单粗暴记录下所有gl调用和资源,而随之带来的当然是无论BackgroundCapturing还是ActiveCapturing时的各种卡顿和内存占用过大。
frame reference 机制
- what
标记一帧里边某个资源有被使用 - why
优化内存与性能: 抓流时,该帧没被标记的资源,就不需要保存了。 - 相关接口
template <typename Configuration>
void ResourceManager<Configuration>::MarkResourceFrameReferenced(ResourceId id, FrameRefType refType)
//EndFrameCapture 时,序列化frame reference 资源
template <typename Configuration>
void ResourceManager<Configuration>::InsertReferencedChunks(WriteSerialiser &ser)
dirty机制
- what
用 MarkDirtyResource( )标记该资源脏了,用在该资源gpu侧内容被改变时;
区别于frame reference resource, 用MarkResourceFrameReferenced()标记该帧有使用了该资源 - why
BackgroundCapturing场景,应用每次对资源更新数据时,renderdoc不需要每次都保存,因为实际上抓帧时,只要在这一帧开始前资源的最后一次内容就行了。
例如对于一块buffer:
//BackgroundCapturing
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffName);
//frame1
glBufferData(GL_ELEMENT_ARRAY_BUFFER, data_size_in_bytes, data1, GL_STATIC_DRAW);
//frame2
glBufferData(GL_ELEMENT_ARRAY_BUFFER, data_size_in_bytes, data2, GL_STATIC_DRAW);
//framen
glBufferData(GL_ELEMENT_ARRAY_BUFFER, data_size_in_bytes, dataN, GL_STATIC_DRAW);
//ActiveCapturing
//frame capture
//对于当前要抓的这一帧,若用到了buffName,那么我们需要保存的就只是dataN的内容,
//所以对于BackgroundCapturing时更新的数据data1、data2等,只要每次更新数据时做个dirty标记就行,
//表示这块资源dirty,可是没有保存,咱们在抓帧时把他保存下来就行
- 相关接口
void MarkDirtyResource(ResourceId id);
void MarkDirtyResource(GLResource res);
//reference和dirty一起标记了
void MarkDirtyWithWriteReference(GLResource res)
void MarkVAOReferenced(GLResource res, FrameRefType ref, bool allowFake0 = false);
void MarkFBOReferenced(GLResource res, FrameRefType ref);
void MarkFBODirtyWithWriteReference(GLResourceRecord *record);
- 使用流程
- BackgroundCapturing时,对资源MarkDirtyResource,
- 真正抓帧开始前,即StartFrameCapture()里边,做dirty资源的gpu侧拷贝,cpu侧映射(因为要通过cpu侧handle去访问gpu侧资源内容嘛)
- 抓帧结束时,即EndFrameCapture()里边,若这些资源在该帧有引用到,从gpu侧拷贝前面备份的资源到cpu,对dirty资源做序列化保存。
相关接口
ActiveCapturing
/*****************StartFrameCapture**********************/
//遍历所有标记为dirty的资源,创建map resource, 即origin resource <----> map resource,
//gpu侧,origin resource ---data copy---> map resource
template <typename Configuration>
void ResourceManager<Configuration>::PrepareInitialContents()
/*****************EndFrameCapture**********************/
//对每块dirty resource , origin resource ---->取出 map resource,
//map resource ---gpu data----> cpu,
//cpu 侧对数据序列化,写入rdc文件
template <typename Configuration>
void ResourceManager<Configuration>::InsertInitialContentsChunks(WriteSerialiser &ser)
//序列化保存所有需要初始化的dirty resource,frame reference resource
template <typename Configuration>
void ResourceManager<Configuration>::Serialise_InitialContentsNeeded(WriteSerialiser &ser)
重放
//创建map resource, origin resource <-----> map resource ,
//rdc ---data--> cpu---data---> gpu, gpu侧这边为map resource
//map resourece 在cpu侧以InitContents形式与 origin resource 建立映射
template <typename SerialiserType>
bool GLResourceManager::Serialise_InitialState(SerialiserType &ser, ResourceId id,
GLResourceRecord *record,
const GLInitialContents *initial)
//读出所有需要初始化的resource id,
//若此时该origin resource 有对应的live resource, 且还没有创建初始化内容,
//则做一遍类似上面的事情:
//创建map resource, origin resource <-----> map resource ,
// gpu侧 live resource --data copy---> gpu侧map resource
//map resourece 在cpu侧以InitContents形式与 origin resource 建立映射
template <typename Configuration>
void ResourceManager<Configuration>::CreateInitialContents(ReadSerialiser &ser)
template <typename Configuration>
//每次重放开始时,执行 gpu 侧map resource ---data copy ---> gpu侧 live resource
void ResourceManager<Configuration>::ApplyInitialContents()
共用
//获取一块 resource的初始化内容
InitialContentData GetInitialContents(ResourceId id);
//保存一块resource 的初始化内容
void SetInitialContents(ResourceId id, InitialContentData contents);
关于抓流的具体过程,欢迎通过 renderdoc抓流过程 讲一步了解~
-
判断机制
有数据更新的gl接口,标记该资源dirty -
数据保存时机与流程
StartFrameCapture时做了gpu侧数据拷贝;
EndFramecapture时从gpu拷到cpu,并序列化保存; -
恢复数据时机
那么,重放时数据的恢复,当然是上面的过程反过来啦
-
时序图
哎,本来不想画这个图的,实在太费时间,可是renderdoc代码写的实在太乱太复杂,还是画下吧方便下次通个这图就能快速理解代码流程
抓流
重放
- 相关调用流
抓流
StartFrameCapture()
-->
PrepareInitialContents();
-->
Prepare_InitialState(GLResource res)
-->
//当前上下文里边做资源在gpu侧的拷贝
ContextPrepare_InitialState()
-->
创建初始化资源存到这,还没有回读与序列化
m_InitialContents[id].data = contents;
===========================================
EndFrameCapture()
-->
InsertInitialContentsChunks()
-->
Serialise_InitialState()
-->
template <typename SerialiserType>
bool GLResourceManager::Serialise_InitialState(SerialiserType &ser,
将数据从gpu侧回读并做序列化保存
重放
//数据加载到gpu侧,创建InitialContents
Serialise_InitialState()
-->
//若有live resource,且没有创建InitialContents
//gpu侧拷贝live resource 数据
CreateInitialContents()
-->
//执行gpu侧数据拷贝
Create_InitialState()
-->
//每次重放开始时,gpu侧,map resource --data copy--> live resource
ApplyInitialContents()
-->
//执行gpu侧数据拷贝
Apply_InitialState()
persistent 机制
-
persistent 资源
创建后就一直常驻的资源,对于gles,为创建后,对其更新了内容,过了3s后还未被销毁的texture、buffer资源;
即:对于texture、buffer资源,若超过3s,资源才再次更新,也表明资源在此期间没有被销毁,就认为是postpone资源,即非帧内创建的常驻资源; -
persistent 规则
距离上次更新(写)时间操作3s。 -
相关接口与数据结构
//texture、buffer资源,更新速度没那么快(大于PERSISTENT_RESOURCE_AGE,即3s外),或者前面没有被引用
bool HasPersistentAge(ResourceId id);
//texture、buffer资源
virtual bool IsResourceTrackedForPersistency(const WrappedResourceType &res) { return false; }
//StartFrameCapture时,对于persistent resource,不需要在prepareInitialContents()时
//先拷贝一份资源出来, 而是可以延迟到写rdc时,
//原因: 因为read only资源?反正内容不会被改变?
//场景:texture、buffer资源,更新速度没那么快(大于PERSISTENT_RESOURCE_AGE,即3s外),或者前面没有被引用
std::unordered_set<ResourceId> m_PostponedResourceIDs;
//资源被引用(写)的时间,只记录最后那次
rdcarray<ResourceRefTimes> m_ResourceRefTimes;
postpone 机制
-
why
为啥要有这个机制?是必须项还是优化项?
ans: 优化项,减少PrepareInitialContents的资源数目,
因为这些资源,在当前要抓的这一帧,不一定会用到;
优化了啥?
ans:性能,内存 -
postpone 资源
针对persistent resources 而言
// During initial resources preparation, persistent resources are
// postponed until serializing to RDC file.
std::unordered_set m_PostponedResourceIDs;
-
postpone 规则
若是persistent resource,则postpone处理,等到markFrameRef时才做PrepareInitialContents,
而不是在StartFrameCapture时做, -
使用场景
StartFrameCapture时,对于buffer、texture 资源,若距离上次更新时间超过3s了,则也是认为对于一般开发流程来将,当前帧不会再去更新它了,
认为是postpone资源,则延后到EndFrameCapture处理,
抓帧过程会不会去处理它 -
相关接口与数据结构
bool IsResourcePostponed(ResourceId id);
//返回true情况:
// texture、buffer资源,更新速度没那么快(大于PERSISTENT_RESOURCE_AGE,即3s外),或者前面没有被引用 ,即 对于texture、buffer资源,若超过3s,资源还在,就认为是postpone资源
bool ShouldPostpone(ResourceId id);
template <typename Configuration>
//EndFrameCapture()时, 处理postpone 资源,前面PrepareInitialContents没有处理嘛
//所以需要留到这才处理
void Prepare_InitialStateIfPostponed(ResourceId id, bool midframe);
//gles里边没啥逻辑,应该没有skiped资源,vulkan才有,先忽略
void SkipOrPostponeOrPrepare_InitialState(ResourceId id, FrameRefType refType);
std::unordered_set<ResourceId> m_PostponedResourceIDs;
- 资源序列化
与正常序列化流程没啥区别
EndFrameCapture()
-->
InsertInitialContentsChunks()
-->
Prepare_InitialStateIfPostponed()
-->
Prepare_InitialState()
-->
Serialise_InitialState()
- 资源的恢复
与正常恢复流程无区别
Serialise_InitialState()
-->
//从rdc读出数据,创建副本,数据加载到gpu侧副本
Serialise_InitialState()
-->
//gpu侧从副本拷贝
ApplyInitialContents();
- why
性能优化,对于buffer、texture,一些可以不处理的情况,则不处理,
opengl 目前来说应该是没有一个资源能被skip,因为没有一个eFrameRef_CompleteWriteAndDiscard场景
vulkan才有,这个可以先不管, - 相关接口
//opengl 目前来说应该是没有一个资源能被skip,因为没有一个eFrameRef_CompleteWriteAndDiscard场景
//vulkan才有,这个可以先不管
template <typename Configuration>
inline bool ResourceManager<Configuration>::ShouldSkip(ResourceId id)
template <typename Configuration>
inline bool ResourceManager<Configuration>::HasSkippableAge(ResourceId id)
//应该是一直为空
// During initial resources preparation, resources that are completely written
// over are skipped
std::unordered_set<ResourceId> m_SkippedResourceIDs;
- skip规则
- 使用场景
replacement 机制
//TODO
- why
- 规则
- 使用场景
- 相关接口
相关数据结构
- FrameRefType
用来对资源的使用做标记
enum FrameRefType
{
// Initial state, no reads or writes
eFrameRef_None = 0,
//标记有对资源做了写操作
eFrameRef_PartialWrite = 1,
//标记有对资源做了写操作, 并且后面没有读操作了
eFrameRef_CompleteWrite = 2,
//标记对资源做了写操作
eFrameRef_Read = 3,
//每次重放前都需要重置该资源,
//因为使用顺序是read-->write, 可能后面write后又回到前面了嘛
//先对资源进行了读,又对它进行了写
eFrameRef_ReadBeforeWrite = 4,
//使用顺序是write --> read
//先对资源进行了写,又对他进行了读
eFrameRef_WriteBeforeRead = 5,
//目前没用
eFrameRef_CompleteWriteAndDiscard = 6,
eFrameRef_Unknown = 1000000000,
};
//用来算新状态的,即老状态+新状态 结果
FrameRefType ComposeFrameRefs(FrameRefType first, FrameRefType second)
以上的资源read、write状态记录, 主要用在开始抓帧时,具体为StartFrameCapture()–>ClearReferencedResources(),判断是否要对被标记的资源做拷贝处理 , StartFrameCapture()细节参考renderdoc 抓流过程
template <typename Configuration>
void ResourceManager<Configuration>::ClearReferencedResources()
{
SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);
for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)
{
RecordType *record = GetResourceRecord(it->first);
if(record)
{
//这块资源在前面是有被写过的,要标记为dirty,保留
//可能在后面的帧中有用到时,
//需要在重放的一开始就去初始化它
if(IncludesWrite(it->second))
MarkDirtyResource(it->first);
record->Delete(this);
}
}
m_FrameReferencedResources.clear();
}
-
ResourceId
顾名思义, 标记一块资源的ID -
ResourceRecord 、GLResourceRecord
存用到该资源的相关chunk,关于chunk,参考renderdoc抓流过程;
以contex为界(可能不同context会共享资源), 对于一块资源,需要记录其被使用的过程,
ques: 啥样的资源操作接口需要记录到资源自己的record里?
ans: 资源初始化过程,如texture的创建、绑定、cpu侧upload数据到gup侧、gpu侧数据拷贝、资源属性配置、等,这些都通过chunks存在ResourceRecord里边。
目的是为了在重放场景,重放开始时,方便取出这些初始化接口,对资源进行初始化。 -
GLResourceManager
顾名思义,用来管理资源的,本文章的男主角,所有资源操作管理逻辑都在这里边;
抓流: 记录当前有哪些资源,有哪些是标记为dirty的,
重放:管理original resource Id 与 live resource Id的映射,
主要对资源的标记操作为dirty、frameReflence,
抓流时,主要需要处理的也是这2类资源 -
GLInitialContents
存一块resource的初始化数据 -
InitialContentDataOrChunk
-
ResourceRefTimes
标记该资源最后一次被frame reference的时间,
capture frame时若距离上次被标记超过3s,则认为是persistent 资源 -
TextureData
用来在renderdoc里边描述一块texture的 -
ResourceDescription
描述这块资源的初始化需要的chunks id -
FBOCache
记录一块fbo,到底有多少资源attach到其上面 -
GLNamespace
标记资源类型,如buffer、texture、shader,program等 -
GLContextTLSData
gl context thread share data? 还不太清楚
TIPS
- VERBOSE_DIRTY_RESOURCES
顾名思义,用来debug dirty 机制的开关,打开后可以打印该机制的相关debug log - GLResource 和GLResourceRecord关系
GLResource描述资源,
GLResourceRecord记录使用到该资源的相关gl接口,即chunks
FAQ
- 对一块texture mark dirty,一条drawcall结束后,把texture删除了,renderdoc怎么处理?
ANS: 目前看来是没法处理,WrappedOpenGL::glDeleteTextures 把resource record给删了
不过感觉一般开发代码不会这么写,要么初始化时统一创建,后面统一销毁,
要么帧内创建与销毁。