Leohearts

拥有一颗坚强而又温柔的心 *version 1.1

cover

如何把 OpenWrt 软路由变成 Waydroid 电视盒子

  1. 0. 这是一个怎样的 Setup ?
  2. 0.5 部署前先看:最容易把路由器弄断网的坑
    1. 坑 1:不要一开始就给 Fedora LXC /sys:rw 和桥接网卡
    2. 坑 2:不要用 lxc.net.0.type = none 共享 OpenWrt 宿主网络
    3. 坑 3:OpenWrt LXC 的 common.conf 可能触发 cgroup2 BPF 设备过滤
    4. 坑 4:Waydroid 内层 Android LXC 需要可写 cgroup2 子树
    5. 坑 5:OpenWrt 的 /tmp 是 tmpfs,必须在 Android 层禁用蓝牙
  3. 1. 变量约定
  4. 2. 整体部署流程
  5. 3. OpenWrt 宿主内核准备
    1. 3.1 必要能力
    2. 3.1.1 binderfs 挂载和检查
    3. 3.2 OpenWrt 内核配置
    4. 3.3 重要坑:OpenWrt 的 DMA-BUF debloat patch
    5. 3.4 构建内核
    6. 3.5 部署内核,并修改 GRUB 配置
      1. 3.5.1 准备部署包
      2. 3.5.2 复制 kernel 和 modules 到 OpenWrt
      3. 3.5.3 不要覆盖原始 OpenWrt 启动项
      4. 3.5.4 编辑 /boot/grub/grub.cfg
      5. 3.5.5 重启前检查
      6. 3.5.6 重启并验证
    7. 3.6 OpenWrt 宿主运行时前置检查:LXC 工具、GPU 固件和设备节点
      1. 3.6.1 LXC 工具和容器存放目录
      2. 3.6.2 GPU firmware 不要漏:OpenWrt 通常没有完整 firmware
        1. 从 Fedora LXC 复制 GPU firmware 到 OpenWrt
      3. 3.6.3 loop、uinput、input、snd 设备
  6. 4. 创建外层 Fedora 44 LXC
    1. 4.1 rootfs 创建说明
    2. 4.2 外层 LXC 配置:分两阶段写,先安全初始化,再正式运行
      1. 阶段 A:首次启动安全配置
      2. 阶段 B:正式运行配置
    3. 4.3 验证外层容器设备
    4. 4.4 给 Fedora 一个最小可用网络:不用 NetworkManager/networkd
  7. 5. Fedora LXC 内安装基础软件
  8. 6. 安装 upstream Waydroid / libgbinder / gbinder-python
  9. 7. 安装 WayDroid-ATV 镜像
    1. 7.1 版本选择:新版 A16 vs 旧版 A13
    2. 7.2 方法一:用 waydroid init 接 WayDroid-ATV OTA server(推荐)
    3. 7.3 方法二:手动下载 zip 并安装
    4. 7.4 全新初始化时的清理边界
    5. 7.5 配置 Waydroid 使用正确协议和镜像目录
    6. 7.6 初始化后立刻禁用 Android 蓝牙
  10. 8. 配置 Fedora 图形会话:systemd user + LXQt + KWin
    1. 8.1 创建桌面用户,并加入图形/音频/输入需要的 groups
    2. 8.2 写入 waydroid-full-lxqt.service
    3. 8.3 Waydroid UI 启动脚本
    4. 8.4 总启动脚本
  11. 9. 配置桥接网络
    1. 9.1 Fedora 内 bridge setup 脚本
    2. 9.2 systemd 服务
    3. 9.3 修改 Waydroid inner LXC 网络模板
  12. 10. Waydroid 内层 LXC:config_nodes、cgroup2 子树和 cgroup:rw
    1. 10.1 自动生成的 config_nodes:不要漏 DMA-BUF 和 binderfs
    2. 10.2 cgroup2 子树和 cgroup:rw
      1. 10.2.1 修改 Waydroid LXC config 模板
      2. 10.2.2 写入 pre-start delegation 脚本
      3. 10.2.3 验证 cgroup 子树
      4. 10.2.4 lmkd 要使用 cgroup2 / PSI 策略
  13. 11. 启动
    1. 11.1 OpenWrt 启动外层 Fedora
    2. 11.2 Fedora 内启动 Waydroid 和桌面会话
    3. 11.3 如果 waydroid-autostart.servicesunshine-wayland.service 是 inactive
  14. 12. 验证
    1. 12.1 宿主验证
    2. 12.2 Fedora 验证
    3. 12.3 Android 验证
  15. 13. Bonus:Android media / Codec2 / VAAPI / P010 支持(可选)
    1. 13.1 默认镜像能启动,不等于 Main10/P010 完整
    2. 13.2 如果你有 Android 源码树
    3. 13.3 部署到 vendor overlay
    4. 13.4 期望属性
  16. 13.5 Bonus 验证:视频播放验证(可选)
    1. 13.5.1 推荐测试方式
    2. 13.5.2 已观察到的结论边界
  17. 14. 可选:OpenWrt 4G swap
  18. 14.5 最容易漏的部署细节复盘
  19. 15. 常见问题
    1. 15.0 OpenWrt /tmp 被写满或内存上涨
    2. 15.1 waydroid status 显示 IP UNKNOWN
    3. 15.2 Android 提示 Missing DMA-BUF support
    4. 15.3 waydroidplatform 不通或 waydroid app list 卡住
    5. 15.4 Waydroid inner LXC 因 AppArmor 配置失败
    6. 15.5 lxc.hook.post-stop = /dev/null 导致 status 126
    7. 15.6 Android 早期日志出现 /dev/hw_random 缺失
    8. 15.7 NetworkManager / systemd-networkd 抢网络
  20. 16. 最小检查清单
  21. 17. 不建议做的事
  22. 18. Debug 指引

家里没有多余的电视盒子了,电视的性能也不是很好,怎么办呢?刚好软路由上有个 HDMI,于是...
向您介绍:LineageOS in LXC(Waydroid) on Fedora in LXC on OpenWrt,无须虚拟化,直接跑在 OpenWrt 的 kernel 和 sysfs 上的 Android!
断断续续折腾了5天,烧掉了$200+的偷啃,终于和一大坨Agent一起把这玩意给搞出来啦。其实基础安装,包括编译调试 OpenWrt 内核在内也就花了两天,后面的时间都是修视频硬件解码,给上游 Waydroid-ATV 提 pr 啥的。

此次部署涉及编译 OpenWrt 内核,编译 Android Vendor,编译 ffmpeg 组件,编译 Waydroid 上游组件等,还有一大堆的配置文件测试,没有 AI 的协助是绝对做不到的(传统人力可能得要一个人一个月的时间?特别感谢凌莞宝给的偷啃!)。整理出了一大堆文档,索性把这篇教程也让 Agent 来写啦,希望你能看懂。建议扔给你的AI来部署,我们已经把坑替你踩好了。

整理日期:2026-05-23
适用目标:在一台 OpenWrt x86_64 主机上,通过“OpenWrt 宿主 + Fedora LXC + Waydroid Android LXC”的三层结构运行 Waydroid ATV,并启用 Intel iGPU、DMA-BUF、桥接网络、KWin/LXQt 图形会话和 Android Codec2/VAAPI 视频解码。

本文来自我们已经跑通的 OpenWrt/Fedora/Waydroid 实机部署记录。为了给其他人复用,文中不使用 openwrt.lan、固定内网地址、固定用户目录作为硬要求,而是改成变量。你可以按自己的机器替换。


0. 这是一个怎样的 Setup ?

把 Android TV 放进 Waydroid 的 Android LXC,再把 Waydroid 放进一个 Fedora privileged LXC,最底下仍然使用 OpenWrt 宿主的 Linux kernel 和硬件设备。

整体结构是这样:

OpenWrt x86_64 宿主
└── Fedora 44 privileged LXC
    └── Waydroid Android LXC / WayDroid-ATV

它不是传统虚拟机,也不是在 Docker 里硬塞 Android。Android 这一层会直接使用宿主内核提供的 binder、cgroup、DMA-BUF、DRM render node、input、audio 等能力;Fedora 这一层负责提供更完整、对 Waydroid 更友好的 userspace。

三层各自负责的事情大概是:

第一层:OpenWrt 宿主

  • 跑真正的 Linux kernel。
  • 提供 Waydroid 需要的内核能力,例如 binder/binderfs、cgroup2、DMA-BUF heap。
  • 提供 GPU/DRM 设备,例如 /dev/dri/renderD128
  • 提供输入、声音等设备,例如 /dev/input/dev/uinput/dev/snd
  • 准备 GPU firmware。OpenWrt 通常没有完整 firmware,这一步后面会单独写。
  • 提供外层 Fedora LXC 使用的桥接网络,例如接到 OpenWrt 的 br-lan

第二层:Fedora privileged LXC

  • 运行 systemd、Waydroid、LXC、KWin Wayland、LXQt 等组件。
  • 提供 Wayland 图形会话,让 Waydroid 的 Android 画面有地方显示。
  • 提供 libgbinder、Waydroid helper、Mesa/VAAPI 等 Linux userspace 组件。
  • 管理普通用户会话,例如本文使用的 user 用户,以及它所属的 videoaudioinputrenderseat 等组。
  • 作为 Android 内层 LXC 的父环境,负责把必要设备、cgroup 子树和 binder 节点交给 Waydroid。

第三层:Waydroid Android LXC / WayDroid-ATV

  • 运行 Android TV,也就是 LineageOS / WayDroid-ATV。
  • 使用 binderhwbindervndbinder 完成 Android IPC。
  • 使用 /dev/dma_heap/system 和 DRM render node 参与图形缓冲区分配、渲染和显示。
  • 通过桥接网络获得一个和 LAN 同网段的地址。
  • 后面可选启用 Android Codec2 / VAAPI / P010 等视频解码能力。

基础部署目标很朴素:先把 Android TV 桌面跑起来,让网络、显示、输入、声音都能工作。至于视频硬解、Codec2、VAAPI、P010 这些内容,本文会放到后面的 Bonus 部分;它们很有价值,但不应该挡在第一次跑通前面。

本文后续命令会默认使用这几个名字,你可以按自己的机器替换:

外层 Fedora LXC 名称:waydroid
外层 Fedora 普通用户:user
外层 Fedora 图形会话:systemd user session + LXQt Wayland + KWin Wayland
Android 镜像:WayDroid-ATV Android 16 / Lineage 23.0 / waydroid_tv_x86_64

0.5 部署前先看:最容易把路由器弄断网的坑

请先看这一节,再动 LXC 配置。我们早期踩过的最大坑不是 Waydroid 本身,而是外层 Fedora LXC 里 systemd-networkd / systemd-udevd 配合可写 /sys 后,可能会改动网络设备状态或尝试按 Fedora 规则重命名网卡。

坑 1:不要一开始就给 Fedora LXC /sys:rw 和桥接网卡

建议第一次启动 Fedora LXC 时先用安全配置:

# 第一次启动只用于进容器做系统服务屏蔽,不要先桥接到 LAN
lxc.net.0.type = empty

# 第一次启动保留 /sys 只读
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed

这样做的目的很具体:

  • Fedora 容器第一次启动时,即使 systemd-networkdsystemd-udevd 自动运行,也不容易改 OpenWrt 宿主的网卡状态。
  • empty 网络命名空间不会共享 OpenWrt 宿主网络,也不会立刻桥接到 br-lan
  • /sys:ro 可以降低容器内 udev 写 sysfs、改设备状态、触发网卡重命名的风险。

第一次启动后,进入 Fedora 容器:

lxc-start -n waydroid
lxc-attach -n waydroid -- /bin/bash

在 Fedora 容器内先屏蔽这些服务:

systemctl mask systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service 2>/dev/null || true
systemctl disable --now systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service 2>/dev/null || true

systemctl mask systemd-resolved.service firewalld.service NetworkManager.service wpa_supplicant.service 2>/dev/null || true
systemctl disable --now systemd-resolved.service firewalld.service NetworkManager.service wpa_supplicant.service 2>/dev/null || true

再处理 udev 的网卡自动重命名。建议至少写入一个 “保持内核原名” 的 .link 文件,并屏蔽默认 link 规则:

mkdir -p /etc/systemd/network
cat > /etc/systemd/network/10-keep-kernel-ifnames.link <<'EOF'
[Match]
OriginalName=*

[Link]
NamePolicy=keep
MACAddressPolicy=none
EOF

ln -sfn /dev/null /etc/systemd/network/99-default.link

如果你的目标是最保守的路由器环境,可以先屏蔽 systemd-udevd,等确认不会重命名网卡之后,再按图形会话需要决定是否恢复 udev 数据库:

systemctl mask systemd-udevd.service systemd-udevd-control.socket systemd-udevd-kernel.socket 2>/dev/null || true
systemctl disable --now systemd-udevd.service systemd-udevd-control.socket systemd-udevd-kernel.socket 2>/dev/null || true

注意边界:KWin/Weston/libinput/libudev 有时需要 udev 数据库来识别 DRM 和输入设备。如果后面图形会话缺 udev tags,可以在确认网卡不会被重命名后,再有控制地恢复 udev,并只触发需要的设备,例如 DRM/input。不要在 /sys:rw、网络还没保护好的阶段直接让 udev 全量扫描。

完成上面步骤后,退出 Fedora,停止外层 LXC:

exit
lxc-stop -n waydroid

然后再把外层 LXC 改成正式运行配置:

lxc.net.0.type = veth
lxc.net.0.veth.pair = lxc-waydroid
lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:59:af:02

lxc.mount.auto = proc:mixed sys:rw cgroup:mixed

这一步顺序很重要:先在容器内禁用 networkd / 自动网卡重命名,再给桥接网络和 /sys:rw

坑 2:不要用 lxc.net.0.type = none 共享 OpenWrt 宿主网络

我们早期试过 lxc.net.0.type = none。这个设置会让 Fedora 容器共享 OpenWrt 宿主网络命名空间。结果是 Fedora 里的网络管理服务有机会直接影响 OpenWrt 宿主网卡,路由器可能失联。

安全选择是:

  • 首次保护阶段用 empty
  • 正式运行阶段用 veth 接 OpenWrt 的 br-lan
  • 不要用 none

坑 3:OpenWrt LXC 的 common.conf 可能触发 cgroup2 BPF 设备过滤

OpenWrt 的 LXC common.conf 里可能有:

lxc.cgroup2.devices.deny = a

这会让 LXC 尝试加载 cgroup2 device BPF 程序。有些 OpenWrt 内核没有对应 BPF 支持,启动会失败,日志类似:

Failed to load bpf program
Failed to setup cgroup2 device controller limits

我们验证过的处理方式是:外层 Fedora LXC 不 include common.conf,自己写最小配置,并且不要写 cgroup2 device deny 规则。当前方案依赖 privileged LXC 和明确的设备 bind mount。

坑 4:Waydroid 内层 Android LXC 需要可写 cgroup2 子树

这是另一个非常关键的坑。Waydroid 内层 LXC 不是普通进程,它还要在 Android 里让 init / libprocessgroup / lmkd 创建和管理 cgroup。

如果内层 LXC 只有:

lxc.mount.auto = cgroup:ro sys:ro proc

可能出现这些现象:

libprocessgroup: Failed to make and chown /sys/fs/cgroup/uid_1000: Read-only file system
libprocessgroup: Failed to open /dev/cpuset/foreground/tasks: No such file or directory
lmkd: Old kill strategy can only be used with v1 cgroup hierarchy
critical process 'lmkd' exited ... before boot completed
Android 容器启动几秒后退出

我们日志里确认过的关键要求是:Waydroid 内层 Android LXC 必须运行在明确的 cgroup2 子树中,并且 Android 看到的 cgroup 挂载必须是可写的,也就是给内层 LXC cgroup:rw

lxc.mount.auto = cgroup:rw sys:ro proc

lxc.cgroup.dir.container = waydroid-android
lxc.cgroup.dir.monitor = waydroid-android-monitor
lxc.cgroup.dir.monitor.pivot = waydroid-android-monitor-pivot
lxc.cgroup.dir.container.inner = payload
lxc.hook.pre-start = /var/lib/waydroid/lxc/waydroid/waydroid-cgroup2-delegate.sh

waydroid-cgroup2-delegate.sh 的作用是打开 /sys/fs/cgroup 的可用 controllers,并让 LXC 在 waydroid-android/payload 这个 leaf 里运行 Android。这样 Android 可以在自己的 cgroup 子树下面创建 uid_* / pid_* 子目录。

这不是为了“美化日志”,而是 Android 进程管理需要的运行条件。后文会给出完整脚本。

坑 5:OpenWrt 的 /tmp 是 tmpfs,必须在 Android 层禁用蓝牙

OpenWrt 的 /tmp 通常是 tmpfs,也就是占用内存。Waydroid / Android 启动失败、服务反复崩溃或 logcat 长时间写入 /tmp 时,可能直接吃掉路由器内存。

我们遇到的一个具体风险是:Waydroid-ATV 的蓝牙栈在当前环境里不可靠,蓝牙相关服务可能反复崩溃并刷日志。如果日志写到 OpenWrt/Fedora 的 tmpfs,内存会被慢慢吃满,最后可能导致路由器重启。

部署时请明确做两件事:

  1. 不要把长时间日志写到 OpenWrt /tmp 长日志请写到持久存储,例如构建机本地目录、外接磁盘,或 OpenWrt rootfs 上确认不是 tmpfs 的目录。
  2. 在 Android 层禁用蓝牙。 不要通过加载 OpenWrt Bluetooth 模块来“修蓝牙”。我们之前的记录里,Bluetooth kernel 模块尝试导致过 boot loop 风险。

Android 启动后执行:

waydroid shell -- sh -c '
setprop persist.bluetooth.disabled true
settings put global bluetooth_on 0
settings put global ble_scan_always_enabled 0
cmd bluetooth_manager disable 2>/dev/null || true
pm disable-user --user 0 com.android.bluetooth 2>/dev/null || true
pm disable com.android.bluetooth 2>/dev/null || true
'

验证:

waydroid shell -- sh -c '
getprop persist.bluetooth.disabled
cmd package list packages -d | grep -i bluetooth || true
service list | grep -i bluetooth || true
'

期望至少看到:

persist.bluetooth.disabled=true
package:com.android.bluetooth

service list 里可能仍有 bluetooth_manager binder service,某些 vendor bluetooth service 也可能存在;关键是 Android framework 层的 com.android.bluetooth 已被禁用,蓝牙不再作为可用功能反复启动。


1. 变量约定

请先把这些变量换成你自己的环境。

# OpenWrt 宿主
OPENWRT_HOST="你的OpenWrt主机名或IP"

# 外层 Fedora LXC
FEDORA_LXC_NAME="waydroid"
FEDORA_ROOTFS="/srv/lxc/waydroid/rootfs"
FEDORA_LXC_CONFIG="/srv/lxc/waydroid/config"
FEDORA_USER="user"
FEDORA_UID="1000"

# OpenWrt LAN
OPENWRT_LAN_BRIDGE="br-lan"
OPENWRT_LAN_GATEWAY="192.168.1.1"
FEDORA_LAN_ADDR="192.168.1.237/24"      # 示例,按你的 LAN 改
ANDROID_LAN_ADDR="由 DHCP 分配"          # 不建议硬编码

# Waydroid 镜像存放位置,位于 Fedora LXC 内
WAYDROID_IMAGE_DIR="/etc/waydroid-extra/images"

# 本地 Android 源码树。如果你只使用现成镜像和产物,可以跳过源码构建章节。
ANDROID_SRC="/path/to/android-waydroid-atv23"

后文命令如果写死了 /srv/lxc/waydroiduser192.168.1.1,请按这里的变量替换。


2. 整体部署流程

建议按这个顺序做:

  1. 准备 OpenWrt 内核:binder、binderfs、cgroup、PSI、DMA-BUF heaps。
  2. 创建外层 Fedora 44 privileged LXC,并透传设备。
  3. 在 Fedora LXC 中安装基础软件、Waydroid userspace、libgbinder、gbinder-python。
  4. 如果 OpenWrt 缺 GPU firmware,从 Fedora/Linux 的 firmware 包复制对应 firmware 到 OpenWrt,再重启验证 GPU 驱动。
  5. 安装 WayDroid-ATV Android 16 镜像。
  6. 配置 Fedora 图形会话:systemd user session + LXQt Wayland + KWin。
  7. 配置网络:外层 Fedora 和内层 Android 桥接到 OpenWrt LAN。
  8. 配置 Waydroid 内层 LXC 的 config_nodes 和 cgroup2 子树。
  9. 启动并验证三层状态。
  10. 可选 Bonus:需要视频硬解调优时,再部署 Android media / Codec2 / VAAPI / P010 支持,并用 Android 播放器看日志验证。

3. OpenWrt 宿主内核准备

3.1 必要能力

OpenWrt 宿主必须提供:

Android binder / binderfs
cgroup / cgroup v1 compatibility pieces used by this setup
PSI
DMA-BUF core
DMA-BUF system heap
UDMABUF
DRM render node, usually /dev/dri/renderD128

部署后必须能看到:

ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system 2>/dev/null || true
grep -E 'dma_heap|udmabuf' /proc/misc /proc/devices 2>/dev/null || true

期望至少有:

/dev/dri
/dev/dma_heap
/dev/dma_heap/system
/proc/devices 里有 dma_heap
/proc/misc 里有 udmabuf

如果 /dev/dma_heap/system 不存在,不要去改 Android XML、Magisk 模块、build.prop 来隐藏提示。正确方向是修 OpenWrt 内核。

3.1.1 binderfs 挂载和检查

Waydroid 需要 binder、hwbinder、vndbinder。常见位置是:

/dev/binderfs/binder
/dev/binderfs/hwbinder
/dev/binderfs/vndbinder

OpenWrt 宿主要先具备 binder / binderfs 内核能力。可以检查:

grep -E 'binder' /proc/filesystems /proc/misc /proc/devices 2>/dev/null || true
ls -ld /dev/binderfs 2>/dev/null || true
ls -l /dev/binderfs/binder-control /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder 2>/dev/null || true
mount | grep binder || true

如果 /dev/binderfs 不存在,但内核已经启用了 CONFIG_ANDROID_BINDERFS=y,可以手动挂载做诊断:

mkdir -p /dev/binderfs
mount -t binder binder /dev/binderfs 2>/dev/null || true
ls -l /dev/binderfs 2>/dev/null || true

如果挂载后只有 binder-control,没有 binderhwbindervndbinder,新版 Waydroid 的 driver helper 通常会尝试通过 binder-control 创建节点,并把它们链接到 /dev/

注意边界:OpenWrt 宿主需要有 binder/binderfs 内核能力,但不一定要长期把 binderfs 挂在宿主 /dev/binderfs。我们当前远程环境里,外层 LXC 配置没有显式 bind /dev/binderfs,Fedora 容器内仍然由 Waydroid helper 建出了:

/dev/binderfs/binder
/dev/binderfs/hwbinder
/dev/binderfs/vndbinder
/dev/binder -> /dev/binderfs/binder
/dev/hwbinder -> /dev/binderfs/hwbinder
/dev/vndbinder -> /dev/binderfs/vndbinder

所以最终请至少确认 Fedora 外层容器里能看到这些设备:

lxc-attach -n waydroid -- /bin/bash -lc 'ls -l /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder /dev/binder /dev/hwbinder /dev/vndbinder 2>/dev/null || true'

如果 Fedora 里没有 /dev/binderfs,请先看 Waydroid container 日志。可以选择让 Waydroid helper 在 Fedora 里挂载 binderfs,也可以在 OpenWrt 宿主挂载后通过第 4.2 节的 optional bind mount 传进 Fedora。不要把“OpenWrt 宿主当前没挂 /dev/binderfs”单独当成失败;关键是 Fedora 和内层 Android 最终能访问 binder/hwbinder/vndbinder。

3.2 OpenWrt 内核配置

在 OpenWrt source tree 中,x86 通用配置类似:

target/linux/x86/config-6.12
target/linux/x86/64/config-6.12

我们验证过的关键配置如下。

target/linux/x86/config-6.12

CONFIG_ANDROID=y
CONFIG_ANDROID_BINDER_IPC=y
CONFIG_ANDROID_BINDERFS=y
CONFIG_ANDROID_BINDER_DEVICES="binder,hwbinder,vndbinder"
CONFIG_PSI=y
CONFIG_MEMCG_V1=y
CONFIG_CPUSETS_V1=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_NET_PRIO=y
CONFIG_CGROUP_PERF=y

target/linux/x86/64/config-6.12

CONFIG_DMA_SHARED_BUFFER=y
CONFIG_DMABUF_HEAPS=y
CONFIG_DMABUF_HEAPS_SYSTEM=y
CONFIG_UDMABUF=y
CONFIG_IA32_EMULATION=y

如果你的 OpenWrt kernel 版本不是 6.12,请按对应版本的 config 文件修改。本文只验证过 6.12 系列。

3.3 重要坑:OpenWrt 的 DMA-BUF debloat patch

OpenWrt 有一个 patch 可能影响 DMA-BUF heaps:

target/linux/generic/hack-6.12/904-debloat_dma_buf.patch

我们遇到过这样的情况:

  • 内核配置已经有 CONFIG_DMABUF_HEAPS_SYSTEM=y
  • 但是 /dev/dma_heap/system 仍然没有出现。
  • 追查后发现 system_heap.o 没有链接进最终内核。
  • 原因是这个 patch 改写了 drivers/dma-buf/heaps/Makefile

验证通过的处理方式是:保留 OpenWrt 对 DMA_SHARED_BUFFER 的模块化改动,但不要改写:

drivers/dma-buf/heaps/Makefile

也就是说,904-debloat_dma_buf.patch 里不应再包含类似这一段:

---- a/drivers/dma-buf/heaps/Makefile
-+++ b/drivers/dma-buf/heaps/Makefile
-@@ -1,3 +1,3 @@
- # SPDX-License-Identifier: GPL-2.0
--obj-$(CONFIG_DMABUF_HEAPS_SYSTEM) += system_heap.o
--obj-$(CONFIG_DMABUF_HEAPS_CMA)    += cma_heap.o
-+dma-buf-objs-$(CONFIG_DMABUF_HEAPS_SYSTEM) += system_heap.o
-+dma-buf-objs-$(CONFIG_DMABUF_HEAPS_CMA)    += cma_heap.o

修改后重新编译内核,再确认 /dev/dma_heap/system

3.4 构建内核

示例命令:

cd /path/to/openwrt-source
make target/linux/clean
make -j"$(nproc)" target/linux/compile V=s

我们为了避免和系统原模块目录混用,给 Waydroid 内核使用过单独 release 名称,例如:

6.12.74-waydroid-v10

这需要 OpenWrt build system 额外支持 WAYDROID_KERNEL_RELEASE。如果你没有做这个本地改动,就不要直接照抄这个变量。核心原则是:

运行中的 uname -r 必须和 /lib/modules/<uname -r> 匹配。

3.5 部署内核,并修改 GRUB 配置

这一节以常见 OpenWrt x86_64 安装为例。我们验证过的机器是:

/boot 是单独 vfat 分区
GRUB 配置文件:/boot/grub/grub.cfg
内核文件:/boot/vmlinuz 或 /boot/vmlinuz-自定义名字
模块目录:/lib/modules/<uname -r>

你的机器可能略有不同。请先确认:

mount | grep ' /boot '
ls -lah /boot /boot/grub 2>/dev/null || true
sed -n '1,220p' /boot/grub/grub.cfg 2>/dev/null || true
cat /proc/cmdline

如果 GRUB 配置不在 /boot/grub/grub.cfg,请按你机器实际文件改。下面以 /boot/grub/grub.cfg 为例。

3.5.1 准备部署包

假设你已经在 OpenWrt build tree 编译出:

vmlinuz-waydroid
/lib/modules/<WAYDROID_KERNEL_RELEASE>/

推荐给 Waydroid 内核一个明确名字,例如:

WAYDROID_KERNEL_RELEASE=6.12.74-waydroid-v10
/boot/vmlinuz-waydroid-v10
/lib/modules/6.12.74-waydroid-v10/

核心原则:

GRUB 启动的 kernel 文件、uname -r、/lib/modules/<uname -r> 必须互相匹配。

3.5.2 复制 kernel 和 modules 到 OpenWrt

在 OpenWrt 上先备份:

TS=$(date +%Y%m%d-%H%M%S)
mkdir -p /root/kernel-backup-$TS
cp -a /boot/grub/grub.cfg /root/kernel-backup-$TS/grub.cfg 2>/dev/null || true
cp -a /boot/vmlinuz /root/kernel-backup-$TS/vmlinuz.original 2>/dev/null || true

如果你用 scp 从构建机传文件,示例:

# 在构建机执行,按你的实际文件位置替换
scp /path/to/vmlinuz-waydroid-v10 root@<OPENWRT_HOST>:/boot/vmlinuz-waydroid-v10
scp -r /path/to/lib/modules/6.12.74-waydroid-v10 root@<OPENWRT_HOST>:/lib/modules/

在 OpenWrt 上确认:

ls -lah /boot/vmlinuz-waydroid-v10
ls -ld /lib/modules/6.12.74-waydroid-v10

3.5.3 不要覆盖原始 OpenWrt 启动项

不要直接把原来的 menuentry "OpenWrt" 改没。推荐做法是新增一个 OpenWrt-Waydroid 启动项,并保留原始 OpenWrtOpenWrt (failsafe)

先取当前 root 参数。最稳妥的方法是看现有 grub.cfg 里的 root=...

grep -o 'root=[^ ]*' /boot/grub/grub.cfg | head -1

也可以看当前内核命令行:

cat /proc/cmdline

常见形式类似:

root=PARTUUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

3.5.4 编辑 /boot/grub/grub.cfg

示例 GRUB 配置:

serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 --rtscts=off
terminal_input console serial; terminal_output console serial

set default="0"
set timeout="3"
search -l kernel -s root

menuentry "OpenWrt-Waydroid" {
        linux /boot/vmlinuz-waydroid-v10 root=PARTUUID=替换成你的ROOT_PARTUUID rootwait console=tty1 console=ttyS0,115200n8 noinitrd
}

menuentry "OpenWrt-original" {
        linux /boot/vmlinuz root=PARTUUID=替换成你的ROOT_PARTUUID rootwait console=tty1 console=ttyS0,115200n8 noinitrd
}

menuentry "OpenWrt-original-failsafe" {
        linux /boot/vmlinuz failsafe=true root=PARTUUID=替换成你的ROOT_PARTUUID rootwait console=tty1 console=ttyS0,115200n8 noinitrd
}

注意:

  • search -l kernel -s root 是我们实机上的 OpenWrt x86 GRUB 写法。它依赖 /boot 分区 label 是 kernel。如果你的 /boot 分区 label 不同,请按原始 grub.cfg 保留原来的 search 行。
  • root=PARTUUID=... 请从原始 grub.cfg 复制,不要猜。
  • set timeout="3"1 更适合调试,给你一点时间在串口/显示器上选择回退项。
  • cgroup_enable=memory cgroup_memory=1 在我们后来的 6.12 / cgroup2 环境里不是关键项,内核还会提示 unknown kernel command line parameters。通用教程不建议新增它们。若你原配置已有,先不要随意删除;但新 Waydroid entry 可以不加。

可以用脚本方式追加 entry,但手工编辑前请备份:

cp -a /boot/grub/grub.cfg /boot/grub/grub.cfg.pre-waydroid-$(date +%Y%m%d-%H%M%S)
vi /boot/grub/grub.cfg
sync

3.5.5 重启前检查

重启前确认:

ls -lah /boot/vmlinuz-waydroid-v10
ls -ld /lib/modules/6.12.74-waydroid-v10
grep -nA8 -B2 'OpenWrt-Waydroid' /boot/grub/grub.cfg
sync

如果是远程路由器,请确保你有回退手段:

  • 串口
  • HDMI + 键盘
  • IPMI / 远程 KVM
  • 或确认 GRUB 保留原始启动项且 timeout 足够选择

不要在没有回退手段时盲目改默认启动项。

3.5.6 重启并验证

重启:

reboot

回来后验证:

uname -r
ls -ld /lib/modules/$(uname -r)
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system 2>/dev/null || true
grep -E 'dma_heap|udmabuf' /proc/misc /proc/devices 2>/dev/null || true
cat /proc/cmdline

期望:

uname -r = 你的 WAYDROID_KERNEL_RELEASE
/lib/modules/$(uname -r) 存在
/dev/dri 存在
/dev/dma_heap/system 存在

注意:/proc/cmdline 里的 BOOT_IMAGE=/boot/xxx 只是 GRUB 加载的文件名。远程实机里曾出现过文件名还像旧名字、但 uname -r 已经是新 release 的情况。判断是否部署成功,请优先看 uname -r/lib/modules/$(uname -r)/dev/dma_heap/system、GPU firmware/DRM 初始化日志,不要只靠 kernel 文件名。

如果启动失败,请从 GRUB 选择原始 OpenWrt-original 或 failsafe 项回退,再检查:

  • kernel 文件名是否写错
  • root=PARTUUID=... 是否复制错
  • /lib/modules/<uname -r> 是否缺失
  • OpenWrt DMA-BUF patch 是否导致 /dev/dma_heap/system 没出现

3.6 OpenWrt 宿主运行时前置检查:LXC 工具、GPU 固件和设备节点

内核启动后,请先把这些运行时前置项查完,再创建 Fedora LXC。它们不是 Android 配置问题,漏掉时后面会表现成 KWin、Waydroid 或镜像挂载失败。

3.6.1 LXC 工具和容器存放目录

确认 OpenWrt 上有 LXC 工具:

command -v lxc-start
command -v lxc-attach
command -v lxc-stop
command -v lxc-info

如果没有,请按你的 OpenWrt 包管理器安装。新 OpenWrt 可能是 apk,旧环境可能是 opkg。包名会随发行版变化,请用包搜索确认;我们记录里用过的方向是安装 LXC 主程序、模板和 start/attach/stop/info 这些命令。

本文示例把外层 Fedora LXC 放在:

/srv/lxc/waydroid

有些 OpenWrt 的 LXC 默认容器目录不是 /srv/lxc。如果你执行:

lxc-start -n waydroid

提示找不到容器,请不要改 rootfs。先显式指定 LXC 容器目录:

lxc-start -P /srv/lxc -n waydroid
lxc-attach -P /srv/lxc -n waydroid -- /bin/bash
lxc-info -P /srv/lxc -n waydroid
lxc-stop -P /srv/lxc -n waydroid

后文为了简洁,仍写成 lxc-start -n waydroid。如果你的机器需要 -P /srv/lxc,请所有外层 LXC 命令都加上它。

3.6.2 GPU firmware 不要漏:OpenWrt 通常没有完整 firmware

OpenWrt 系统通常不会自带完整的 GPU firmware。内核里有 DRM/GPU 驱动,不等于 GPU 可以正常初始化。这里不要只理解成 Intel i915;不同机器要按实际 GPU 和 dmesg 里请求的文件来处理。

常见例子:

Intel 核显:/lib/firmware/i915/...
AMD GPU:  /lib/firmware/amdgpu/...
其他 GPU:按内核日志里 request 的 firmware 文件名准备

我们这台实机是 Intel iGPU,所以后面的具体命令以 i915 为例。通用原则是:在 Fedora LXC 或另一台 Linux 机器里安装对应 firmware 包,把 GPU 驱动实际请求的 firmware 文件复制到 OpenWrt 宿主的 /lib/firmware/...,然后重启 OpenWrt 让 GPU 驱动重新初始化。

现场观察到的事实是:

Fedora LXC:
  linux-firmware-20260410-1.fc44.noarch
  intel-gpu-firmware-20260410-1.fc44.noarch
  /lib/firmware/i915/*.bin.xz

OpenWrt 宿主:
  /lib/firmware/i915/*.bin
  其中 tgl_guc_70.bin 是后来补进去的 firmware

我们早期遇到过 i915 GuC firmware 加载失败,GPU 被内核标记为 wedged。Fedora 里 Mesa/Iris 可能会报一个容易误导的错误:

Kernel is too old (4.16+ required) or unusable for Iris

当时宿主 dmesg 里的关键事实是类似:

i915 ... GuC firmware i915/tgl_guc_70.bin: fetch failed
i915 ... GuC initialization failed
i915 ... Failed to initialize GPU, declaring it wedged

所以部署时请检查 OpenWrt 宿主:

dmesg | grep -i -E 'drm|gpu|i915|amdgpu|nouveau|xe|guc|huc|dmc|firmware|wedged|reset' | tail -160
ls -lah /lib/firmware 2>/dev/null | head -80
ls -lah /lib/firmware/i915 /lib/firmware/amdgpu 2>/dev/null | head -120 || true
ls -ld /dev/dri /dev/dri/renderD128 2>/dev/null || true

如果看到 firmware 缺失、GPU wedged、GPU reset 失败,或 DRM render node 没有出现,请先补齐对应 GPU firmware,再重启 OpenWrt。不要先去改 Waydroid、Mesa 或 Android MediaCodec。

从 Fedora LXC 复制 GPU firmware 到 OpenWrt

如果你已经有 Fedora LXC,请先进入 Fedora 安装 firmware 包。Intel 机器可以安装:

# Fedora LXC 内,Intel 例子
dnf install -y linux-firmware intel-gpu-firmware
rpm -q linux-firmware intel-gpu-firmware
find /lib/firmware/i915 /usr/lib/firmware/i915 -maxdepth 1 -type f 2>/dev/null | grep -E 'guc|huc|dmc' | head -80

如果是 AMD 或其它 GPU,请安装 Fedora 中对应的 firmware 包,并按 dmesg 请求的目录复制,例如 amdgpu/。本文没有验证非 Intel GPU,不能把 i915 文件名照搬到其它硬件。

然后回到 OpenWrt 宿主,从 Fedora rootfs 复制并解压。下面命令假设 Fedora rootfs 是 /srv/lxc/waydroid/rootfs,并以 Intel i915 为例:

# OpenWrt 宿主执行,Intel i915 例子
FEDORA_ROOTFS=/srv/lxc/waydroid/rootfs
FW_SUBDIR=i915
mkdir -p /lib/firmware/$FW_SUBDIR

# 优先复制压缩的 Fedora firmware,并在 OpenWrt 上解压成内核会请求的 .bin 文件。
# BusyBox xz/unxz 不一定安装;如果没有 unxz,可以在 Fedora 里先解压后再复制。
for srcdir in "$FEDORA_ROOTFS/lib/firmware/$FW_SUBDIR" "$FEDORA_ROOTFS/usr/lib/firmware/$FW_SUBDIR"; do
  [ -d "$srcdir" ] || continue
  find "$srcdir" -maxdepth 1 -type f \( -name '*.bin.xz' -o -name '*.bin' \) -print | while read -r f; do
    base=$(basename "$f")
    case "$base" in
      *.bin.xz)
        out=/lib/firmware/$FW_SUBDIR/${base%.xz}
        if command -v unxz >/dev/null 2>&1; then
          unxz -c "$f" > "$out"
        elif command -v xz >/dev/null 2>&1; then
          xz -dc "$f" > "$out"
        else
          echo "missing xz/unxz, cannot decompress $f" >&2
        fi
        ;;
      *.bin)
        cp -f "$f" /lib/firmware/$FW_SUBDIR/
        ;;
    esac
  done
done

ls -lah /lib/firmware/$FW_SUBDIR | head -80
sync

如果你的 GPU 不是 Intel,请把 FW_SUBDIR=i915 换成你的 GPU firmware 目录,例如 FW_SUBDIR=amdgpu,并确认文件名和 dmesg 请求的一致。

如果 OpenWrt 没有 xz / unxz,也可以在 Fedora LXC 内先解压到临时目录,再从 OpenWrt 宿主复制解压后的 .bin。Intel 例子:

# Fedora LXC 内,Intel i915 例子
mkdir -p /root/gpu-fw-unpacked/i915
find /lib/firmware/i915 /usr/lib/firmware/i915 -maxdepth 1 -type f -name '*.bin.xz' 2>/dev/null | while read -r f; do
  xz -dc "$f" > /root/gpu-fw-unpacked/i915/$(basename "${f%.xz}")
done

# OpenWrt 宿主
mkdir -p /lib/firmware/i915
cp -f /srv/lxc/waydroid/rootfs/root/gpu-fw-unpacked/i915/*.bin /lib/firmware/i915/
sync

完成后重启 OpenWrt,让 GPU 驱动在启动时重新加载 firmware:

reboot

重启后看 dmesg。我们当前 Intel 实机上曾观察到类似这些成功事实:

i915 ... Finished loading DMC firmware i915/adlp_dmc.bin
i915 ... GT0: GuC firmware i915/tgl_guc_70.bin version 70.49.4
i915 ... GT0: GUC: submission enabled
i915 ... GT0: GUC: SLPC enabled
i915 ... GT0: GUC: RC enabled

注意边界:当前 Intel 实机日志里曾还有 tgl_huc.bin 获取失败,但 GuC 已启用,GPU 没有 wedged,Waydroid 图形和后续验证可以继续。其他 GPU 请以你的 dmesg 为准。

如果你的 Fedora LXC 还没创建,也可以先用另一台 Fedora/Linux 机器安装对应 firmware 包,把 GPU 驱动请求的 firmware 文件复制到 OpenWrt。关键不是来源必须是 Fedora LXC,而是 OpenWrt 宿主的 /lib/firmware/... 里要有 GPU 驱动实际请求的 firmware。

3.6.3 loop、uinput、input、snd 设备

Waydroid 镜像挂载需要 loop 设备;输入和音频需要 /dev/uinput/dev/input/dev/snd。请在 OpenWrt 宿主先检查:

ls -l /dev/loop-control /dev/loop0 2>/dev/null || true
ls -ld /dev/uinput /dev/input /dev/snd 2>/dev/null || true

如果缺 loop 设备,先加载或安装 OpenWrt 的 loop 内核模块:

modprobe loop 2>/dev/null || true
ls -l /dev/loop-control /dev/loop0 2>/dev/null || true

如果缺输入设备,检查 uinput、evdev、HID 相关模块:

modprobe uinput 2>/dev/null || true
modprobe evdev 2>/dev/null || true
ls -ld /dev/uinput /dev/input 2>/dev/null || true

如果需要 HDMI/声卡音频,检查 ALSA/HDA 相关模块:

modprobe snd-hda-intel 2>/dev/null || true
ls -ld /dev/snd 2>/dev/null || true

包名同样按你的 OpenWrt 包源搜索。我们记录里出现过的方向包括 kmod-loopkmod-uinputkmod-input-evdevkmod-hidkmod-usb-hidkmod-sound-hda-intelkmod-sound-hda-codec-hdmi。这些名字只作为搜索提示,不保证每个 OpenWrt 分支完全一致。

4. 创建外层 Fedora 44 LXC

4.1 rootfs 创建说明

我们的记录没有完整保存最初创建 Fedora 44 rootfs 的每一条命令。所以本文不把某个 rootfs 生成命令写成唯一答案。

如果你的 OpenWrt LXC download template 可用,通用做法通常类似下面这样。请先用 --list 确认模板里确实有 Fedora 44,再执行创建:

# 可选:查看 download template 支持的 Fedora 版本
lxc-create -n list-test -t download -- --list | grep -i fedora || true

# 示例:创建 Fedora 44 amd64 rootfs
lxc-create -P /srv/lxc -n waydroid -t download -- --dist fedora --release 44 --arch amd64

如果你的 OpenWrt 没有 lxc-create、没有 download template,或模板源不可用,请使用你现场可用的标准方式准备 Fedora 44 rootfs。最终只要放到类似:

/srv/lxc/waydroid/rootfs

最低要求:

架构:x86_64
发行版:Fedora 44
容器类型:privileged LXC
init:systemd
网络:有 eth0,可以接 OpenWrt br-lan

创建完成后,从 OpenWrt 进入 Fedora:

lxc-start -n waydroid
lxc-attach -n waydroid -- /bin/bash
cat /etc/fedora-release

期望:

Fedora release 44

4.2 外层 LXC 配置:分两阶段写,先安全初始化,再正式运行

请不要第一次启动就直接使用桥接网络和 /sys:rw。建议分两阶段。

阶段 A:首次启动安全配置

编辑 OpenWrt 上的:

/srv/lxc/waydroid/config

首次启动建议用这个最小安全配置:

lxc.arch = x86_64
lxc.rootfs.path = dir:/srv/lxc/waydroid/rootfs
lxc.uts.name = waydroid

lxc.tty.max = 2
lxc.pty.max = 1024

# 当前验证的是 privileged 容器。
lxc.seccomp.profile =
lxc.cap.drop =

# 首次启动不要桥接,不要共享宿主网络。
lxc.net.0.type = empty

# 首次启动保留 /sys 只读,先避免 Fedora udev/networkd 改动宿主网络设备。
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed

# 设备透传可以先写好。
lxc.mount.entry = /dev/dri dev/dri none bind,create=dir 0 0
lxc.mount.entry = /dev/dma_heap dev/dma_heap none bind,create=dir,optional 0 0
lxc.mount.entry = /dev/uinput dev/uinput none bind,create=file 0 0
lxc.mount.entry = /dev/input dev/input none bind,create=dir 0 0
lxc.mount.entry = /dev/snd dev/snd none bind,create=dir 0 0
lxc.mount.entry = tmpfs dev/shm tmpfs create=dir,mode=01777,nosuid,nodev 0 0

# binderfs:可选。当前远程环境主要由 Fedora 内的 Waydroid helper 挂载/创建 binderfs。
# 如果你选择在 OpenWrt 宿主挂载 binderfs,也可以用这行 optional bind 传入 Fedora。
lxc.mount.entry = /dev/binderfs dev/binderfs none bind,create=dir,optional 0 0

# loop 设备:Waydroid 镜像挂载需要。
lxc.mount.entry = /dev/loop-control dev/loop-control none bind,create=file 0 0
lxc.mount.entry = /dev/loop0 dev/loop0 none bind,create=file 0 0
lxc.mount.entry = /dev/loop1 dev/loop1 none bind,create=file 0 0
lxc.mount.entry = /dev/loop2 dev/loop2 none bind,create=file 0 0
lxc.mount.entry = /dev/loop3 dev/loop3 none bind,create=file 0 0

# 给内层 Waydroid LXC 使用的辅助挂载点。
lxc.mount.entry = proc dev/.lxc/proc proc create=dir,optional 0 0
lxc.mount.entry = sys dev/.lxc/sys sysfs create=dir,optional 0 0

启动并进入容器:

lxc-start -n waydroid
lxc-attach -n waydroid -- /bin/bash

在 Fedora 里做三件事:

# 1. 禁用会接管网络的服务
systemctl mask systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service 2>/dev/null || true
systemctl disable --now systemd-networkd.service systemd-networkd.socket systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-resolved.service firewalld.service NetworkManager.service wpa_supplicant.service 2>/dev/null || true
systemctl disable --now systemd-resolved.service firewalld.service NetworkManager.service wpa_supplicant.service 2>/dev/null || true

# 2. 禁用网卡自动重命名
mkdir -p /etc/systemd/network
cat > /etc/systemd/network/10-keep-kernel-ifnames.link <<'EOF'
[Match]
OriginalName=*

[Link]
NamePolicy=keep
MACAddressPolicy=none
EOF
ln -sfn /dev/null /etc/systemd/network/99-default.link

# 3. 如果你要最保守,先屏蔽 udev。后面图形会话需要 udev 数据库时,再有控制地恢复。
systemctl mask systemd-udevd.service systemd-udevd-control.socket systemd-udevd-kernel.socket 2>/dev/null || true
systemctl disable --now systemd-udevd.service systemd-udevd-control.socket systemd-udevd-kernel.socket 2>/dev/null || true

退出并停止容器:

exit
lxc-stop -n waydroid

阶段 B:正式运行配置

完成阶段 A 后,再把外层 LXC 改成正式运行配置:

lxc.arch = x86_64
lxc.rootfs.path = dir:/srv/lxc/waydroid/rootfs
lxc.uts.name = waydroid

lxc.tty.max = 2
lxc.pty.max = 1024

lxc.seccomp.profile =
lxc.cap.drop =

# 正式运行:用 veth 接 OpenWrt 的 br-lan。不要用 none 共享宿主网络。
lxc.net.0.type = veth
lxc.net.0.veth.pair = lxc-waydroid
lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:59:af:02

# 正式运行:Waydroid / nested LXC 需要可写 sysfs/cgroup 视图。
lxc.mount.auto = proc:mixed sys:rw cgroup:mixed

lxc.mount.entry = /dev/dri dev/dri none bind,create=dir 0 0
lxc.mount.entry = /dev/dma_heap dev/dma_heap none bind,create=dir,optional 0 0
lxc.mount.entry = /dev/uinput dev/uinput none bind,create=file 0 0
lxc.mount.entry = /dev/input dev/input none bind,create=dir 0 0
lxc.mount.entry = /dev/snd dev/snd none bind,create=dir 0 0
lxc.mount.entry = tmpfs dev/shm tmpfs create=dir,mode=01777,nosuid,nodev 0 0

# binderfs:可选。当前远程环境主要由 Fedora 内的 Waydroid helper 挂载/创建 binderfs。
# 如果你选择在 OpenWrt 宿主挂载 binderfs,也可以用这行 optional bind 传入 Fedora。
lxc.mount.entry = /dev/binderfs dev/binderfs none bind,create=dir,optional 0 0

lxc.mount.entry = /dev/loop-control dev/loop-control none bind,create=file 0 0
lxc.mount.entry = /dev/loop0 dev/loop0 none bind,create=file 0 0
lxc.mount.entry = /dev/loop1 dev/loop1 none bind,create=file 0 0
lxc.mount.entry = /dev/loop2 dev/loop2 none bind,create=file 0 0
lxc.mount.entry = /dev/loop3 dev/loop3 none bind,create=file 0 0

lxc.mount.entry = proc dev/.lxc/proc proc create=dir,optional 0 0
lxc.mount.entry = sys dev/.lxc/sys sysfs create=dir,optional 0 0

如果你的 OpenWrt 主机没有 /dev/dma_heap/system,先回到第 3 节修内核。

4.3 验证外层容器设备

启动 Fedora LXC:

lxc-start -n waydroid
lxc-attach -n waydroid -- /bin/bash -lc '
cat /etc/fedora-release
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system /dev/uinput /dev/input /dev/snd /dev/binderfs 2>/dev/null || true
ls -l /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder 2>/dev/null || true
'

必须看到 /dev/dri/dev/dma_heap/system/dev/binderfs 可能要等 Waydroid container helper 运行后才出现;如果后面 Waydroid 报 binder service manager 不出现,再确认 Fedora 里是否已经有 /dev/binderfs/binder/dev/binderfs/hwbinder/dev/binderfs/vndbinder

4.4 给 Fedora 一个最小可用网络:不用 NetworkManager/networkd

这一节很容易漏。前面我们已经把 NetworkManager / systemd-networkd / systemd-resolved 都 mask 掉了,所以 Fedora LXC 不会自己给 eth0 配 IP。可是第 5 节马上要 dnf install,这时 Fedora 必须先能联网。

最简单的做法是先手动配置一次:

# Fedora LXC 内
ip link set lo up || true
ip link set eth0 up
ip addr replace 192.168.1.237/24 dev eth0      # 按你的 FEDORA_LAN_ADDR 改
ip route replace default via 192.168.1.1 dev eth0
rm -f /etc/resolv.conf
echo 'nameserver 192.168.1.1' > /etc/resolv.conf
chmod 0644 /etc/resolv.conf

ping -c 2 192.168.1.1
curl -I https://mirrors.fedoraproject.org/ 2>/dev/null | head || true

我们当前远程环境还保留了一个很小的 systemd oneshot,用来在启动早期做同样的安全网络初始化。它不启用 NetworkManager,也不启用 systemd-networkd。你可以按自己的 LAN 改成变量化版本:

文件:

/usr/local/bin/waydroid-lxc-network-safe.sh

内容:

#!/bin/bash
set -euo pipefail

PHY=${WAYDROID_PARENT_IF:-eth0}
ADDR=${WAYDROID_LXC_IPV4:-192.168.1.237/24}
GW=${WAYDROID_LXC_GW:-192.168.1.1}
DNS=${WAYDROID_LXC_DNS:-$GW}

ip link set lo up || true
ip link set "$PHY" up

if [ -n "$ADDR" ]; then
  ip addr replace "$ADDR" dev "$PHY"
fi
if [ -n "$GW" ]; then
  ip route replace default via "$GW" dev "$PHY" || true
fi

rm -f /etc/resolv.conf
printf 'nameserver %s\n' "$DNS" > /etc/resolv.conf
chmod 0644 /etc/resolv.conf

授权并写入 service:

chmod 0755 /usr/local/bin/waydroid-lxc-network-safe.sh

cat > /etc/systemd/system/waydroid-lxc-network-safe.service <<'EOF'
[Unit]
Description=Minimal safe network setup for Waydroid Fedora LXC
DefaultDependencies=no
After=local-fs.target
Before=network-pre.target network.target waydroid-container.service
Wants=network-pre.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/waydroid-lxc-network-safe.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

mkdir -p /etc/systemd/system/waydroid-lxc-network-safe.service.d
cat > /etc/systemd/system/waydroid-lxc-network-safe.service.d/10-lan.conf <<EOF
[Service]
Environment=WAYDROID_LXC_IPV4=192.168.1.237/24
Environment=WAYDROID_LXC_GW=192.168.1.1
Environment=WAYDROID_LXC_DNS=192.168.1.1
EOF

systemctl daemon-reload
systemctl enable --now waydroid-lxc-network-safe.service

再给 waydroid-container.service 加一个 drop-in,确保手动或自动启动 Waydroid container 时,都会先跑这个最小网络 service:

mkdir -p /etc/systemd/system/waydroid-container.service.d
cat > /etc/systemd/system/waydroid-container.service.d/20-after-lxc-network-safe.conf <<'EOF'
[Unit]
Wants=waydroid-lxc-network-safe.service
After=waydroid-lxc-network-safe.service
EOF

systemctl daemon-reload

如果后面要启用第 9 节的 waydroid-bridge-setup.service,它会把 eth0 放进 br0,并把地址迁到 br0。这个早期 service 的作用只是保证 Fedora 在没有网络管理器时也能先安装包、解析 DNS,并且让 Waydroid container 启动前网络状态是可预期的。


5. Fedora LXC 内安装基础软件

进入 Fedora:

lxc-attach -n waydroid -- /bin/bash

安装工具和图形/媒体相关包。包名来自我们当前 Fedora 44 实机中已经存在的组件。

先启用 RPM Fusion。原因是 Fedora 默认仓库里的 VAAPI / Mesa / 编解码包会避开一部分有专利或授权限制的编解码能力;我们当前实机启用了 RPM Fusion free/nonfree,并安装了 mesa-va-drivers-freeworld 等包。

dnf install -y \
  https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
  https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

dnf repolist --enabled | grep -Ei 'rpmfusion|fedora|updates' || true

然后安装基础软件和图形/媒体包:

dnf install -y \
  git curl unzip lzip make gcc gcc-c++ \
  python3 python3-devel python3-setuptools python3-Cython python3-pip python3-virtualenv \
  lxc lxc-templates lxcfs \
  android-tools \
  linux-firmware intel-gpu-firmware \
  mesa-dri-drivers mesa-libgbm mesa-libEGL mesa-libGL mesa-vulkan-drivers mesa-va-drivers-freeworld \
  libva libva-utils libva-intel-media-driver intel-media-driver intel-mediasdk \
  ffmpeg-free \
  dbus dbus-broker dbus-daemon dbus-tools dbus-x11 \
  pipewire pipewire-pulseaudio pipewire-utils wireplumber \
  lxqt-session lxqt-wayland-session lxqt-sway-session lxqt-panel lxqt-notificationd lxqt-policykit lxqt-runner lxqt-powermanagement pcmanfm-qt qterminal \
  kwin plasma-workspace-common xdg-desktop-portal xdg-desktop-portal-kde \
  seatd acl Sunshine

如果某个包名在你的 Fedora 仓库里不同,请用 Fedora 的包搜索确认。不要启用 NetworkManager / firewalld 来解决这个部署里的网络问题。

关于 VAAPI 解码器:

  • mesa-va-drivers-freeworld 是 Fedora 上常见的“更完整但不那么 free”的 Mesa VAAPI 包,主要影响 Mesa VAAPI 驱动暴露的编解码能力。
  • Intel 实机还需要 intel-media-driver / libva-intel-media-driver,当前现场 vainfo 使用的是 /usr/lib64/dri-nonfree/iHD_drv_video.so
  • 不同 GPU 要按实际驱动选择包。Intel iGPU 看 iHD/i965,AMD 通常看 Mesa VAAPI/freeworld,不能把 Intel 包照搬成唯一答案。

安装后建议验证:

rpm -qa | grep -Ei 'rpmfusion|libva|vaapi|intel-media|mesa.*va|ffmpeg|openh264' | sort
vainfo --display drm --device /dev/dri/renderD128 2>&1 | tee /root/vainfo-drm.log
grep -E 'VAProfileH264|VAProfileHEVC|VAProfileVP9|VAProfileAV1' /root/vainfo-drm.log || true

我们当前实机能看到 VAProfileHEVCMain10VAProfileVP9Profile2VAProfileAV1Profile0。这些事实只说明 Fedora/VAAPI 层暴露了对应 profile;Android MediaCodec/Codec2 是否使用它,还要看后面 Bonus 章节的 Android logcat。

这里安装 linux-firmware / intel-gpu-firmware 不只是给 Fedora 自己用。OpenWrt 宿主通常没有完整 GPU firmware,前面第 3.6.2 节会用 Fedora rootfs 里的 firmware 文件作为来源,复制并解压到 OpenWrt 的 /lib/firmware/...intel-gpu-firmware 是当前 Intel 实机的例子;其它 GPU 请安装对应 firmware 包。

禁用不需要的网络管理服务:

systemctl mask NetworkManager systemd-networkd systemd-resolved firewalld wpa_supplicant 2>/dev/null || true
systemctl disable --now NetworkManager systemd-networkd systemd-resolved firewalld wpa_supplicant 2>/dev/null || true

验证:

for s in NetworkManager systemd-networkd systemd-resolved firewalld wpa_supplicant; do
  printf '%s: ' "$s"
  systemctl is-active "$s" 2>/dev/null || true
done

期望都是 inactive。


6. 安装 upstream Waydroid / libgbinder / gbinder-python

Fedora 包里的 Waydroid/libgbinder 版本记录为:

waydroid RPM metadata: 1.6.2
libgbinder RPM metadata: 1.1.43
python3-gbinder RPM metadata: 1.3.0

但 Android 16 / API 36 需要 Waydroid 正确使用:

binder_protocol = aidl3
service_manager_protocol = aidl6

我们验证过用 upstream source 覆盖安装。有效 commits:

Waydroid:       73fed11b465138130eb7067c9645db81b32ac929
libgbinder:     680bedcc335586502980f9a28614416352e6f369
gbinder-python: 86b8feba4cacd0952b010d1c3af6a29a0c146ced

在 Fedora LXC 内执行:

systemctl --user -M user@ stop waydroid-full-lxqt.service 2>/dev/null || true
waydroid session stop 2>/dev/null || true
systemctl stop waydroid-container.service 2>/dev/null || true

mkdir -p /usr/local/src

git clone https://github.com/waydroid/waydroid.git /usr/local/src/waydroid-upstream || true
cd /usr/local/src/waydroid-upstream
git fetch --prune origin
git checkout 73fed11b465138130eb7067c9645db81b32ac929
make install PREFIX=/usr USE_SYSTEMD=1 USE_DBUS_ACTIVATION=1
systemctl daemon-reload

git clone https://github.com/mer-hybris/libgbinder.git /usr/local/src/libgbinder-upstream || true
cd /usr/local/src/libgbinder-upstream
git fetch --prune origin --tags
git checkout 680bedcc335586502980f9a28614416352e6f369
make clean || true
make -j"$(nproc)" release pkgconfig
make install-dev PREFIX=/usr LIBDIR=/usr/lib64
ldconfig

git clone https://github.com/waydroid/gbinder-python.git /usr/local/src/gbinder-python-upstream || true
cd /usr/local/src/gbinder-python-upstream
git fetch --prune origin --tags
git checkout 86b8feba4cacd0952b010d1c3af6a29a0c146ced
python3 setup.py build_ext --inplace
python3 setup.py install

检查:

python3 - <<'PY'
import gbinder
print(gbinder.__file__)
PY
pkg-config --modversion libgbinder

期望类似:

/usr/local/lib64/python3.14/site-packages/gbinder.cpython-314-x86_64-linux-gnu.so
1.1.45

7. 安装 WayDroid-ATV 镜像

WayDroid-ATV 已经内置了 arm 转译层。不再需要手动运行脚本安装啦!

WayDroid-ATV 现在有比较人类友好的部署方式。推荐优先使用 Waydroid 命令从 WayDroid-ATV OTA server 初始化和下载镜像;手动下载 zip 只作为备用。

7.1 版本选择:新版 A16 vs 旧版 A13

推荐版本:

WayDroid-ATV Android 16 / Lineage 23.0 / waydroid_tv_x86_64
OTA: https://waydroid-atv.github.io/ota/a16-tv/system
     https://waydroid-atv.github.io/ota/a16-tv/vendor

新版优点:

  • 支持当前我们验证过的 VAAPI / Codec2 方向。
  • 适合继续做 HEVC Main10 / P010 / Android media 测试。

新版注意点:

  • 需要比较新的 Waydroid / libgbinder / gbinder-python。Fedora 包里的 Waydroid 1.6.2 metadata 不够说明实际能力。请按第 6 节拉 upstream source 自己编译安装。
  • 如果 waydroid app list 卡住、waydroidplatform 不通,优先检查第 6 节的 service_manager_protocol = aidl6 和 libgbinder 版本。

旧版测试用镜像:

WayDroid-ATV Android 13 release 20260106
https://github.com/WayDroid-ATV/waydroid-androidtv-builds/releases/tag/20260106
system: lineage-20.0-20260105-GAPPS-waydroid_tv_x86_64-system.zip
vendor: lineage-20.0-20260105-MAINLINE-waydroid_tv_x86_64-vendor.zip

旧版适合用来验证“OpenWrt + Fedora LXC + Waydroid 能不能基本跑通”。但旧版不适合拿来验证当前视频硬解方案:它不具备我们后来验证的最新版 VAAPI / Codec2 / P010 行为。

7.2 方法一:用 waydroid init 接 WayDroid-ATV OTA server(推荐)

在 Fedora LXC 内执行。若你是 root,不需要 sudo;如果是普通用户,请按 Waydroid 常规方式加 sudo

GApps 版本:

waydroid init -f \
  -c https://waydroid-atv.github.io/ota/a16-tv/system \
  -v https://waydroid-atv.github.io/ota/a16-tv/vendor \
  -r lineage \
  -s GAPPS

waydroid upgrade

如果你想要非 GApps build,把 GAPPS 换成 VANILLA

waydroid init -f \
  -c https://waydroid-atv.github.io/ota/a16-tv/system \
  -v https://waydroid-atv.github.io/ota/a16-tv/vendor \
  -r lineage \
  -s VANILLA

waydroid upgrade

下载完成后确认:

sed -n '1,160p' /var/lib/waydroid/waydroid.cfg
ls -lah /var/lib/waydroid/images /etc/waydroid-extra/images 2>/dev/null || true

如果你希望固定使用 /etc/waydroid-extra/images,请确认:

images_path = /etc/waydroid-extra/images

否则按 waydroid.cfg 实际 images_path 继续,不要猜。

7.3 方法二:手动下载 zip 并安装

如果 OTA 下载不可用,可以手动下载 SourceForge 上的:

lineage-23.0-xxxxxxx-GAPPS-waydroid_tv_xxxxx-system.zip
lineage-23.0-xxxxxxx-MAINLINE-waydroid_tv_xxxxx-vendor.zip

如果想要非 GApps build,把 GAPPS 换成 VANILLA

解压出:

system.img
vendor.img

复制到 Fedora LXC 内:

mkdir -p /etc/waydroid-extra/images/
cp /path/to/system.img /etc/waydroid-extra/images/system.img
cp /path/to/vendor.img /etc/waydroid-extra/images/vendor.img

然后重新初始化:

waydroid init -f

我们的早期 A16 实机记录使用过 release 20260403,zip sha256 是:

system zip: ddf5995f9104fe7164ad326def8bbed5cbf6340b55da5012e240b5855c44953b
vendor zip: 8b8b176c7d3b34719889f3c891d86b98af8dbbb2f737a970e89302fa3d0cf077

这只是我们当时验证的版本,不代表你部署时必须固定到这个日期。通用部署建议优先使用 7.2 的 OTA server。

7.4 全新初始化时的清理边界

如果是全新初始化,可以停止 Waydroid 后清理旧数据:

waydroid session stop 2>/dev/null || true
systemctl stop waydroid-container.service 2>/dev/null || true
rm -rf /home/user/.local/share/waydroid
rm -rf /var/lib/waydroid/overlay /var/lib/waydroid/overlay_rw /var/lib/waydroid/lxc

如果你是在维护已有环境,不要直接删除这些目录。overlay_rw/vendor 里可能有已经部署的 media/Codec2 修复文件。

7.5 配置 Waydroid 使用正确协议和镜像目录

确认 /var/lib/waydroid/waydroid.cfg 有:

[waydroid]
arch = x86_64
vendor_type = MAINLINE
mount_overlays = True
binder = binder
vndbinder = vndbinder
hwbinder = hwbinder
binder_protocol = aidl3
service_manager_protocol = aidl6

images_path 可以是 OTA 初始化生成的位置,也可以是:

images_path = /etc/waydroid-extra/images

请以你实际文件位置为准。

7.6 初始化后立刻禁用 Android 蓝牙

第一次能进入 Android shell 后,请按第 0.5 节禁用蓝牙:

waydroid shell -- sh -c '
setprop persist.bluetooth.disabled true
settings put global bluetooth_on 0
settings put global ble_scan_always_enabled 0
cmd bluetooth_manager disable 2>/dev/null || true
pm disable-user --user 0 com.android.bluetooth 2>/dev/null || true
pm disable com.android.bluetooth 2>/dev/null || true
'

不要等系统长时间运行后再处理。这个问题和 OpenWrt /tmp 是 tmpfs 叠在一起时,可能会变成内存压力和重启问题。


8. 配置 Fedora 图形会话:systemd user + LXQt + KWin

8.1 创建桌面用户,并加入图形/音频/输入需要的 groups

当前验证过的桌面用户状态是:

uid=1000(user) gid=1000(user) groups=1000(user),10(wheel),39(video),63(audio),104(input),105(render),992(seat)

这些 groups 很重要:

  • video / render:访问 DRM/KMS、render node。
  • audio:访问 ALSA/PipeWire 相关音频设备。
  • input:访问输入设备。
  • seat:配合 seatd / 桌面会话管理座席设备。
  • wheel:方便需要管理员权限时使用。

不同 Fedora rootfs 里的 group 数字可能不同,不要手写 gid。请按 group 名称添加。

如果还没有 user

useradd -m -u 1000 user 2>/dev/null || true

确保必要 groups 存在,并把用户加入:

for g in wheel video audio input render seat; do
  getent group "$g" >/dev/null || groupadd "$g"
  usermod -aG "$g" user
done

loginctl enable-linger user
id user

期望 id user 至少包含:

wheel video audio input render seat

音频设备 ACL:

setfacl -m u:user:rw /dev/snd/* 2>/dev/null || true

8.2 写入 waydroid-full-lxqt.service

文件:

/home/user/.config/systemd/user/waydroid-full-lxqt.service

创建目录:

mkdir -p /home/user/.config/systemd/user
chown -R user:user /home/user/.config

写入:

[Unit]
Description=Full LXQt Wayland desktop session for Waydroid
After=dbus.socket pipewire.socket pipewire-pulse.socket
Wants=dbus.socket pipewire.socket pipewire-pulse.socket graphical-session.target
BindsTo=graphical-session.target
Before=graphical-session.target

[Service]
Type=simple
UnsetEnvironment=WLR_LIBINPUT_NO_DEVICES WAYLAND_DISPLAY DISPLAY SWAYSOCK I3SOCK
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=XDG_SESSION_TYPE=wayland
Environment=LIBSEAT_BACKEND=noop
Environment=QT_QPA_PLATFORM=wayland
ExecStart=/usr/sbin/startlxqtwayland
Restart=on-failure
RestartSec=2s

[Install]
WantedBy=default.target

启用:

runuser -u user -- env \
  XDG_RUNTIME_DIR=/run/user/1000 \
  DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
  systemctl --user daemon-reload

runuser -u user -- env \
  XDG_RUNTIME_DIR=/run/user/1000 \
  DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
  systemctl --user enable --now waydroid-full-lxqt.service

8.3 Waydroid UI 启动脚本

当前验证过的最终方式是:waydroid-autostart-session.shsunshine 作为 LXQt 会话内进程运行,而不是作为单独 enabled user service 运行。

写入:

/usr/local/bin/waydroid-autostart-session.sh

内容:

#!/bin/bash
set -euo pipefail

if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
  echo "XDG desktop session environment is incomplete" >&2
  exit 1
fi

for _ in $(seq 1 90); do
  [ -S "$XDG_RUNTIME_DIR/bus" ] && [ -n "${WAYLAND_DISPLAY:-}" ] && [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ] && break
  sleep 1
done

if [ -z "${WAYLAND_DISPLAY:-}" ] || [ ! -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; then
  echo "Wayland session environment is not ready" >&2
  exit 1
fi

while true; do
  if ! systemctl is-active --quiet waydroid-container.service; then
    systemctl start waydroid-container.service || true
  fi

  if waydroid status 2>/dev/null | grep -Eq 'Session:[[:space:]]*RUNNING'; then
    sleep 30
    continue
  fi

  /usr/local/bin/waydroid-show-full-ui-after-start.sh >/tmp/waydroid-show-full-ui-after-start.log 2>&1 &
  /usr/sbin/waydroid session start || true
  sleep 5
done

写入:

/usr/local/bin/waydroid-show-full-ui-after-start.sh

内容:

#!/bin/bash
set -euo pipefail

if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] || [ -z "${WAYLAND_DISPLAY:-}" ]; then
  echo "XDG desktop session environment is incomplete" >&2
  exit 1
fi

for _ in $(seq 1 90); do
  if waydroid status 2>/dev/null | grep -Eq 'Container:[[:space:]]*RUNNING'; then
    break
  fi
  sleep 1
done

sleep 5
/usr/sbin/waydroid show-full-ui || true

授权:

chmod 0755 /usr/local/bin/waydroid-autostart-session.sh /usr/local/bin/waydroid-show-full-ui-after-start.sh

你需要让 LXQt session 自己启动:

/usr/local/bin/waydroid-autostart-session.sh
/usr/sbin/sunshine

我们当前实机是由 LXQt session 拉起它们。不同 LXQt 配置可以用 autostart desktop 文件、LXQt session 设置或等效方式。重点是不要手动伪造不完整的 XDG session。

8.4 总启动脚本

写入:

/usr/local/bin/start-waydroid.sh

内容:

#!/bin/bash
set -euo pipefail

loginctl enable-linger user >/dev/null 2>&1 || true
systemctl daemon-reload
systemctl enable --now waydroid-bridge-setup.service waydroid-container.service

# 用户所属 groups 和设备权限请在第 8.1 节部署时一次性设置。
# 启动脚本不要每次反复 usermod / setfacl。

runuser -u user -- env \
  XDG_RUNTIME_DIR=/run/user/1000 \
  DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
  systemctl --user daemon-reload

runuser -u user -- env \
  XDG_RUNTIME_DIR=/run/user/1000 \
  DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
  systemctl --user enable --now waydroid-full-lxqt.service

runuser -u user -- env \
  XDG_RUNTIME_DIR=/run/user/1000 \
  DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
  systemctl --user --no-pager status waydroid-full-lxqt.service

授权:

chmod 0755 /usr/local/bin/start-waydroid.sh

9. 配置桥接网络

目标:让 Android 直接出现在 OpenWrt LAN 中,而不是使用默认 waydroid0 NAT。

最终结构:

OpenWrt br-lan
└── 外层 Fedora eth0
    └── Fedora 内 br0
        ├── Fedora 自己的 IPv4 地址
        └── Waydroid Android veth
            └── Android eth0 通过 DHCP 获取 LAN 地址

9.1 Fedora 内 bridge setup 脚本

写入:

/usr/local/sbin/waydroid-bridge-setup

内容:

#!/usr/bin/env bash
set -euo pipefail
BR=${WAYDROID_BRIDGE:-br0}
PHY=${WAYDROID_PARENT_IF:-eth0}
DEFAULT_IPV4=${WAYDROID_BRIDGE_IPV4:-}
DEFAULT_GW=${WAYDROID_BRIDGE_GW:-}

if ! ip link show "$PHY" >/dev/null 2>&1; then
  echo "parent interface $PHY does not exist" >&2
  exit 1
fi

PHY_MAC=$(cat /sys/class/net/$PHY/address)
PHY_MTU=$(cat /sys/class/net/$PHY/mtu)
CURRENT4=$(ip -o -4 addr show dev "$BR" scope global 2>/dev/null | awk 'NR==1{print $4}' || true)
PHY4=$(ip -o -4 addr show dev "$PHY" scope global 2>/dev/null | awk 'NR==1{print $4}' || true)
GW4=$(ip -4 route show default dev "$BR" 2>/dev/null | awk 'NR==1{for(i=1;i<=NF;i++) if($i=="via") print $(i+1)}' || true)
PHY_GW4=$(ip -4 route show default dev "$PHY" 2>/dev/null | awk 'NR==1{for(i=1;i<=NF;i++) if($i=="via") print $(i+1)}' || true)

if ! ip link show "$BR" >/dev/null 2>&1; then
  ip link add name "$BR" type bridge
fi
ip link set dev "$BR" address "$PHY_MAC" || true
ip link set dev "$BR" mtu "$PHY_MTU" || true
ip link set dev "$BR" type bridge stp_state 0 forward_delay 0 || true
ip link set dev "$BR" up

if [ "$(basename "$(readlink -f /sys/class/net/$PHY/master 2>/dev/null || true)" || true)" != "$BR" ]; then
  ip addr flush dev "$PHY" || true
  ip link set dev "$PHY" master "$BR"
fi
ip link set dev "$PHY" up

ADDR="${CURRENT4:-${PHY4:-$DEFAULT_IPV4}}"
GW="${GW4:-${PHY_GW4:-$DEFAULT_GW}}"
if [ -n "$ADDR" ] && ! ip -4 addr show dev "$BR" | grep -q "${ADDR%/*}"; then
  ip addr add "$ADDR" dev "$BR" 2>/dev/null || true
fi
if [ -n "$GW" ]; then
  ip route replace default via "$GW" dev "$BR" 2>/dev/null || true
fi
DNS="${WAYDROID_BRIDGE_DNS:-$GW}"
if [ -n "$DNS" ]; then
  rm -f /etc/resolv.conf
  printf 'nameserver %s\n' "$DNS" > /etc/resolv.conf
  chmod 0644 /etc/resolv.conf
fi
ip link set dev "$BR" promisc on || true

授权:

chmod 0755 /usr/local/sbin/waydroid-bridge-setup

9.2 systemd 服务

写入:

/etc/systemd/system/waydroid-bridge-setup.service

内容:

[Unit]
Description=Prepare br0 bridge for bridged Waydroid Android container
DefaultDependencies=no
After=local-fs.target waydroid-lxc-network-safe.service
Before=waydroid-container.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/waydroid-bridge-setup
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

按你的 LAN 写入 drop-in:

mkdir -p /etc/systemd/system/waydroid-bridge-setup.service.d
cat > /etc/systemd/system/waydroid-bridge-setup.service.d/10-lan.conf <<EOF2
[Service]
Environment=WAYDROID_BRIDGE_IPV4=${FEDORA_LAN_ADDR:-192.168.1.237/24}
Environment=WAYDROID_BRIDGE_GW=${OPENWRT_LAN_GATEWAY:-192.168.1.1}
Environment=WAYDROID_BRIDGE_DNS=${OPENWRT_LAN_GATEWAY:-192.168.1.1}
EOF2

启用:

systemctl daemon-reload
systemctl enable --now waydroid-bridge-setup.service

9.3 修改 Waydroid inner LXC 网络模板

需要把 Waydroid inner LXC 从默认 waydroid0 改到 br0。涉及文件:

/var/lib/waydroid/lxc/waydroid/config
/usr/lib/waydroid/data/configs/config_3
/usr/lib/waydroid/data/configs/config_1

你要确认这些文件里的 Waydroid 网络配置使用:

lxc.net.0.type = veth
lxc.net.0.link = br0
lxc.net.0.flags = up
lxc.net.0.mtu = 1450

不同 Waydroid 版本模板内容可能略有不同。修改前请备份:

TS=$(date +%Y%m%d-%H%M%S)
mkdir -p /root/waydroid-bridge-backup-$TS
cp -a /var/lib/waydroid/lxc/waydroid/config /root/waydroid-bridge-backup-$TS/ 2>/dev/null || true
cp -a /usr/lib/waydroid/data/configs/config_1 /root/waydroid-bridge-backup-$TS/ 2>/dev/null || true
cp -a /usr/lib/waydroid/data/configs/config_3 /root/waydroid-bridge-backup-$TS/ 2>/dev/null || true

验证:

ip -br addr
bridge link
waydroid shell -- sh -c 'ip addr show eth0; dumpsys ethernet | head -80'

waydroid status 仍可能显示 IP address: UNKNOWN。桥接模式下请以 Android eth0 和 OpenWrt DHCP lease 为准。


10. Waydroid 内层 LXC:config_nodes、cgroup2 子树和 cgroup:rw

10.1 自动生成的 config_nodes:不要漏 DMA-BUF 和 binderfs

Waydroid 会根据 Fedora 外层容器里能看到的设备生成内层 Android LXC 的设备挂载,文件是:

/var/lib/waydroid/lxc/waydroid/config_nodes

当前记录里,/dev/dma_heap/system 是由 Waydroid helper 扫描 /dev/dma_heap/* 后自动写入 config_nodes 的。外层 Fedora 看不到 /dev/dma_heap/system 时,内层 Android 通常也看不到它。

请在启动 Waydroid 前后检查:

sed -n '1,220p' /var/lib/waydroid/lxc/waydroid/config_nodes 2>/dev/null || true
grep -E 'dma_heap|binder|hwbinder|vndbinder|dri|uinput|snd' /var/lib/waydroid/lxc/waydroid/config_nodes 2>/dev/null || true

期望至少能看到类似:

lxc.mount.entry = /dev/dri/renderD128 dev/dri/renderD128 none bind,create=file,optional 0 0
lxc.mount.entry = /dev/dma_heap/system dev/dma_heap/system none bind,create=file,optional 0 0
lxc.mount.entry = /dev/binderfs/binder dev/binder none bind,create=file,optional 0 0
lxc.mount.entry = /dev/binderfs/hwbinder dev/hwbinder none bind,create=file,optional 0 0
lxc.mount.entry = /dev/binderfs/vndbinder dev/vndbinder none bind,create=file,optional 0 0

不同 Waydroid 版本生成的顺序和附加参数可能不同,不要只按行号判断。重点是:

  • Android 内层要能看到 /dev/dri/renderD128
  • Android 内层要能看到 /dev/dma_heap/system
  • Android 内层要通过 /dev/binder/dev/hwbinder/dev/vndbinder 使用宿主 binderfs 对应设备。

如果你是在修复宿主内核后才出现 /dev/dma_heap/system,请重启 Waydroid container/session,让 config_nodes 重新生成:

waydroid session stop 2>/dev/null || true
systemctl restart waydroid-container.service
sed -n '1,220p' /var/lib/waydroid/lxc/waydroid/config_nodes 2>/dev/null || true

如果你曾经手动删除过 config_nodes,也先用 Waydroid 自己的 container start 流程重新生成。不要把缺设备的问题误判成 Android media、SurfaceFlinger 或播放器问题。

10.2 cgroup2 子树和 cgroup:rw

这一节非常重要。外层 Fedora LXC 能启动,不代表 Waydroid 的内层 Android LXC 能稳定启动。Android 里面的 initlibprocessgrouplmkd 需要在 cgroup2 上创建自己的 uid_* / pid_* 子目录;所以内层 LXC 必须在一个可委托的 cgroup2 子树里运行,并看到可写 cgroup。

如果内层 Waydroid LXC 仍然是只读 cgroup:

lxc.mount.auto = cgroup:ro sys:ro proc

遇到 Android 启动几秒退出、libprocessgroup 报只读、lmkd 反复重启时,请先修这里。

10.2.1 修改 Waydroid LXC config 模板

需要同时改当前生成文件和模板文件:

/var/lib/waydroid/lxc/waydroid/config
/usr/lib/waydroid/data/configs/config_base

请注意:waydroid init -fwaydroid upgrade、包升级、或某些重新生成 LXC 配置的操作,可能把 /var/lib/waydroid/lxc/waydroid/config/usr/lib/waydroid/data/configs/config_base 改回默认值。我们远程环境里就观察到过运行态又回到:

lxc.mount.auto = cgroup:ro sys:ro proc
lxc.hook.post-stop = /dev/null

Android 当时仍能运行,但宿主 dmesg 里有不少 libprocessgroup 找不到 /sys/fs/cgroup/system/uid_*/dev/blkio/dev/cpuset 的噪音。通用部署建议仍按下面的 cgroup:rw 和明确 cgroup2 子树做,配置完后一定重新 grep 确认。

把:

lxc.mount.auto = cgroup:ro sys:ro proc

改成:

lxc.mount.auto = cgroup:rw sys:ro proc

并加入:

# Keep Android in an explicitly delegated cgroup2 subtree.
# The payload runs in a leaf so Android init can create uid_* and process-group children below it.
lxc.cgroup.dir.container = waydroid-android
lxc.cgroup.dir.monitor = waydroid-android-monitor
lxc.cgroup.dir.monitor.pivot = waydroid-android-monitor-pivot
lxc.cgroup.dir.container.inner = payload
lxc.hook.pre-start = /var/lib/waydroid/lxc/waydroid/waydroid-cgroup2-delegate.sh

同时检查是否还有这行:

lxc.hook.post-stop = /dev/null

我们的早期日志里,这行会让 LXC 在 Android 退出时尝试执行 /dev/null,并报:

Script exec /dev/null ... Permission denied
Script exited with status 126
Failed to run lxc.hook.post-stop

当前稳定配置里已经把它注释掉:

# lxc.hook.post-stop disabled

如果你看到同样的 status 126,请删除或注释 lxc.hook.post-stop = /dev/null。这个报错不一定是 Android 崩溃根因,但会干扰判断。

10.2.2 写入 pre-start delegation 脚本

文件:

/var/lib/waydroid/lxc/waydroid/waydroid-cgroup2-delegate.sh

内容:

#!/bin/sh
set -eu

LOG=/var/lib/waydroid/cgroup2-delegate.log
ROOT=/sys/fs/cgroup
CTR="$ROOT/waydroid-android"
PAYLOAD="$CTR/payload"
MONITOR="$ROOT/waydroid-android-monitor"
PIVOT="$ROOT/waydroid-android-monitor-pivot"

{
    echo "=== $(date -Is) pre-start cgroup2 delegation ==="
    echo "self=$(cat /proc/self/cgroup 2>/dev/null || true)"
    findmnt -n -T "$ROOT" 2>/dev/null || true

    if [ ! -f "$ROOT/cgroup.controllers" ]; then
        echo "no cgroup2 controllers file at $ROOT"
        exit 0
    fi

    # LXC should create final monitor/payload cgroups itself.
    # Remove stale empty dirs from previous failed tests, but do not pre-create leaf cgroups.
    for stale in "$PAYLOAD" "$CTR" "$MONITOR" "$PIVOT"; do
        if [ -d "$stale" ]; then
            rmdir "$stale" 2>/dev/null || true
        fi
    done

    # Open every available cgroup2 controller at the root level.
    if [ -f "$ROOT/cgroup.controllers" ] && [ -w "$ROOT/cgroup.subtree_control" ]; then
        for controller in $(cat "$ROOT/cgroup.controllers"); do
            echo "+$controller" > "$ROOT/cgroup.subtree_control" 2>/dev/null || true
        done
    fi

    echo "$ROOT controllers=$(cat "$ROOT/cgroup.controllers" 2>/dev/null || true)"
    echo "$ROOT subtree=$(cat "$ROOT/cgroup.subtree_control" 2>/dev/null || true)"
    echo "$ROOT procs=$(wc -l < "$ROOT/cgroup.procs" 2>/dev/null || echo '?')"
    echo "planned container=$CTR inner=payload monitor=$MONITOR pivot=$PIVOT"
} >> "$LOG" 2>&1

exit 0

授权:

chmod 0755 /var/lib/waydroid/lxc/waydroid/waydroid-cgroup2-delegate.sh

10.2.3 验证 cgroup 子树

启动 Waydroid 后先确认配置没有被重新生成覆盖:

grep -nE 'lxc.mount.auto|lxc.cgroup.dir|pre-start|post-stop' /var/lib/waydroid/lxc/waydroid/config /usr/lib/waydroid/data/configs/config_base 2>/dev/null || true

再检查 cgroup 子树:

sed -n '1,160p' /var/lib/waydroid/cgroup2-delegate.log 2>/dev/null || true
find /sys/fs/cgroup -maxdepth 3 -type d | grep -E 'waydroid-android|payload|monitor' || true

for d in /sys/fs/cgroup/waydroid-android /sys/fs/cgroup/waydroid-android/payload; do
  [ -d "$d" ] || continue
  echo "-- $d --"
  echo "controllers=$(cat "$d/cgroup.controllers" 2>/dev/null || true)"
  echo "subtree=$(cat "$d/cgroup.subtree_control" 2>/dev/null || true)"
  echo "procs=$(cat "$d/cgroup.procs" 2>/dev/null | tr '\n' ' ' || true)"
done

期望能看到类似:

/sys/fs/cgroup controllers=cpuset cpu io memory pids rdma
/sys/fs/cgroup subtree=cpuset cpu io memory pids rdma
/sys/fs/cgroup/waydroid-android
/sys/fs/cgroup/waydroid-android/payload

如果 Android 日志里还有:

libprocessgroup: Failed to make and chown /sys/fs/cgroup/uid_...: Read-only file system

说明内层 Waydroid LXC 仍然没有拿到可写 cgroup2 子树。先修这一节,不要先去改播放器或 media 配置。

10.2.4 lmkd 要使用 cgroup2 / PSI 策略

如果 lmkd 报:

Old kill strategy can only be used with v1 cgroup hierarchy
Kernel does not support memory pressure events or in-kernel low memory killer
critical process 'lmkd' exited ... before boot completed

需要让 Android 使用 PSI / cgroup2 策略。早期稳定配置用过这些 vendor properties:

ro.lmk.use_new_strategy=true
ro.lmk.use_psi=true
ro.lmk.use_minfree_levels=false
ro.lmk.kill_heaviest_task=true
ro.lmk.psi_partial_stall_ms=70
ro.lmk.psi_complete_stall_ms=700
persist.device_config.lmkd_native.use_psi=true

这些属性是否需要放入当前镜像,要看你使用的 Android build。原则是:不要让 lmkd 走旧的 v1 memcg / vmpressure 策略。

完成 10.2 后,再重启 Waydroid:

waydroid session stop || true
systemctl restart waydroid-container.service
/usr/local/bin/start-waydroid.sh

11. 启动

11.1 OpenWrt 启动外层 Fedora

lxc-start -n waydroid

进入 Fedora:

lxc-attach -n waydroid -- /bin/bash

11.2 Fedora 内启动 Waydroid 和桌面会话

/usr/local/bin/start-waydroid.sh

检查:

systemctl is-active waydroid-container.service
systemctl --user -M user@ is-active waydroid-full-lxqt.service
waydroid status

期望:

active
active
Session: RUNNING
Container: RUNNING

11.3 如果 waydroid-autostart.servicesunshine-wayland.service 是 inactive

这不一定是错。我们当前验证过的状态是:

  • waydroid-full-lxqt.service active。
  • sunshine 是 LXQt session 下的进程。
  • waydroid-autostart-session.sh 是 LXQt session 下的进程。
  • 单独的 waydroid-autostart.service / sunshine-wayland.service 可以 disabled / inactive。

检查进程:

ps -eo user,pid,ppid,stat,cmd | grep -E 'kwin|lxqt|waydroid|sunshine' | grep -v grep
ls -la /run/user/1000 | grep wayland

期望看到:

kwin_wayland ... --socket wayland-0
lxqt-session
sunshine
waydroid-autostart-session.sh
wayland-0

12. 验证

12.1 宿主验证

在 OpenWrt:

uname -r
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system 2>/dev/null || true

12.2 Fedora 验证

在 Fedora:

cat /etc/fedora-release
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system 2>/dev/null || true
ip -br addr
bridge link
systemctl is-active waydroid-container.service
waydroid status

12.3 Android 验证

waydroid shell -- sh -c '
getprop ro.build.version.release
getprop ro.lineage.version
getprop sys.boot_completed
ls -ld /dev/dma_heap /dev/dma_heap/system /dev/dri /dev/dri/renderD128 /dev/binder /dev/hwbinder /dev/vndbinder 2>/dev/null
service check activity
service check window
service check display
service check SurfaceFlinger
service check waydroidplatform
'

期望:

16
23.0-20260403-UNOFFICIAL-WayDroidATV-waydroid_tv_x86_64
1
Service activity: found
Service window: found
Service display: found
Service SurfaceFlinger: found
Service waydroidplatform: found

13. Bonus:Android media / Codec2 / VAAPI / P010 支持(可选)

如果你只是想先把 Waydroid ATV 跑起来,可以跳过第 13 节,直接看第 14 节或按第 16 节做最终检查。需要调视频硬解、HEVC Main10/P010 时,再回来读这一节。

13.1 默认镜像能启动,不等于 Main10/P010 完整

Bonus 方向里,我们后来额外修过 stagefright-pluginswaydroid-init,用于改善 Android MediaCodec/Codec2 下 HEVC Main10 / P010 / VAAPI 行为。

关键目标:

  • HEVC Main10 时,c2.ffmpeg.hevc.decoder 能动态输出 P010。
  • VAAPI hardware frame download 不把 P010 错误降成 8-bit。
  • HEVC decoder capability 暴露 Main10 profile。
  • waydroid-init 暴露 host codec profiles。
  • 全局 debug.ffmpeg-codec2.pixel_format 保持 YUV_420,避免 H.264 / VP9 被全局强制 P010。

13.2 如果你有 Android 源码树

源码树目标:

WayDroid-ATV Android 16 tree
lunch target: lineage_waydroid_tv_x86_64 bp2a userdebug

构建命令:

cd /path/to/android-waydroid-atv23
export ANDROID_USE_GAPPS=false
export ALLOW_MISSING_DEPENDENCIES=true
export SOONG_ALLOW_MISSING_DEPENDENCIES=true
export DISABLE_DEXPREOPT_CHECK=true
source build/envsetup.sh
lunch lineage_waydroid_tv_x86_64 bp2a userdebug

m -j8 android.hardware.media.c2-ffmpeg-service media_codecs_ffmpeg_c2.xml android.hardware.media.c2-ffmpeg.policy
m -j8 waydroid-init

产物:

out/target/product/waydroid_tv_x86_64/vendor/bin/hw/android.hardware.media.c2-ffmpeg-service
out/target/product/waydroid_tv_x86_64/vendor/etc/media_codecs_ffmpeg_c2.xml
out/target/product/waydroid_tv_x86_64/vendor/etc/seccomp_policy/android.hardware.media.c2-ffmpeg.policy
out/target/product/waydroid_tv_x86_64/vendor/etc/vintf/manifest/manifest_media_c2_ffmpeg.xml
out/target/product/waydroid_tv_x86_64/vendor/bin/waydroid-init
out/target/product/waydroid_tv_x86_64/vendor/etc/init/waydroid-init.rc

相关上游工作:

stagefright-plugins branch: [main10-p010-codec2](https://github.com/WayDroid-ATV/android_external_stagefright-plugins/pull/1)
waydroid-init branch: [auto-p010-for-main10](https://github.com/WayDroid-ATV/android_vendor_waydroid_init/pull/1)

13.3 部署到 vendor overlay

不要直接修改 vendor.img。使用 Waydroid vendor overlay upperdir:

/var/lib/waydroid/overlay_rw/vendor

在 Fedora LXC 中备份:

TS=$(date +%Y%m%d-%H%M%S)
BACKUP=/root/waydroid-media-backup-$TS
mkdir -p "$BACKUP"
cp -a /var/lib/waydroid/overlay_rw/vendor "$BACKUP/vendor-overlay-rw" 2>/dev/null || true

复制产物到:

/var/lib/waydroid/overlay_rw/vendor/bin/hw/android.hardware.media.c2-ffmpeg-service
/var/lib/waydroid/overlay_rw/vendor/etc/media_codecs_ffmpeg_c2.xml
/var/lib/waydroid/overlay_rw/vendor/etc/seccomp_policy/android.hardware.media.c2-ffmpeg.policy
/var/lib/waydroid/overlay_rw/vendor/etc/vintf/manifest/manifest_media_c2_ffmpeg.xml
/var/lib/waydroid/overlay_rw/vendor/bin/waydroid-init

授权:

chmod 0755 /var/lib/waydroid/overlay_rw/vendor/bin/hw/android.hardware.media.c2-ffmpeg-service
chmod 0755 /var/lib/waydroid/overlay_rw/vendor/bin/waydroid-init

部署完这些文件后,请确认第 10 节的 Waydroid inner LXC config_nodes 和 cgroup2 配置已经完成,再重启 Waydroid。

13.4 期望属性

进入 Android:

waydroid shell -- sh -c '
getprop debug.ffmpeg-codec2.pixel_format
getprop debug.ffmpeg-codec2.hwaccel.drm
getprop ro.waydroid.hwcodecs
getprop ro.waydroid.hwcodec_profiles
getprop ro.hardware.gralloc
getprop ro.hardware.egl
getprop ro.hardware.vulkan
getprop media.sf.hwaccel
'

期望:

YUV_420
1
MPG2H264VP80HEVCVP90AV10
HEVCMain10,VP9Profile2,AV1Profile0
minigbm
mesa
intel
1

注意:debug.ffmpeg-codec2.pixel_format=YUV_420 是当前安全设计,不是错误。HEVC Main10 由 codec component 根据 VAAPI frame context 动态切换到 P010。


13.5 Bonus 验证:视频播放验证(可选)

如果你只是要先跑起 Waydroid ATV,可以先跳过本节。这里验证的是 Android MediaCodec / Codec2 / C2FFMPEG / VAAPI / P010 这一组可选视频能力。

13.5.1 推荐测试方式

要验证 Android MediaCodec / Codec2 / C2FFMPEG / VAAPI,不要只看“某个播放器能播放”。请看 logcat。

用一个 2160p HEVC Main10 样片。我们测试过的样片事实是:

codec_name=hevc
profile=Main 10
width=3840
height=2160
pix_fmt=yuv420p10le

用 Next Player 或其它会调用 Android MediaCodec 的播放器播放。

同时抓日志:

waydroid logcat | grep -E 'CCodec|Codec2|C2FFMPEG|HWACCEL|MediaCodec|pixel-format|color-format'

关键日志应该类似:

allocate(c2.ffmpeg.hevc.decoder)
Available Codec2 services: "ffmpeg" "software"
createComponent: c2.ffmpeg.hevc.decoder
ffmpeg_hwaccel_init: ... [hevc], hw device = vaapi
outputFrame: pixel format changed - 0x36
raw.pixel-format.value = 54
android._color-format = 54
playbackState=PLAYING
playbackState=STOPPED

其中:

0x36 / 54 = HAL_PIXEL_FORMAT_YCBCR_P010 / YCBCR_P010

13.5.2 已观察到的结论边界

已经观察到:

  • H.264 2160p 样片没有被强制 P010,可以播放到结束。
  • VP9 Profile 0 1080p 样片没有被强制 P010,可以播放到结束。
  • HEVC Main10 2160p 样片会动态切到 P010,可以播放到结束。
  • Emby HEVC Main10 用户测试改善明显。

不能由这些事实推出:

  • 所有 HDR 都正常。
  • 所有 Dolby Vision 都正常。
  • Emby AV1 HDR 已解决。
  • VLC 能播放就说明 Android MediaCodec 正常。

VLC 可以播放某个文件,只能说明 VLC 可以播放这个文件。VLC 可能使用自己的 ffmpeg 解码和 GLES 输出,不一定经过 Android MediaCodec / Codec2。


14. 可选:OpenWrt 4G swap

如果机器内存压力大,可以在 OpenWrt 宿主加 swap。我们用过 4G:

dd if=/dev/zero of=/swapfile bs=1M count=4096 conv=fsync
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

BusyBox 注意:

  • swapon --show 可能不支持。
  • dd status=progress 可能不支持。

检查:

cat /proc/swaps
free

如果 OpenWrt 没有 /etc/init.d/fstab,可以写一个简单 init 脚本开机 swapon /swapfile。脚本只应在 /swapfile 存在且没有激活时执行 swapon


14.5 最容易漏的部署细节复盘

按已经跑通的实机重新核对后,下面这些点最容易在通用部署时漏掉:

  1. Fedora 断开 NetworkManager/networkd 后,要先有最小网络配置。 可以用第 4.4 节的 waydroid-lxc-network-safe.service,先给 Fedora 的 eth0 配 LAN IP、默认网关和 /etc/resolv.conf。否则第 5 节 dnf install 很容易没网。
  2. binderfs 不一定由 OpenWrt 宿主直接 bind 进 Fedora。 也可以由 Fedora 内的 Waydroid helper 创建 /dev/binderfs/*/dev/binder symlink。判断成功与否看 Fedora 和 Android 最终是否能访问 binder/hwbinder/vndbinder。
  3. Waydroid LXC config 可能被重新生成。 waydroid init -fwaydroid upgrade 或包升级后,lxc.mount.auto 可能回到 cgroup:rolxc.hook.post-stop = /dev/null 也可能回来。按第 10.2 节修完后,重启 Waydroid 前后都要 grep 确认。
  4. waydroid statusIP address: UNKNOWN 不代表 Android 没网。 桥接模式下请用 lxc-info -P /var/lib/waydroid/lxc -n waydroid、Android ip addr show eth0、OpenWrt DHCP lease 来判断真实网络。
  5. OpenWrt swap 建议作为低内存机器的可选项。 内存压力大的路由器可以按第 14 节加 swap,避免日志、图形会话、Android 服务叠加后太容易触发内存压力。
  6. 蓝牙要在 Android 层禁用并验证。 期望看到 persist.bluetooth.disabled=truebluetooth_on=0com.android.bluetooth 在 disabled packages 里。service list 仍可能显示 bluetooth_manager,这不等于蓝牙 app 又启用了。
  7. Bonus media overlay 不是“先跑起来”的必要步骤。 /var/lib/waydroid/overlay_rw/vendor 里的 android.hardware.media.c2-ffmpeg-servicewaydroid-init、ffmpeg libs 和 media XML 属于第 13 节视频硬解方向。基础部署先跳过也可以。

15. 常见问题

15.0 OpenWrt /tmp 被写满或内存上涨

先确认是不是日志写到了 tmpfs:

# OpenWrt
mount | grep ' /tmp '
df -h /tmp
free

如果 /tmp 是 tmpfs,请不要把长时间 waydroid logcat、Android tombstone 收集、LXC debug log 写到 /tmp。把日志写到持久存储。

同时检查 Android 蓝牙是否已禁用:

waydroid shell -- sh -c '
getprop persist.bluetooth.disabled
cmd package list packages -d | grep -i bluetooth || true
'

如果没有禁用,执行第 0.5 节的蓝牙禁用命令。

15.1 waydroid status 显示 IP UNKNOWN

桥接模式下这可能正常。Waydroid helper 仍可能读取旧的 waydroid0 dnsmasq lease。

请用这些命令判断真实网络:

ip -br addr
bridge link
waydroid shell -- sh -c 'ip addr show eth0; dumpsys ethernet | head -80'

15.2 Android 提示 Missing DMA-BUF support

依次检查三层:

# OpenWrt
ls -ld /dev/dma_heap /dev/dma_heap/system /dev/dri

# Fedora
lxc-attach -n waydroid -- /bin/bash -lc 'ls -ld /dev/dma_heap /dev/dma_heap/system /dev/dri'

# Android
lxc-attach -n waydroid -- /bin/bash -lc 'waydroid shell -- sh -c "ls -ld /dev/dma_heap /dev/dma_heap/system /dev/dri"'

如果 OpenWrt 没有 /dev/dma_heap/system,回到内核章节。不要改 Android 文件掩盖。

15.3 waydroidplatform 不通或 waydroid app list 卡住

检查:

sed -n '1,120p' /var/lib/waydroid/waydroid.cfg
python3 - <<'PY'
import gbinder
print(gbinder.__file__)
PY
pkg-config --modversion libgbinder

需要:

binder_protocol = aidl3
service_manager_protocol = aidl6
gbinder 来自 /usr/local/lib64/python3.14/site-packages
libgbinder 1.1.45

15.4 Waydroid inner LXC 因 AppArmor 配置失败

如果日志出现:

Built without AppArmor support
Invalid argument

检查 Waydroid inner LXC config,删除不兼容行:

lxc.apparmor.profile = unconfined

15.5 lxc.hook.post-stop = /dev/null 导致 status 126

如果手动启动内层 Android LXC 时看到:

Script exec /dev/null ... Permission denied
Script exited with status 126
Failed to run lxc.hook.post-stop

请检查:

grep -n 'post-stop' /var/lib/waydroid/lxc/waydroid/config /usr/lib/waydroid/data/configs/config_base 2>/dev/null || true

把下面这行删除或注释:

lxc.hook.post-stop = /dev/null

当前稳定配置是注释掉它。这个问题可能不是 Android 退出的根因,但会让 LXC 日志多一个失败点。

15.6 Android 早期日志出现 /dev/hw_random 缺失

早期 Android 13 调试记录里出现过:

prng_seeder: Hanging forever because setup failed: Unable to open hwrng /dev/hw_random

当时还试过把 /dev/random/dev/urandom 绑定进 Android,结果 Android init 自己创建 /dev/random/dev/urandom 时发生冲突:

init: mknod("/dev/random"...) failed File exists
init: mknod("/dev/urandom"...) failed File exists
Init encountered errors starting first stage

所以请不要默认把 /dev/random/dev/urandom 写进 config_nodes

如果你用旧镜像,且明确看到 /dev/hw_random 这一条错误,可以只做临时诊断:

lxc.mount.entry = /dev/urandom dev/hw_random none bind,create=file,optional 0 0

这不是当前 Android 16 ATV 部署的默认步骤。当前更重要的是先确认第 10.2 节的 cgroup2 子树、cgroup:rwlmkd 策略,以及第 10.1 节的 config_nodes 设备挂载。

15.7 NetworkManager / systemd-networkd 抢网络

当前方案不用它们。保持:

NetworkManager inactive
systemd-networkd inactive
systemd-resolved inactive
firewalld inactive
wpa_supplicant inactive

网络由 OpenWrt 外层 LXC 配置、Fedora 内 waydroid-bridge-setup 和 Android DHCP 完成。


16. 最小检查清单

部署结束后,按这个清单检查:

# OpenWrt
uname -r
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system /dev/binderfs 2>/dev/null || true
ls -l /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder 2>/dev/null || true
dmesg | grep -i -E "drm|gpu|i915|amdgpu|nouveau|xe|guc|huc|firmware|wedged|reset" | tail -60 || true

# Fedora
lxc-attach -n waydroid -- /bin/bash -lc '
cat /etc/fedora-release
systemctl is-active waydroid-container.service || true
systemctl --user -M user@ is-active waydroid-full-lxqt.service || true
waydroid status || true
ip -br addr
bridge link || true
ls -ld /dev/dri /dev/dma_heap /dev/dma_heap/system /dev/binderfs 2>/dev/null || true
ls -l /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder 2>/dev/null || true
grep -E "dma_heap|binder|hwbinder|vndbinder|dri" /var/lib/waydroid/lxc/waydroid/config_nodes 2>/dev/null || true
for s in NetworkManager systemd-networkd systemd-resolved firewalld wpa_supplicant; do printf "%s: " "$s"; systemctl is-active "$s" 2>/dev/null || true; done
'

# Android
lxc-attach -n waydroid -- /bin/bash -lc '
waydroid shell -- sh -c "
getprop ro.build.version.release
getprop ro.lineage.version
getprop sys.boot_completed
getprop debug.ffmpeg-codec2.pixel_format
getprop debug.ffmpeg-codec2.hwaccel.drm
getprop ro.waydroid.hwcodec_profiles
getprop ro.hardware.gralloc
getprop ro.hardware.egl
getprop ro.hardware.vulkan
getprop media.sf.hwaccel
ls -ld /dev/dma_heap /dev/dma_heap/system /dev/dri /dev/dri/renderD128 /dev/binder /dev/hwbinder /dev/vndbinder 2>/dev/null
service check activity
service check window
service check display
service check SurfaceFlinger
service check waydroidplatform
"
'

期望重点:

OpenWrt 有 /dev/dma_heap/system,且 GPU 驱动没有 firmware 缺失或 wedged/reset 失败日志
Fedora 能看到 /dev/binderfs 和 /dev/dma_heap/system
config_nodes 里有 /dev/dma_heap/system、binder/hwbinder/vndbinder、/dev/dri/renderD128
内层 Waydroid LXC 使用 cgroup:rw,并运行在 waydroid-android/payload cgroup2 子树中
Fedora 44
Waydroid Session RUNNING / Container RUNNING
Android 16 / Lineage 23.0-20260403
sys.boot_completed=1
YUV_420
hwaccel.drm=1
HEVCMain10,VP9Profile2,AV1Profile0
minigbm / mesa / intel
SurfaceFlinger found
waydroidplatform found

17. 不建议做的事

请不要:

  • 为了隐藏 DMA-BUF 提示去改 Android framework、build.prop 或 Magisk 模块。
  • 随便启用 NetworkManager、systemd-networkd、systemd-resolved、firewalld。
  • 随便加载 Bluetooth 模块。我们的记录里 Bluetooth 模块尝试导致过 boot loop。
  • 默认给 Android inner LXC 绑定 /dev/random/dev/urandom。Android init 会自己创建设备节点,乱绑可能让 first stage init 失败。
  • 保留 lxc.hook.post-stop = /dev/null,如果它在你的 LXC 版本里导致 status 126。
  • 只看 VLC 播放结果就判断 Android MediaCodec 正常。
  • 删除整个 /var/lib/waydroid/overlay_rw/vendor,除非你确认里面没有需要保留的部署产物。
  • 在不知道原因时把 H.264、VP9、HEVC 全局强制成 P010。

18. Debug 指引

如果部署失败,请优先收集:

OpenWrt: uname -r, dmesg, /dev/dma_heap, /dev/binderfs, GPU firmware 检查
Fedora: journalctl -u waydroid-container.service
Fedora user: journalctl --user -u waydroid-full-lxqt.service
Waydroid: waydroid status, waydroid logcat
Android: getprop, service check, /dev/dma_heap, /dev/dri, /dev/binder, /dev/hwbinder, /dev/vndbinder
LXC: /srv/lxc/waydroid/config, /var/lib/waydroid/lxc/waydroid/config, config_nodes

你可能还希望使用 netconsole 等方式在软路由 kernel panic 时抓取到错误。或者至少给它接个键盘。

This work is licensed under
Creative Commons Attribution-NonCommercial 4.0 International

添加新评论