3. QNX侧显示服务与Android SurfaceFlinger对接

任务说明

在QNX与Android双系统融合的座舱方案中,Android侧UI由SurfaceFlinger合成,而屏幕显示资源由QNX侧管理。本案例实现QNX侧显示服务与Android SurfaceFlinger的对接,使Android UI能够显示在QNX管理的物理屏幕上。核心思路是:QNX侧通过共享内存接收Android SurfaceFlinger输出的合成帧,并驱动显示控制器输出。

环境与依赖

  • QNX 7.1或以上版本,支持共享内存驱动(shm)和显示控制器接口
  • Android侧需支持将SurfaceFlinger输出重定向到QNX共享内存(通过虚拟显示或HWC HAL扩展)
  • 编译工具:QNX SDP中的qcc编译器

实现方案

QNX侧显示服务通过以下步骤完成对接:

  1. 创建命名共享内存区域,用于接收Android侧SurfaceFlinger输出的帧数据
  2. 监听Android侧通过消息队列发送的帧就绪通知
  3. 从共享内存读取帧数据,通过显示控制器接口输出到屏幕

关键完整代码

// qnx_display_service.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>
#include <sys/dispatch.h>
#include <screen/screen.h>
#include <errno.h>

#define SHM_NAME "/android_fb"
#define MSG_QUEUE_NAME "/android_frame_ready"
#define FRAME_WIDTH 1920
#define FRAME_HEIGHT 1080
#define FRAME_SIZE (FRAME_WIDTH * FRAME_HEIGHT * 4) // RGBA8888

typedef struct {
    uint64_t timestamp;
    uint32_t width;
    uint32_t height;
    uint32_t stride;
} frame_info_t;

// 消息队列结构
typedef struct {
    uint16_t type;
    frame_info_t info;
} frame_msg_t;

static int shm_fd = -1;
static void *shm_ptr = NULL;
static screen_context_t screen_ctx;
static screen_window_t screen_win;
static screen_buffer_t screen_buffers[2];
static int current_buffer = 0;

// 初始化QNX Screen显示窗口
static int init_display(void) {
    if (screen_create_context(&screen_ctx, SCREEN_APPLICATION_CONTEXT) != 0) {
        fprintf(stderr, "Failed to create screen context\n");
        return -1;
    }

    screen_create_window(&screen_win, screen_ctx);
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_FORMAT, (int[]){SCREEN_FORMAT_RGBA8888});
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_USAGE, (int[]){SCREEN_USAGE_NATIVE});
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_SIZE, (int[]){FRAME_WIDTH, FRAME_HEIGHT});

    // 创建双缓冲
    screen_create_window_buffers(screen_win, 2);
    screen_get_window_property_pv(screen_win, SCREEN_PROPERTY_BUFFERS, (void **)screen_buffers);

    // 设置窗口位置和大小
    int pos[] = {0, 0};
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_POSITION, pos);
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_SIZE, (int[]){FRAME_WIDTH, FRAME_HEIGHT});
    screen_set_window_property_iv(screen_win, SCREEN_PROPERTY_VISIBLE, (int[]){1});

    return 0;
}

// 将共享内存中的帧数据拷贝到Screen缓冲区并显示
static int display_frame(const frame_info_t *info) {
    if (info->width != FRAME_WIDTH || info->height != FRAME_HEIGHT) {
        fprintf(stderr, "Frame size mismatch: %ux%u\n", info->width, info->height);
        return -1;
    }

    // 获取当前可写的Screen缓冲区
    screen_buffer_t *buf = &screen_buffers[current_buffer];
    void *buf_ptr;
    screen_get_buffer_property_pv(*buf, SCREEN_PROPERTY_POINTER, (void **)&buf_ptr);

    // 从共享内存拷贝帧数据到Screen缓冲区
    memcpy(buf_ptr, shm_ptr, FRAME_SIZE);

    // 提交缓冲区到屏幕
    screen_post_window(screen_win, *buf, 0, NULL, 0);
    screen_flush_context(screen_ctx, 0);

    // 切换缓冲区
    current_buffer = (current_buffer + 1) % 2;
    return 0;
}

int main(int argc, char *argv[]) {
    // 1. 初始化显示
    if (init_display() != 0) {
        return EXIT_FAILURE;
    }

    // 2. 创建/打开共享内存
    shm_fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        return EXIT_FAILURE;
    }
    ftruncate(shm_fd, FRAME_SIZE);
    shm_ptr = mmap(NULL, FRAME_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (shm_ptr == MAP_FAILED) {
        perror("mmap");
        return EXIT_FAILURE;
    }

    // 3. 创建消息通道接收Android侧通知
    name_attach_t *attach = name_attach(NULL, MSG_QUEUE_NAME, 0);
    if (attach == NULL) {
        perror("name_attach");
        return EXIT_FAILURE;
    }

    printf("QNX display service started. Waiting for Android frames...\n");

    // 4. 主循环:等待帧就绪消息
    while (1) {
        frame_msg_t msg;
        int rcvid = MsgReceive(attach->chid, &msg, sizeof(msg), NULL);
        if (rcvid == -1) {
            perror("MsgReceive");
            continue;
        }

        if (rcvid == 0) { // 脉冲消息
            continue;
        }

        // 处理帧就绪通知
        if (msg.type == 1) { // 帧就绪
            display_frame(&msg.info);
            MsgReply(rcvid, EOK, NULL, 0);
        } else {
            MsgReply(rcvid, ENOSYS, NULL, 0);
        }
    }

    // 清理资源(实际运行中不会到达此处)
    munmap(shm_ptr, FRAME_SIZE);
    close(shm_fd);
    name_detach(attach, 0);
    return EXIT_SUCCESS;
}

编译与运行

# 在QNX SDP环境中编译
qcc -Vgcc_ntox86_64 -o qnx_display_service qnx_display_service.c -lscreen

# 运行QNX侧显示服务
./qnx_display_service

Android侧对接说明

Android侧需在HWC或SurfaceFlinger的VirtualDisplay输出路径中,将合成后的帧写入QNX共享内存/android_fb,并通过QNX消息队列/android_frame_ready发送帧就绪通知。Android侧通过libhardware的HAL接口实现与QNX共享内存的映射,关键步骤包括:

  1. 通过ashmem或ion分配与QNX共享内存对应的物理内存
  2. 在HWC的setOutputBuffer回调中将帧数据写入共享内存
  3. 通过QNX消息队列接口(MsgSend)发送帧信息

验证方法

  • 启动验证:运行QNX显示服务后,终端应输出Waiting for Android frames...,且无错误信息
  • 功能验证:Android侧启动任意应用(如Launcher),QNX屏幕上应实时显示Android UI画面
  • 帧率验证:在Android侧滑动屏幕时,QNX屏幕应流畅跟随,无明显卡顿或撕裂
  • 异常验证:Android侧停止发送帧后,QNX屏幕保持最后一帧画面,服务不崩溃

常见问题

  • 共享内存打开失败:确认/dev/shm目录存在且有写权限,或使用shm_open前先mkdir -p /dev/shm
  • 画面不显示:检查Screen窗口的SCREEN_PROPERTY_VISIBLE是否设为1,以及窗口位置是否在屏幕范围内
  • 帧数据错乱:确认Android侧写入的像素格式为RGBA8888,且stride与宽度一致(无行对齐填充)