#RK3588 Android 14 虚拟相机 HAL 开发踩坑实录:从 Mali Gralloc 报错到成功显示画面

摘要:本文记录了在 RK3588 + Android 14 + Redroid 云手机环境下,开发虚拟相机 HAL 实现过程中遇到的一系列问题及解决方案。涉及 Mali Gralloc 的 buffer 权限问题、格式覆盖机制、Shutter 通知等关键知识点,对 Android Camera HAL 开发者具有参考价值。


一、背景介绍

1.1 项目目标

基于 Redroid(Docker 化的 Android 容器方案)实现云手机的虚拟相机功能。目标是让相机应用打开时能显示自定义的虚拟画面(本文以红色画面为例进行调试)。

1.2 硬件与软件环境

  • 硬件平台:RK3588
  • 操作系统:Android 14
  • GPU:Mali (使用 Mali Gralloc)
  • Camera HAL 版本:HIDL Camera HAL 3.2

1.3 开发思路

参考 Android Emulator 的 goldfish 虚拟相机实现,编写一个 VirtualCameraSession 类来处理相机请求,将虚拟画面填充到 buffer 中返回给 Camera Framework。


二、踩坑之旅

2.1 第一个错误:Usage 不匹配

错误日志
复制代码
E mali_gralloc: Usage not a subset. Buffer usage = 0x100, Descriptor usage = 0x20000
E GraphicBufferMapper: validateBufferSize(0x7a07a21448) failed: 3
E VirtualCameraSession: importBuffer failed: 3 (trying without import)
问题分析
复制代码
Buffer usage = 0x100        → GRALLOC_USAGE_HW_TEXTURE (Buffer 创建时的 usage)
Descriptor usage = 0x20000  → GRALLOC_USAGE_HW_CAMERA_WRITE (我代码中使用的 usage)

Mali Gralloc 要求 importBuffer 时的 usage 必须是原始 buffer usage 的子集 。由于 buffer 创建时只有 HW_TEXTURE 权限,没有 SW_WRITE 或 HW_CAMERA_WRITE 权限,导致 import 失败。

错误代码
cpp 复制代码
// ❌ 错误:使用了 buffer 创建时不包含的 usage
err = mapper.importBuffer(handle, ..., GRALLOC_USAGE_HW_CAMERA_WRITE, ...);
解决方案

从源头解决! 在 configureStreams 中通过 producerUsage 告诉 Framework 分配 buffer 时需要包含 CPU 写入权限:

cpp 复制代码
// ✅ 正确:在 configureStreams 中请求 SW_WRITE 权限
halConfig.streams[i].producerUsage = GRALLOC_USAGE_SW_WRITE_OFTEN;

这样 Framework 分配 buffer 时就会包含 SW_WRITE_OFTEN 权限,后续 importBuffer 使用相同的 usage 就不会报错了。


2.2 第二个错误:Lock 前未 Import

修改 usage 后,尝试跳过 import 直接 lock,又遇到新问题:

错误日志
复制代码
E mali_gralloc: Attempt to lock buffer before importBuffer
E mali_gralloc: Locking buffer failed with error: -22
问题分析

这涉及到 跨进程 Buffer 管理机制:

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    跨进程 Buffer 传递流程                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Camera Framework (cameraserver)        Camera HAL (你的进程)       │
│                                                                     │
│  1. allocateBuffer()                                                │
│     └─ buffer 分配在 cameraserver                                   │
│                                                                     │
│  2. HIDL 传递 native_handle_t ──────────> 3. 收到 handle            │
│     (只是文件描述符的引用)                   (未 import 状态)        │
│                                                                     │
│                                           4. 必须 importBuffer()    │
│                                              (在本进程建立映射)      │
│                                                                     │
│                                           5. lock() → 写入 → unlock │
│                                                                     │
│                                           6. freeBuffer()           │
│                                              (释放本进程的映射)      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

关键认知 :Camera Framework 传过来的 native_handle_t 只是一个文件描述符的引用,在 HAL 进程中必须先 import 才能建立本地映射,然后才能 lock。

解决方案
cpp 复制代码
// 步骤 1: Import buffer 到当前进程
buffer_handle_t importedHandle = nullptr;
uint64_t usage = GRALLOC_USAGE_SW_WRITE_OFTEN;

err = mapper.importBuffer(handle, width, height, 1, format, usage, width, &importedHandle);
if (err != android::OK) {
    ALOGE("importBuffer failed");
    return;
}

// 步骤 2: Lock 并写入数据
err = mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr);
if (err == android::OK) {
    // 填充数据...
    mapper.unlock(importedHandle);
}

// 步骤 3: 释放 imported handle
mapper.freeBuffer(importedHandle);

2.3 第三个错误:格式不匹配

解决了 import 问题后,又遇到新的错误:

错误日志
复制代码
I VirtualCameraSession: Stream 1: overriding IMPLEMENTATION_DEFINED(34) to YCBCR_420_888(35)
I VirtualCameraSession: Configured stream 1: 1280x720 format=34->35
...
I VirtualCameraSession: Processing stream 1: 1280x720 format=34
E mali_gralloc: Buffer requested format: 0x23 does not match descriptor format: 0x22
  • 0x23 = 35 = YCBCR_420_888(覆盖后的格式)
  • 0x22 = 34 = IMPLEMENTATION_DEFINED(原始格式)
问题分析

在 configureStreams 中设置了 overrideFormat = YCBCR_420_888,Framework 会用覆盖后的格式分配 buffer。

但在 processCaptureRequest 中,我还在使用 mStreams[i].format(原始格式)来 import,导致格式不匹配!

解决方案

保存覆盖后的格式,在处理请求时使用正确的格式:

cpp 复制代码
// configureStreams 中保存覆盖后的格式
std::map<int32_t, int32_t> mStreamFormatMap;

for (size_t i = 0; i < mStreams.size(); i++) {
    int32_t overrideFormat;
    if (mStreams[i].format == PixelFormat::IMPLEMENTATION_DEFINED) {
        overrideFormat = HAL_PIXEL_FORMAT_YCbCr_420_888;
        halConfig.streams[i].overrideFormat = PixelFormat::YCBCR_420_888;
    } else {
        overrideFormat = static_cast<int32_t>(mStreams[i].format);
    }
    // ⭐ 保存覆盖后的格式
    mStreamFormatMap[mStreams[i].id] = overrideFormat;
}

// processCaptureRequest 中使用覆盖后的格式
auto it = mStreamFormatMap.find(stream.id);
int32_t format = (it != mStreamFormatMap.end()) ? it->second : stream.format;

2.4 第四个错误:lockYCbCr 返回空指针

格式匹配后,日志显示 import 和 lock 都成功了,但程序崩溃:

错误日志
复制代码
I VirtualCameraSession: importBuffer success: imported=0xb400007b7a5addd0
I VirtualCameraSession: lockYCbCr success: y=0x0, cb=0x0, cr=0x0   ← 指针全是 NULL!
... 
E Camera3-Device: Unable to submit capture request 0 to HAL device: Broken pipe (-32)
问题分析

lockYCbCr 返回成功(err == OK),但 ycbcr.y、ycbcr.cb、ycbcr.cr 都是 NULL!代码直接写入 NULL 指针导致 进程崩溃(Broken pipe)。

这是因为 Mali Gralloc 对某些格式(如 IMPLEMENTATION_DEFINED)可能返回无效的 YCbCr 结构。

解决方案

添加空指针检查,如果指针无效则回退到普通 lock:

cpp 复制代码
err = mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr);

if (err == android::OK) {
    // ⭐ 关键:检查指针是否有效
    if (ycbcr.y != nullptr && ycbcr.cb != nullptr && ycbcr.cr != nullptr) {
        fillYUVRed(ycbcr, width, height);
        mapper.unlock(importedHandle);
    } else {
        ALOGW("lockYCbCr returned OK but pointers are NULL!");
        mapper.unlock(importedHandle);
        // 回退到普通 lock...
    }
}

2.5 第五个错误:Buffer 返回超时

前面的问题都解决后,画面填充成功了,但 Framework 报超时:

错误日志
复制代码
I VirtualCameraSession: ✔ Filled YUV red frame 1280x720
I VirtualCameraSession: Frame 0: returning 1 filled buffers
I VirtualCameraSession: Frame 1: returning 1 filled buffers
I VirtualCameraSession: Frame 2: returning 1 filled buffers
I VirtualCameraSession: Frame 3: returning 1 filled buffers
E Camera3-Stream: getBuffer: wait for output buffer return timed out after 8000ms (max_buffers 4)

4 个 buffer 都填充成功并"返回"了,但 Framework 等待 8 秒后超时!

问题分析

对比 Google 官方的 goldfish 虚拟相机实现,发现两个关键差异:

差异 1:缺少 Shutter 通知

Goldfish 在返回结果之前 会先发送 notify(SHUTTER) 通知:

cpp 复制代码
// goldfish 代码
notifyShutter(&*mCb, frameNumber, shutterTimestampNs);  // ⭐ 先发 Shutter
// ... 处理 buffer ...
mCb->processCaptureResult(results);  // 再返回结果

Camera Framework 期望的流程是:

  1. 收到 notify(SHUTTER)
  2. 收到 processCaptureResult()

缺少 Shutter 通知,Framework 会一直等待!

差异 2:返回 buffer 时不应包含 handle

cpp 复制代码
// ❌ 错误
result.outputBuffers[i].buffer = request.outputBuffers[i].buffer;

// ✅ 正确:返回时设为 nullptr,Framework 通过 bufferId 识别
result.outputBuffers[i].buffer = nullptr;
解决方案
cpp 复制代码
Return<void> VirtualCameraSession::processCaptureRequest(...) {
    for (const auto& request : requests) {
        const uint32_t frameNumber = request.frameNumber;
        int64_t timestamp = getTimestampNs();
        
        // ⭐ 第一步:发送 Shutter 通知
        {
            hidl_vec<NotifyMsg> msgs(1);
            msgs[0].type = MsgType::SHUTTER;
            msgs[0].msg.shutter.frameNumber = frameNumber;
            msgs[0].msg.shutter.timestamp = timestamp;
            mCallback->notify(msgs);
        }

        // 第二步:填充 buffer
        for (const auto& outputBuffer : request.outputBuffers) {
            fillRedFrame(outputBuffer.buffer, width, height, format);
        }

        // 第三步:返回结果
        CaptureResult result;
        result.frameNumber = frameNumber;
        
        // ⭐ buffer 返回时设为 nullptr
        for (size_t i = 0; i < request.outputBuffers.size(); i++) {
            result.outputBuffers[i].streamId = request.outputBuffers[i].streamId;
            result.outputBuffers[i].bufferId = request.outputBuffers[i].bufferId;
            result.outputBuffers[i].buffer = nullptr;  // ⭐ 关键
            result.outputBuffers[i].status = BufferStatus::OK;
        }
        
        mCallback->processCaptureResult(results);
    }
}

三、完整解决方案

3.1 核心修复点总结

问题 原因 解决方案
Usage 不匹配 Buffer 创建时没有 SW_WRITE 权限 configureStreams 中设置 producerUsage = SW_WRITE_OFTEN
Lock 前未 Import 跨进程 buffer 必须先 import 先 importBuffer,再 lock,最后 freeBuffer
格式不匹配 使用原始格式而非覆盖后格式 用 map 保存覆盖后的格式
空指针崩溃 lockYCbCr 返回空指针 检查 ycbcr.y/cb/cr 是否为空
Buffer 返回超时 缺少 Shutter 通知 先 notify(SHUTTER) 再 processCaptureResult

3.2 关键代码片段

configureStreams
cpp 复制代码
Return<void> VirtualCameraSession::configureStreams(
        const StreamConfiguration& requestedConfiguration,
        configureStreams_cb _hidl_cb) {

    for (size_t i = 0; i < mStreams.size(); i++) {
        // 格式覆盖
        if (mStreams[i].format == PixelFormat::IMPLEMENTATION_DEFINED) {
            halConfig.streams[i].overrideFormat = PixelFormat::YCBCR_420_888;
            mStreamFormatMap[mStreams[i].id] = HAL_PIXEL_FORMAT_YCbCr_420_888;
        }
        
        // ⭐ 请求 CPU 写入权限
        halConfig.streams[i].producerUsage = GRALLOC_USAGE_SW_WRITE_OFTEN;
        halConfig.streams[i].maxBuffers = 4;
    }
    
    _hidl_cb(Status::OK, halConfig);
    return Void();
}
processCaptureRequest
cpp 复制代码
Return<void> VirtualCameraSession::processCaptureRequest(
        const hidl_vec<CaptureRequest>& requests, ...) {

    for (const auto& request : requests) {
        int64_t timestamp = getTimestampNs();
        
        // ⭐ 1. 发送 Shutter 通知
        {
            hidl_vec<NotifyMsg> msgs(1);
            msgs[0].type = MsgType::SHUTTER;
            msgs[0].msg.shutter.frameNumber = request.frameNumber;
            msgs[0].msg.shutter.timestamp = timestamp;
            mCallback->notify(msgs);
        }

        // ⭐ 2. 填充 buffer
        for (const auto& outputBuffer : request.outputBuffers) {
            int32_t format = mStreamFormatMap[outputBuffer.streamId];
            fillRedFrame(outputBuffer.buffer, width, height, format);
        }

        // ⭐ 3. 返回结果
        hidl_vec<CaptureResult> results(1);
        results[0].frameNumber = request.frameNumber;
        results[0].outputBuffers.resize(request.outputBuffers.size());
        
        for (size_t i = 0; i < request.outputBuffers.size(); i++) {
            results[0].outputBuffers[i].buffer = nullptr;  // ⭐ 关键
            results[0].outputBuffers[i].status = BufferStatus::OK;
        }
        
        mCallback->processCaptureResult(results);
    }
    
    return Void();
}
fillRedFrame
cpp 复制代码
void VirtualCameraSession::fillRedFrame(const native_handle_t* handle,
                                        uint32_t width, uint32_t height,
                                        int32_t format) {
    GraphicBufferMapper& mapper = GraphicBufferMapper::get();
    
    // ⭐ 1. Import buffer
    buffer_handle_t importedHandle = nullptr;
    uint64_t usage = GRALLOC_USAGE_SW_WRITE_OFTEN;
    mapper.importBuffer(handle, width, height, 1, format, usage, width, &importedHandle);

    // ⭐ 2. Lock 并写入
    android_ycbcr ycbcr;
    if (mapper.lockYCbCr(importedHandle, usage, bounds, &ycbcr) == OK) {
        // ⭐ 检查空指针
        if (ycbcr.y && ycbcr.cb && ycbcr.cr) {
            fillYUVRed(ycbcr, width, height);
        }
        mapper.unlock(importedHandle);
    }

    // ⭐ 3. 释放
    mapper.freeBuffer(importedHandle);
}

四、经验总结

4.1 调试技巧

  1. 善用 logcat 过滤:

    bash 复制代码
    adb logcat -v threadtime VirtualCameraSession Camera3-Device mali_gralloc GraphicBufferMapper *:S
  2. 理解错误码 :Mali Gralloc 的错误信息很详细,如 Usage not a subset 直接指明了问题。

  3. 参考官方实现 :Google 的 goldfish 虚拟相机是最好的参考,尤其是 CameraDeviceSession 和 FakeRotatingCamera 类。

4.2 关键知识点

  1. Gralloc Buffer 权限管理:

    • importBuffer 时的 usage 必须是 buffer 创建时 usage 的子集
    • 需要 CPU 写入权限时,必须在 configureStreams 中通过 producerUsage 请求
  2. Camera HAL 回调时序:

    • 必须先发送 notify(SHUTTER)
    • 然后才能调用 processCaptureResult
  3. Buffer 返回规范:

    • 返回结果时 buffer 字段设为 nullptr
    • Framework 通过 bufferId 识别 buffer
  4. 格式覆盖机制:

    • IMPLEMENTATION_DEFINED 需要覆盖为具体格式
    • processCaptureRequest 中要使用覆盖后的格式

4.3 避坑清单

  • configureStreams 中设置正确的 producerUsage
  • importBuffer 使用与 producerUsage 相同的 usage
  • 保存并使用覆盖后的格式
  • lockYCbCr 后检查返回的指针是否为空
  • 先发送 notify(SHUTTER) 再返回结果
  • 返回 buffer 时设置 buffer = nullptr
  • 使用 CLOCK_BOOTTIME 获取时间戳

五、参考资料

  1. Android Camera HAL3 官方文档
  2. Android Emulator goldfish 虚拟相机源码
  3. Mali Gralloc 源码
  4. Redroid 项目

作者:FrameNotWork

环境:RK3588 + Android 14 + Redroid

关键词:虚拟相机、Camera HAL、Mali Gralloc、RK3588、Android 14、Redroid

如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流讨论。

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui