中文翻译为荧光,是一个基于 Go 实现的,以简洁、百变、万能为名头的 Wayland 合成器
与此同时,我们正在基于 MyGo 开发一系列 Lumine 扩展的原生软件(如文件管理器 Phoenix、照片查看器 Charlotte、音频播放器 Fermata)
目前还是实验性产物,远远无法达到可使用层级
纯 Go 实现的 Wayland 合成器试验品。不使用 CGO。
Wayland 协议、Vulkan 绑定、共享内存、配置解析全部使用 Go 标准库与纯 Go 第三方库实现。需要原生库时(Vulkan loader)通过 purego 在运行时 dlopen,不链接任何 C 代码。
- Vulkan 渲染器:通过 purego 直接调用 Vulkan 1.0(单个纹理四边形管线, 离屏渲染后回读到 CPU 可见帧缓冲)。设备不可用时自动回退到软件渲染器, 两条路径共用同一套合成逻辑,便于对照验证。
- 双布局模式:
tile:类似 Niri 的无限横向滚动平铺。窗口按竖直列排列,视口沿列条带 滚动,新窗口在焦点列右侧开新列。stack:自由浮动窗口,按 z 序叠放。
- 色彩管理:输出侧对合成结果做 sRGB → 目标色彩空间转换(PQ / HLG /
linear),并通过
wp_color_manager_v1暴露协议,客户端可创建参数化 / scRGB / BT.2100 image description、赋给 surface 并查询输出首选描述。 - CSD 与 SSD 共存:实现
zxdg_decoration_manager_v1。合成器默认绘制 服务端装饰(标题栏 + 边框),客户端请求 CSD 时则完全不画,两种窗口可同时 存在于同一输出。[decor].mode控制服务端偏好,客户端最终决定。 - 配置热重载:INI 风格的
.conf配置,inotify 监听文件变化,编辑保存即 生效(屏幕 / 布局 / 色彩 / WM 环境变量),无需重启,客户端不受影响。 - luminectl:通过 Unix 控制套接字操作运行中的合成器(焦点、滚动、列 移动、模式切换、关闭窗口、生成进程、强制重载、状态查询)。
- 无 CGO:
CGO_ENABLED=0 go build ./...可直接构建,产物是静态友好的 纯 Go 二进制。
Lumine/
├── cmd/
│ ├── lumine/ # 合成器入口:后端、渲染器、60Hz 帧循环
│ └── luminectl/ # 控制工具
├── internal/
│ ├── wl/ # 手写 Wayland 线协议 + 代码生成绑定(gen_*.go 勿手改)
│ ├── compositor/ # 全局对象、surface 状态机、seat、布局粘合、IPC、色彩管理
│ ├── layout/ # tile(列 + 滚动)与 stack(浮动 z 序)算法
│ ├── render/ # Framebuffer、CPU 渲染器、Vulkan 渲染器、色彩变换
│ ├── backend/ # 嵌套 Wayland 后端(纯 Go Wayland 客户端)
│ ├── config/ # .conf(类 ini)配置 + inotify 热重载
│ ├── ipc/ # Unix socket 上的行式 JSON 控制协议
│ └── shm/ # memfd / mmap / 像素解码
├── protocol/ # Wayland 协议 XML(生成 internal/wl 绑定的输入)
└── config.example.conf # 带注释的示例配置
需要 Go(go.mod 指定 go 1.26)。无需 meson、无需 wlroots、无需 libwayland。
go build ./cmd/lumine ./cmd/luminectl
# 验证纯 Go(无 CGO)构建:
CGO_ENABLED=0 go build ./...仓库根目录没有
.go文件,因此不要执行go build .或go run .。 使用make build构建到./bin,使用make run ARGS="..."启动,或者 明确指定包路径,例如go run ./cmd/lumine ...。
说明:
go list -deps会列出purego/internal/cgo包,这是 purego 的源码 布局,编译时在CGO_ENABLED=0下走纯 Go 的dlopen分支,不会产生 CGO 依赖。用CGO_ENABLED=0构建即可确认。
Lumine 目前提供三个后端:
nested(默认):作为一个 Wayland 客户端运行在现有合成器里,把帧提交为wl_shm缓冲并转发父合成器的输入。需要父合成器的WAYLAND_DISPLAY。drm:直接驱动 DRM/KMS 连接器(裸机,无宿主合成器)。用 Vulkan 离屏渲染 后把结果拷进 DRM dumb buffer 并 page-flip 上屏,键盘/鼠标直接读/dev/input/event*。需要一块未被其他合成器占用的空闲 VT。headless:不连任何父合成器,仅在内存中渲染,适合测试。
# 嵌套在现有会话中运行(父 WAYLAND_DISPLAY 指向当前桌面):
WAYLAND_DISPLAY=wayland-1 \
go run ./cmd/lumine -config ~/.config/lumine-wm/config.conf
# 裸机 DRM/KMS(在空闲 VT 上运行;见下文 “DRM/KMS 后端”):
sudo ./lumine -backend drm
# 指定显卡节点:
./lumine -backend drm -drm-device /dev/dri/card0
# 无头运行:
go run ./cmd/lumine -backend headless -renderer cpu
# 指定渲染器与窗口尺寸:
go run ./cmd/lumine -renderer vulkan -size 1920x1080命令行参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-backend |
nested |
nested / drm / headless |
-renderer |
auto |
auto / vulkan / cpu;Vulkan 不可用时回退 CPU |
-socket |
lumine-wayland |
Wayland socket 名或绝对路径 |
-config |
~/.config/lumine-wm/config.conf |
配置文件路径(类 ini 的 .conf) |
-terminal |
$LUMINE_TERMINAL 或 kitty |
spawn-terminal 动作执行的命令 |
-size |
1280x720 |
嵌套窗口初始尺寸 |
-drm-device |
自动探测 | -backend drm 使用的卡节点,如 /dev/dri/card0;也接受 LUMINE_DRM_DEVICE / WLR_DRM_DEVICES |
-verbose |
false |
打印渲染器与后端选择 |
-backend drm 走纯 Go 的 DRM/KMS ioctl(internal/drm),不依赖 libdrm,
也没有 CGO。上屏流程刻意保持简单、可移植:
- 用现有 Vulkan 渲染器把整帧画进离屏
B8G8R8A8_UNORM图像,再读回到 CPU (render.Framebuffer); - 把这帧按行拷进一块 DRM dumb buffer(线性、可 mmap 的扫描缓冲);
- 用
DRM_IOCTL_MODE_PAGE_FLIP把它翻到 CRTC 上,SYN_REPORT完成事件回 收缓冲。
为什么不用 dma-buf 直接扫描(把 dumb buffer 作为 Vulkan 图像导入、GPU 直接写 进去):
- i915 拒绝这种导入(
vkGetImageSubresourceLayout报 131584 > 131072 的分配 大小); - NVIDIA 上 GPU 写进导入的 dma-buf 后,CPU 映射读回不保证一致。
CPU 拷贝多花一点内存带宽,但在 Intel / AMD / NVIDIA 上都可靠,也让渲染器与输出
后端解耦(CPU 渲染器同样能配 -backend drm)。
NVIDIA 支持:internal/render 的 Vulkan 初始化会枚举物理设备,挑选第一个
能离屏渲染 B8G8R8A8_UNORM 的设备;NVIDIA 的 Vulkan ICD(nvidia / 开源
nvidia-open 内核模块)满足该条件,会被直接选中。DRM 侧则使用
nvidia-drm 暴露的通用 KMS 接口(dumb buffer + page flip + atomic modeset),
因此不依赖任何 NVIDIA 专有 API。混合显卡机器上可用 -drm-device 指定要让
NVIDIA 还是集显承担扫描输出,两者都能工作。
启动前需要:
- 在空闲 VT 上运行(不要在当前图形会话里跑,否则
drmSetMaster会因 "already mastered" 失败并给出提示); - 对
/dev/dri/card*与/dev/input/event*有读写权限(加入video/input组,或用 udev 规则 / root)。
-drm-device、LUMINE_DRM_DEVICE 或 WLR_DRM_DEVICES(取第一项)可指定卡节点;
连接器、分辨率、刷新率来自配置文件的 [Display] 段(monitor / resolution /
refresh-Rate),留空则选第一个已连接的连接器和它的首选模式。
-backend drm 直接读 /dev/input/event*,输入行为与其它合成器对齐:
- 每个键盘/鼠标设备以 EVIOCGRAB 独占打开:VT 控制台不再消耗按键(不再回显、
不再由内核注入重复事件)。拿不到 grab 只在
-verbose中警告,不阻塞启动。 - 按键自动重复由合成器合成(节奏来自
[input.repeat] delay/rate,并通过wl_keyboard.repeat_info广播)。裸机上 evdev 只报一次按下,没有这个计时器 长按只会出一个字符;嵌套后端里宿主的重复事件会被透传且不会与本合成器的重复流 叠加。 - 时间戳全链路携带:evdev 的单调时钟毫秒直接转成 Wayland 时间戳传给客户端。 指针加速度、平滑滚动与拖拽判定都依赖事件间隔,恒为 0 的时间戳会让 kitty、 Qt、Chrome 类客户端表现得"鼠标失灵"。
- 每个
wl_pointer.motion批次以frame收尾(GTK 等按 frame 提交的工具包 在收到frame之前会缓存运动事件)。 - 点击窗口在 tile 与 stack 模式都会移动键盘焦点(此前只有 stack 会,tile 下点击焦点会被每帧的布局焦点回写吞掉)。
- 滚轮只采用 高分辨率轴(设备同时报
REL_WHEEL与REL_WHEEL_HI_RES时不 双计);BTN_TOOL_*之类的接近/触笔状态不会误报为鼠标按键。 - 触控板默认跳过(原始 ABS_MT 坐标直接映射会瞬移光标并产生幻影点击,手势
支持尚未实现)。设
LUMINE_MAP_TOUCHPAD=1可强行按绝对指针映射; 数位板(BTN_TOOL_PEN)仍正常按绝对坐标工作。 - 裸机上调试输入:
LUMINE_DEBUG_INPUT=1把每个原始 evdev 事件打到 stderr, 确认"内核是否看到了我的鼠标"。
Lumine 通过 -socket(默认 lumine-wayland)在 $XDG_RUNTIME_DIR 下监听。
spawn-terminal 动作(以及 luminectl spawn)启动子进程时会剥离父进程的
WAYLAND_DISPLAY,改为指向 Lumine 自己的 socket,因此子客户端会连到
Lumine 而不是宿主桌面:
# 让一个终端连到已运行的 Lumine:
WAYLAND_DISPLAY=lumine-wayland kitty
# 或让 Lumine 直接拉起命令:
WAYLAND_DISPLAY=wayland-1 go run ./cmd/lumine kitty默认路径 $XDG_CONFIG_HOME/lumine-wm/config.conf
(即 ~/.config/lumine-wm/config.conf)。文件是 INI 风格:; 与 #
开启注释,键名大小写不敏感,值可加引号。由 inotify 监听,保存后立即重新
加载并应用到运行中的窗口。完整示例见
config.example.conf。
[Display]
monitor = eDP-1
resolution = 2560x1440
refresh-Rate = highest ; 或具体数值(Hz)
color-managment = icc ; icc | edid | none
icc-file = "/path/to/panel.icc"
main-monitor = "" ; true 表示该段是主屏
hdr = false
triple-buffering = true
[auto-start]
exec-once = fcitx5 -d
exec-once = /usr/bin/fastfetch
[wm-environment]
GTK_THEME = Breeze
[keybinding]
"super+t" = "mode-toggle"- 每个输出写成一个 section。多屏使用
[Display-1]/[Display-2]/ …, 顺序越前,优先级越高,最高优先级的一段即主输出;也可用main-monitor = true显式指定。 monitor是连接器 / 输出名(如eDP-1、HDMI-A-1),留空则使用后端默认输出。resolution形如2560x1440,auto表示沿用后端尺寸。嵌套后端下它只决定 宿主窗口的初始尺寸,无头后端下即最终输出尺寸。refresh-Rate可填highest/auto,或144、59.94这样的数值。color-managment(也接受正确拼写color-management)取icc/edid/none;icc搭配icc-file。hdr = true会把该输出切到 PQ / BT.2020;triple-buffering作为渲染提示 保留给后续 DRM 后端。
[auto-start]里可重复书写exec-once,每条命令在 compositor 开始监听后 只执行一次(热重载不会重跑)。[wm-environment]的键值会导入进程环境,自启动命令、终端快捷键及所有spawn出来的客户端都会继承。
- 缺失字段使用内置默认值;缺失文件不报错(等价于全默认配置)。
[keybinding]与默认绑定合并,只写想改的按键即可;完整绑定语言后续 迭代再加入。- 热重载会应用屏幕 / 布局 / 色彩 / 环境变量;
exec-once与绑定表变化不重跑。 - 也可用
luminectl reload强制重载。
| 按键 | 动作 | 说明 |
|---|---|---|
Super+Return |
spawn-terminal |
打开终端 |
Super+T |
mode-toggle |
切换 tile / stack |
Super+Left/Right |
focus-left/right |
跨列移动焦点 |
Super+Up/Down |
focus-up/down |
列内移动焦点(stack 模式为 z 序循环) |
Super+Shift+J/K |
move-left/right |
在列条带中左右移动当前列 |
Super+Shift+Left/Right |
scroll-left/right |
横向滚动词口 |
Super+Q |
close |
关闭焦点窗口 |
动作名即 [keybinding] 里可填的值,与 luminectl 的语义一致。
控制套接字位于 $XDG_RUNTIME_DIR/lumine-ctl.sock。请求为行式 JSON,响应为
JSON。
luminectl status # 当前布局状态
luminectl outputs # 输出列表
luminectl focus left # 移动焦点(left/right/up/down)
luminectl move right # 当前列右移
luminectl scroll 240 # 视口右移 240px
luminectl mode stack # 切换布局模式(tile/stack/toggle)
luminectl close # 关闭焦点窗口
luminectl spawn kitty # 生成进程(连到 Lumine)
luminectl reload # 强制重载配置- 输出侧:渲染器在合成完成后按
[color]的 transfer / primaries / max_luma 对整帧做 sRGB → 目标空间的转换。transfer = "srgb"时为零开销 直通;pq/hlg/linear会做逐像素重编码。 - 协议侧(
wp_color_manager_v1):对外声明 parametric、set_luminances、 windows_scrgb、windows_bt2100;客户端可创建 image description(命名 transfer function / primaries + 亮度范围),描述创建后立即置为 ready,get_information可回读参数。wp_color_management_surface_v1上的 image description 会被记录在对应 surface 上,surface feedback 的get_preferred返回输出首选描述。 - 范围说明:per-surface 的像素级转换(ICC profile、3D LUT、逐窗口
色彩空间映射)尚未实现——描述目前作为元数据跟踪与回读,真正的像素变换在
输出侧统一完成。不支持的参数(如
set_tf_power、显式色度坐标)会按协议 返回failed(unsupported),而不是静默接受。
go test ./...
go vet ./...
# 纯 Go 路径:
CGO_ENABLED=0 go build ./... && CGO_ENABLED=0 go test ./...internal/wl:线协议编解码、FD 传递、对象表。internal/compositor:完整客户端会话(registry → shm → xdg_toplevel → buffer release → frame callback → keyboard keymap → SSD 几何)与wp_color_manager_v1参数化描述的就绪路径。internal/layout:列生成/聚焦/滚动/重排、模式往返、浮动窗口夹取。internal/config:.conf解析、[Display]/ 自启动 / 环境、绑定合并、 非法值回退与热重载。internal/render:CPU 与 Vulkan 合成结果一致性。
- 裸机 DRM/KMS 后端已可用(
-backend drm),但仍以单连接器为主;真实面板上的 HDR 输出元数据([Display].hdr目前只在合成阶段生效)尚未落地。 - layer-shell / foreign-toplevel / screencopy / xdg_popup 的协议绑定已在
internal/wl生成,但尚未注册为全局对象,相关客户端暂不可用。 - 指针拖拽/缩放未实现:
xdg_toplevel.move/resize为空操作,stack 模式窗口 目前不能拖动;也没有wl_pointer.set_cursor光标面渲染。 - 触控板只有裸坐标映射,没有手势(点按、双指滚动、惯性)。DRM 后端默认跳过
触控板设备以免光标瞬移与幻影点击,
LUMINE_MAP_TOUCHPAD=1可强制启用。 - per-surface 色彩像素转换(ICC/LUT)未实现(见上)。
- 单输出为主,
wl_output热插拔未实现。 wl_surface的 buffer scale / transform / damage 被忽略,每帧整屏重绘。
见仓库根目录。仓库历史中的旧 C / wlroots 实现已由当前 Go 实现取代。