跳转至

把手机上的 Android 换成 Omarchy:一帧画面的调用栈和五处改动

结论:内核里的显示驱动和 Mali kbase 驱动留下,Android 用户态整体删掉,换成 Arch Linux ARM + Omarchy;GPU 用户态改用 Arm 的 Linux 版 Mali DDK。 换完之后,一帧画面依次经过六段:应用 EGL、Wayland 协议、Hyprland GLES 合成、Aquamarine atomic 提交、内核 DRM、显示控制器。 本文把每一段写到函数和行号,开发时可以对着源码逐层下断点。

要写的代码集中在五处:

# 改动 归属 不做会怎样
① 显示驱动补齐 atomic 标准属性,加 DRIVER_SYNCOBJ_TIMELINE 内核组 不补属性 Hyprland 没有输出;不加标志就没有显式同步
② 拿到 Linux 版 Mali DDK(GBM + Wayland 窗口系统),打成 pacman 包 GPU 组 没有 GPU 加速,Hyprland 起不来
③ Hyprland 修两处在 Mali 驱动上必现的 bug 图形组 启动段错误,或卡死在 99% CPU
④ 确认 kbase 把渲染栅栏挂到 dma-buf 上(隐式同步) 内核组 + GPU 组 画面撕裂、闪出半帧
⑤ Omarchy 新增 aarch64-<soc> 平台,配屏幕方向和触摸 系统组 装不上 DDK,竖屏横显,触摸坐标错位

源码版本

文中 文件:行号 都对应下列提交,升级后行号会漂移,按函数名搜索即可。

仓库 提交 日期
hyprwm/Hyprland ecbcfe6 2026-10-31
hyprwm/aquamarine eac1b82 2026-10-30
qt/qtbase(Wayland 客户端已从 qtwayland 搬进 qtbase) 10b0e4b 2026-10-31
quickshell 9b0f339 2026-10-21
mesa(只作对照,本方案不用 Mesa 的 GPU 驱动) cc5680b 2026-11-01
torvalds/linux cbbddb6 2026-10-31
omacom/omarchy(原 basecamp/omarchy) d367a56 2026-10-31

开工前先办三件事

我们是 SoC 厂商,bootloader、ATF、UEFI 固件、内核和用户态的源码都在自己手里,所以不存在“解锁手机”这类障碍。真正卡住路径的只有下面三件事,需要项目负责人先拍板或先去办。

事项 为什么必须先办
从 Arm 拿 Linux 版 Mali DDK,版本号(rXpY)和现在的 kbase 一致,带 gbm 和 wayland 两个窗口系统 Android 版 DDK 链接 bionic,Linux 上用不了;上游开源驱动(panthor + Mesa)还不支持 G2,见上一篇。作为 Mali 的授权方,这是向 Arm 要一个交付件,不是新签授权
确认走“保留内核驱动、换用户态”这条路线(下文“选路线”一节) 另外两条路线的工作量和维护方式完全不同,分工也不同
准备两台引出串口的工程样机,UEFI 固件能从 U 盘或 eMMC/UFS 的 ESP 分区启动 显示没出来之前,只能靠串口看日志

问题界定

情境 我们自研的手机 SoC 已经能完整运行 Android:显示、触摸、GPU 分别由 SurfaceFlinger、HWC、gralloc、InputFlinger 和 Android 版 Mali DDK 负责;从 bootloader、ATF、UEFI 到内核的源码都由我们维护。
冲突 Omarchy 的合成器 Hyprland 只认 DRM/KMS + GBM + EGL 这一套 Linux 接口,没有 Android HAL 后端,也加载不了链接 bionic 的 Android 驱动。
问题 哪些部件能留下、哪些必须换?换完后一帧画面怎么走,每一层要写什么代码?

选路线:保留内核驱动,用户态整体换成 Linux 版

把 Linux 桌面跑在 Android 手机上,开源社区有三种做法。对比下来只有第三种能同时满足“现在就有 GPU 加速”和“Hyprland 不用大改”。

路线 代表项目 做法 对本项目的问题
A. libhybris Droidian、Ubuntu Touch、Sailfish 保留整个 Android 厂商分区,用 libhybris 在 glibc 进程里加载 bionic 驱动,送屏走 HWC2 Aquamarine 只有 DRM、Wayland、Headless、Null 四种后端,没有 hwcomposer 后端;要仿照 Droidian 的 wlroots 分支新写一个,并长期维护
B. 主线内核 + Mesa postmarketOS 主线内核、主线显示驱动、Mesa 开源 GPU 驱动 G2(架构 v15)的 panthor 内核补丁和 Mesa 支持都没合入,现在没有 GPU 加速
C. 保留内核驱动 + Linux 版 DDK Rockchip 的 libmali 留下厂商内核的 DRM 显示驱动和 kbase,用户态换成 glibc 的 Linux 版 DDK Hyprland 在 libmali 上有两个已知 bug,要自己打补丁(见改动 ③)

选 C 的依据:

  • kbase 是 Android 和 Linux 共用的内核驱动。 Arm 的下载页把它写作“Android and Linux version of the Mali GPU Device Driver”,只要 DDK 用户态和 kbase 版本号一致就能配对。Rockchip 和 CIX 的 Linux 板卡都是这么用的。
  • 现在的 Android 显示驱动多半已经是 DRM/KMS 驱动。 Google 从 2019 年起推动 Android 平台的显示驱动统一走 DRM/KMS,HWC 只是它上面的一层。Hyprland 正好直接对接 DRM/KMS,HWC 删掉也不影响。
  • B 路线不放弃,只是推后。 等 v15 进入内核主线和 Mesa 正式版,再把改动 ② 换成 Mesa,其他四处不变。作为 SoC 厂商,显示驱动和设备树本来就该推进上游,改动 ① 做完后可以顺势提交主线,为 B 路线铺路。

逻辑视图:Android 的八个显示部件,用户态六个全换,内核两个留下

逻辑视图:Android 显示部件与 Omarchy 部件的对照

  • 应用、合成器、送屏后端、输入:换成发行版里的现成软件包,不用自己写。Hyprland 要打补丁,见改动 ③。
  • 缓冲区:两边底层都是 dma-buf,只是接口不同。Android 用 gralloc 分配、BufferQueue 传递;Linux 用 GBM 分配、linux-dmabuf 协议传递。
  • GPU 用户态:唯一要找 Arm 拿的部件,见改动 ②。
  • 内核两个驱动:原样留下。显示驱动补几个标准属性和标志,见改动 ①;kbase 要验证隐式同步,见改动 ④。

Android 里的概念在 Linux 上都有一一对应的东西,熟悉 Android 显示栈的同事可以按下表换算:

Android Omarchy 上对应的东西 关键差别
SurfaceFlinger Hyprland 合成全部用 GLES,没有“交给 HWC 叠加”的分支
HWC2 presentDisplay Aquamarine CDRMOutput::commit → drmModeAtomicCommit Hyprland 默认只用主平面,全屏时才直接扫描输出
gralloc allocate gbm_bo_create_with_modifiers2 GBM 建在显示控制器的 card0 上,不在 GPU 上
BufferQueue queueBuffer wl_surface.attach + commit,缓冲区用 zwp_linux_dmabuf_v1 创建 跨进程传 dma-buf fd 的方式不同,内核对象相同
acquire / release fence(sync_file) 同一个 sync_file;显式同步走 wp_linux_drm_syncobj_v1 客户端多数仍靠 dma-buf 隐式同步
Choreographer VSYNC wl_surface.frame 回调 + wp_presentation 反馈 frame 回调在合成时发出,不等翻页
InputFlinger libinput(经 libseat/logind 打开 evdev) 触摸坐标先归一化到 0~1,再映射到屏幕

进程视图:一帧画面跨三个进程边界,走九步

进程视图:一帧画面从客户端到屏幕再回到客户端

  1. 客户端先向 Hyprland 要一个 wl_surface.frame 回调,收到后才画下一帧。
  2. 客户端调 eglSwapBuffers,DDK 把 GPU 任务交给 kbase,Mali 把画面写进客户端自己的 dma-buf。
  3. DDK 把这块 dma-buf 包成 wl_buffer,attach + commit 交给 Hyprland;Hyprland 从 dma-buf 导出隐式栅栏,等 Mali 画完。
  4. 栅栏就绪后,Hyprland 记下损伤区域,请 Aquamarine 排一帧。
  5. Aquamarine 在空闲时发出 frame 事件,Hyprland 用 GLES 把所有窗口合成到 GBM 缓冲区,同时给客户端发 frame done。
  6. Aquamarine 组一个 atomic 请求,经 drmModeAtomicCommit 交给内核。
  7. 内核等合成栅栏完成后写显示控制器寄存器,显示控制器在下一个 vblank 开始扫描新帧。
  8. vblank 中断到来,内核给 Hyprland 发翻页完成事件。
  9. Hyprland 给客户端发 wp_presentation 反馈,释放上一帧占用的客户端缓冲区。

下面按这九步拆到函数级,每一步给出调用栈、本项目要注意的地方和验证方法。调用栈里缩进表示调用,→ 表示经事件或回调间接到达。

第 0 步 启动:Hyprland 在 card0 上建 GBM 和 EGL,缺任何一个硬性扩展就退出

Aquamarine 先通过 logind 拿到显示控制器的 card0,再在它上面建 GBM;Hyprland 用同一个 fd 建 EGL 显示。Mali kbase 的 /dev/mali0 不是 DRM 设备,两边都不认它,GPU 由 DDK 在内部自己打开。

Hyprland main()                                              Hyprland src/main.cpp:265
  CCompositor::initServer                                    src/Compositor.cpp:278
    Aquamarine::CBackend::create                             :329
      CDRMBackend::attempt                                   aquamarine src/backend/drm/DRM.cpp:296
        CSession::attempt                                    src/backend/Session.cpp:294
          libseat_open_seat                                  :308   经 logind 拿 seat
          libinput_udev_create_context / assign_seat         :345, :369
        scanGPUs                                             DRM.cpp:122
          只枚举 /dev/dri/card*,按 AQ_DRM_DEVICES 过滤        :79, :203-245
          CSessionDevice::openIfKMS → libseat_open_device    Session.cpp:287, :135
          resolveMatchingRenderNode                          Session.cpp:176
            找同一父设备下的 renderD*;系统只有一个 renderD 时直接用它;否则 -1
        CDRMBackend::registerGPU → checkFeatures             DRM.cpp:1098, :712
          DRM_CAP_PRIME 导入、CRTC_IN_VBLANK_EVENT、TIMESTAMP_MONOTONIC、
          UNIVERSAL_PLANES 任一不满足:放弃这块卡(致命)
          DRM_CAP_SYNCOBJ_TIMELINE → supportsTimelines
          DRM_CLIENT_CAP_ATOMIC 失败 → 退回 legacy 接口
        initResources → 枚举 CRTC、平面、连接器                DRM.cpp:775
    CBackend::start                                          Backend.cpp:121
      CGBMAllocator::create(reopenDRMNode(card0 fd))         :163-172
        gbm_create_device(fd)                                allocator/GBM.cpp:352   ← DDK 的 libgbm 要接受 KMS fd
    m_drm.fd = drmFD(); m_drmRenderNode.fd = drmRenderNodeFD()   Compositor.cpp:362-366
    drmGetCap(DRM_CAP_SYNCOBJ_TIMELINE) 探测两个 fd          :367-385
    initManagers(STAGE_BASICINIT)                            :704
      CHyprOpenGLImpl::CHyprOpenGLImpl                       src/render/OpenGL.cpp:297
        m_drmFD = 渲染节点有效 ? 渲染节点 : card0             :297
        硬性断言:eglCreateSyncKHR、eglDupNativeFenceFDANDROID、
                  eglWaitSyncKHR                             :317-319
        有 EXT_platform_device → eglDeviceFromDRMFD → EGL_PLATFORM_DEVICE_EXT   :341-347
        否则 KHR_platform_gbm → gbm_create_device → EGL_PLATFORM_GBM_KHR         :350-363
        initEGL → eglInitialize,GLES 3.2 上下文,失败退到 3.0                      :160-228
        硬性断言:GL_EXT_texture_format_BGRA8888             :382
        有 EGL_ANDROID_native_fence_sync → 打开显式同步        :394
      CProtocolManager:有 dmabuf 格式才注册 linux-dmabuf      ProtocolManager.cpp:259-261

本项目要注意:

  • DDK 要能在 card0 上建 GBM。 Aquamarine 的分配器和 Hyprland 的 EGL 都把显示控制器的 fd 交给 gbm_create_device。DDK 的 libgbm 收到一个“只能显示、不能渲染”的 fd,要能自己找到 Mali 并分配显示控制器扫描得了的缓冲区。拿到 DDK 后第一件事就是验证这一点。
  • EGL 走 GBM 平台,而不是 device 平台。 libmali 一般只提供 EGL_KHR_platform_gbm,没有 EGL_EXT_device_*。这正好撞上 OpenGL.cpp:341 的 bug,见改动 ③。
  • 显式同步要两边都满足。 Hyprland 只要求 DDK 有 EGL_ANDROID_native_fence_sync。Aquamarine 还要求显示驱动支持 DRM_CAP_SYNCOBJ_TIMELINE,并且 CRTC 有 OUT_FENCE_PTR、主平面有 IN_FENCE_FD(DRM.cpp:1991)。

验证:AQ_TRACE=1 HYPRLAND_TRACE=1 start-hyprland 起一次,在 $XDG_RUNTIME_DIR/hypr/*/hyprland.log 里搜下面几行。

日志关键字 含义
drm: Explicit sync supported / unsupported 改动 ① 的 syncobj 标志是否生效
EGL: EXT_platform_device or EGL_EXT_device_query not supported, using gbm 走了 GBM 平台,正常
EGL: GPU Supports Format DDK 报出的格式和 modifier,空的话 linux-dmabuf 不会注册
Display driver doesn't support ... 缺硬性扩展,回头查 DDK

第 1~2 步 客户端画一帧:Qt 先等 frame 回调,再由 EGL 交出 dmabuf

Omarchy 的状态栏(Quickshell)和登录界面(SDDM)都是 Qt Quick 程序,代表了大多数客户端的路径。Qt 自己管 frame 回调,把 EGL 的交换间隔设成 0,所以 DDK 的 eglSwapBuffers 不用再阻塞等回调。

Quickshell:建层表面(状态栏贴在屏幕边上)
  WlrLayershell::createQQuickWindow                quickshell src/wayland/wlr_layershell/wlr_layershell.cpp:57
    LayerSurfaceBridge::init                       surface.cpp:93
      LayerSurface::LayerSurface
        zwlr_layer_shell_v1.get_layer_surface      surface.cpp:160-165
        set_size / set_anchor / set_exclusive_zone surface.cpp:168-176

Qt:每一帧
  QSG 渲染循环(qtdeclarative)
    QWaylandGLContext::swapBuffers                 qtbase src/plugins/platforms/wayland/plugins/hardwareintegration/wayland-egl/qwaylandglcontext.cpp:471
      eglSwapInterval(dpy, 0)                      :502   支持非阻塞交换时设 0,由 Qt 自己节流
      QWaylandWindow::handleUpdate                 qwaylandwindow.cpp:1834
        wl_surface_frame                           :1850  要下一帧的回调
      eglSwapBuffers                               qwaylandglcontext.cpp:509
        ── 以下进入 DDK 的 Wayland 窗口系统(闭源)。Mesa 的对应实现可作参照:
        dri2_wl_swap_buffers                       mesa src/egl/drivers/dri2/platform_wayland.c:1855
          flush,把渲染任务交给内核                  :1880
          首次使用这块缓冲:create_wl_buffer          :1915
            zwp_linux_dmabuf_v1_create_params      :1727
            zwp_linux_buffer_params_v1_add(每个 plane 的 fd、stride、offset、modifier)  :1768
            zwp_linux_buffer_params_v1_create_immed   :1776
          wl_surface_attach                        :1929
          wl_surface_damage_buffer                 :1814-1832
          wl_surface_commit                        :1963
          wl_display_flush                         :1975
  QWaylandWindow::handleFrameCallback              qwaylandwindow.cpp:861   收到 done,开始下一帧
  wl_buffer.release → 缓冲区标记为空闲             mesa platform_wayland.c:333

本项目要注意:

  • DDK 的 Wayland 窗口系统必须用 zwp_linux_dmabuf_v1 传缓冲区。 Hyprland 不支持 Mesa 老的 wl_drm 协议。如果 DDK 只会用 wl_drm 或私有协议,客户端会退回 wl_shm(CPU 拷贝),帧率会掉到不可用。
  • 客户端不做显式同步。 Mesa 的 Wayland 平台没有 wp_linux_drm_syncobj 代码,DDK 大概率也没有。客户端缓冲区能否正确同步,全看改动 ④。
  • Qt 出问题时先排除 GPU。 设 QT_QUICK_BACKEND=software 让 Qt Quick 用 CPU 画,走 wl_shm:能显示就是 DDK 的问题,还是不显示就是 Qt 或协议的问题。

验证:WAYLAND_DEBUG=1 quickshell 2>&1 | grep -E 'linux_buffer_params|wl_surface.*(attach|commit)|wl_buffer.*release',能看到 create_immed 和成对的 attach/commit,就说明走的是 dmabuf。

第 3 步 Hyprland 收缓冲区:建 wl_buffer 时就导入成纹理,提交时导出隐式栅栏

导入发生在客户端创建 wl_buffer 的那一刻,不在每次 commit 时。每次 commit 要做的是同步:没有显式同步时,Hyprland 从 dma-buf 导出一个 sync_file,等它可读了才采用这次提交。

创建缓冲区(每块 dma-buf 只做一次)
  CLinuxDMABUFParamsResource::create               Hyprland src/protocols/LinuxDMABUF.cpp:237
    verify / commence(drmPrimeFDToHandle 试导入)   :295, :274-291
    CLinuxDMABuffer → CDMABuffer::CDMABuffer       :119-120, src/protocols/types/DMABuffer.cpp:12
      CHyprGLRenderer::createTexture(attrs)        src/render/GLRenderer.cpp:249
        CHyprOpenGLImpl::createEGLImage            src/render/OpenGL.cpp:633
          eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) :678   modifier 不是 INVALID 时带上 modifier
        CGLTexture::CGLTexture                     src/render/gl/GLTexture.cpp:68
          glEGLImageTargetTexture2DOES(GL_TEXTURE_2D)   :93
      导入失败就用 DRM_FORMAT_MOD_INVALID 再试一次    DMABuffer.cpp:31-32

每次 commit
  CWLSurfaceResource(wl_surface.commit 处理函数)  src/protocols/core/Compositor.cpp:132
    m_stateQueue.enqueue                           :199
    m_events.stateCommit.emit → 显式同步监听器        :203, src/protocols/DRMSyncobj.cpp:77-111
    没有显式同步的 acquire 点:
      CDMABuffer::exportSyncFiles                  :206 → DMABuffer.cpp:94
        DMA_BUF_IOCTL_EXPORT_SYNC_FILE(READ)       src/helpers/Drm.cpp:103-118
          内核 dma_buf_export_sync_file            linux drivers/dma-buf/dma-buf.c:436
            dma_resv_get_singleton → sync_file_create   :459, :466
    scheduleState                                  Compositor.cpp:612
      sync_file 可读时 → drainSyncFds → tryProcess    :634, :641-662
    commitState                                    :664
      m_current.updateFrom(state)                  :669
      wl_shm 缓冲区:glTexSubImage2D 上传,随即释放    :673, :706
  → CXDGSurfaceResource 提交监听(首次有纹理时映射窗口)   src/protocols/XDGShell.cpp:554-584

本项目要注意:

  • 导出的 sync_file 只有在 kbase 把栅栏挂到 dma-buf 上时才有意义。 如果 kbase 在 Mali 还没画完时就让 dma_resv 里没有未完成的栅栏,导出的 sync_file 立即可读,Hyprland 就会拿半帧去合成。这是改动 ④ 要验证的。
  • modifier 要两头一致。 DDK 报给 Hyprland 的 modifier(eglQueryDmaBufModifiersEXT)决定客户端能分配什么格式。显示控制器如果不支持 AFBC,合成用的缓冲区和直接扫描输出就只能用 LINEAR。

第 4 步 排帧:损伤区域触发 scheduleFrame,空闲时发 frame 事件

Hyprland 不按固定节拍画,而是有损伤才排一帧。翻页完成后如果还有损伤,再排下一帧。

CWindow::commitWindow                              Hyprland src/desktop/view/window/Window.cpp:1757
  IHyprRenderer::damageSurface                     src/render/Renderer.cpp:2981
    有待发的 frame 回调 → scheduleFrame(AQ_SCHEDULE_NEEDS_FRAME)   :2993-3000
    CMonitor::addDamage                            src/output/Monitor.cpp:1084
      CMonitor::scheduleFrame                      :1071   会话不活跃或屏幕关闭时直接返回
        CDRMOutput::scheduleFrame                  aquamarine src/backend/drm/DRM.cpp:3012
          addIdleEvent(frameIdle)                  :3033-3052
          → sched.frameReady → events.frame        :3315
→ CMonitorFrameScheduler::onFrame                  Hyprland src/output/MonitorFrameScheduler.cpp:85
    canRender:会话活跃、屏幕开启、上一次提交已完成      :147-160
    IHyprRenderer::renderMonitor                   :106

第 5 步 合成:renderMonitor 只排队,endRender 才真正画

renderMonitor 遍历窗口和图层,只往渲染通道里放“绘制元素”;真正的 GL 调用全部在 endRender 里。合成目标是 Aquamarine 交换链里的 GBM 缓冲区,通过 EGLImage 绑定成渲染缓冲区。

IHyprRenderer::renderMonitor                       Hyprland src/render/Renderer.cpp:2248
  没有损伤且不需要帧 → 直接返回                      :2307
  尝试直接扫描输出(第 6 步的旁路)                   :2317-2329
  IHyprRenderer::beginRender                       :2379 → :1860
    swapchain->next(&bufferAge)                    :1915   取一块 GBM 缓冲区
    getOrCreateRenderbuffer                        src/render/GLRenderer.cpp:78, Renderer.cpp:3300
      CGLRenderbuffer::CGLRenderbuffer             src/render/gl/GLRenderbuffer.cpp:32
        createEGLImage(buffer->dmabuf())           :37
        glEGLImageTargetRenderbufferStorageOES     :43-45
        glFramebufferRenderbuffer + glCheckFramebufferStatus   :48-58
    按 bufferAge 算出这块缓冲区要重画的区域          :1933-1934
    CHyprOpenGLImpl::begin                         src/render/OpenGL.cpp:733
  CMonitorScene::draw                              :2415 → src/render/scene/MonitorScene.cpp:13
    renderWorkspace → renderAllClientsForWorkspace Renderer.cpp:2757 → :1200
      背景、图层 renderLayer、预模糊、窗口 renderWindow    :1016, :1272, :614
        每个表面 → addPassElement(CSurfacePassElement)    :759-771
  CHyprGLRenderer::endRender                       :2470 → GLRenderer.cpp:113
    m_pass.render                                  :129 → src/render/pass/Pass.cpp:144
      IElementRenderer::drawElement                src/render/ElementRenderer.cpp:19
        drawSurface → presentFeedback              :232, :432   这里给客户端发 frame done
        CHyprOpenGLImpl::renderTexture             OpenGL.cpp:1267
          renderTextureInternal                    :1633
            每个损伤矩形:glScissor + glDrawArrays(GL_TRIANGLE_STRIP)   :1717, :1723
    CHyprOpenGLImpl::end                           OpenGL.cpp:794   工作缓冲 → GBM 缓冲区
    m_output->state->setBuffer                     GLRenderer.cpp:139
    没有显式同步:glFlush,立即释放客户端缓冲区引用     :153-166
    有显式同步:
      CEGLSync::create                             OpenGL.cpp:2680
        eglCreateSyncKHR(EGL_SYNC_NATIVE_FENCE_ANDROID)   :2683
        eglDupNativeFenceFDANDROID                 :2693
      state->setExplicitInFence(fd)                GLRenderer.cpp:174-175
      releaseSurfaceBuffersOnReadable              :190   GPU 合成完才释放客户端缓冲区
  commitPendingAndDoExplicitSync                   Renderer.cpp:2490 → :2734
    COutputCommitCoordinator::submit               src/output/OutputCommitCoordinator.cpp:80
      submitSynchronously → CMonitorState::commit → m_output->commit()   :124, Monitor.cpp:2626

本项目要注意:

  • frame done 在合成时就发出,不等翻页。 这和 Android 的 VSYNC 节拍不同。客户端测出的帧间隔会比屏幕刷新周期略早,这是正常现象。
  • 没有显式同步时,送屏也靠隐式同步。 glFlush 之后,Hyprland 直接把 GBM 缓冲区交给 KMS,由内核在 prepare_fb 时从 dma-buf 上取栅栏等待(第 6 步)。所以改动 ① 的 syncobj 标志能去掉一半对改动 ④ 的依赖。

第 6~7 步 送屏:Aquamarine 组 atomic 请求,内核 commit_tail 写显示控制器

Hyprland 默认只用一个主平面加一个光标平面,不像 HWC 那样把多个图层分给硬件叠加。所以显示驱动只要把主平面做对,就能出图。

CDRMOutput::commit                                 aquamarine src/backend/drm/DRM.cpp:2355
  commitState                                      :2515
    检查:会话活跃、格式在主平面支持的列表里、上一次翻页已完成
    CDRMFB::create → import                        :3321, :3353
      drmPrimeFDToHandle(每个 plane)
      drmModeAddFB2WithModifiers 或 drmModeAddFB2   :3461
    SDRMConnector::commitState → CDRMAtomicImpl::commit   :2167, src/backend/drm/impl/Atomic.cpp:593
      prepareConnector:建 MODE_ID 属性 blob         :471
      CDRMAtomicRequest::addConnector               :197
        CRTC:MODE_ID、ACTIVE、OUT_FENCE_PTR、VRR_ENABLED、GAMMA_LUT、CTM
        连接器:CRTC_ID、link-status
        主平面:FB_ID、CRTC_ID、SRC_*、CRTC_*、FB_DAMAGE_CLIPS
                IN_FENCE_FD(仅当 supportsExplicit)   :290-293
      CDRMAtomicRequest::commit                     :339
        drmModeAtomicCommit(fd, req, PAGE_FLIP_EVENT | NONBLOCK [| ALLOW_MODESET], flipID)   :366
    非 modeset 提交失败 → 带 modeset 阻塞重试一次       DRM.cpp:2844

内核
  drm_ioctl → drm_mode_atomic_ioctl                linux drivers/gpu/drm/drm_ioctl.c:834, drm_atomic_uapi.c:1618
    drm_atomic_set_property                        :1210
      平面 FB_ID → drm_framebuffer_lookup          :544-548
      平面 IN_FENCE_FD → sync_file_get_fence       :552-558
      CRTC OUT_FENCE_PTR → 记下用户态指针           :467
    prepare_signaling                              :1425
      PAGE_FLIP_EVENT → create_vblank_event(DRM_EVENT_FLIP_COMPLETE)   :1449, :1135
      OUT_FENCE_PTR → setup_out_fence → sync_file_create   :1488, :1418
    drm_atomic_nonblocking_commit                  drm_atomic.c:1901
      drm_atomic_check_only → drm_atomic_plane_check → drm_plane_has_format   :1755, :794, drm_plane.c:916
      funcs->atomic_commit = drm_atomic_helper_commit    drm_atomic_helper.c:2251
        drm_atomic_helper_setup_commit             :2268   上一次翻页没完成返回 -EBUSY(:2403)
        drm_atomic_helper_prepare_planes           :2274
          没有 prepare_fb 时用 drm_gem_plane_helper_prepare_fb   :2853   从 dma_resv 取隐式栅栏
        drm_atomic_helper_swap_state               :2290
        queue_work(commit_work)                    :2316
  commit_tail(工作队列)                            drm_atomic_helper.c:2040
    drm_atomic_helper_wait_for_fences              :2064 → :1840   等 IN_FENCE 或隐式栅栏
    drm_atomic_helper_commit_tail(_rpm)            :1989 / :2019
      commit_modeset_disables                      :1572
      commit_planes                                :2978
        crtc->atomic_begin → plane->atomic_update → crtc->atomic_flush   :3001, :3044, :3064
      commit_modeset_enables                       :1765
        crtc->atomic_enable → bridge pre_enable → DSI 上电 → panel prepare/enable
      commit_hw_done → wait_for_vblanks / wait_for_flip_done   :2735, :1889 / :1950

以 MediaTek 的上游驱动(drivers/gpu/drm/mediatek)为参照,手机级显示控制器在这些回调里做的事:

回调 MediaTek 的实现 你们的驱动要对上的地方
atomic_begin mtk_crtc_atomic_begin(mtk_crtc.c:844):取走 event,drm_crtc_vblank_get 把 crtc_state->event 存下来,翻页完成时再发
plane->atomic_update mtk_plane.c:274:只标记 pending.dirty 只记状态,不碰硬件
atomic_flush mtk_crtc_atomic_flush(:869)→ mtk_crtc_update_config(:556):用 CMDQ 等 vsync 后写寄存器 在一个 vblank 内原子地切换所有图层
DSI 使能 mtk_dsi_bridge_atomic_pre_enable / enable(mtk_dsi.c:849、:838) 屏的上电时序、DCS 初始化序列从 Android 驱动移过来
平面 modifier format_mod_supported 只认 LINEAR(mtk_plane.c:63) 如实上报 AFBC 支持与否,Hyprland 按 IN_FORMATS 选

第 8~9 步 翻页完成:vblank 中断发事件,Hyprland 回执并释放缓冲区

内核
  显示控制器 vblank 中断
    (MediaTek:mtk_disp_ovl_irq_handler → mtk_crtc_ddp_irq → mtk_drm_finish_page_flip   mtk_disp_ovl.c:170, mtk_crtc.c:644, :110)
    drm_crtc_handle_vblank                         drivers/gpu/drm/drm_vblank.c:1998
      drm_handle_vblank → 唤醒 wait_for_vblanks     :1924, :1955
    drm_crtc_send_vblank_event                     :1147
      send_vblank_event:填 sequence 和时间戳        :1048, :1056-1065
      → OUT_FENCE 发信号,事件挂到 drm_file,fd 变可读

Aquamarine
  CDRMBackend::dispatchEvents                      aquamarine src/backend/drm/DRM.cpp:1508
    drmHandleEvent(page_flip_handler2 = handlePF)  :1519
      CDRMBackend::handlePageFlip                  :1433
        SDRMConnector::onPresent:前后缓冲交换,退还旧 FB   :2307
        output->events.present.emit                :1494
        还有待画内容 → frameReady → 下一轮第 4 步       :1505

Hyprland
  CMonitor presented 监听                           src/output/Monitor.cpp:143
    CPresentationProtocol::onPresented             src/protocols/PresentationTime.cpp:140
      sendPresented → 客户端收到 wp_presentation_feedback.presented
  客户端缓冲区引用计数归零 → IHLBuffer::sendRelease → wl_buffer.release   src/protocols/types/Buffer.cpp:8-24
  显式同步客户端:CSyncReleaser 析构时把合成栅栏转成 release 点   src/helpers/sync/SyncReleaser.cpp:12-22

本项目要注意:

  • vblank 事件必须由真实中断产生。 驱动没有接 vblank 中断时,DRM 核心会用 fake_vblank 立即发事件(drm_atomic_helper.c:2697)。画面看起来能动,但帧率失控、撕裂,而且 Hyprland 的时间戳全错。
  • 命令模式(command mode)DSI 屏要用 TE 信号代替 vblank。 不少手机屏是命令模式,Android 的 HWC 可能替驱动处理了部分时序。换掉 HWC 后,驱动要自己在 TE 中断里调 drm_crtc_handle_vblank。

第 7′ 步 直接扫描输出:全屏且格式合适时跳过合成

这条旁路默认关闭(render:direct_scanout = 0)。调通主路径之前不要打开。

CMonitor::attemptDirectScanout                     Hyprland src/output/Monitor.cpp:1998
  isDSBlocked:配置关闭、非全屏、缓冲区尺寸或旋转不等于屏幕、wl_shm 缓冲区……   :1899-1993
  state->setBuffer(客户端缓冲区)                     :2065-2073
  m_commitCoordinator->submit(FRAME_DIRECT_SCANOUT) → m_output->commit()   :2116

客户端缓冲区直接上了显示控制器,所以 DDK 分配的客户端缓冲区必须是显示控制器扫描得了的(连续内存或经 IOMMU 映射,格式和 modifier 在主平面的 IN_FORMATS 里)。

触摸:libinput 事件归一化后映射到屏幕坐标

CSession::dispatchLibinputEvents                   aquamarine src/backend/Session.cpp:487
  handleLibinputEvent                              :534
    LIBINPUT_EVENT_TOUCH_DOWN → touch->events.down{touchID = seat_slot, pos = 0~1}   :729
    TOUCH_MOTION / UP / CANCEL / FRAME             :746, :738, :755, :763
→ CPointerManager::attachTouch 监听                 Hyprland src/pointer/PointerManager.cpp:1165-1199
  CInputManager::onTouchDown                       src/managers/input/Touch.cpp:17
    选屏:input:touchdevice:output 指定的屏,否则当前焦点屏   :48-50
    坐标 = 屏幕位置 + pos × 屏幕逻辑尺寸              :58
    CSeatManager::sendTouchDown → wl_touch.down    src/managers/SeatManager.cpp:507, src/protocols/core/Seat.cpp:39

本项目要注意:

  • 触摸驱动不用改。 Android 的触摸驱动本来就按 evdev 多点触控 B 协议上报,libinput 能直接用。用 libinput debug-events 确认设备被识别成 touch。
  • 旋转只能靠校准矩阵。 屏幕设了 transform 后,触摸要用 input:touchdevice:transform 跟着转,而且只对带校准矩阵的设备生效(InputManager.cpp:2038-2043)。
  • 手势和软键盘是 Omarchy 没有的。 桌面版 Omarchy 不考虑触屏。手势可用 hyprgrass 插件,软键盘可用 wvkbd,交互验收由产品负责人确认。

开发视图:五处改动分属四个组,内核和 DDK 可以并行

开发视图:五处改动在软件栈中的位置和依赖

① 显示驱动:补齐 atomic 标准属性,加 syncobj 标志

目标是让 Aquamarine 的 checkFeatures 全部通过,并打开显式同步。

逐项检查:

要求 检查方法 在 Aquamarine 里缺了会怎样
声明 DRIVER_MODESET 和 DRIVER_ATOMIC,支持通用平面 drm_info 能看到 Primary 和 Cursor 平面 退回 legacy 接口,或没有输出
vblank 中断接好,CRTC_IN_VBLANK_EVENT drm_info 看 caps;modetest -v 翻页帧率等于刷新率 致命,放弃这块卡
主平面支持 XRGB8888 / ARGB8888,IN_FORMATS 如实上报 modifier drm_info 看平面格式 格式不匹配,提交被拒
能导入 DDK 分配的 dma-buf(PRIME 导入) drm_info 看 DRM_CAP_PRIME 致命
点亮屏幕不依赖 HWC 设置的私有属性 只用 modetest -s 能点亮并显示彩条 黑屏
声明 DRIVER_SYNCOBJ 和 DRIVER_SYNCOBJ_TIMELINE drm_info 看 DRM_CAP_SYNCOBJ_TIMELINE = 1 没有显式同步,送屏全靠隐式栅栏

IN_FENCE_FD 和 OUT_FENCE_PTR 不用自己写:只要驱动是 atomic 的,DRM 核心会自动挂上(drm_plane.c:467、drm_crtc.c:322)。syncobj 也由 DRM 核心实现(drm_file.c:167),驱动只需声明标志:

static const struct drm_driver xxx_drm_driver = {
    .driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC |
               DRIVER_SYNCOBJ | DRIVER_SYNCOBJ_TIMELINE,
    /* ... */
};

加了这两个标志,Hyprland 合成完的栅栏就能通过 IN_FENCE_FD 交给内核,送屏不再依赖 kbase 的隐式栅栏;Hyprland 也会向客户端提供 wp_linux_drm_syncobj_v1 协议。上游的 MediaTek 驱动没有声明这两个标志(mtk_drm_drv.c:604),所以这一项要自己加,加完要跑一遍下面的验证。

② Linux 版 Mali DDK:要 GBM + Wayland 窗口系统,打成 pacman 包

DDK 要提供四个库,替换 Mesa 的同名库:libEGL.so.1、libGLESv2.so.2、libgbm.so.1、libwayland-egl.so.1。打包方式参照 Rockchip 的 libmali:一个窗口系统一个 .so,再生成上面四个包装库。

拿到 DDK 后按这个顺序验收:

  1. eglinfo -B -p gbm:GLES 版本 ≥ 3.0。
  2. eglinfo -p gbm 逐项查:EGL_KHR_platform_gbm、EGL_ANDROID_native_fence_sync、EGL_EXT_image_dma_buf_import、EGL_EXT_image_dma_buf_import_modifiers、GL_EXT_texture_format_BGRA8888。完整清单和缺了的后果见上一篇的硬性检查表。
  3. kmscube -D /dev/dri/card0:立方体在屏幕上转。这一步验证了“在 card0 上建 GBM”和 PRIME 导入。
  4. ldd $(which Hyprland) | grep -E 'EGL|GLES|gbm':链接到的是 DDK 的库,不是 Mesa 的。Arch 的 hyprland、qt6-base、chromium 都会把 Mesa 拉回来,包里要写好 provides 和 conflicts。

Rockchip 的经验是:原版 libmali 的 GBM 缺新接口,modifier 只认 LINEAR 和 INVALID,所以他们加了一层 hook 库补 gbm_bo_create_with_modifiers、gbm_bo_get_fd_for_plane。Aquamarine 正好依次调用 gbm_bo_create_with_modifiers2 → gbm_bo_create_with_modifiers → gbm_bo_create(GBM.cpp:64 起),缺新接口时能往下退,但要确认退到哪一级。

③ Hyprland:修两处在 Mali 驱动上必现的 bug

两处都在 src/render/OpenGL.cpp,已有用户在 RK3588(Mali G610 + libmali)上报过(#16084、#16085),截至 ecbcfe6 都没修。

第一处:缺 EGL device 扩展时调用空指针,启动即段错误,没有日志。 条件写成了 ||:只要 eglQueryDevicesEXT 没加载到,就会进入分支,在 eglDeviceFromDRMFD 里调用空函数指针。libmali 不提供 EGL_EXT_device_*,必中。

--- a/src/render/OpenGL.cpp
+++ b/src/render/OpenGL.cpp
@@ -341 +341 @@
-    if (EGLEXTENSIONS.contains("EXT_platform_device") || !m_proc.eglQueryDevicesEXT || !m_proc.eglQueryDeviceStringEXT) {
+    if (EGLEXTENSIONS.contains("EXT_platform_device") && m_proc.eglQueryDevicesEXT && m_proc.eglQueryDeviceStringEXT) {

第二处:modifier 排序的比较函数不满足严格弱序,modifier 多了就死循环。 两个都不是 LINEAR、都不含 DCC 的 modifier,comp(a, b) 和 comp(b, a) 都返回 true,std::ranges::sort 行为未定义。libmali 报 26 个 modifier,超过插入排序的阈值,表现为卡在 99% CPU。改成按“LINEAR 排最后、DCC 排倒数”的键稳定排序:

--- a/src/render/OpenGL.cpp
+++ b/src/render/OpenGL.cpp
@@ -605,7 +605,4 @@
-        std::ranges::sort(modifierData, [](const auto& a, const auto& b) {
-            if (a.first == 0)
-                return false;
-            if (a.second.contains("DCC"))
-                return false;
-            return true;
-        });
+        // LINEAR last, DCC just before it, everything else keeps the driver's order
+        std::ranges::stable_sort(modifierData, {}, [](const auto& m) { return m.first == 0 ? 2 : (m.second.contains("DCC") ? 1 : 0); });

补丁做成 hyprland-<soc> 包,同时提交上游(Hyprland 目前不接受用户直接提 issue,走 GitHub Discussions 或 PR)。下面的 diff 只示意改法,hunk 行号以实际源码为准。Aquamarine 不用改:它自己的 CDRMRenderer 在没有 EXT_platform_device 时只告警(Renderer.cpp:573),不影响出图。

④ kbase 隐式同步:GPU 写完之前,dma-buf 上必须挂着未完成的栅栏

客户端到 Hyprland 这一段,没有显式同步可用(见第 1~2 步),只能靠 dma-buf 的 dma_resv:kbase 在 Mali 开始写一块 dma-buf 时,要把这次任务的栅栏加进它的 dma_resv;写完再让栅栏发信号。Hyprland 用 DMA_BUF_IOCTL_EXPORT_SYNC_FILE 取的就是这个栅栏。

Android 上这件事由 gralloc 和 BufferQueue 的显式栅栏完成,kbase 的隐式同步路径可能从来没被用过,所以必须单独验证:

  1. 写一个小程序:用 DDK 的 GBM 分配一块缓冲区,用 GLES 画一个很重的着色器(例如循环一万次),glFlush 后立刻对这块 dma-buf 调 DMA_BUF_IOCTL_EXPORT_SYNC_FILE。
  2. 对拿到的 sync_file 立刻 poll:应该不可读,等 GPU 画完才变成可读。
  3. 如果立即可读,说明 kbase 没有挂栅栏,要在 kbase 里打开对应的 dma-fence / 隐式同步配置,或找 Arm 确认这个 DDK 版本的做法。

改动 ① 加了 syncobj 标志后,Hyprland 到 KMS 这一段会改走显式栅栏,但客户端到 Hyprland 这一段仍然依赖本项。

⑤ Omarchy 平台:新增 aarch64-<soc>,配屏幕和触摸

Omarchy 已经支持 aarch64,并按硬件家族区分平台(aarch64-apple、aarch64-qualcomm 等),新 SoC 照着加一个家族即可。要加的文件:

文件 内容 参照
bin/omarchy-hw-platform 按设备树根节点的 compatible(如 <vendor>,<soc>)识别出 aarch64-<soc> 已有的 qcom, 判断
bin/omarchy-hw-aarch64-<soc> 平台判断脚本 bin/omarchy-hw-aarch64-qualcomm
install/omarchy-aarch64-<soc>.packages mali-ddk-<soc>、hyprland-<soc>、linux-<soc>、固件包 install/omarchy-aarch64-qualcomm.packages
install/hardware/<soc>/ UKI 设备树、内核参数、固件等安装脚本 install/hardware/qualcomm/

Hyprland 配置里加上屏幕和触摸。Omarchy 的 Hyprland 配置是 Lua 写的(default/hypr/monitors.lua、input.lua),平台默认值放进 default/hypr/ 下的新文件。手机屏原生是竖屏,按实际方向调整 transform:

hl.monitor({ output = "DSI-1", mode = "preferred", position = "auto", scale = 3, transform = 0 })
hl.config({
  input = {
    touchdevice = { output = "DSI-1", transform = 0 },
  },
})

启动链:SoC 已有 UEFI,直接走 Omarchy 的标准流程,不用 Android 的 boot.img。

环节 做法 要改什么
BL1/BL2、ATF(BL31) 原样使用 一般不改。确认两点:PSCI 的关机、重启、挂起能用(systemd 要调);显示控制器和 Mali 的寄存器没有被划进安全世界(Android 上若做过安全显示通路,要放开非安全访问)
UEFI 固件 原样使用,从 ESP 分区加载 EFI 程序 通过 EFI 配置表把设备树交给内核(固件已有 DT 时最省事)
Limine Omarchy 默认的引导程序(install/omarchy-other.packages),加载 UKI 不改
UKI(内核 + initramfs + 命令行) limine-mkinitcpio 生成;固件不给设备树时,用 systemd-stub 的 DeviceTreeAuto 从 UKI 里挑 参照 install/hardware/qualcomm/dtb-uki.sh,在 install/hardware/<soc>/ 写一份
内核 现在 Android 用的那个内核,按 Linux 发行版的需要重新配置 补 systemd 需要的选项,对照 systemd README 的内核要求;打成 linux-<soc> 包
根文件系统 Arch Linux ARM,装在 UFS/eMMC 的一个分区上 分区表里给 ESP 和根分区留位置,Android 分区可以保留做双启动

推进顺序:先不用 GPU 点亮屏幕,再接 DDK,最后装 Omarchy

上一篇的六个检查点照样适用。手机上要在最前面加一步,并在后面补上触摸:

# 检查点 通过标准
0 UEFI → Limine → UKI 启动现有内核,挂上 Arch Linux ARM 根文件系统 串口能登录 shell,systemctl 正常,/dev/dri/card0 和 /dev/mali0 都在
1~2 显示驱动(改动 ①) modetest 在 DSI-1 出彩条;weston --renderer=pixman 出桌面
3~4 DDK(改动 ②、④) kmscube 转起来;隐式同步测试通过
5 Hyprland 裸跑(改动 ③) 本地登录后 start-hyprland 出桌面,日志里 Explicit sync supported
6 装 Omarchy(改动 ⑤) 开机进 SDDM,登录后出现状态栏
7 触摸 libinput debug-events 有 TOUCH_DOWN;屏幕四角点击位置准确

排查:先看现象落在哪一步

现象 落在哪一步 先看哪里
Hyprland 启动即段错误,日志为空 第 0 步 改动 ③ 第一处
启动后卡在 99% CPU,屏幕不亮 第 0 步 改动 ③ 第二处;HYPRLAND_EGL_NO_MODIFIERS=1 可临时绕过
日志 Display driver doesn't support ... 第 0 步 DDK 缺扩展,回到改动 ② 的 eglinfo 清单
黑屏,日志正常 第 6~7 步 AQ_TRACE=1 看 atomic 提交返回值;用 modetest 单独验证显示驱动
应用很卡,CPU 占用高 第 1~3 步 WAYLAND_DEBUG=1 看是不是退回了 wl_shm
画面撕裂、闪出半帧 第 3 步或第 6 步 改动 ④ 的隐式同步测试;改动 ① 的 syncobj 标志
帧率不等于刷新率、时间戳乱 第 8 步 vblank 中断或 TE 信号没接好,内核在发假 vblank
触摸位置偏、方向反 触摸 input:touchdevice:output 和 transform

参考