Bitmap 与像素操作
Bitmap 是 tgfx 中处理 CPU 侧像素读写、管理栅格内存的核心容器。它是像素数据的物理“持有者”,负责维护从内存分配到 GPU 同步的完整生命周期。
在现代渲染流水线中,虽然大部分绘制由 GPU 完成,但 CPU 侧的像素操作依然不可或缺:
- 物理所有权:不同于
Image的逻辑引用,Bitmap是真实的内存管理者。它通过引用计数(PixelRef)确保内存的安全释放。 - 可变性 (Mutable):它是唯一允许开发者直接通过指针修改内容的容器。
- 桥梁作用:它作为“数据代理”,连接了磁盘解码出的字节流与 GPU 纹理上传接口。
1. Bitmap 内存管理
1.1 分配策略 (allocPixels)
Bitmap 的核心能力始于其对内存的灵活分配方案。
- 构造初始化:也可以通过构造函数直接初始化:
Bitmap(width, height, alphaOnly, tryHardware, colorSpace),效果等价于默认构造加allocPixels()。 allocPixels(w, h, alphaOnly, tryHardware, colorSpace):- alphaOnly:如果开启,系统仅分配单通道 Alpha 内存(每像素 1 字节)。这在处理字体笔迹或模糊算法的中间掩码(Mask)时,能减少 75% 的内存压力。
- tryHardware:这是移动端性能优化的关键开关。开启后,tgfx 会优先向系统申请 AHardwareBuffer (Android) 或 CVPixelBuffer (iOS)。
1.2 物理载体 PixelRef 与引用计数
Bitmap 内部并不直接管理 void* 指针,而是通过 PixelRef 包装。
- 共享机制:多个 Bitmap 实例可以共享同一个
PixelRef。这允许开发者在不拷贝内存的情况下,创建出指向原图不同区域的多个 Bitmap 子视图。 - 生命周期:只要还有一个 Bitmap 引用该
PixelRef,物理内存就不会被销毁。
1.3 HardwareBuffer 深度解析
HardwareBuffer 是连接 CPU 像素操作与 GPU 高性能渲染的“桥梁”,实现了一份内存,两端共享。
- 平台映射:在 Android 上映射为
AHardwareBuffer,在 iOS/macOS 上映射为CVPixelBuffer,在 Windows 上映射为ID3D11Texture2D。 - 零拷贝 (Zero-Copy) 机制:
- CPU 侧:通过
lockPixels()将硬件内存映射到进程地址空间,实现直接读写。 - GPU 侧:GPU 纹理通过
EGLImage或IOSurface直接绑定到该物理内存。 - 优势:规避了传统的
glTexImage2D像素拷贝过程,极大降低了在大尺寸图片下的纹理上传耗时和总线带宽消耗。
- CPU 侧:通过
- 性能权衡:硬件层内存虽然支持零拷贝显示,但在 CPU 侧调用
lockPixels()会引发底层缓存一致性同步(Cache Flush),因此对于纯 CPU 的像素算法,不建议盲目开启tryHardware。
2. Pixmap:轻量级逻辑视图
Pixmap 是一个极其轻量的、不管理生命周期的观察者结构体。
2.1 结构与职责
- 构造初始化:
Pixmap也支持直接从Bitmap构造:Pixmap pixmap(bitmap),会自动绑定像素指针并在析构时解锁。 - 逻辑构成:由
ImageInfo(宽/高/步进/格式) 和一个裸指针 (void*) 组成。 - 设计动机:在函数调用链中传递像素时,使用
Pixmap可以避免频繁创建Bitmap或shared_ptr带来的开销。它仅仅是告诉系统:“请按照这个格式,去读写这个地址开头的内存”。
2.2 视图协作
- 下取子集 (makeSubset):Pixmap 的
makeSubset操作仅涉及到指针偏移量和宽高的修改,不产生任何像素拷贝,性能开销几乎为零。 - 格式转换 (readPixels):它是跨格式处理的神器。你可以用一个
BGRA格式的 Pixmap 调用readPixels写入一个Gray8格式的目标,内部会自动完成像素重采样和亮度计算。
3. 像素格式详解 (ImageInfo)
3.1 ColorType:色彩空间布局
描述了比特位如何映射到物理颜色通道:
| 格式 | 每像素字节 | 典型应用场景 |
|---|---|---|
RGBA_8888 | 4 字节 | 跨平台最通用的标准格式。 |
BGRA_8888 | 4 字节 | iOS/macOS 渲染管线的原生偏好格式。 |
RGB_565 | 2 字节 | 不透明图像的紧凑格式,内存占用低。 |
ALPHA_8 | 1 字节 | 用于图像遮罩、字体、手写笔迹效果。 |
Gray_8 | 1 字节 | 灰度图像,单通道亮度信息。 |
RGBA_F16 | 8 字节 | HDR (高动态范围) 支持,适合超高色彩精度的专业处理。 |
RGBA_1010102 | 4 字节 | 10 位色彩通道 + 2 位 Alpha,适合宽色域显示。 |
3.2 AlphaType:透明度解释权
Opaque:完全不透明,忽略 Alpha 通道。Unpremultiplied:R/G/B 分量独立。常用于保存图片文件。Premultiplied(预乘模式 - 核心推荐):- 数学动机:在经典的 Porter-Duff 混合公式中,最终颜色计算为
Src + Dst * (1 - SrcA)。如果 Src 已经是预乘后的(RGB * A),则公式内少了一次除法运算。 - 渲染优势:避免了在进行线性缩放或滤镜处理时,边缘处因 Alpha 值为 0 导致的黑色杂边问题。
- 注意:在手动修改像素值时,必须确保 RGB 分量的值小于等于其对应的 Alpha 分量。
- 数学动机:在经典的 Porter-Duff 混合公式中,最终颜色计算为
4. Bitmap ↔ Image 的互转机制
4.1 从 Bitmap 到 Image (包装)
这是将 CPU 侧处理好的像素提交给渲染管线的标准方式。
- API:
Image::MakeFrom(bitmap)。 - 行为:创建一个不可变的包装器,两者共享底层像素内存。
- 写时拷贝 (COW):为了保证
Image的不可变性,如果生成的Image尚未销毁,而你又对原Bitmap调用了lockPixels()获取写权限或执行了写入操作,TGFX 会自动为Bitmap执行一次全量像素克隆(Clone)。
代码示例:
// 创建 Bitmap 并填充数据
Bitmap bitmap;
bitmap.allocPixels(200, 200);
// ... 对 bitmap 进行 CPU 绘图或算法处理 ...
// 包装为 Image 供 Canvas 绘制
auto image = Image::MakeFrom(bitmap);
canvas->drawImage(image, 0, 0);
4.2 从 Image 到 Bitmap (回读)
由于 Image 可能是由文件、Picture 或 GPU 纹理组成的,它不一定直接持有 CPU 像素。因此,通常需要通过 Surface 进行中转回读。
代码示例:
// 1. 创建一个与 Image 同尺寸的 Surface
auto surface = Surface::Make(context, image->width(), image->height());
auto canvas = surface->getCanvas();
// 2. 将 Image 绘制到 Surface 上
canvas->drawImage(image, 0, 0);
context->flushAndSubmit(true); // 确保 GPU 渲染完成
// 3. 准备目标 Bitmap 并回读像素
Bitmap bitmap;
bitmap.allocPixels(image->width(), image->height());
surface->readPixels(bitmap.info(), bitmap.lockPixels());
bitmap.unlockPixels();
5. 高性能异步像素回读
在高性能应用中,从 GPU 获取像素数据(Readback)通常是性能瓶颈。传统的同步读取会强制 CPU 等待 GPU 完成所有待处理任务并同步内存,导致显著的卡顿。
5.1 协作链路:Surface::asyncReadPixels()
为了解决同步阻塞问题,TGFX 提供了异步回读机制。该机制允许 CPU 发起指令后立即继续执行其他任务,让 GPU 在后台异步搬运像素。
协作链路: asyncReadPixels (发起请求) → SurfaceReadback (状态持有) → isReady (非阻塞轮询) → lockPixels (访问数据)
- 发起请求:调用
surface->asyncReadPixels(rect)。此操作不会立即读取像素,而是向渲染管线提交一个下载任务,并返回一个SurfaceReadback对象。 - GPU 搬运 (GPU → CPU):GPU 在空闲时会将指定的矩形区域像素从显存下载到 CPU 可见的映射缓冲区(或 PBO)中。
- 非阻塞查询:通过
readback->isReady(context)检查数据是否已就绪。这通常在主循环或后续帧中进行,不会阻塞当前线程。 - 结果获取:调用
readback->lockPixels()获取像素指针。- 注意:如果调用时
isReady()为 false,此方法会退化为同步阻塞,直到数据同步完成。
- 注意:如果调用时
5.2 同步与异步对比
| 特性 | readPixels() (同步) | asyncReadPixels() (异步) |
|---|---|---|
| CPU 行为 | 立即阻塞,直到 GPU 任务全部完成。 | 立即返回,继续执行后续 CPU 代码。 |
| 渲染流水线 | 强制冲刷(Stall),破坏 GPU 并行性。 | 与 GPU 渲染任务并行执行。 |
| 延迟 (Latency) | 较低(立即获取当前帧结果)。 | 较高(通常需 1-2 帧后获取结果)。 |
| 吞吐量 (Throughput) | 低。 | 极高,支持高频连续抓帧。 |
5.3 适用场景
- 截图导出 (One-off Export):
如果用户点击“保存到相册”,且此时没有后续动画任务,使用同步
readPixels()逻辑更简单。 - 录屏帧捕获 (Screen Recording):
在 60fps 录屏场景下,必须使用
asyncReadPixels()。通过“在第 N 帧发起请求,第 N+2 帧提取数据”的策略,可以完全消除回读带来的掉帧。 - 实时算法分析 (Video Analysis): 当需要对渲染结果进行实时人脸识别或滤镜分析时,异步读取能保证 UI 交互的流畅性。
6. 代码实战:像素手动处理
以下示例展示了从 Surface 异步读取像素,并利用 Pixmap 视图进行反色处理的完整逻辑:
void ProcessPixels(Surface* surface) {
// 1. 发起异步请求
auto readback = surface->asyncReadPixels(Rect::MakeWH(100, 100));
surface->getContext()->flushAndSubmit();
// 2. 开发者可以在此处做其他 CPU 工作...
// 3. 准备 Bitmap 容器
Bitmap bitmap;
bitmap.allocPixels(100, 100);
// 4. 提取结果(带懒阻塞逻辑)
const void* gpuPixels = readback->lockPixels(surface->getContext());
if (gpuPixels) {
// 利用 Pixmap 视图工具进行内存写入
bitmap.writePixels(readback->info(), gpuPixels);
// 5. 手动遍历像素(反色算法)
uint32_t* row = static_cast<uint32_t*>(bitmap.lockPixels());
size_t rb = bitmap.rowBytes();
for (int y = 0; y < 100; ++y) {
for (int x = 0; x < 100; ++x) {
row[x] ^= 0x00FFFFFF; // 修改 RGB 通道
}
row = (uint32_t*)((uint8_t*)row + rb);
}
bitmap.unlockPixels();
}
// 6. 安全清理
readback->unlockPixels(surface->getContext());
}
渲染结果对比
| 原始图像 (Original) | 处理后图像 (Inverted) |
|---|---|
7. 最佳实践与性能提示
- 步进安全提示:在手动遍历像素时,严禁使用
y * width * bpp。必须使用rowBytes(),因为不同系统可能对内存行进行字节对齐(Alignment),rowBytes会比width * bytesPerPixel大。 - HardwareBuffer 同步代价:开启
tryHardware的 Bitmap 在 CPU 访问时,底层需要清理 L1/L2 缓存以确保数据一致性。如果是纯 CPU 处理算法,不涉及 GPU 共享,建议关闭tryHardware。 - 预乘混合:如果你的算法涉及到 Alpha 调整,请始终在非预乘(Unpremultiplied)下计算,完成后再转回预乘模式绘制。
