在把 OBS Studio 移植到 HarmonyOS 的过程中,图形后端修复完 shader 四连击后,obs_reset_video 已经返回 rc=0,采集帧也能源源不断进入渲染树,但预览区始终一片黑。本文记录这条“每段水管都通、就是池子没进水”的黑盒排查过程:十轮探针、两个合成实验、一次 GPU 缓冲内省 dump,最后的答案是一行手写补位的宏定义抄错了数字。这次排查的最大价值不在结论,而在收窄问题的方法,可供所有用 XComponent 做原生渲染的鸿蒙开发者参考。
一、现象:链路全绿,画面全黑
先摆事实。排查起点时,日志显示各环节都对:采集帧进入 source(中心像素采样等于真实屏幕内容)、源 activate 在图形线程正确触发、draw tick 稳定 60Hz、eglSwapBuffers 无错误、async 纹理 read-back 有真实像素。唯独 canvas(合成目标纹理)恒黑。链路大致如下:
- 采集帧(有内容) -> async→sync纹理转换 -> sprite draw(源→canvas) -> canvas(恒黑) -> blit→swap(无错误) -> XComponent显示
复制代码
断点必然藏在 async→sync、sprite draw 与 canvas 之间,但这几段属于 GPU 驱动的黑盒,printf 只能打到 GL 调用边界为止。
二、探针阶梯:先揪出 viewport 案犯
第 1–2 轮在 draw callback 里用 glReadPixels 采样当前 framebuffer,结果恒为背景色,说明 blit 根本没写字。顺着查出来第一个问题其实出在窗口尺寸上:
原实现通过 OH_NativeWindow_NativeWindowHandleOpt(GET_BUFFER_GEOMETRY) 获取窗口尺寸。在 PC/2in1 形态下,该接口返回的是合成器旋转过的竖屏基准 233×2046,而 Y 翻转公式把 2046 当成高度使用,viewport 被算成 0,1813,2046,233——y 偏移 1813,整个视口被推到 surface 外部,所有绘制静默落在不可见区。GL 不会报错,因为“在不存在的区域绘制”在 API 语义上完全合法。
修法是改用 eglQuerySurface 获取绘制尺寸。这里沉淀一条经验:多形态设备上“窗口尺寸”至少有 ArkUI 布局、NativeWindow 几何、EGL surface 三个来源,而绘制坐标系永远应该信任 EGL surface 返回的值。
viewport 修好后再采样,framebuffer 从背景色变成 0,0,0,255——blit 确实在写字了,但写的仍是黑色。五点位采样 canvas 全黑,async 纹理 read-back 却仍有内容,问题于是精确收窄到 async 纹理到 canvas 的那次 sprite draw。
第 4–6 轮把探针一次性撒到最大密度:draw 时的 program/uniform 表(ViewProj 已正确上传)、attribute 表(pos VEC4→loc0、uv VEC2→loc1,绑定正确)、viewport/scissor/cull/blend/FBO 附件/颜色掩码/深度,全部正确且无 GL 错误;再把生成的 GLSL 源码 dump 出来与桌面版逐字 diff,也完全一致。到这里,常规手段已经穷尽:GL 状态全对、代码全对、数据全对,画面就是不对。
三、两个合成实验,十分钟锁死根因
当“看不到画面”时,先证明“能不能看到任何画面”,把呈现问题和内容问题一刀切开,这是黑盒渲染排查里最有效的思路。
红帧实验:在 CPU 侧把采集帧整幅涂成纯红,其余不动,装机后预览条变红了。这一下信息量极大——管线其实是通的,“黑”不是画不上去,而是恒采样到了某个黑色 texel。问题从“渲染管线”收窄为“采样坐标”。
UV 渐变实验:把采集帧涂成 r=u、g=v 的空间渐变(左上黑、右下黄绿),理论上 canvas 应显示同样渐变。装机后五点位采样恒为 (255,255,55),换算回去正好是右上角 texel 的 raw 值 (255,255,128) 经 sRGB 解码的结果。结论已容不得其他可能:着色器对整幅四边形采样了一个恒定的角点 UV。
第 9 轮继续用 glMapBufferRange 把 pos/uv 绑定的实际 GPU 缓冲 dump 出来:pos 缓冲是 1×1 精灵的四个角点,正确;uv 缓冲却是 0..1920 / 0..1080 的像素坐标——UV 怎么可能是像素坐标?答案在 libobs 里只有矩形纹理的 sprite 构建路径(build_sprite_rect)才会生成像素坐标 UV,因为矩形纹理采样器的约定本就是像素坐标;普通 2D 纹理走的是归一化路径 0..1,配合 CLAMP 到边缘后,0..1920 的 UV 恒落在右上角那个 texel 上。所有现象就此闭环:角点恒采样导致黑,而红帧实验“也对”,因为整幅同色时采哪个点都一样。
四、判决:手写常量 0x0DE1 撞车
问题只剩一个:普通 2D 纹理为什么会走进矩形纹理分支?查 gs_texture_is_rect(),它恒返回 true;继续看实现,它比对纹理 target 是否等于 GL_TEXTURE_RECTANGLE;再去看 port 手写补位的 glad shim 头文件 libobs-opengl/harmony/glad/glad.h,发现:
- // 修复前
- #define GL_TEXTURE_RECTANGLE 0x0DE1 // 这其实是 GL_TEXTURE_2D 自己的值
复制代码
GL_TEXTURE_2D 的官方值就是 0x0DE1。手写补位时把桌面常量抄错了列,而 GL_TEXTURE_RECTANGLE 的真值是 0x84F5。于是每个 2D 纹理都被判定为矩形纹理,sprite 构建走 rect 分支生成像素 UV,普通采样器 CLAMP 后恒采角点,整个画布等于一个 texel 的颜色。修复只有一行:
- #define GL_TEXTURE_RECTANGLE 0x84F5 // 与官方 gl.xml 逐值核对后的真值
复制代码
改完装机,预览实时显示真实桌面。这个 bug 编译器不报、驱动不报、GL 错误队列也不报——它语义完全合法,只是含义全错。0x0DE1 长得非常像一个 GL 枚举值,肉眼 review 根本不会起疑;桌面版 OBS 始终链接 Khronos 官方生成的头文件,永远碰不到这个分支,所以这是移植端口独有的坑。
五、同案并办:EGL_BAD_NATIVE_WINDOW 的引用问题
排查过程中还处理了一个同时出现的问题:present 每帧刷 EGL_BAD_NATIVE_WINDOW。根因是 OnSurfaceCreated 回调给的 OHNativeWindow* 是借用引用,回调返回后 buffer queue 就被回收,基于已失效 window 建的 surface 自然每帧必败。修法是在 Created/Changed 时调用 OH_NativeWindow_NativeObjectReference() 增加引用,在 Destroyed 时释放。鸿蒙 C API 的回调参数所有权没有明确标注,是借用还是转移全靠实践踩出来——这类问题存在于所有用 XComponent 做原生渲染的应用里,值得单独记住。
六、可复用的排查经验
复盘这十轮排查,“收窄的艺术”比结论更重要,有三条经验值得沉淀:
其一,合成实验是黑盒渲染的核武器。无法直接观测 GPU 内部时,就构造“答案唯一”的输入:纯色帧证明通路存在,渐变帧暴露采样坐标。两个实验各十分钟,抵得上十几个小时读驱动源码。
其二,探针要一次撒全,再逐轮收窄。第 4 轮的全状态探针虽然“全部正确”,但正是这些阴性结果把排查从“状态错”逼向“数据错”,才有了第 9 轮的 attrbuf dump。阴性探针与阳性探针同样有价值。
其三,手写头文件与常量的补位必须逐值核对官方源。这次是 glad shim 里的一个宏,同类问题在依赖移植中还有无数化身——结构体布局、枚举顺序、错误码表。“看起来对”就等于没核对。
验证环境:HUAWEI MatePad Edge(PC/2in1 模式),HarmonyOS 7.0.0.109,API 26;OBS Studio 32.2.2,fork 分支 qingkouwei/obs-studio@harmonyos-port。 |