功能:接入Linux原生光标并完善设备接入工具
This commit is contained in:
1 parent
a14f028402
commit
a47968496e
92 files changed
+3090
-22
No files matched your search
@@ -9,12 +9,14 @@
|
||||
- `0001-vdi-input-and-latest-frame.patch` 在 Moonlight Qt 根目录应用。鼠标移动只合并队首连续事件,遇到按钮、键盘、窗口、呈现事件或其他鼠标设备立即停止;每次最多额外合并 64 个事件。绝对模式在发送按键前使用该按键事件的坐标。低延迟队列可选地把已等待的解码帧替换成新帧。
|
||||
- `0002-vdi-absolute-click-barrier.patch` 在 `moonlight-common-c/moonlight-common-c` 中应用。鼠标按钮封住此前的位置批次,后续移动只能进入新批次;发送线程只锁定自己的批次,避免旧包出队时误清除新批次。所有包依旧走原可靠鼠标通道,保留 1ms 合并限流和原坐标边界修正。
|
||||
- `0003-vdi-input-priority.patch` 在 Qt 根目录应用。可选地让帧就绪后的连续输入先执行,最多 32 条或 2ms,不能跨窗口/焦点/其他事件。`VDI_INPUT_PRIORITY=1` 才启用,默认关闭。另提供 `VDI_INPUT_TIMING=1` 的汇总统计,区分 SDL 输入派发年龄与主线程呈现调用耗时,不代表端到端延迟。
|
||||
- `0004-vdi-power-key-policy.patch` 在 Moonlight Qt 根目录应用。客户端丢弃电源/睡眠键,以及 `Ctrl+Alt+Delete`、`Alt+F4`、`Win+L`、`Win+X`,防止云桌面被关机、重启、锁定或打开包含电源操作的系统菜单;按键不会发送到 Windows、Ubuntu 或麒麟客体系统。
|
||||
- `0005-vdi-native-linux-cursor.patch` 在 Qt 根目录应用,为已配置的单座席Linux/X11/2K/绝对鼠标会话创建原生SDL光标,形状/热点/显隐经认证SSH同步,位置不等待视频帧;主机以精确SHA固定的捕获适配器在新连接前去除视频光标。状态过期等故障结束整次会话,恢复原服务后重连。Windows无对应profile,继续原逻辑。安装和限制见[模块说明](../native-cursor/README.md)。
|
||||
|
||||
2026-11-03 的现场 RKMPP/EGL 使用独立 PacerRender 线程,诊断会话主线程 render 调用数为 0;因此第三补丁的新调度没有已证明的现场收益,未启用。诊断候选二进制保存为 `moonlight.input-priority-20261103`;默认客户端已恢复到本轮前 SHA256 `35de7b1a4ade583823b23b5f2e012307f6d0a3848c8eb97bd6d4de9660afa00f`。详细计划/初测限制见 `docs/mouse-optimization-plan-20261103.md`,独立光标接入设计见 `docs/independent-cursor-design-20261103.md`。
|
||||
2026-11-03 的现场 RKMPP/EGL 使用独立 PacerRender 线程,诊断会话主线程 render 调用数为 0;因此第三补丁的新调度没有已证明的现场收益,未启用。当时默认客户端恢复至 SHA256 `35de7b1a4ade583823b23b5f2e012307f6d0a3848c8eb97bd6d4de9660afa00f`,该版本现保留作备份。2026-11-05加入0005后现场SHA为 `5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03`,Linux profile在获取远端租约前严格校验此SHA;独立光标回归见[实施记录](../../docs/linux-native-cursor-20261105.md)。现场构建保留已有修改并加入0005,尚未重建部署其他任务的0004;构建器0001~0005清洁应用通过与现场补丁集合应分开判断。
|
||||
|
||||
`VDI_LATEST_FRAME=1` 只在没有 VSync pacing source 的呈现队列生效。最多保留一个待呈现解码帧,丢弃数计入原 `pacerDroppedFrames`。不丢压缩包,不改变 H.264 参考链,也不提前释放正在呈现或延迟释放的上一帧。未设置或设为 `0` 时保留上游帧队列策略;kiosk 私有 wrapper 默认设为 `1`。smooth 档仍使用原 pacing 行为。
|
||||
|
||||
这些补丁不增加本地光标,不隐藏 Windows 光标,不改变控制协议。远程光标仍随视频显示,其延迟仍受服务器捕获/编码、网络、解码和显示影响。
|
||||
0001~0004不增加本地光标。0005仅在已配置Linux主机启用原生独立光标,不改变Windows光标或Moonlight原可靠输入通道。Windows远程光标仍随视频显示,其延迟受服务器捕获/编码、网络、解码和显示影响。
|
||||
|
||||
## 构建与测试
|
||||
|
||||
@@ -41,4 +43,4 @@ VDI_TEST_SANITIZE=1 ASAN_OPTIONS=detect_leaks=1 bash tools/test-moonlight-latenc
|
||||
|
||||
GPU修复后实测见`docs/2k60-validation-20261103.md`:当前正常二进制保持不变,VDD60Hz/串流45FPS已配置并从kiosk重连;约43~44FPS渲染。2K60请求受当前盒子呈现路径限制,不能宣称稳定60。`experiments/0004-vdi-egl-exact-size-sampling.patch`为未采用的精确尺寸采样实验,未进入正常构建器;默认仍为原GL_LINEAR取样。该实验候选二进制也未被正常wrapper使用。
|
||||
|
||||
独立光标已完成受控X11辅助程序原型,结果见`docs/independent-cursor-feasibility-20261103.md`。尚无自动服务端形状同步和可靠帧交接,未加入正常构建/启动流程;不能把Ctrl+Alt+Shift+C显示本地箭头和Sunshine全局N切换直接当生产方案。现有客户端保持单个Windows远程白色光标。
|
||||
2026-11-03的Windows受控X11辅助程序原型见`docs/independent-cursor-feasibility-20261103.md`;该Windows原型没有自动形状同步或可靠帧交接,未进入正常启动流程。2026-11-05的Linux方案由0005和`terminal/native-cursor/`实现自动形状同步,通过连接前/整次会话后交接避开运行中帧切换;完整失焦和广泛缩放等场景尚未验收。不能把固定本地箭头和Sunshine全局N切换当作该方案。Windows保持单个远程白色光标。
|
||||
@@ -0,0 +1,237 @@
|
||||
--- a/app/streaming/session.cpp
|
||||
+++ b/app/streaming/session.cpp
|
||||
@@ -1,3 +1,4 @@
|
||||
+#include "vdi_native_cursor.h"
|
||||
#include "session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "streaming/streamutils.h"
|
||||
@@ -1968,8 +1969,10 @@
|
||||
VdiInputTiming vdiInputTiming(qEnvironmentVariableIntValue("VDI_INPUT_TIMING") == 1);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "VDI input priority: %s",
|
||||
qEnvironmentVariableIntValue("VDI_INPUT_PRIORITY") == 1 ? "enabled" : "disabled");
|
||||
+ VdiNativeCursor vdiNativeCursor(m_Window, m_InputHandler);
|
||||
SDL_Event event;
|
||||
for (;;) {
|
||||
+ if (!vdiNativeCursor.tick()) goto DispatchDeferredCleanup;
|
||||
if (!vdiInputPriority.takeDeferred(event, SDL_GetWindowID(m_Window))) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK)
|
||||
// SDL 2.0.18 has a proper wait event implementation that uses platform
|
||||
@@ -1981,7 +1984,7 @@
|
||||
// NB: This behavior was introduced in SDL 2.0.16, but had a few critical
|
||||
// issues that could cause indefinite timeouts, delayed joystick detection,
|
||||
// and other problems.
|
||||
- if (!SDL_WaitEventTimeout(&event, 1000)) {
|
||||
+ if (!SDL_WaitEventTimeout(&event, vdiNativeCursor.enabled() ? 4 : 1000)) {
|
||||
presence.runCallbacks();
|
||||
continue;
|
||||
}
|
||||
@@ -2006,6 +2009,7 @@
|
||||
continue;
|
||||
}
|
||||
}
|
||||
+ vdiNativeCursor.observe(event);
|
||||
vdiInputTiming.observe(event, SDL_GetWindowID(m_Window));
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
@@ -2322,6 +2326,7 @@
|
||||
}
|
||||
|
||||
DispatchDeferredCleanup:
|
||||
+ vdiNativeCursor.shutdown();
|
||||
vdiInputTiming.report(vdiInputPriority.prioritized());
|
||||
// Switch back to synchronous logging mode
|
||||
StreamUtils::exitAsyncLoggingMode();
|
||||
--- a/app/streaming/input/input.h
|
||||
+++ b/app/streaming/input/input.h
|
||||
@@ -89,6 +89,12 @@
|
||||
~SdlInputHandler();
|
||||
|
||||
void setWindow(SDL_Window* window);
|
||||
+
|
||||
+ bool isNativeCursorAbsoluteMode() const { return m_AbsoluteMouseMode; }
|
||||
+ void enableNativeCursorVisibility() {
|
||||
+ m_MouseCursorCapturedVisibilityState = SDL_ENABLE;
|
||||
+ SDL_ShowCursor(SDL_ENABLE);
|
||||
+ }
|
||||
|
||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||
|
||||
--- a/app/streaming/input/keyboard.cpp
|
||||
+++ b/app/streaming/input/keyboard.cpp
|
||||
@@ -68,6 +68,7 @@
|
||||
break;
|
||||
|
||||
case KeyComboToggleMouseMode:
|
||||
+ if (SDL_getenv("VDI_NATIVE_CURSOR_STATE")) break;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo");
|
||||
|
||||
@@ -82,6 +83,7 @@
|
||||
break;
|
||||
|
||||
case KeyComboToggleCursorHide:
|
||||
+ if (SDL_getenv("VDI_NATIVE_CURSOR_STATE")) break;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo");
|
||||
|
||||
--- /dev/null
|
||||
+++ b/app/streaming/vdi_native_cursor.h
|
||||
@@ -0,0 +1,158 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include <QFile>
|
||||
+#include <QImage>
|
||||
+#include <QSaveFile>
|
||||
+#include <QtEndian>
|
||||
+#include <time.h>
|
||||
+#include "input/input.h"
|
||||
+#include "streamutils.h"
|
||||
+
|
||||
+// Linux X11 single-seat native cursor. Called only by the SDL main thread.
|
||||
+// The producer starts cursor-free before this session's first captured frame.
|
||||
+class VdiNativeCursor {
|
||||
+public:
|
||||
+ VdiNativeCursor(SDL_Window* window, SdlInputHandler* input) :
|
||||
+ m_Window(window), m_Input(input), m_Path(qgetenv("VDI_NATIVE_CURSOR_STATE")),
|
||||
+ m_Epoch(qgetenv("VDI_NATIVE_CURSOR_EPOCH").toULongLong()) {}
|
||||
+
|
||||
+ bool enabled() const { return !m_Path.isEmpty(); }
|
||||
+
|
||||
+ void observe(const SDL_Event& event) {
|
||||
+ if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN ||
|
||||
+ event.type == SDL_MOUSEBUTTONUP) {
|
||||
+ m_LastInput = nowNs();
|
||||
+ ++m_InputSequence;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ bool tick() {
|
||||
+ if (!enabled()) return true;
|
||||
+ if (QByteArray(SDL_GetCurrentVideoDriver()) != "x11") return fail("requires X11 cursor backend");
|
||||
+ const quint64 pollTime = nowNs();
|
||||
+ if (pollTime - m_LastPoll < 4000000) return true;
|
||||
+ m_LastPoll = pollTime;
|
||||
+ QFile f(QString::fromUtf8(m_Path));
|
||||
+ if (!f.open(QIODevice::ReadOnly) || f.size() < 76 || f.size() > 65608)
|
||||
+ return fail("missing or oversized state");
|
||||
+ const QByteArray packet = f.readAll();
|
||||
+ // The producer may atomically replace state after pollTime. Compare
|
||||
+ // against a timestamp taken AFTER the read, never before opening it.
|
||||
+ const quint64 now = nowNs();
|
||||
+ const auto* p = reinterpret_cast<const uchar*>(packet.constData());
|
||||
+ auto u32 = [p](int offset) { return qFromLittleEndian<quint32>(p + offset); };
|
||||
+ auto u64 = [p](int offset) { return qFromLittleEndian<quint64>(p + offset); };
|
||||
+ if (packet.left(8) != QByteArray("VDICUR1\0", 8) || u64(8) != m_Epoch ||
|
||||
+ u64(16) < m_Sequence || u64(24) > now || now - u64(24) > 2000000000)
|
||||
+ return fail("stale or mismatched lease");
|
||||
+ const int w = u32(32), h = u32(36), hx = u32(40), hy = u32(44);
|
||||
+ const int x = qint32(u32(48)), y = qint32(u32(52));
|
||||
+ const int sw = u32(56), sh = u32(60);
|
||||
+ if (w < 1 || w > 128 || h < 1 || h > 128 || hx < 0 || hx >= w ||
|
||||
+ hy < 0 || hy >= h || sw != 2560 || sh != 1440 ||
|
||||
+ x < 0 || x >= sw || y < 0 || y >= sh || packet.size() != 72 + w*h*4 ||
|
||||
+ !m_Input->isNativeCursorAbsoluteMode())
|
||||
+ return fail("unsupported shape, display or input mode");
|
||||
+ m_Sequence = u64(16);
|
||||
+ int ww, wh;
|
||||
+ SDL_GetWindowSize(m_Window, &ww, &wh);
|
||||
+ SDL_Rect src = {0, 0, sw, sh}, dst = {0, 0, ww, wh};
|
||||
+ StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
+ if (dst.w <= 0 || dst.h <= 0) return fail("empty viewport");
|
||||
+ const int cw = qMax(1, qRound(double(w) * dst.w / sw));
|
||||
+ const int ch = qMax(1, qRound(double(h) * dst.h / sh));
|
||||
+ if (cw > 256 || ch > 256) return fail("oversized scaled cursor");
|
||||
+ const quint64 acknowledged = u64(64 + w*h*4);
|
||||
+ const QByteArray pixels = packet.mid(64, w*h*4);
|
||||
+ if (pixels != m_Pixels || w != m_Width || h != m_Height || hx != m_HotX ||
|
||||
+ hy != m_HotY || dst.w != m_ViewWidth || dst.h != m_ViewHeight) {
|
||||
+ QImage image(reinterpret_cast<const uchar*>(pixels.constData()), w, h,
|
||||
+ w * 4, QImage::Format_RGBA8888);
|
||||
+ image = image.scaled(cw, ch, Qt::IgnoreAspectRatio, Qt::FastTransformation);
|
||||
+ // SDL2's X11 Xcursor backend copies pixels without premultiplying.
|
||||
+ // XRender requires premultiplied channels, including antialiased edges.
|
||||
+ image = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
+ SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormatFrom(
|
||||
+ image.bits(), cw, ch, 32, image.bytesPerLine(), SDL_PIXELFORMAT_RGBA32);
|
||||
+ if (!surface) return fail("SDL surface failed");
|
||||
+ SDL_Cursor* next = SDL_CreateColorCursor(surface,
|
||||
+ qMin(cw - 1, qRound(double(hx) * dst.w / sw)),
|
||||
+ qMin(ch - 1, qRound(double(hy) * dst.h / sh)));
|
||||
+ SDL_FreeSurface(surface);
|
||||
+ if (!next) return fail("SDL cursor failed");
|
||||
+ SDL_SetCursor(next);
|
||||
+ if (m_Cursor) SDL_FreeCursor(m_Cursor);
|
||||
+ m_Cursor = next;
|
||||
+ m_Pixels = pixels;
|
||||
+ m_Width = w; m_Height = h; m_HotX = hx; m_HotY = hy;
|
||||
+ m_ViewWidth = dst.w; m_ViewHeight = dst.h;
|
||||
+ SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
+ "VDI native shape: %dx%d hotspot=%d,%d sequence=%llu",
|
||||
+ w, h, hx, hy, (unsigned long long)m_Sequence);
|
||||
+ }
|
||||
+ int mx, my;
|
||||
+ const auto buttons = SDL_GetMouseState(&mx, &my);
|
||||
+ const bool inside = mx >= dst.x && my >= dst.y && mx < dst.x + dst.w && my < dst.y + dst.h;
|
||||
+ const bool focused = SDL_GetKeyboardFocus() == m_Window;
|
||||
+ if (m_PublishedInput != m_InputSequence) {
|
||||
+ QByteArray message(16, '\0');
|
||||
+ qToLittleEndian<quint64>(m_InputSequence, reinterpret_cast<uchar*>(message.data()));
|
||||
+ qToLittleEndian<qint32>(qBound(0, (mx-dst.x)*sw/dst.w, sw-1),
|
||||
+ reinterpret_cast<uchar*>(message.data()+8));
|
||||
+ qToLittleEndian<qint32>(qBound(0, (my-dst.y)*sh/dst.h, sh-1),
|
||||
+ reinterpret_cast<uchar*>(message.data()+12));
|
||||
+ QSaveFile outgoing(QString::fromUtf8(m_Path)+".input");
|
||||
+ if (!outgoing.open(QIODevice::WriteOnly) || outgoing.write(message) != 16 || !outgoing.commit())
|
||||
+ return fail("input acknowledgement file failed");
|
||||
+ m_PublishedInput = m_InputSequence;
|
||||
+ }
|
||||
+ m_Input->enableNativeCursorVisibility();
|
||||
+ SDL_SetCursor(focused && inside ? m_Cursor : SDL_GetDefaultCursor());
|
||||
+ // Initial alignment, and host-initiated warps only after local input has
|
||||
+ // settled. Never pull the pointer back using delayed motion echoes.
|
||||
+ if (focused && inside && !buttons && acknowledged == m_InputSequence &&
|
||||
+ now - m_LastInput > 500000000) {
|
||||
+ const int tx = dst.x + qRound(double(x) * dst.w / sw);
|
||||
+ const int ty = dst.y + qRound(double(y) * dst.h / sh);
|
||||
+ if (qAbs(tx - mx) > 2 || qAbs(ty - my) > 2) {
|
||||
+ // SDL2's X11 in-window warp is a no-op with pointer barriers
|
||||
+ // (used by fullscreen absolute mode). Global warp honors this
|
||||
+ // in-viewport target while actually moving the native pointer.
|
||||
+ int wx, wy;
|
||||
+ SDL_GetWindowPosition(m_Window, &wx, &wy);
|
||||
+ if (SDL_WarpMouseGlobal(wx+tx, wy+ty) < 0)
|
||||
+ return fail("host cursor warp failed");
|
||||
+ m_LastInput = now;
|
||||
+ }
|
||||
+ }
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ void shutdown() {
|
||||
+ if (m_Cursor) {
|
||||
+ SDL_SetCursor(SDL_GetDefaultCursor());
|
||||
+ SDL_FreeCursor(m_Cursor);
|
||||
+ m_Cursor = nullptr;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+private:
|
||||
+ static quint64 nowNs() {
|
||||
+ struct timespec t;
|
||||
+ clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
+ return quint64(t.tv_sec)*1000000000 + t.tv_nsec;
|
||||
+ }
|
||||
+ bool fail(const char* reason) {
|
||||
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VDI native cursor ended: %s", reason);
|
||||
+ shutdown();
|
||||
+ return false;
|
||||
+ }
|
||||
+ SDL_Window* m_Window;
|
||||
+ SdlInputHandler* m_Input;
|
||||
+ QByteArray m_Path, m_Pixels;
|
||||
+ quint64 m_Epoch, m_Sequence = 0, m_LastPoll = 0, m_LastInput = nowNs();
|
||||
+ quint64 m_InputSequence = 0, m_PublishedInput = 0;
|
||||
+ SDL_Cursor* m_Cursor = nullptr;
|
||||
+ int m_Width = 0, m_Height = 0, m_HotX = 0, m_HotY = 0;
|
||||
+ int m_ViewWidth = 0, m_ViewHeight = 0;
|
||||
+};
|
||||
Reference in new issue
Block a user