查看: 77|回复: 3

鸿蒙XComponent原生渲染全黑:纹理枚举值撞车排查记

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在把 OBS Studio 移植到 HarmonyOS 的过程中,图形后端修复完 shader 四连击后,obs_reset_video 已经返回 rc=0,采集帧也能源源不断进入渲染树,但预览区始终一片黑。本文记录这条“每段水管都通、就是池子没进水”的黑盒排查过程:十轮探针、两个合成实验、一次 GPU 缓冲内省 dump,最后的答案是一行手写补位的宏定义抄错了数字。这次排查的最大价值不在结论,而在收窄问题的方法,可供所有用 XComponent 做原生渲染的鸿蒙开发者参考。

一、现象:链路全绿,画面全黑

先摆事实。排查起点时,日志显示各环节都对:采集帧进入 source(中心像素采样等于真实屏幕内容)、源 activate 在图形线程正确触发、draw tick 稳定 60Hz、eglSwapBuffers 无错误、async 纹理 read-back 有真实像素。唯独 canvas(合成目标纹理)恒黑。链路大致如下:
  1. 采集帧(有内容) -> 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,发现:
  1. // 修复前
  2. #define GL_TEXTURE_RECTANGLE 0x0DE1 // 这其实是 GL_TEXTURE_2D 自己的值
复制代码

GL_TEXTURE_2D 的官方值就是 0x0DE1。手写补位时把桌面常量抄错了列,而 GL_TEXTURE_RECTANGLE 的真值是 0x84F5。于是每个 2D 纹理都被判定为矩形纹理,sprite 构建走 rect 分支生成像素 UV,普通采样器 CLAMP 后恒采角点,整个画布等于一个 texel 的颜色。修复只有一行:
  1. #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。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙XComponent原生渲染全黑:纹理枚举值撞车排查记

楼主这篇排查记看得人很过瘾,信息密度高但脉络特别清楚。最佩服的是那种“先证明能不能看到任何画面”的切分思路——红帧实验和 UV 渐变实验几乎是把黑盒强行变成白盒,尤其 UV 渐变推回去正好落在右上角 texel 这个细节,等于自己把答案念出来了。这种先隔离“呈现问题”和“内容问题”的顺序,比一上来就盯着 GL 状态码翻要有用得多。 viewport 那段也很实用,多形态设备上 ArkUI 布局、NativeWindow 几何、EGL surface 三个尺寸来源确实容易打架,直接用 eglQuerySurface 作为绘制坐标系的唯一信任源,这条经验值得记下来。GET_BUFFER_GEOMETRY 返回旋转基准导致 y 偏移 1813、绘制静默落在不可见区还不报错,属于典型的“合法但错误”,跟最后那个枚举值事故是一个性质。 最后 0x0DE1 抄成 GL_TEXTURE_RECTANGLE 这个真值撞车,确实肉眼 review 根本看不出来,长得太像枚举值了,而且桌面版永远走官方生成的头文件,只有移植端口会踩。能不能顺手把手写补位那批宏跟官方 gl.xml 全量对一遍,甚至加个启动期断言或静态检查?同类数字抄错的可能不止这一个。另外结尾 EGL_BAD_NATIVE_WINDOW 那段好像被截断了,引用问题的细节还等着看,方便的话可以补个续篇。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙XComponent原生渲染全黑:纹理枚举值撞车排查记

好文,收藏了。这个案子最有意思的地方就是它把“编译器不报、驱动不报、GL 错误队列也不报”的一类问题展示得很彻底——语义合法但含义全错,这种 bug 用常规单步调试图虽然也能查,但很容易在十几个“状态全对”的结论里绕圈。 几处我特别认同: 一是红帧和 UV 渐变这两个合成实验。很多人遇到全黑第一反应是继续 dump 状态,其实先回答“能不能看到任何画面”这个问题,用几秒钟的 CPU 侧涂色就能把“呈现问题”和“内容问题”分开,信息密度远高于再看一遍 uniform 表。渐变那一步直接把结论推到“恒定角点采样”,基本就是致命一击。 二是窗口尺寸不要信 NativeWindow 几何。多形态设备上 ArkUI 布局、NativeWindow 几何、EGL surface 三套坐标系确实经常各说各话,尤其 2in1 和折叠屏旋转状态下,GET_BUFFER_GEOMETRY 给的值未必是你要画的坐标系。以后默认用 eglQuerySurface 取宽高,这条建议可以当常识写进团队规范。 三是手写 glad shim 这个坑。移植端绕开 Khronos 官方生成的头文件,等于把所有“肉眼看起来像枚举值”的常量都变成了定时炸弹。我给两个建议:一是补位头文件别手写数字,直接从 gl.xml 生成或者抄官方的值,并且加一行注释标出处;二是在初始化时加个低成本自检,比如 assert(GL_TEX
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙XComponent原生渲染全黑:纹理枚举值撞车排查记

这个排查记写得很扎实,尤其是“红帧实验 + UV 渐变实验”这两步,把“呈现问题”和“内容问题”一刀切开的思路太值得学了,比在 GL 状态里盲扫高效得多。之前我也在别处吃过 viewport 算到 surface 外面的亏,GL 一声不吭,确实得记住“绘制坐标系信任 eglQuerySurface 返回的尺寸”这条。 最让人拍大腿的还是那个 0x0DE1 撞车。手写 glad 补位时把 GL_TEXTURE_2D 的值抄到 GL_TEXTURE_RECTANGLE 上,这种错肉眼 review 真的很难发现,编译器、驱动、GL 错误队列全都沉默,因为它语义合法只是含义反了。感觉可以补两个小防线:一是用脚本从官方 gl.xml 生成枚举头,别手动抄;二是在移植端口初始化时把几个关键 target 值打印出来和预期对一下,或者给 gs_texture_is_rect 这种判定加个一次性日志,至少能在异常路径上先冒个泡。 另外帖子到 EGL_BAD_NATIVE_WINDOW 的 OnSurfaceCreat 那里好像被截断了,那个引用问题后面怎么解的还挺想看的,如果方便的话期待补完。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-1 01:12 , Processed in 0.022962 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部