把手机上的 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 的八个显示部件,用户态六个全换,内核两个留下¶
- 应用、合成器、送屏后端、输入:换成发行版里的现成软件包,不用自己写。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,再映射到屏幕 |
进程视图:一帧画面跨三个进程边界,走九步¶
- 客户端先向 Hyprland 要一个
wl_surface.frame回调,收到后才画下一帧。 - 客户端调
eglSwapBuffers,DDK 把 GPU 任务交给 kbase,Mali 把画面写进客户端自己的 dma-buf。 - DDK 把这块 dma-buf 包成
wl_buffer,attach+commit交给 Hyprland;Hyprland 从 dma-buf 导出隐式栅栏,等 Mali 画完。 - 栅栏就绪后,Hyprland 记下损伤区域,请 Aquamarine 排一帧。
- Aquamarine 在空闲时发出 frame 事件,Hyprland 用 GLES 把所有窗口合成到 GBM 缓冲区,同时给客户端发 frame done。
- Aquamarine 组一个 atomic 请求,经
drmModeAtomicCommit交给内核。 - 内核等合成栅栏完成后写显示控制器寄存器,显示控制器在下一个 vblank 开始扫描新帧。
- vblank 中断到来,内核给 Hyprland 发翻页完成事件。
- 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 后按这个顺序验收:
eglinfo -B -p gbm:GLES 版本 ≥ 3.0。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。完整清单和缺了的后果见上一篇的硬性检查表。kmscube -D /dev/dri/card0:立方体在屏幕上转。这一步验证了“在 card0 上建 GBM”和 PRIME 导入。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 的隐式同步路径可能从来没被用过,所以必须单独验证:
- 写一个小程序:用 DDK 的 GBM 分配一块缓冲区,用 GLES 画一个很重的着色器(例如循环一万次),
glFlush后立刻对这块 dma-buf 调DMA_BUF_IOCTL_EXPORT_SYNC_FILE。 - 对拿到的 sync_file 立刻
poll:应该不可读,等 GPU 画完才变成可读。 - 如果立即可读,说明 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 |
参考¶
- 上一篇:在 Mali G2-Ultra NX 的 SoC 上点亮 Omarchy(GPU 驱动现状、硬性检查表、六个检查点)
- Hyprland 在 libmali 上的问题:#16084、#16085;Aquamarine #374(已修)、#456
- Rockchip libmali 的打包方式:JeffyCN/mirrors libmali 分支
- libhybris 路线:Droidian 的 wlroots hwcomposer 后端
- 主线路线:postmarketOS;Arch Linux ARM + Hyprland 跑在 OnePlus 6T 上的先例:oneplus6t-install-linux-Arch-on-arm-845
- Arm kbase 下载页:Mali GPU Device Driver
- Android 显示驱动走 DRM/KMS:Android DRM/KMS Update, XDC 2019