跳到正文

在 WSL 中用 noVNC 操作 KDE Wayland:一次嵌套桌面的实践

在 Windows 上装好 WSL 和 KDE,再把桌面放进浏览器,看起来已经很接近一台普通的 Linux 电脑:有任务栏,可以打开应用,也可以最小化、最大化窗口。

但在 4K 屏幕上用起来,问题接连出现。画面铺满了,字却不一定清楚;分辨率提高了,按钮又变得很小;在 KDE 设置里修改缩放,关闭 noVNC 再打开,界面还是老样子。

我因此想把原来的 KDE X11 会话换成 Wayland,继续保留浏览器访问。第一轮尝试里,甚至已经看到了 Wayland 桌面,鼠标点击却完全没有效果。最后跑通的方案,是在 VNC 的虚拟 X11 屏幕里嵌套一个 KWin Wayland 窗口:里面的 Plasma 和应用使用 Wayland,外面的 VNC 继续传送画面与输入。

这次实践最有用的地方,是逐渐分清了几件容易混在一起的事:谁创建桌面,谁传画面,谁处理输入,以及高分屏上的「像素更多」和「界面更大」分别由谁负责。

浏览器里看到的桌面,究竟是谁提供的

先从最初能工作的 X11 方案看起。

WSL 负责运行 Linux 环境。现在的 WSL 2 已经可以通过 WSLg 运行 X11 和 Wayland 图形应用,把单个应用窗口集成到 Windows 桌面;不过,WSLg 不会替用户提供一个完整的 KDE 桌面会话。这两种使用方式需要分开理解。微软的 WSL 图形应用说明也明确区分了图形应用支持与完整 Linux 桌面。

X11 和 Wayland 是图形应用与显示系统协作的两套协议,决定应用怎样提交画面、接收输入。桌面环境可以选择不同的会话类型;切换到 Wayland 后,仍然可以使用熟悉的 KDE 任务栏和窗口。

KDE Plasma 提供任务栏、启动菜单、系统设置等桌面组件。KWin 负责管理窗口;在 Wayland 会话里,它还承担合成器的职责,把各个应用提交的内容组合成最终画面,并分发输入事件。

最初使用的 Xtigervnc 则同时承担了 X server 和 VNC server 的角色:它创建一块虚拟 X11 屏幕,让 KDE X11 在上面运行,再把屏幕内容提供给 VNC 客户端。它本身不提供 KDE 那样的任务栏和应用菜单。

VNC 使用的 RFB 协议围绕远端屏幕的像素更新、键盘和鼠标事件工作。客户端收到的主要是画面与协议消息,不会因此获得 KDE 按钮、窗口或设置项的语义。

浏览器不能直接连接这种普通 socket,于是中间加入 websockify:它处理 WebSocket 连接,并在 WebSocket 与服务端 socket 之间双向转发数据。实际部署里,它还可以顺便提供 noVNC 的静态网页文件。websockify 的项目说明把这两项职责分别列了出来。

noVNC 才是浏览器中的 VNC 客户端。它用 JavaScript 解析 RFB 数据、绘制远端画面,再把浏览器捕获的键鼠操作编码发回去。它是独立的开源项目,可以连接不同桌面背后的 VNC 服务端,并不与 KDE 绑定。noVNC 项目本身也没有承诺代替远端桌面管理显示器或应用设置。

原先的 KDE X11 与嵌套 KDE Wayland 共用 VNC 传输通道

图中的两行都通过同一类通道传送画面和输入,区别发生在 Linux 桌面内部。Windows 上那个标题写着 noVNC 的「独立窗口」,实际也是 Edge 的网页应用窗口;去掉了地址栏,并没有换成另一种远程桌面协议。

因此,「noVNC 能不能用 Wayland」不能只看浏览器端。要看的是:服务端如何取得 Wayland 画面,又通过什么机制把输入交给对应会话。

高分屏适配有两项独立工作

在 200% 缩放的 Windows 屏幕上,浏览器可能报告 window.devicePixelRatio === 2。这个值表达设备像素与 CSS 像素的比率;页面缩放也可能改变它,具体含义可以参考 MDN 对 devicePixelRatio 的说明。

假设浏览器里可用于远程桌面的区域是 1920 × 1080 个 CSS 像素,那么要按 DPR 2 提供足够的画面细节,可以向服务端请求 3840 × 2160 的 framebuffer,也就是远端画面的像素缓冲区,再把它显示在原来的网页区域里。

这解决了像素数量,却没有规定远端按钮应该画多大。

要处理的事情 这个例子中的目标 负责的一侧
远端画面的像素数量 3840 × 2160 noVNC 请求,VNC 服务端响应
画面在网页中的显示尺寸 1920 × 1080 CSS 像素 noVNC 的画布与显示缩放
远端文字、按钮和窗口的逻辑尺寸 KDE 200% 缩放 KDE / KWin 与应用

如果只把远端分辨率乘以二,而 KDE 仍按 100% 绘制,浏览器把这张更大的画面放回原区域后,文字和控件就会显得很小。

本机此前对已部署 noVNC 做过一个局部修改:在自动调整远端尺寸时乘上 DPR,并相应调整画布显示比例。这个修改负责前两行;KDE 的 200% 是另外配置的。此次 Wayland 实验继续使用了这份已部署网页,没有为了建立 Wayland 会话再修改 noVNC 或 TigerVNC 的源码。

这也解释了为什么「本地已经能看清」还不足以证明一份通用的 noVNC 自动缩放方案成立。要自动联动桌面缩放,需要服务端和桌面之间有相应约定。客户端知道 DPR,并不意味着现有连接里就有一条通用指令,可以让任意远端桌面把 UI 缩放改成同一个值。

还有一个容易误判的操作:关闭 noVNC 窗口,通常只是断开客户端,不会结束后面的 KDE 会话。如果某项设置需要应用重启或会话重新初始化,重新打开浏览器仍然会连回原来的进程。这和注销 KDE、重启桌面服务是不同的动作。

直接共享 Wayland:先有了画面,再卡在输入

Wayland 下,应用通常不能像在传统 X11 环境里那样,随意读取整个桌面或向其他应用注入输入。屏幕共享、远程控制需要合成器及相关接口配合。

这里的 portal 是桌面提供的一组受控接口。远程桌面程序可以通过它申请屏幕和输入权限,由桌面完成授权,而不是获得整个会话的任意访问权。

这次先尝试让 KWin 创建无实体显示器的虚拟输出,再用 KDE 的 Krfb 提供 VNC 访问。过程中碰到了两个独立的限制。

有合成画面,还要有可用的捕获路径

开始时,无显示器的 KWin 使用 QPainter 软件合成。桌面进程能够启动,但当前版本的屏幕共享路径要求 OpenGL 合成,不能直接从这个状态取得可用视频流。这个条件可以在 KWin 6.6.6 的 screencast 实现中核对。

后来临时加载了与当前 WSL 内核匹配的 vgem 模块,让虚拟 DRM 设备可用,再以软件 OpenGL 运行 KWin,才得到了 Krfb 虚拟显示器的画面。这里仍是软件渲染,不能据此判断硬件 GPU 加速已经跑通。

直接共享路线中已经能看到的 KDE Wayland 虚拟显示器

这是直接共享路线当时的真实画面。中央的彩色图案是测试窗口,用来确认画面更新;看到桌面和动态内容,只能证明捕获与传输已经工作。

画面通了,鼠标仍然可能没有入口

接下来发现,noVNC 里的点击并不能操作这张桌面。

查看 Krfb 25.12.3 的实现,虚拟显示器路径与普通的 portal 会话路径并不相同。它的 XDP 输入插件需要可用的 stream_node_id 和 session_handle;指针事件处理中有这样的判断:

// events/xdp/xdpevents.cpp,handlePointer() 中的节选
if (streamNodeId == 0 || sessionHandle.path().isEmpty()) {
    return;
}

结合实际点击无效的结果,这条虚拟显示器路线没有形成可用的输入通道。相关实现分别在虚拟显示器入口、PipeWire framebuffer 插件和 XDP 事件处理里。这个判断针对本次版本和启动方式,不应扩大成「所有 Krfb 都只能观看」。

随后又尝试了 TigerVNC 1.16.2 的 w0vncserver。它是面向 Wayland 的服务端;官方手册说明,它可以使用兼容的 Wayland 接口或 RemoteDesktop portal 共享已有显示,并不负责创建桌面。

在 KDE 上,这次运行走到了远程控制授权框。但授权框出现在那个无实体显示器的桌面里,而当时能看到它的 Krfb 通道又无法点击,于是首次授权卡住了。

无实体显示器场景中的远程控制首次授权循环

当时没有可用的本地交互入口,已有远程入口只能看,新入口又需要先确认权限。普通桌面可以由本地用户完成首次授权;w0vncserver 也提供记住显示选择的相关选项。这次没能完成无显示器会话的首次授权,还不足以判断 noVNC 能否访问其他 Wayland 环境。

嵌套 KWin,让外层 VNC 继续处理输入

换一个运行位置,就可以避开「先远程授权,才能远程点击授权框」的循环。

KWin 支持嵌套运行:它可以作为一个窗口,运行在已有的 X11 或 Wayland 显示上,同时为内部应用提供一个新的 Wayland 显示连接。KDE 的嵌套 KWin 说明介绍了这种用法;本机也核对了当前版本的启动参数和运行结果。

最终方案保留一块 Xtigervnc 虚拟 X11 屏幕,把它当作 KWin Wayland 窗口的宿主。Plasma 和应用连接内层 Wayland socket。VNC 捕获外层屏幕中的最终图像;从浏览器返回的键鼠事件先到外层 X server,再作为普通窗口输入交给 KWin,由它分发给内层应用。

这样就不需要通过 KWin 的屏幕共享接口捕获内层桌面。最终内层使用 QPainter 软件合成,也没有遇到前面那条 screencast 路径的 OpenGL 限制。这条嵌套路径不依赖前面为屏幕共享临时启用的 vgem 设备。

它保留了外层 X11,代价是多了一层嵌套窗口。对于「在 WSL 中体验 KDE Wayland,同时保留 noVNC 浏览器入口」这个目标,这个取舍可以接受;如果目标是验证完整的原生 Wayland 显示栈、硬件加速或远程桌面性能,还需要另外评估。

启动参数只解决一部分问题

下面是本机启动 KWin 时的参数节选。它依赖已经建立好的私有 X11 显示 :11、Xauthority 和会话环境,不能当作从空白 Ubuntu 开始的一行安装命令:

kwin_wayland --x11-display :11 --xwayland \
  --socket plasma-wsl-wayland-0 \
  --width 1920 --height 1080 --scale 2 \
  --no-lockscreen --no-kactivities

--x11-display 指定宿主显示,--socket 指定内层 Wayland socket。--xwayland 为仍需要 X11 的内部应用提供兼容入口,不会把整个内层会话变成 X11。

在 KWin 6.6.6 的这个后端中,初始像素尺寸由 outputSize * outputScale 得到,所以这里的 1920 × 1080 与 scale 2 对应 3840 × 2160 的初始输出。对应版本的 X11 windowed backend 源码比单凭参数名称猜测更可靠。

要让它与原来的桌面并存,还补了几项会话管理:

  • 通过 dbus-run-session 创建独立会话总线,让控制命令和桌面服务指向内层 KDE。
  • 分离 XDG_CONFIG_HOME、XDG_DATA_HOME、缓存和状态目录,避免两套桌面互相覆盖显示配置。
  • 使用独立的运行时目录、Wayland socket 和 Xauthority,让应用连接到正确的显示。
  • 避免把这一套显示环境全局导入共享的用户服务管理器,以免后启动的应用连接到另一套桌面。

这些都属于会话启动器的工作,不是 noVNC 的功能。原来的 X11 桌面继续保留,新入口使用另一组显示与端口。

浏览器改尺寸时,还要让嵌套窗口跟着改

外层 VNC 屏幕变大,不代表里面那个 KWin 窗口一定会自动铺满。

本地启动器因此增加了一个尺寸同步步骤:按 _NET_WM_PID 找到当前 KWin 进程对应的宿主窗口,读取外层根窗口的尺寸,再用 XMoveResizeWindow 调整它。KWin 收到宿主窗口的 ConfigureNotify 后,会更新内层输出尺寸;当前后端的事件处理也能核对这条行为。

这里需要两个条件同时成立:浏览器要求的尺寸已经到达 VNC 服务端,KWin 的宿主窗口也已经适配新尺寸。只检查其中一端,很容易误以为「远程桌面自动调整分辨率」已经全部完成。

缩放的保存也需要单独处理。KDE 把选择写入这套会话自己的 kwinoutputconfig.json;启动器下一次启动时读取已保存倍率,并在启动 Plasma 前应用它,避免初始化参数把用户选择覆盖回默认值。首次默认 200% 仍然是本地预设,没有从浏览器自动同步 KDE 缩放。

环境变量和截图不足以证明会话类型,还要核对合成器信息。此次入口仅监听回环地址,未验收完整音频、剪贴板、输入法和高负载 3D;跨机器使用需要另设计认证与传输保护。完整内容见嵌套会话的实际验证弹窗。

总结

最终得到的是一个可通过 noVNC 操作的嵌套 KDE Wayland 桌面:外层 X11 提供虚拟屏幕与宿主输入,内层 Wayland 负责桌面和应用。这条路径避开了直接共享路线中尚未打通的捕获、输入与首次授权组合,代价是保留外层 X11 和软件渲染,不能据此判断原生 GPU 桌面的性能。

高分屏还需要分别处理画面像素、网页显示尺寸和 KDE 控件倍率;关闭浏览器也只是在断开客户端,并不等于重启桌面会话。本次验证了内层 Wayland、实际输入、尺寸同步和保存倍率的冷启动,尚未覆盖新入口的资源管理器双击,以及完整音频、剪贴板、输入法和高负载 3D 工作流。

下次遇到类似的问题,我们的排查思路可以是:

如果窗口打不开,先检查 Windows 启动器与本地网页服务;如果已经连上但黑屏,继续看桌面进程、输出和渲染后端;如果画面正常却不能点击,再查输入接口与授权。关于 Windows 快捷方式找到的文件为何可能与工具看到的不同,另有一篇MSIX 文件视图排查,那是这条链路更靠前的问题。

缩放问题也可以沿同样的方法拆开:浏览器报告多少 DPR,服务端实际有多少像素,KWin 使用多少倍率,应用是否连接到了预期会话。需要验证重启时,就重启对应的桌面服务;需要验证重连时,才关闭客户端。

嵌套会话的实际验证

怎么确认内层确实是 Wayland

只看截图无法区分 X11 和 Wayland。环境变量也只说明应用被要求连接哪里,最好再核对合成器自己的运行信息。

本次环境采样于 2026-10-20,主要版本如下。这里把最终使用的服务端与排查时尝试的服务端分开列出,避免误以为必须升级所有组件才能复现这个结构。

组件 实测版本与用途
Windows / WSL Windows 11;WSL 2.6.3,WSLg 1.0.71
Linux Ubuntu 26.04;WSL 内核 6.6.87.2
KDE / Qt KWin、Plasma 6.6.6;Qt 6.10.2
最终的 VNC 服务端 Xtigervnc 1.15.0
浏览器访问 noVNC 1.6.0,带此前本地 DPR 修改;websockify 0.13.0
直接共享路线 Krfb 25.12.3;另行尝试 w0vncserver 1.16.2

从内层会话的 D-Bus 调用 org.kde.KWin.supportInformation,选取相关行,可以得到:

KWin version: 6.6.6
Qt Version: 6.10.2
Operation Mode: Wayland
Compositing Type: QPainter

在这套会话中运行 kscreen-doctor -o,一次采样的关键值为:

Output: 1 X11-0
Modes:  1:2560x1440@60.00*!
Geometry: 0,0 1280x720
Scale: 2

以上是原始输出的节选,省略了输出 UUID、其他能力字段和终端颜色控制符。X11-0 是内层输出使用的宿主后端名称,与 KWin 报告 Wayland 模式并不矛盾;2560 × 1440 像素、1280 × 720 逻辑区域与倍率 2 也能相互对照。可核对的环境与运行信息节选随文保留。

通过 noVNC 访问的嵌套 KDE Wayland,显示设置为 200%

这张截图中,分辨率和缩放是两项独立设置。实际使用时应同时检查画面像素、逻辑区域和控件大小,而不是只看「全局缩放率」的数字。

输入验证则从桌面启动菜单打开 Kate,再用浏览器里的键盘输入 wayland。对应应用进程的环境里,QT_QPA_PLATFORM=wayland,WAYLAND_DISPLAY=plasma-wsl-wayland-0。结合 KWin 的运行模式和实际输入结果,才确认这次不再停留在「只能看到桌面」。

从嵌套 Wayland 桌面打开 Kate 并输入文本

随后还分别检查了几种容易混淆的生命周期:

检查动作 实际验证到的结果
在 KDE 设置里降低缩放倍率 控件和窗口即时改变;需要在倒计时内点击「保留」,否则会回退
保存倍率后重启桌面服务 用 kscreen-doctor 设置 1.75 后冷启动,仍读到 1.75;随后恢复为 2
关闭 noVNC 再连接 可以重新访问原会话,桌面服务没有随客户端退出
外层画面改为 3000 × 1800 内层逻辑区域跟到 1500 × 900,倍率保持 2
从独立 Windows 进程检查启动器 新入口的服务检查通过;尚未覆盖资源管理器里的实际双击入口

图形设置那次没有及时点击「保留」,设置按倒计时恢复。重启检查则通过命令设置倍率后进行,确认了已保存的配置能够重新读取;这项结果不能代替图形界面里点击确认的检查。

最后的网络入口只监听本机回环地址,VNC 使用权限为 0600 的 Unix socket。这是一套本机桌面入口;如果以后改成跨机器访问,还需要重新设计连接认证和传输保护。此次并未验证完整的音频、剪贴板、输入法和高负载 3D 工作流,软件渲染也不能代表原生 GPU 桌面的性能。