嵌入式GPU为何遮罩与透视不能兼得

图形变换的四个级别

2D和3D变换的本质区别,在于变换能保持图形的哪些性质不变。按由简到繁可以分为四个级别:

  1. 平移缩放:最基础的变换
  2. 仿射(Affine):旋转、斜切。变换后平行线依然平行,比如四边形无论怎么旋转缩放,仍是四边形
  3. 透视(2.5D,Perspective):模拟人眼"近大远小",平行线会汇聚
  4. 真3D:完整的三维空间,包含深度、光照、模型

其中,斜切是把图形往某个方向推歪,让原本的直角变成斜角,但保持平行——典型例子就是斜体字。

透视变换、2.5D 与 3D 的区别

这三个概念容易混淆,需要区分清楚:

  • 透视变换是一种数学手段,通过矩阵计算实现"近大远小"
  • 2.5D是用这种手段达到的视觉效果的叫法:用2D图片伪造出3D的视觉效果,但底层并没有真正的三维模型、深度缓冲和光照计算
  • 3D渲染同样会用到透视变换,但它有真正的三维模型和三维计算

区分2.5D和3D的核心,就看有没有真正的三维模型和三维计算,尤其是有没有深度(Z轴)计算

遮罩不支持透视变换

在我所使用的杰理(JieLi)平台上,遮罩绘制指令(GPU_FULL_MASK / GPU_TEXTURE_MASK)在硬件层面不支持透视(2.5D)变换。这类限制在资源受限的小型嵌入式2D GPU上比较常见,但具体是否受限、指令名称如何,都取决于芯片,需以自己平台的手册为准。

一旦给一个用了遮罩的绘制任务再叠加2.5D变换,GPU就会进入无法处理的状态而卡死。

一个典型的坑:圆角矩形。矩形里的圆角并不是真的画一个圆角,而是用遮罩图"扣"出来的。所以只要对圆角矩形做2.5D变换,就会卡死。

与遮罩不同,填充、贴图指令都有对应的透视版本,需要一并替换:

GPU_FILL  ->  GPU_FILL_PERSPECTIVE

也就是说,填充、贴图都有透视版本,唯独遮罩没有

拷贝任务链

拷贝任务链,就是把一条现成的页面任务链复制一份,然后给这份副本整体套上一个变换矩阵,重新画到另一个缓冲区里。

关键点在于:拷贝的是任务链,而不是画好的像素图。套上矩阵后,是让GPU用新的角度/缩放重新渲染一遍,得到清晰的变形结果,而不是把一张位图硬拉变形导致模糊。

它的一个核心用途是页面切换特效。切换时屏幕上要同时出现"当前页"和"下一页",两个页面还要各自带不同的变换。如果没有拷贝任务链,就得每一帧都把两个完整页面的所有控件重新构建一遍任务,代价极大。有了它,流程就简化为:

  1. 页面已有现成的任务链,直接拷贝一份
  2. 每帧改一下矩阵
  3. GPU重画即可

但这里有个限制:拷贝任务链要求一整页里不能混入MASK,否则整条链在做特效变换时就会卡死——这正是上一节遮罩不支持透视的直接后果。

为什么要做这样的取舍

在桌面/手机的GPU上,“遮罩 + 透视"是家常便饭,毫无问题。但在小型嵌入式2D GPU里,“遮罩和透视二选一"是非常常见的取舍。为什么?

透视变换的本质是"逐像素反查”:对屏幕上每一个输出像素,GPU要反算"这个点对应源图里的哪个坐标”。而"近大远小"的效果,靠的是每个像素都要做一次除法。所以透视需要一套逐像素的坐标映射 + 除法引擎在满负荷工作。

MASK的本质也是"逐像素查表":对每一个输出像素,GPU要去mask缓冲区取出该点对应的alpha值,与颜色做混合。这同样需要一套逐像素的坐标寻址逻辑去mask数据里定位。

两件事抢的是同一个资源:逐像素的坐标计算/寻址单元。

更麻烦的是,透视时输出像素和源坐标之间是非线性关系。如果还要叠加MASK,就得同时再算一套"非线性处理后的像素该去mask的哪个位置取值"。

这在资源充足的大GPU里能实现,因为它们的坐标单元可编程,想怎么算就怎么算。但在小GPU里,为了省芯片面积、省功耗、保证吞吐,硬件把能做的几种组合写死成固定的几条电路通路。芯片里可能根本没有为"MASK + 变换"这条组合布线,因此透视和MASK只能二选一。