#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_WRITEHW_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.yycbcr.cbycbcr.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 虚拟相机是最好的参考,尤其是 CameraDeviceSessionFakeRotatingCamera 类。

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

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

相关推荐
恋猫de小郭2 小时前
回顾 Flutter Flight Plans ,关于 Flutter 的现状和官方热门问题解答
android·前端·flutter
张风捷特烈2 小时前
FlutterUnit3.4.1 | 来场三方库的收录狂欢吧~
android·前端·flutter
e***58233 小时前
Spring Cloud GateWay搭建
android·前端·后端
x***13396 小时前
【MyBatisPlus】MyBatisPlus介绍与使用
android·前端·后端
n***54387 小时前
【MySQL】MySQL内置函数--日期函数字符串函数数学函数其他相关函数
android·mysql·adb
z***75158 小时前
【Springboot3+vue3】从零到一搭建Springboot3+vue3前后端分离项目之后端环境搭建
android·前端·后端
程序员陆业聪8 小时前
Android模拟器检测全面指南:从基础到高级策略
android
2501_9160088910 小时前
iOS 性能测试的深度实战方法 构建从底层指标到真实场景回放的多工具测试体系
android·ios·小程序·https·uni-app·iphone·webview
w***954910 小时前
SQL美化器:sql-beautify安装与配置完全指南
android·前端·后端