angle追加折腾笔记

Author Avatar
Kanglai Qian 6月 04, 2023

之前在angle折腾笔记里提到恢复angle作为ES3模拟器,方便PC上查BUG。当时为了模拟EXT_copy_image使用了Vulkan Backend,别的用起来都挺好,但是很蛋疼的一点是RenderDoc抓帧的API其实都是vlk的… 这礼拜查别的BUG的时候,索性痛定思痛尝试使用GLES Backend。

这里记录下补全功能以及修复了若干Angle/RenderDoc相关问题的笔记…

depot_tools更新

无脑先拉了最新的Angle代码,结果老方法遭遇开幕雷击orz…看了下log是因为新版本的depot_tools不在自带ninja二进制文件了,所以编译一堆失败。
这个很好处理,去ninja拉一份最新的exe,然后设置进系统路径就行。

Angle部分修改

按照之前经验,编译出libEGL.dll和libGLESv2.dll两个文件,测了下打开GLES Backend可以直接运行起来,但也发现了效果错误+闪退问题。

glCopySubImage补全

之前在angle折腾笔记里也提到,暂时只有Vulkan这个Backend支持了跨格式拷贝贴图的功能(虽然这块其实也有点小瑕疵,见CopyImageSubData in Vulkan Backend,但本地改改、糊弄一个可以用的版本很快)。

调试了下ES Backend下的调用堆栈(pdb在手真香),确实是没有实现对应逻辑:

angle::Result TextureImpl::copyTextureSubData(const gl::Context *context,
const gl::Texture *srcTexture,
GLint srcLevel,
GLint srcX,
GLint srcY,
GLint srcZ,
GLint dstLevel,
GLint dstX,
GLint dstY,
GLint dstZ,
GLsizei srcWidth,
GLsizei srcHeight,
GLsizei srcDepth)
{
UNREACHABLE();
return angle::Result::Stop;
}

这时候先用OpenGL hardware capability viewer确认了下这个扩展其实是存在的。

这样就好办了——索性自己实现一发TextureGL::copyTextureSubData,对应调用的函数塞在BlitGL::copyImageSubData里面即可,加起来差不多也就十多行。

Extensions

补完逻辑之后发现游戏运行起来,还是自动禁用了这块功能。继续调试下,发现原因是GLES 3.1 Context下的Extension列表中压根没有GL_EXT_copy_image,所以引擎认为当前RHI下这个功能是不可用的。

翻了翻ANGLE生成扩展列表相关逻辑,它其实是通过检查当前Backend Device情况,来决定可以支持多少GLES Extensions的。所以在renderergl_utils.cpp里补一行:

extensions->copyImageEXT = functions->hasGLESExtension("GL_EXT_copy_image");

从而告诉ANGLE此时是可以支持copyImageEXT,自此引擎对应功能就正常了。

EXT or not?

这个也是之前用Vulkan Backend就发现的问题: 打开RenderDoc后会优先使用非EXT版本的API,导致glCopyImageSubDataEXT调用被劫持成glCopyImageSubData

我之前给RenderDoc提过split code path for glCopyImageSubDataEXT&glCopyImageSubData,但是作者的回复还是比较有道理的——在GLES 3.1 Context的情况下,ANGLE其实就不应该返回glCopyImageSubData这个接口(虽然里面是空实现)。

临时解决方案是ANGLE里ValidateCopyImageSubData里的ES 3.2限制降到3.1。后面会提到更加釜底抽薪的解决方案。

glProgramBinary闪退

关掉RenderDoc之后发现启动时有一定概率闪退,看了下堆栈是崩在了glProgramBinary中(其实开了RenderDoc就没问题也很有意思,回头有机会展开)

libGLESv2.dll!rx::ProgramGL::uniLoc(int glLocation) Line 148	C++
libGLESv2.dll!rx::ProgramGL::setUniform1iv(int location, int count, const int * v) Line 555 C++
libGLESv2.dll!gl::Program::setUniform1iv(gl::Context * context, gl::UniformLocation location, int count, const int * v) Line 2267 C++
libGLESv2.dll!gl::Program::setUniformValuesFromBindingQualifiers() Line 3278 C++
libGLESv2.dll!gl::Program::postResolveLink(const gl::Context * context) Line 3723 C++
libGLESv2.dll!gl::Program::deserialize(const gl::Context * context, gl::BinaryInputStream & stream, gl::InfoLog & infoLog) Line 3691 C++
libGLESv2.dll!gl::Program::loadBinary(const gl::Context * context, unsigned int binaryFormat, const void * binary, int length) Line 1474 C++
libGLESv2.dll!gl::Context::programBinary(gl::ShaderProgramID program, unsigned int binaryFormat, const void * binary, int length) Line 8006 C++
libGLESv2.dll!GL_ProgramBinary(unsigned int program, unsigned int binaryFormat, const void * binary, int length) Line 2135 C++

看了下这块代码,然后在ANGLE的test case里用现有的ProgramBinaryUniformResetCase改了改,发现能稳定复现了这个问题: 加载shader binary cache时,压根没考虑es 3.1下的uniform sampler binding point…
给官方提了glProgramBinary crash with unifor binding point,回复的非常快但是吐槽无力…两年前就有人提过这个事情还没修(难怪我搜glProgramBinary没找到这个issue…)

本来想直接禁用shader binary cache绕开,后来发现glLinkProgram的时候ANGLE内部也会尝试复用Program,导致一样的闪退。

Fix Attemp 1

一开始非常临时的方案糊弄了下:在setUniformValuesFromBindingQualifiers调用时候检查mUniformRealLocationMap为空就跳过,至少不会闪退了。

但是跑测时候发现有概率导致加载的shader的uniform sampler信息丢失(毕竟被跳过了orz) ,渲染效果出错。

Fix Attemp 2

索性重新梳理了下这块逻辑,特别是对比了下正常创建Shader的行为: 本质问题其实是postResolveLink必须保证在link之后调用。
定位到这点之后就好改了,索性把postResolveLinkdeserialize里挪出,放到后面合适的位置调用即可。

更新: 官方repo上也有人处理掉了Do not call postResolveLink in Program::deserialize.,倒是和我前面的方案一致。

RenderDoc Replay

Runtime部分到目前已经比较稳定了,接下来要处理的是配合RenderDoc使用时发现的两个问题。

贴图错乱

使用RenderDoc Capture&Replay会发现,有些贴图的数据完全错乱了,然而在运行时是完全正常的。临时用PVR对比了下(右)倒是一切正常的。

具体分析了下,发现内容错误的贴图都是非压缩格式,而压缩贴图则全员良好,于是构造了一个极简例子来分析。

Debug Attemp 1

首先需要确认下是Capture过程中出的问题还是Replay的问题。

一开始我想的突破口是找到Replay阶段是如何从rdc中读取数据并创建贴图的。翻了下gl_texture_funcs.cpp里的WrappedOpenGL::Serialise_glTextureImage2DEXT实现,打断点确实能命中同时包括width/height/format等参数都对的上。
于是我尝试把这块数据直接dump下来看看内容

if (width == 1024 && height == 128)
{
FILE *fp = NULL;
fopen_s(&fp, "test.raw", "wb");
fwrite(pixels, 1, subimageSize, fp);
fclose(fp);
}
GL.glTextureImage2DEXT(texture.name, target, level, internalformat, width, height, border,
format, type, pixels);

发现不管是pvr还是angle的rdc,对应的数据居然是分毫不差的…

为了从别的角度证明或者证伪,我还掏出apitrace直接去抓RenderDoc的Replay过程中GL API,发现调用的参数和二进制数据确实分毫不差…

场面非常尴尬: 明明这里调用的参数和数据一模一样,为什么结果会不一样?

ps. 当时为了验证没找错地方(这里要加粗的原因是当时的我有多么自信,后来就有多么哭笑不得orz)

  • 断点确实命中且只命中一次,因为构造的test case里只有一张未压缩纹理
  • 如果注释掉这行、确实Replay里看不到对应这张纹理了。

Debug Attemp 2s

陷入僵局之后,果断找朋友进行小黄鸭调试…在此感谢阿兰和群主的各种协助和讨论:

  • 确认glBindTextureglActiveTexture
  • 确认GL_PIXEL_UNPACK_BUFFER=0GL_UNPACK_ALIGNMENT=1
  • 确认slot是对的情况下进行的各种设置

而且Replay过程已经和Angle/PVR无关了,是RenderDoc直接调用的Desktop OpenGL。

这时候突然阿兰提到: 如果故意在前面的GL.glTextureImage2DEXT中传入空指针作为数据会怎么样? 试了下发现居然对应纹理的内容没变。
这就得到一个结论: 我前面找到的那个接口不是真正起决定性的地方,肯定还有别的接口在填充数据orz

Debug Attemp 3

确认自己前面走进误区之后反而好办,重新定位了下相关函数,找到了真·填充数据的口子在gl_initstate.cppGLResourceManager::Apply_InitialState里。
老样子在GL.glTextureSubImage2DEXT调用前把scratchBuf保存到文件,对比确实数据不一致——而且用IrfanView直接打开RAW,确实就是那张错误贴图。

至此明确了Replay部分其实是没问题的,问题转化到Capture时是如何获取scratchBuf数据的。

在Capture过程挂断点跑进GL.glGetTexImage(targets[trg], i, fmt, type, scratchBuf);,立刻发现问题所在: gl_emulated.cpp_glGetTexImage实现需要获取贴图的大小

GLint width = 0, height = 0, depth = 0;
GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_WIDTH, &width);
GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_HEIGHT, &height);
GL.glGetTexLevelParameteriv(target, level, eGL_TEXTURE_DEPTH, &depth);

但是Angle认为这个调用是不合法的,所以导致后面RenderDoc压根没有真正去glReadPixels获取数据,scratchBuf就是纯纯脏数据。

Fix

定位到这里之后可以说是老眼熟了…不就和前面提到的ValidateCopyImageSubData一回事么…
这次想着全局禁用检查得了,看了下Context.cpp实现,应该只需要创建的时候带上EGL_CONTEXT_OPENGL_NO_ERROR_KHR属性。

然而试了下是无效的…怒而下断点,结果发现这次是egl_hooks.cpp在使坏

if(name == EGL_CONTEXT_OPENGL_NO_ERROR_KHR)
{
// remove this attribute so that we can be more stable
continue;
}

这个注释好有道理导致我无言以对emmm
算了算了,那我就在Angle代码里mSkipValidation(true)写死,反正也没法直接用官方版本dll。

glCopyImageSubData效果错误

这个问题我其实当年提交supporting full glCopyImageSubData的时候就已知,只不过后面一直没空去填坑…这次准备一起搞一下orz

问题本质是分帧、分块调用glCopyImageSubDataEXT时,RenderDoc需要一个机制来获取对应的数据。如果数据来源是某张压缩纹理,那么RD本身就其保存在了compressedData里面,直接读取即可;对于非压缩的情况,就暂时没有机制解决(这个对应的就是RGBA32UI->ETC/BC3这种GPU Compress的情况)。

后来抽空把这块补全了下,结果提交complete glCopyImageSubData时候反而断断续续根据review修改花了一周,总算也填了一个两年前的坑orz