# Linux 原生独立光标(X11 / 单座席 / 2K) 用于当前盒子 `.188` 的 Ubuntu `.72` 和麒麟 `.71`。主机光标图像、热点、动画帧和透明隐藏状态通过认证 SSH 同步;位置由盒子的 X11 原生指针立即显示。没有改系统光标主题,也没有用固定本地箭头替代原生形状。点击、拖窗内容和视频编码延迟仍存在。 本实现绕过两个不同 Sunshine 版本的大型重编译,使用**按精确 SHA256 固定的私有副本和捕获适配器**。这是本项目的受限接入方式,不是上游 Sunshine/Moonlight 已支持的协议能力。后续要支持其他捕获后端、相对鼠标或多个并发客户端,应扩展双方正式控制协议。 ## 生命周期 1. `vdi-moonlight-rkmpp` 调用 `client.py`。没有对应主机 JSON 配置,或 `VDI_NATIVE_CURSOR=0`,直接 exec 原客户端。Windows 没有该配置。启用时先核验客户端 SHA256,再获取远端租约;旧客户端或未复核的新构建会在修改主机服务前被拒绝。 2. 客户端使用各主机独立密钥、固定 known_hosts、BatchMode 和严格主机校验打开 SSH。服务器 authorized_keys 使用 `restrict,from="192.168.1.188",command="/usr/bin/python3 .../host.py stream"`,不提供任意 shell。 3. `host.py stream` 获取独占 flock,检查版本、随机租约、单屏尺寸与光标边界;启动用户级 guard。guard 校验私有 Sunshine 和适配器 SHA256,停止原服务,在 `/run/user/UID/systemd/user/` 创建临时 drop-in,再以适配器启动私有副本。 4. `capture-no-cursor.c` 只在该 Sunshine 进程的 XFixesGetCursorImage 捕获结果中清零像素,不修改 X server 的实际光标、全局主题或系统库。adapter 的 dlsym receipt 和 `/proc/PID/maps` 都确认后才返回 READY,随后才允许新视频连接。因此所有新视频帧从一开始都不含光标,不在旧视频仍显示光标时直接切换。 5. SSH 消息包含版本、128位随机租约、递增序号、尺寸、热点、RGBA 像素、源分辨率和输入确认。最多128×128像素;坏包、错误尺寸、重复序号或超时结束当前串流。客户端/bridge状态等待上限2秒,主机租约4秒;短暂停顿期间保留上次形状,本地指针位置继续移动。源几何变化要求重新连接。 6. 盒子监督进程写入0700私有目录的原子状态文件。SDL主线程每约4ms检查状态,只在形状/缩放改变时创建和释放光标;X11原生指针移动不等该轮询或视频帧。SDL2 X11后端直接复制Xcursor像素,需要预乘alpha,已单独适配。 7. 本地输入序号和目标坐标通过SSH回传;服务器实际指针到达该位置后确认。只在最新输入已确认、没有按键按下且本地静止500ms后接受服务器主动warp,避免迟到的位置回拉正在移动的光标。触摸/相对游戏模式不在本接入范围。 8. 客户端保留原PID(fork监督进程后exec),正常kiosk窗口激活仍能识别。退出、父进程被kill或SSH异常时结束租约。guard先停止无光标生产进程,再删除临时drop-in并启动原服务;原视频连接不会被新普通生产进程接续。 9. guard本身被kill时,systemd ExecStopPost仍执行恢复。重启后 `/run` 中的drop-in自然不存在;原已启用服务仍正常启动。没有启用全局LD_PRELOAD或永久无光标模式。 ## 构建和安装 - 客户端补丁:`../moonlight/patches/0005-vdi-native-linux-cursor.patch`;由 `tools/build-rkmpp-client.sh` 在0001~0004之后应用。头文件源码保留在本目录,修改时必须同步补丁中的新增头文件。 - `tools/deploy_kiosk.sh` 部署 `client.py` 为 `~/vdi-terminal/bin/vdi-native-cursor`。配置/密钥不会由普通kiosk部署脚本生成。 - 两个客体的私有目录:`~/vdi-terminal/native-cursor-v1/`,包含 `host.py`、`capture-no-cursor.c/.so`、**无额外file capability的**原版Sunshine副本,以及 `manifest.json`。原系统二进制/能力保持原值。 - `.so`在麒麟glibc2.31/GCC9上编译,两台复用。最小编译不依赖缺失的libc开发包: ```sh gcc -shared -nostdlib -fPIC -O2 -Wall -Wextra -Werror -Wl,-z,defs \ capture-no-cursor.c /lib/x86_64-linux-gnu/libdl.so.2 \ /lib/x86_64-linux-gnu/libc.so.6 -o capture-no-cursor.so ``` 麒麟仅对本次 `host.py`、私有 `sunshine` 和 `.so` 做 `kysec_set -n exectl -v verified` 单文件认证;源码变更后须重新认证。没有关闭KYSEC。Ubuntu原文件具有capabilities,安全加载器会忽略LD_PRELOAD,因此仅租约内使用相同SHA的无capability私有副本;X11捕获无需SYS_ADMIN,启动时其他捕获后端的能力探测告警不代表X11失败。 用户级 `vdi-native-cursor-guard.service` 的ExecStart为 `/usr/bin/python3 <目录>/host.py guard`,ExecStopPost为同脚本的 `recover`;Type=simple,TimeoutStopSec=50。服务按需启动,不enable。`manifest.json`仅包含两个键 `sunshine`、`capture-no-cursor.so`,值为部署文件的SHA256。 盒子 `~/.config/vdi-terminal/native-cursor/.json`: ```json {"host":"192.168.1.72","user":"test2","key":"/绝对路径/该主机.key","known_hosts":"/绝对路径/该主机.known_hosts","size":[2560,1440],"client_sha256":"5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03"} ``` 每台主机单独的ed25519私钥和known_hosts留在盒子配置目录(目录700、私钥600),不进仓库。known_hosts是在工作站用用户提供的SSH凭据成功认证后取得的真实主机公钥,没有在运行时自动接受新主机密钥。JSON和密钥应在已验证0005的客户端部署后才启用。上例SHA为2026-11-05现场构建;重新构建后须先停用profile、完成对应验证,再更新SHA并启用,不能把旧值机械替换为任意新文件的SHA。 ## NVIDIA 硬件采集(2026-11-11) 当前两台 Linux 的现场部署已切换为 GTX 965M 渲染、NvFBC/CUDA 采集与 NVENC H.264 编码;不再沿用上文历史 X11 软件编码基线。使用兼容 Maxwell 的 Sunshine v2025.924.154138;不得仅更新主二进制而不复测 CUDA 架构支持和 pinned manifest。 Ubuntu 的 NVIDIA→modesetting PRIME 路径会把服务端软件光标提前画进画面,仅设 NvFBC `bWithCursor=false` 仍会出现本地光标拖着视频光标。该主机在 `host.py` 同目录启用空标记文件 `hide-server-cursor`:捕获 guard 用独立 X11 连接持有 `XFixesHideCursor`,原生光标形状仍由另一个连接通过 `XFixesGetCursorImage` 读取。结束或异常退出时先停 Sunshine,再显式恢复并关闭隐藏连接;X server 也会在连接断开时释放该客户端的隐藏状态。标记仅在 Ubuntu 现场启用,麒麟保持原路径。撤销须先退出串流、停 guard,再移除标记后重连,不在同一串流中途切换。 适配器编译增加 `-DVDI_NVFBC -Ivendor`。麒麟缺少 libc 开发启动对象时另加 `-ffreestanding -nostdlib`,并显式链接系统版本化 libc/libdl。`vendor/NvFBC.h` 保留上游 MIT 许可。拦截 NvFBC 函数表时检查 SDK 参数版本,仅在租约内关闭视频内光标,不修改调用方参数;`nvfbc-ready-v1` 是加载就绪凭据,`nvfbc-v1` 才表示实际捕获会话创建成功。 麒麟的 Optimus 卡没有可用物理输出,旧内核 PRIME 显存导入也失败,因此显式启用 `VDI_NVFBC_HEADLESS=1`。适配器只在驱动报告支持采集、没有 RandR 输出且真实 X screen 为 2560×1440 时枚举整屏,标记 RandR 不可用,使 Sunshine 使用 `NVFBC_TRACKING_SCREEN`。它不生成真实显示输出,也不把不可采集屏幕报告为可用。UKUI 已目视验证桌面正常;Ubuntu GNOME 在相同无输出条件下黑屏,已恢复 NVIDIA + modesetting PRIME 输出,不能直接照搬麒麟配置。 麒麟使用 `/opt/vdi-sunshine-maxwell/runtime/` 的私有动态加载器和依赖库运行该 Sunshine,不替换系统 glibc;可执行文件、适配器和新增脚本按文件进行 KYSEC verified 认证。GPU 路径及测量边界见 [本轮实施证据](../../docs/evidence/linux-gpu-20261111/README.md)。 重启显示管理器可能使临时 guard 单元消失而遗留旧 ready/receipt;新租约在获得独占锁并确认旧 guard 停止后清理这两个标记,再等待新会话凭据。`test_native_cursor_host.py` 在实际 Ubuntu Linux 上覆盖该恢复情形及不完整 manifest 拒绝;`run_nvfbc_adapter.py` 配合 `nvfbc_adapter_stub.c` 在麒麟实际 glibc 上覆盖函数表 ABI、光标与整屏枚举边界。 ## 停用与回退 退出当前Linux串流,等guard inactive且原vdi-sunshine active。将对应 `.json` 改名为 `.disabled`,下次从同一正常列表连接即使用原视频光标。临时诊断也可设置 `VDI_NATIVE_CURSOR=0`,但必须在当前租约结束、原服务恢复之后重连。 紧急恢复可在客体执行 `systemctl --user stop vdi-native-cursor-guard.service`,检查 `/run/user/UID/systemd/user/vdi-sunshine.service.d/90-native-cursor.conf` 已移除、MainPID路径为原二进制。盒子旧客户端和wrapper备份在 `~/vdi-terminal/backups/native-cursor-20261105/`。仅恢复旧二进制前必须先停用两个主机JSON,避免旧客户端连入无光标捕获。 测试结论与边界见[云桌面说明](../../docs/desktop-guide.md#cursor),完整测试与失败实验原文见[历史归档](../../docs/archive/README.md)中的 `linux-native-cursor-20261105.md`;最新部署 SHA 见[终端说明](../../docs/terminal-guide.md#build)。本地边界单测:`python -m unittest discover -s tools/tests -p test_native_cursor.py -v`。