功能:接入Linux原生光标并完善设备接入工具

This commit is contained in:
songyiming committed 2026-11-05 19:18:54 +08:00
1 parent a14f028402
commit a47968496e
92 files changed
+3090 -22

No files matched your search

@@ -0,0 +1,28 @@
[
{
"host": "192.168.1.72",
"normal": {
"width": 24,
"height": 24,
"visible_pixels": 240
},
"adapter": {
"width": 24,
"height": 24,
"visible_pixels": 0
}
},
{
"host": "192.168.1.71",
"normal": {
"width": 16,
"height": 16,
"visible_pixels": 0
},
"adapter": {
"width": 16,
"height": 16,
"visible_pixels": 0
}
}
]
@@ -0,0 +1,16 @@
{
"ubuntu": {
"measured_clicks": 28,
"matched_source_events": 28,
"max_coordinate_error_pixels": 0.9814453125,
"p50_ms": 188.9504005,
"p95_ms": 468.920931
},
"kylin": {
"measured_clicks": 28,
"matched_source_events": 28,
"max_coordinate_error_pixels": 0.74267578125,
"p50_ms": 127.61443249999999,
"p95_ms": 168.630841
}
}
@@ -0,0 +1 @@
5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03
@@ -0,0 +1,13 @@
{
"192.168.1.71": {
"host_py_sha256": "41f6a9a7dcbe3b916864eb3a4bb1b2cbaf669c779256f913fbeef4f53cf09b65",
"client_sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03",
"status": "600\nActiveState=inactive\n"
},
"192.168.1.72": {
"host_py_sha256": "41f6a9a7dcbe3b916864eb3a4bb1b2cbaf669c779256f913fbeef4f53cf09b65",
"client_sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03",
"status": "600\nActiveState=active\n39181 /usr/bin/python3 /home/test2/vdi-terminal/native-cursor-v1/host.py stream\n"
},
"box_helper_sha256": "44b3e8a0ca591fbde42cf3372d343ab4cb603c089e7805bbccf6d261b2e173ac"
}
@@ -0,0 +1,103 @@
"""Run only with the controlled GTK acceptance window filling the stream."""
import ctypes as C
import json
import os
from pathlib import Path
import signal
import statistics
import struct
import subprocess
import time
os.environ.update(DISPLAY=':0', XAUTHORITY='/home/armbian/.Xauthority')
x = C.CDLL('libX11.so.6')
fix = C.CDLL('libXfixes.so.3')
test = C.CDLL('libXtst.so.6')
class Cursor(C.Structure):
_fields_ = [('x', C.c_short), ('y', C.c_short), ('w', C.c_ushort),
('h', C.c_ushort), ('hx', C.c_ushort), ('hy', C.c_ushort),
('serial', C.c_ulong), ('pixels', C.POINTER(C.c_ulong)),
('atom', C.c_ulong), ('name', C.c_char_p)]
x.XOpenDisplay.argtypes = [C.c_char_p]
x.XOpenDisplay.restype = C.c_void_p
x.XDefaultRootWindow.argtypes = [C.c_void_p]
x.XDefaultRootWindow.restype = C.c_ulong
x.XFree.argtypes = [C.c_void_p]
x.XSync.argtypes = [C.c_void_p, C.c_int]
x.XWarpPointer.argtypes = [C.c_void_p, C.c_ulong, C.c_ulong, C.c_int,
C.c_int, C.c_uint, C.c_uint, C.c_int, C.c_int]
x.XGetImage.argtypes = [C.c_void_p, C.c_ulong, C.c_int, C.c_int,
C.c_uint, C.c_uint, C.c_ulong, C.c_int]
x.XGetImage.restype = C.c_void_p
x.XGetPixel.argtypes = [C.c_void_p, C.c_int, C.c_int]
x.XGetPixel.restype = C.c_ulong
x.XDestroyImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.restype = C.POINTER(Cursor)
test.XTestFakeButtonEvent.argtypes = [C.c_void_p, C.c_uint, C.c_int, C.c_ulong]
d = x.XOpenDisplay(None)
assert d
root = x.XDefaultRootWindow(d)
def native():
p = fix.XFixesGetCursorImage(d)
assert p
try:
c = p.contents
return dict(x=c.x, y=c.y, w=c.w, h=c.h, hx=c.hx, hy=c.hy,
pixels=[int(c.pixels[i]) & 0xffffffff for i in range(c.w*c.h)])
finally:
x.XFree(p)
def warp(px, py):
x.XWarpPointer(d, 0, root, 0, 0, 0, 0, px, py)
x.XSync(d, 0)
def decoded_sequence():
im = x.XGetImage(d, root, 40, 1370, 640, 1, 0xffffffff, 2)
assert im
try:
values = [x.XGetPixel(im, i*40+20, 0) & 0xffffff for i in range(16)]
if any(0x202020 <= v <= 0xe0e0e0 for v in values):
return -1
return sum((1 << i) for i, v in enumerate(values) if v > 0xe0e0e0)
finally:
x.XDestroyImage(im)
def flat_video_crop(px, py):
im = x.XGetImage(d, root, px-40, py-40, 100, 100, 0xffffffff, 2)
assert im
try:
pixels = [x.XGetPixel(im, a, b) & 0xffffff for b in range(100) for a in range(100)]
# No baked-in pointer: uniform acceptance-window background at this point.
ref = pixels[0]
bad = sum(any(abs(((v >> s) & 255) - ((ref >> s) & 255)) > 8
for s in (0, 8, 16)) for v in pixels)
return dict(reference=ref, nonuniform_pixels=bad)
finally:
x.XDestroyImage(im)
start_sequence=decoded_sequence()
results=[]
for i in range(16):
px,py=((300,800) if i%2==0 else (1500,900))
warp(px,py);time.sleep(.18)
start=time.monotonic_ns();test.XTestFakeButtonEvent(d,1,1,0);x.XSync(d,0)
time.sleep(.04);test.XTestFakeButtonEvent(d,1,0,0);x.XSync(d,0)
target=start_sequence+i+1
while decoded_sequence()!=target and time.monotonic_ns()-start<2000000000:time.sleep(.002)
seen=decoded_sequence();results.append(dict(seq=target,seen=seen,x=px,y=py));assert seen==target,results[-1]
time.sleep(.08)
# Two quick clicks before waiting for their combined numbered video update.
start=decoded_sequence()
for i in range(2):
test.XTestFakeButtonEvent(d,1,1,0);x.XSync(d,0);time.sleep(.04);test.XTestFakeButtonEvent(d,1,0,0);x.XSync(d,0);time.sleep(.07)
end=time.monotonic()+2
while decoded_sequence()!=start+2 and time.monotonic()<end:time.sleep(.002)
print(json.dumps(dict(clicks=results,double_click_start=start,double_click_end=decoded_sequence())))
@@ -0,0 +1,10 @@
{
"192.168.1.71": {
"status": "MainPID=194186\nExecStart={ path=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine ; argv[]=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 12:28:12 CST] ; stop_time=[n/a] ; pid=194186 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=0\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test1/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=inactive\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\nScreen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192\nVirtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm\n'Adwaita'\n24\n'default'\n1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71 /opt/vdi-sunshine-0.21.0/usr/bin/sunshine\nno-runtime-dropin\n",
"stderr": ""
},
"192.168.1.72": {
"status": "MainPID=42901\nExecStart={ path=/home/test2/vdi-terminal/native-cursor-v1/sunshine ; argv[]=/home/test2/vdi-terminal/native-cursor-v1/sunshine /home/test2/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 12:29:41 CST] ; stop_time=[n/a] ; pid=42901 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=42870\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test2/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=active\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\nScreen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192\nVirtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm\n'Adwaita'\n24\n'default'\n61e1ecbc8db2df786b9760c652aa4b9885dff36c57d4af09616344314cb7c34c /usr/bin/sunshine\n/usr/bin/sunshine cap_sys_admin,cap_sys_nice=p\n",
"stderr": ""
}
}
@@ -0,0 +1,4 @@
{
"192.168.1.72": "MainPID=42703\nExecStart={ path=/usr/bin/sunshine ; argv[]=/usr/bin/sunshine /home/test2/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 12:25:39 CST] ; stop_time=[n/a] ; pid=42703 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=0\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test2/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=inactive\n",
"192.168.1.71": "MainPID=193092\nExecStart={ path=/home/test1/vdi-terminal/native-cursor-v1/sunshine ; argv[]=/home/test1/vdi-terminal/native-cursor-v1/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 12:26:14 CST] ; stop_time=[n/a] ; pid=193092 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=193023\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test1/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=active\n"
}
@@ -0,0 +1,96 @@
"""Run only with the controlled GTK acceptance window filling the stream."""
import ctypes as C
import json
import os
from pathlib import Path
import signal
import statistics
import struct
import subprocess
import time
os.environ.update(DISPLAY=':0', XAUTHORITY='/home/armbian/.Xauthority')
x = C.CDLL('libX11.so.6')
fix = C.CDLL('libXfixes.so.3')
test = C.CDLL('libXtst.so.6')
class Cursor(C.Structure):
_fields_ = [('x', C.c_short), ('y', C.c_short), ('w', C.c_ushort),
('h', C.c_ushort), ('hx', C.c_ushort), ('hy', C.c_ushort),
('serial', C.c_ulong), ('pixels', C.POINTER(C.c_ulong)),
('atom', C.c_ulong), ('name', C.c_char_p)]
x.XOpenDisplay.argtypes = [C.c_char_p]
x.XOpenDisplay.restype = C.c_void_p
x.XDefaultRootWindow.argtypes = [C.c_void_p]
x.XDefaultRootWindow.restype = C.c_ulong
x.XFree.argtypes = [C.c_void_p]
x.XSync.argtypes = [C.c_void_p, C.c_int]
x.XWarpPointer.argtypes = [C.c_void_p, C.c_ulong, C.c_ulong, C.c_int,
C.c_int, C.c_uint, C.c_uint, C.c_int, C.c_int]
x.XGetImage.argtypes = [C.c_void_p, C.c_ulong, C.c_int, C.c_int,
C.c_uint, C.c_uint, C.c_ulong, C.c_int]
x.XGetImage.restype = C.c_void_p
x.XGetPixel.argtypes = [C.c_void_p, C.c_int, C.c_int]
x.XGetPixel.restype = C.c_ulong
x.XDestroyImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.restype = C.POINTER(Cursor)
test.XTestFakeButtonEvent.argtypes = [C.c_void_p, C.c_uint, C.c_int, C.c_ulong]
d = x.XOpenDisplay(None)
assert d
root = x.XDefaultRootWindow(d)
def native():
p = fix.XFixesGetCursorImage(d)
assert p
try:
c = p.contents
return dict(x=c.x, y=c.y, w=c.w, h=c.h, hx=c.hx, hy=c.hy,
pixels=[int(c.pixels[i]) & 0xffffffff for i in range(c.w*c.h)])
finally:
x.XFree(p)
def warp(px, py):
x.XWarpPointer(d, 0, root, 0, 0, 0, 0, px, py)
x.XSync(d, 0)
def decoded_sequence():
im = x.XGetImage(d, root, 40, 1370, 640, 1, 0xffffffff, 2)
assert im
try:
values = [x.XGetPixel(im, i*40+20, 0) & 0xffffff for i in range(16)]
if any(0x202020 <= v <= 0xe0e0e0 for v in values):
return -1
return sum((1 << i) for i, v in enumerate(values) if v > 0xe0e0e0)
finally:
x.XDestroyImage(im)
def flat_video_crop(px, py):
im = x.XGetImage(d, root, px-40, py-40, 100, 100, 0xffffffff, 2)
assert im
try:
pixels = [x.XGetPixel(im, a, b) & 0xffffff for b in range(100) for a in range(100)]
# No baked-in pointer: uniform acceptance-window background at this point.
ref = pixels[0]
bad = sum(any(abs(((v >> s) & 255) - ((ref >> s) & 255)) > 8
for s in (0, 8, 16)) for v in pixels)
return dict(reference=ref, nonuniform_pixels=bad)
finally:
x.XDestroyImage(im)
focus=C.c_ulong();revert=C.c_int()
x.XGetInputFocus.argtypes=[C.c_void_p,C.POINTER(C.c_ulong),C.POINTER(C.c_int)]
x.XSetInputFocus.argtypes=[C.c_void_p,C.c_ulong,C.c_int,C.c_ulong]
x.XGetInputFocus(d,C.byref(focus),C.byref(revert))
before=native()
try:
x.XSetInputFocus(d,0,1,0);x.XSync(d,0);time.sleep(.25);unfocused=native()
finally:
x.XSetInputFocus(d,focus.value,revert.value,0);x.XSync(d,0)
time.sleep(.3);after=native()
print(json.dumps(dict(original_focus=focus.value,unfocused_shape_changed=before['pixels']!=unfocused['pixels'],native_restored=before['pixels']==after['pixels'],position=[after['x'],after['y']])))
@@ -0,0 +1,10 @@
{
"192.168.1.71": {
"output": "MainPID=184769\nExecStart={ path=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine ; argv[]=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:33:00 CST] ; stop_time=[n/a] ; pid=184769 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=0\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test1/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=inactive\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\n",
"stderr": "sha256sum: /usr/bin/sunshine: 没有那个文件或目录\n/usr/bin/sunshine (No such file or directory)\n"
},
"192.168.1.72": {
"output": "MainPID=32278\nExecStart={ path=/usr/bin/sunshine ; argv[]=/usr/bin/sunshine /home/test2/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:43:42 CST] ; stop_time=[n/a] ; pid=32278 ; code=(null) ; status=0/0 }\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=0\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /home/test2/vdi-terminal/native-cursor-v1/host.py guard ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\nId=vdi-native-cursor-guard.service\nActiveState=inactive\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\n61e1ecbc8db2df786b9760c652aa4b9885dff36c57d4af09616344314cb7c34c /usr/bin/sunshine\n/usr/bin/sunshine cap_sys_admin,cap_sys_nice=p\n",
"stderr": ""
}
}
@@ -0,0 +1 @@
{"clicks": [{"seq": 29, "seen": 29, "x": 300, "y": 800}, {"seq": 30, "seen": 30, "x": 1500, "y": 900}, {"seq": 31, "seen": 31, "x": 300, "y": 800}, {"seq": 32, "seen": 32, "x": 1500, "y": 900}, {"seq": 33, "seen": 33, "x": 300, "y": 800}, {"seq": 34, "seen": 34, "x": 1500, "y": 900}, {"seq": 35, "seen": 35, "x": 300, "y": 800}, {"seq": 36, "seen": 36, "x": 1500, "y": 900}, {"seq": 37, "seen": 37, "x": 300, "y": 800}, {"seq": 38, "seen": 38, "x": 1500, "y": 900}, {"seq": 39, "seen": 39, "x": 300, "y": 800}, {"seq": 40, "seen": 40, "x": 1500, "y": 900}, {"seq": 41, "seen": 41, "x": 300, "y": 800}, {"seq": 42, "seen": 42, "x": 1500, "y": 900}, {"seq": 43, "seen": 43, "x": 300, "y": 800}, {"seq": 44, "seen": 44, "x": 1500, "y": 900}], "double_click_start": 44, "double_click_end": 46}
@@ -0,0 +1 @@
{"host": "192.168.1.71", "pid": 267646, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 4, "hy": 0, "x": 182, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 4, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 300, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 548, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 216, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 914, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 204, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 1280, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 290, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 252, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [350, 700], "elapsed_ms": 249.580297, "same_shape": true, "steps": [{"target": [250, 650], "position": [250, 650], "query_ms": 2.364751}, {"target": [280, 670], "position": [280, 670], "query_ms": 2.234379}, {"target": [300, 690], "position": [300, 690], "query_ms": 2.128216}, {"target": [350, 700], "position": [350, 700], "query_ms": 2.286878}]}, "clicks": [{"seq": 1, "x": 182, "y": 750, "seen": 1, "decoded_ms": 108.930461}, {"seq": 2, "x": 548, "y": 850, "seen": 2, "decoded_ms": 98.691574}, {"seq": 3, "x": 914, "y": 750, "seen": 3, "decoded_ms": 152.320189}, {"seq": 4, "x": 1280, "y": 850, "seen": 4, "decoded_ms": 125.352183}, {"seq": 5, "x": 1645, "y": 750, "seen": 5, "decoded_ms": 135.816524}, {"seq": 6, "x": 2011, "y": 850, "seen": 6, "decoded_ms": 111.517755}, {"seq": 7, "x": 2377, "y": 750, "seen": 7, "decoded_ms": 108.898971}, {"seq": 8, "x": 182, "y": 850, "seen": 8, "decoded_ms": 119.075451}, {"seq": 9, "x": 548, "y": 750, "seen": 9, "decoded_ms": 134.943313}, {"seq": 10, "x": 914, "y": 850, "seen": 10, "decoded_ms": 146.081384}, {"seq": 11, "x": 1280, "y": 750, "seen": 11, "decoded_ms": 117.025403}, {"seq": 12, "x": 1645, "y": 850, "seen": 12, "decoded_ms": 152.719197}, {"seq": 13, "x": 2011, "y": 750, "seen": 13, "decoded_ms": 141.342555}, {"seq": 14, "x": 2377, "y": 850, "seen": 14, "decoded_ms": 130.95113}, {"seq": 15, "x": 182, "y": 750, "seen": 15, "decoded_ms": 143.698269}, {"seq": 16, "x": 548, "y": 850, "seen": 16, "decoded_ms": 114.9634}, {"seq": 17, "x": 914, "y": 750, "seen": 17, "decoded_ms": 159.173858}, {"seq": 18, "x": 1280, "y": 850, "seen": 18, "decoded_ms": 175.329882}, {"seq": 19, "x": 1645, "y": 750, "seen": 19, "decoded_ms": 119.051263}, {"seq": 20, "x": 2011, "y": 850, "seen": 20, "decoded_ms": 124.040341}, {"seq": 21, "x": 2377, "y": 750, "seen": 21, "decoded_ms": 121.588678}, {"seq": 22, "x": 182, "y": 850, "seen": 22, "decoded_ms": 129.876682}, {"seq": 23, "x": 548, "y": 750, "seen": 23, "decoded_ms": 124.467041}, {"seq": 24, "x": 914, "y": 850, "seen": 24, "decoded_ms": 153.052003}, {"seq": 25, "x": 1280, "y": 750, "seen": 25, "decoded_ms": 136.166846}, {"seq": 26, "x": 1645, "y": 850, "seen": 26, "decoded_ms": 168.630841}, {"seq": 27, "x": 2011, "y": 750, "seen": 27, "decoded_ms": 119.541845}, {"seq": 28, "x": 2377, "y": 850, "seen": 28, "decoded_ms": 110.41562}], "p50_ms": 127.61443249999999, "p95_ms": 168.630841, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

@@ -0,0 +1 @@
{"original_focus": 48234511, "unfocused_shape_changed": false, "native_restored": true, "position": [400, 600]}
@@ -0,0 +1 @@
{"pid": 297530, "args": "/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.71 Desktop --resolution 2560x1440 --fps 30 --bitrate 30000 --display-mode fullscreen --video-codec H.264 --video-decoder hardware --absolute-mouse --capture-system-keys always --keep-awake --no-vsync --no-frame-pacing --audio-on-host ", "sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03", "native": true, "shape": [24, 24, 4, 0]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

@@ -0,0 +1 @@
{"host": "192.168.1.71", "pid": 297530, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 4, "hy": 0, "x": 182, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 4, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 300, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 548, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 216, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 914, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 204, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 1280, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 290, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 252, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 600, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [350, 700], "elapsed_ms": 249.614056, "same_shape": true, "steps": [{"target": [250, 650], "position": [250, 650], "query_ms": 2.255936}, {"target": [280, 670], "position": [280, 670], "query_ms": 2.398262}, {"target": [300, 690], "position": [300, 690], "query_ms": 2.188272}, {"target": [350, 700], "position": [350, 700], "query_ms": 2.242227}]}, "clicks": [{"seq": 1, "x": 182, "y": 750, "seen": 1, "decoded_ms": 149.895203}, {"seq": 2, "x": 548, "y": 850, "seen": 2, "decoded_ms": 120.380242}, {"seq": 3, "x": 914, "y": 750, "seen": 3, "decoded_ms": 119.660739}, {"seq": 4, "x": 1280, "y": 850, "seen": 4, "decoded_ms": 147.265671}, {"seq": 5, "x": 1645, "y": 750, "seen": 5, "decoded_ms": 145.663335}, {"seq": 6, "x": 2011, "y": 850, "seen": 6, "decoded_ms": 129.562066}, {"seq": 7, "x": 2377, "y": 750, "seen": 7, "decoded_ms": 134.743571}, {"seq": 8, "x": 182, "y": 850, "seen": 8, "decoded_ms": 140.630877}, {"seq": 9, "x": 548, "y": 750, "seen": 9, "decoded_ms": 130.420116}, {"seq": 10, "x": 914, "y": 850, "seen": 10, "decoded_ms": 107.155271}, {"seq": 11, "x": 1280, "y": 750, "seen": 11, "decoded_ms": 117.124838}, {"seq": 12, "x": 1645, "y": 850, "seen": 12, "decoded_ms": 130.236678}, {"seq": 13, "x": 2011, "y": 750, "seen": 13, "decoded_ms": 111.917383}, {"seq": 14, "x": 2377, "y": 850, "seen": 14, "decoded_ms": 136.811123}, {"seq": 15, "x": 182, "y": 750, "seen": 15, "decoded_ms": 106.746093}, {"seq": 16, "x": 548, "y": 850, "seen": 16, "decoded_ms": 108.595469}, {"seq": 17, "x": 914, "y": 750, "seen": 17, "decoded_ms": 123.440014}, {"seq": 18, "x": 1280, "y": 850, "seen": 18, "decoded_ms": 150.429282}, {"seq": 19, "x": 1645, "y": 750, "seen": 19, "decoded_ms": 117.354684}, {"seq": 20, "x": 2011, "y": 850, "seen": 20, "decoded_ms": 147.688919}, {"seq": 21, "x": 2377, "y": 750, "seen": 21, "decoded_ms": 135.505122}, {"seq": 22, "x": 182, "y": 850, "seen": 22, "decoded_ms": 142.014237}, {"seq": 23, "x": 548, "y": 750, "seen": 23, "decoded_ms": 195.466041}, {"seq": 24, "x": 914, "y": 850, "seen": 24, "decoded_ms": 165.559673}, {"seq": 25, "x": 1280, "y": 750, "seen": 25, "decoded_ms": 142.88512}, {"seq": 26, "x": 1645, "y": 850, "seen": 26, "decoded_ms": 127.731718}, {"seq": 27, "x": 2011, "y": 750, "seen": 27, "decoded_ms": 146.832648}, {"seq": 28, "x": 2377, "y": 850, "seen": 28, "decoded_ms": 133.995845}], "p50_ms": 134.369708, "p95_ms": 165.559673, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
@@ -0,0 +1,5 @@
{
"recovered": true,
"seconds": 3.555,
"state": "ExecStart={ path=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine ; argv[]=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:33:00 CST] ; stop_time=[n/a] ; pid=184769 ; code=(null) ; status=0/0 }\nActiveState=active\nScreen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192\nVirtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm\n 1280x800 74.99 +\n 3840x2160 60.00 50.00 59.94 \n 1920x1440 60.00 \ninactive\n"
}
@@ -0,0 +1,9 @@
{
"host": "192.168.1.71",
"target": [
400,
600
],
"readback": "X=400\nY=600\nSCREEN=0\nWINDOW=48234511\n",
"passed": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.
@@ -0,0 +1,12 @@
{
"192.168.1.71": {
"status": "MainPID=186038\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=185981\nId=vdi-native-cursor-guard.service\nActiveState=active\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\nScreen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192\nVirtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm\n 1280x800 74.99 +\n 3840x2160 60.00 50.00 59.94 \n 1920x1440 60.00 \n'Adwaita'\n24\n'default'\ntrue\n1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71 /opt/vdi-sunshine-0.21.0/usr/bin/sunshine\n",
"stderr": "",
"profile_client_sha": "1da552e4855d1562fdd8d6f04f1553382db4ccd364c66fe7486b1688cc584f15"
},
"192.168.1.72": {
"status": "MainPID=32278\nId=vdi-sunshine.service\nActiveState=active\n\nMainPID=0\nId=vdi-native-cursor-guard.service\nActiveState=inactive\n\nMainPID=0\nId=vdi-native-cursor-source.service\nActiveState=inactive\nScreen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192\nVirtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm\n 1280x800 74.99 +\n 1920x1440 60.00 \n 2560x1080 50.00 \n'Adwaita'\n24\n'default'\nfalse\n61e1ecbc8db2df786b9760c652aa4b9885dff36c57d4af09616344314cb7c34c /usr/bin/sunshine\n/usr/bin/sunshine cap_sys_admin,cap_sys_nice=p\n",
"stderr": "",
"profile_client_sha": "1da552e4855d1562fdd8d6f04f1553382db4ccd364c66fe7486b1688cc584f15"
}
}
@@ -0,0 +1,14 @@
{
"baseline": "de2467e433821664cdd2224aad8c89a625be1ad9",
"patches": [
"0001-vdi-input-and-latest-frame.patch",
"0003-vdi-input-priority.patch",
"0004-vdi-power-key-policy.patch",
"0005-vdi-native-linux-cursor.patch",
"0002-vdi-absolute-click-barrier.patch"
],
"native_patch_sha256": "6a5879901ae0d2986271df7441a38d0f1813c0d7fb3f616af5e1212c75eaaa02",
"returncode": 0,
"stdout": "PATCHES_OK\n",
"stderr": ""
}
@@ -0,0 +1 @@
{"patches_apply_cleanly": ["0001-vdi-input-and-latest-frame.patch", "0003-vdi-input-priority.patch", "0004-vdi-power-key-policy.patch", "0005-vdi-native-linux-cursor.patch", "0002-vdi-absolute-click-barrier.patch"], "baseline": "de2467e433821664cdd2224aad8c89a625be1ad9"}
@@ -0,0 +1,188 @@
"""Run only with the controlled GTK acceptance window filling the stream."""
import ctypes as C
import json
import os
from pathlib import Path
import signal
import statistics
import struct
import subprocess
import time
os.environ.update(DISPLAY=':0', XAUTHORITY='/home/armbian/.Xauthority')
x = C.CDLL('libX11.so.6')
fix = C.CDLL('libXfixes.so.3')
test = C.CDLL('libXtst.so.6')
class Cursor(C.Structure):
_fields_ = [('x', C.c_short), ('y', C.c_short), ('w', C.c_ushort),
('h', C.c_ushort), ('hx', C.c_ushort), ('hy', C.c_ushort),
('serial', C.c_ulong), ('pixels', C.POINTER(C.c_ulong)),
('atom', C.c_ulong), ('name', C.c_char_p)]
x.XOpenDisplay.argtypes = [C.c_char_p]
x.XOpenDisplay.restype = C.c_void_p
x.XDefaultRootWindow.argtypes = [C.c_void_p]
x.XDefaultRootWindow.restype = C.c_ulong
x.XFree.argtypes = [C.c_void_p]
x.XSync.argtypes = [C.c_void_p, C.c_int]
x.XWarpPointer.argtypes = [C.c_void_p, C.c_ulong, C.c_ulong, C.c_int,
C.c_int, C.c_uint, C.c_uint, C.c_int, C.c_int]
x.XGetImage.argtypes = [C.c_void_p, C.c_ulong, C.c_int, C.c_int,
C.c_uint, C.c_uint, C.c_ulong, C.c_int]
x.XGetImage.restype = C.c_void_p
x.XGetPixel.argtypes = [C.c_void_p, C.c_int, C.c_int]
x.XGetPixel.restype = C.c_ulong
x.XDestroyImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.argtypes = [C.c_void_p]
fix.XFixesGetCursorImage.restype = C.POINTER(Cursor)
test.XTestFakeButtonEvent.argtypes = [C.c_void_p, C.c_uint, C.c_int, C.c_ulong]
d = x.XOpenDisplay(None)
assert d
root = x.XDefaultRootWindow(d)
def native():
p = fix.XFixesGetCursorImage(d)
assert p
try:
c = p.contents
return dict(x=c.x, y=c.y, w=c.w, h=c.h, hx=c.hx, hy=c.hy,
pixels=[int(c.pixels[i]) & 0xffffffff for i in range(c.w*c.h)])
finally:
x.XFree(p)
def warp(px, py):
x.XWarpPointer(d, 0, root, 0, 0, 0, 0, px, py)
x.XSync(d, 0)
def decoded_sequence():
im = x.XGetImage(d, root, 40, 1370, 640, 1, 0xffffffff, 2)
assert im
try:
values = [x.XGetPixel(im, i*40+20, 0) & 0xffffff for i in range(16)]
if any(0x202020 <= v <= 0xe0e0e0 for v in values):
return -1
return sum((1 << i) for i, v in enumerate(values) if v > 0xe0e0e0)
finally:
x.XDestroyImage(im)
def flat_video_crop(px, py):
im = x.XGetImage(d, root, px-40, py-40, 100, 100, 0xffffffff, 2)
assert im
try:
pixels = [x.XGetPixel(im, a, b) & 0xffffff for b in range(100) for a in range(100)]
# No baked-in pointer: uniform acceptance-window background at this point.
ref = pixels[0]
bad = sum(any(abs(((v >> s) & 255) - ((ref >> s) & 255)) > 8
for s in (0, 8, 16)) for v in pixels)
return dict(reference=ref, nonuniform_pixels=bad)
finally:
x.XDestroyImage(im)
host = os.sys.argv[1]
prefix = '/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream ' + host + ' '
items = [l.split(None, 1) for l in subprocess.check_output(['ps','-eo','pid,args'], text=True).splitlines()[1:]
if len(l.split(None, 1)) == 2 and l.split(None, 1)[1].startswith(prefix)]
assert len(items) == 1, items
pid = int(items[0][0])
focus = subprocess.check_output(['xdotool','getwindowfocus','getwindowpid'], text=True).strip()
assert focus == str(pid), (focus, pid)
env = dict(item.split('=', 1) for item in Path('/proc/%d/environ' % pid).read_bytes().decode().split('\0') if '=' in item)
statepath = Path(env['VDI_NATIVE_CURSOR_STATE'])
initial_sequence = decoded_sequence()
assert 0 <= initial_sequence < 60000, 'test window not ready'
reference = flat_video_crop(180, 600)['reference']
assert all(abs(((reference >> s) & 255)-v) < 10 for s,v in [(16,240),(8,245),(0,250)]), 'acceptance window not visible'
shapes = []
for i, name in enumerate(['default','text','ew-resize','wait','none','crosshair','custom']):
px = int((i+.5)*2560/7)
warp(px, 600)
time.sleep(.6)
state = statepath.read_bytes()
header = struct.unpack('<8sQQQIIIIiiII', state[:64])
local = native()
source = dict(zip(['w','h','hx','hy','x','y','sw','sh'], header[4:]))
# Convert expected straight RGBA to XRender premultiplied ARGB.
expected = []
for j in range(64, len(state)-8, 4):
r,g,b,a = state[j:j+4]
expected.append((a << 24) | ((r*a+127)//255 << 16) |
((g*a+127)//255 << 8) | ((b*a+127)//255))
same_geometry = all(local[k] == source[k] for k in ['w','h','hx','hy'])
# Busy frames animate; these snapshots can legitimately be adjacent frames.
diff = max((abs(((a >> s) & 255)-((b >> s) & 255))
for a,b in zip(local['pixels'],expected) for s in [0,8,16,24]), default=0)
shapes.append(dict(name=name, source=source,
local={k:v for k,v in local.items() if k != 'pixels'},
geometry_matches=same_geometry, max_pixel_difference=diff,
visible_pixels=sum(p >> 24 != 0 for p in local['pixels']),
video=flat_video_crop(px, 600)))
Path('/tmp/native-%s-shapes.json' % host).write_text(json.dumps(shapes))
samples = []
for i in range(28):
px = int(((i % 7)+.5)*2560/7)
py = 750 if i % 2 == 0 else 850
warp(px, py)
time.sleep(.25)
start = time.monotonic_ns()
test.XTestFakeButtonEvent(d, 1, 1, 0)
x.XSync(d, 0)
expected = initial_sequence+i+1
seen = -1
released = False
try:
while time.monotonic_ns()-start < 3000000000:
if not released and time.monotonic_ns()-start >= 80000000:
test.XTestFakeButtonEvent(d, 1, 0, 0)
x.XSync(d, 0)
released = True
seen = decoded_sequence()
if seen == expected:
break
if seen > expected:
raise RuntimeError('unexpected duplicate click %d instead of %d' % (seen,expected))
time.sleep(.002)
finally:
if not released:
time.sleep(max(0, .08-(time.monotonic_ns()-start)/1e9))
test.XTestFakeButtonEvent(d, 1, 0, 0)
x.XSync(d, 0)
samples.append(dict(seq=expected, x=px, y=py, seen=seen,
decoded_ms=(time.monotonic_ns()-start)/1e6))
if seen != expected:
Path('/tmp/native-%s-failed-clicks.json' % host).write_text(json.dumps(samples))
raise AssertionError(samples[-1])
time.sleep(.1)
times = sorted(s['decoded_ms'] for s in samples)
# A stopped Moonlight cannot capture input, receive video, or draw frames.
# X11's native pointer must still move and keep its shape while it is stopped.
warp(200, 600)
time.sleep(.3)
before = native()
try:
os.kill(pid, signal.SIGSTOP)
start = time.monotonic_ns()
steps = []
for px,py in [(250,650),(280,670),(300,690),(350,700)]:
step_start = time.monotonic_ns()
warp(px, py)
after = native()
steps.append(dict(target=[px,py],position=[after['x'],after['y']],
query_ms=(time.monotonic_ns()-step_start)/1e6))
time.sleep(.06)
paused_ms = (time.monotonic_ns()-start)/1e6
finally:
os.kill(pid, signal.SIGCONT)
pause = dict(position=[after['x'],after['y']], elapsed_ms=paused_ms,
same_shape=before['pixels']==after['pixels'], steps=steps)
assert all(s['target']==s['position'] for s in steps) and pause['same_shape']
time.sleep(.4)
print(json.dumps(dict(host=host,pid=pid,shapes=shapes,paused_client=pause,
clicks=samples,p50_ms=statistics.median(times),
p95_ms=times[int(len(times)*.95)],
method='XTest click-down to unique decoded sequence; excludes USB and scanout')))
@@ -0,0 +1,17 @@
{
"client_sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03",
"ubuntu": {
"measured_clicks": 28,
"matched_source_events": 28,
"max_coordinate_error_pixels": 1.0546875,
"p50_ms": 188.3848435,
"p95_ms": 522.420223
},
"kylin": {
"measured_clicks": 28,
"matched_source_events": 28,
"max_coordinate_error_pixels": 0.82855224609375,
"p50_ms": 134.369708,
"p95_ms": 165.559673
}
}
@@ -0,0 +1,53 @@
"""Observe actual deployed state/receiver; keep the test seat awake with motion.
Run on the box only while a controlled native-cursor session is active.
No clicks, protocol injection, or changes to the state producer are made.
"""
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
import time
duration = float(sys.argv[1])
output = Path(sys.argv[2])
pid = int(subprocess.check_output(['pgrep', '-x', 'moonlight']).strip())
proc = Path('/proc') / str(pid)
env = dict(x.split('=', 1) for x in (proc / 'environ').read_bytes().decode().split('\0') if '=' in x)
state = Path(env['VDI_NATIVE_CURSOR_STATE'])
start = time.monotonic()
last_motion = 0
result = dict(pid=pid, requested_seconds=duration, samples=0,
timestamp_after_pre_read_clock=0, timestamp_after_post_read_clock=0,
max_age_ms=0, survived=False)
try:
while time.monotonic() - start < duration:
if not proc.exists() or (proc / 'comm').read_text().strip() != 'moonlight':
raise RuntimeError('receiver exited')
before = time.monotonic_ns()
packet = state.read_bytes()
after = time.monotonic_ns()
stamp = struct.unpack_from('<Q', packet, 24)[0]
result['samples'] += 1
result['timestamp_after_pre_read_clock'] += stamp > before
result['timestamp_after_post_read_clock'] += stamp > after
result['max_age_ms'] = max(result['max_age_ms'], (after - stamp) / 1e6)
result['last_sequence'] = struct.unpack_from('<Q', packet, 16)[0]
if stamp > after or after - stamp > 2_000_000_000:
raise RuntimeError('invalid post-read timestamp')
now = time.monotonic()
if now - last_motion > 10:
px = 1800 + (int(now - start) // 10 % 2) * 50
motion_env = dict(os.environ, DISPLAY=':0', XAUTHORITY='/home/armbian/.Xauthority')
subprocess.run(['xdotool', 'mousemove', str(px), '1100'], env=motion_env, check=True)
last_motion = now
time.sleep(.004)
result['survived'] = True
except Exception as error:
result['error'] = str(error)
finally:
result['elapsed_seconds'] = round(time.monotonic() - start, 3)
output.write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(result), flush=True)
@@ -0,0 +1,92 @@
"""Controlled GTK test window: real native cursors and unique click sequences."""
import gi
import json
import time
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
win = Gtk.Window(title='VDI native cursor acceptance')
area = Gtk.DrawingArea()
win.add(area)
area.set_can_focus(True)
area.set_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK)
display = Gdk.Display.get_default()
names = ['default', 'text', 'ew-resize', 'wait', 'none', 'crosshair', 'custom']
cursors = {name: Gdk.Cursor.new_from_name(display, name) for name in names[:4] + ['crosshair']}
cursors['none'] = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
pix = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 24, 24)
pix.fill(0xff5030ff)
cursors['custom'] = Gdk.Cursor.new_from_pixbuf(display, pix, 7, 9)
current = None
sequence = 0
def report(**kw):
kw['t'] = time.monotonic()
print(json.dumps(kw), flush=True)
def motion(widget, event):
global current
index = min(6, int(event.x // (2560/7)))
name = names[index]
if name != current:
area.get_window().set_cursor(cursors[name])
current = name
report(event='shape', name=name)
return False
def button(widget, event):
global sequence
if event.type in (Gdk.EventType.BUTTON_PRESS, Gdk.EventType.DOUBLE_BUTTON_PRESS,
Gdk.EventType.TRIPLE_BUTTON_PRESS):
# GTK emits an extra double/triple notification, not another input.
if event.type == Gdk.EventType.BUTTON_PRESS:
sequence += 1
area.queue_draw_area(0, 1300, 1000, 140)
report(event=event.type.value_nick, seq=sequence, x=event.x_root,
y=event.y_root, button=event.button)
return False
def draw(widget, cr):
cr.set_source_rgb(.94, .96, .98)
cr.paint()
cr.select_font_face('sans')
cr.set_font_size(25)
for i, name in enumerate(names):
cr.set_source_rgb(.1, .15, .2)
cr.move_to(40 + i*2560/7, 100)
cr.show_text(name)
cr.set_source_rgb(.65, .7, .8)
cr.rectangle(i*2560/7, 120, 2, 1100)
cr.fill()
# Unique monotonic sequence, read from the actual decoded video.
for bit in range(16):
v = 1 if sequence & (1 << bit) else 0
cr.set_source_rgb(v, v, v)
cr.rectangle(40 + bit*40, 1340, 40, 60)
cr.fill()
return False
def key(widget, event):
if event.keyval == Gdk.KEY_Escape:
Gtk.main_quit()
return False
win.connect('destroy', Gtk.main_quit)
win.connect('key-press-event', key)
area.connect('motion-notify-event', motion)
area.connect('button-press-event', button)
area.connect('button-release-event', button)
area.connect('draw', draw)
win.fullscreen()
win.show_all()
win.present()
area.grab_focus()
report(event='ready')
Gtk.main()
@@ -0,0 +1,4 @@
{
"sunshine": "1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71",
"capture-no-cursor.so": "cff4b7532aee69f0f205fa42946f45a89c1330486adc6358b90d13d2ccf1a0ea"
}
@@ -0,0 +1,302 @@
{
"host": "192.168.1.71",
"ready": true,
"samples": [
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 1
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 2
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 3
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 4
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 5
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 6
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 7
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 8
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 9
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 10
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 11
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 12
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 13
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 14
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 15
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 16
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 17
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 18
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 19
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 20
},
{
"w": 16,
"h": 16,
"hx": 8,
"hy": 8,
"x": 1814,
"y": 554,
"sw": 2560,
"sh": 1440,
"shape": 166,
"type": "cursor",
"version": 1,
"seq": 21
}
],
"stderr": "",
"exit": 0
}
@@ -0,0 +1,7 @@
{
"host": "192.168.1.71",
"ready": false,
"samples": [],
"stderr": "RuntimeError: Permission denied by kysec\n",
"exit": 1
}
@@ -0,0 +1,16 @@
[
{
"case": "agent-sigkill",
"host": "192.168.1.71",
"recovered": true,
"recovery_seconds": 3.22,
"state": "ExecStart={ path=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine ; argv[]=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:11:57 CST] ; stop_time=[n/a] ; pid=180535 ; code=(null) ; status=0/0 }\nActiveState=active\n"
},
{
"case": "guard-sigkill",
"host": "192.168.1.71",
"recovered": true,
"recovery_seconds": 0.893,
"state": "ExecStart={ path=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine ; argv[]=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:12:00 CST] ; stop_time=[n/a] ; pid=180701 ; code=(null) ; status=0/0 }\nActiveState=active\n"
}
]
@@ -0,0 +1,4 @@
{
"sunshine": "61e1ecbc8db2df786b9760c652aa4b9885dff36c57d4af09616344314cb7c34c",
"capture-no-cursor.so": "cff4b7532aee69f0f205fa42946f45a89c1330486adc6358b90d13d2ccf1a0ea"
}
@@ -0,0 +1,316 @@
{
"host": "192.168.1.72",
"ready": true,
"samples": [
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 1
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 2
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 3
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 4
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 5
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 6
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 7
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 8
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 9
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 10
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 11
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 12
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 13
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 14
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 15
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 16
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 17
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 18
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 19
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 20
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 21
},
{
"w": 24,
"h": 24,
"hx": 3,
"hy": 1,
"x": 2548,
"y": 1,
"sw": 2560,
"sh": 1440,
"shape": 124,
"type": "cursor",
"version": 1,
"seq": 22
}
],
"stderr": "",
"exit": 0
}
@@ -0,0 +1,7 @@
{
"host": "192.168.1.72",
"ready": false,
"samples": [],
"stderr": "Traceback (most recent call last):\n File \"/home/test2/vdi-terminal/native-cursor-v1/host.py\", line 211, in <module>\n main()\n File \"/home/test2/vdi-terminal/native-cursor-v1/host.py\", line 205, in main\n stream()\n File \"/home/test2/vdi-terminal/native-cursor-v1/host.py\", line 154, in stream\n raise RuntimeError('guard not ready')\nRuntimeError: guard not ready\n",
"exit": 1
}
@@ -0,0 +1,16 @@
[
{
"case": "agent-sigkill",
"host": "192.168.1.72",
"recovered": true,
"recovery_seconds": 2.724,
"state": "ExecStart={ path=/usr/bin/sunshine ; argv[]=/usr/bin/sunshine /home/test2/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:11:56 CST] ; stop_time=[n/a] ; pid=24296 ; code=(null) ; status=0/0 }\nActiveState=active\n"
},
{
"case": "guard-sigkill",
"host": "192.168.1.72",
"recovered": true,
"recovery_seconds": 0.771,
"state": "ExecStart={ path=/usr/bin/sunshine ; argv[]=/usr/bin/sunshine /home/test2/.config/sunshine/sunshine.conf ; ignore_errors=no ; start_time=[Thu 2026-11-05 11:12:14 CST] ; stop_time=[n/a] ; pid=24470 ; code=(null) ; status=0/0 }\nActiveState=active\n"
}
]
@@ -0,0 +1,11 @@
{
"pid": 285520,
"requested_seconds": 360.0,
"samples": 65658,
"timestamp_after_pre_read_clock": 0,
"timestamp_after_post_read_clock": 0,
"max_age_ms": 308.103546,
"survived": true,
"last_sequence": 7956,
"elapsed_seconds": 360.005
}
@@ -0,0 +1 @@
[{"file": "_usr_bin_sunshine.1000.crash", "ProblemType": "Crash", "Date": "Thu Nov 5 11:12:07 2026", "ExecutablePath": "/usr/bin/sunshine", "Package": "sunshine 2026.914.233613-1+ubuntu24.04 [origin: unknown]", "Signal": "5"}]
@@ -0,0 +1 @@
{"clicks": [{"seq": 29, "seen": 29, "x": 300, "y": 800}, {"seq": 30, "seen": 30, "x": 1500, "y": 900}, {"seq": 31, "seen": 31, "x": 300, "y": 800}, {"seq": 32, "seen": 32, "x": 1500, "y": 900}, {"seq": 33, "seen": 33, "x": 300, "y": 800}, {"seq": 34, "seen": 34, "x": 1500, "y": 900}, {"seq": 35, "seen": 35, "x": 300, "y": 800}, {"seq": 36, "seen": 36, "x": 1500, "y": 900}, {"seq": 37, "seen": 37, "x": 300, "y": 800}, {"seq": 38, "seen": 38, "x": 1500, "y": 900}, {"seq": 39, "seen": 39, "x": 300, "y": 800}, {"seq": 40, "seen": 40, "x": 1500, "y": 900}, {"seq": 41, "seen": 41, "x": 300, "y": 800}, {"seq": 42, "seen": 42, "x": 1500, "y": 900}, {"seq": 43, "seen": 43, "x": 300, "y": 800}, {"seq": 44, "seen": 44, "x": 1500, "y": 900}], "double_click_start": 44, "double_click_end": 46}
@@ -0,0 +1 @@
{"host": "192.168.1.72", "pid": 263384, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 3, "hy": 1, "x": 181, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 3, "hy": 1}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 254, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 12, "x": 547, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 240, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 913, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 301, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 1280, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 433, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 281, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [250, 650], "elapsed_ms": 2.232537, "same_shape": true}, "clicks": [{"seq": 1, "x": 182, "y": 750, "seen": 1, "decoded_ms": 183.136301}, {"seq": 2, "x": 548, "y": 850, "seen": 2, "decoded_ms": 150.601919}, {"seq": 3, "x": 914, "y": 750, "seen": 3, "decoded_ms": 162.979614}, {"seq": 4, "x": 1280, "y": 850, "seen": 4, "decoded_ms": 562.228218}, {"seq": 5, "x": 1645, "y": 750, "seen": 5, "decoded_ms": 149.661983}, {"seq": 6, "x": 2011, "y": 850, "seen": 6, "decoded_ms": 187.689807}, {"seq": 7, "x": 2377, "y": 750, "seen": 7, "decoded_ms": 186.617722}, {"seq": 8, "x": 182, "y": 850, "seen": 8, "decoded_ms": 167.201514}, {"seq": 9, "x": 548, "y": 750, "seen": 9, "decoded_ms": 143.805796}, {"seq": 10, "x": 914, "y": 850, "seen": 10, "decoded_ms": 145.803285}, {"seq": 11, "x": 1280, "y": 750, "seen": 11, "decoded_ms": 505.147227}, {"seq": 12, "x": 1645, "y": 850, "seen": 12, "decoded_ms": 156.874419}, {"seq": 13, "x": 2011, "y": 750, "seen": 13, "decoded_ms": 164.724633}, {"seq": 14, "x": 2377, "y": 850, "seen": 14, "decoded_ms": 172.592036}, {"seq": 15, "x": 182, "y": 750, "seen": 15, "decoded_ms": 195.775758}, {"seq": 16, "x": 548, "y": 850, "seen": 16, "decoded_ms": 151.145679}, {"seq": 17, "x": 914, "y": 750, "seen": 17, "decoded_ms": 173.941806}, {"seq": 18, "x": 1280, "y": 850, "seen": 18, "decoded_ms": 352.426864}, {"seq": 19, "x": 1645, "y": 750, "seen": 19, "decoded_ms": 156.050885}, {"seq": 20, "x": 2011, "y": 850, "seen": 20, "decoded_ms": 158.266234}, {"seq": 21, "x": 2377, "y": 750, "seen": 21, "decoded_ms": 126.999009}, {"seq": 22, "x": 182, "y": 850, "seen": 22, "decoded_ms": 194.209712}, {"seq": 23, "x": 548, "y": 750, "seen": 23, "decoded_ms": 183.157837}, {"seq": 24, "x": 914, "y": 850, "seen": 24, "decoded_ms": 186.545894}, {"seq": 25, "x": 1280, "y": 750, "seen": 25, "decoded_ms": 506.013562}, {"seq": 26, "x": 1645, "y": 850, "seen": 26, "decoded_ms": 164.698513}, {"seq": 27, "x": 2011, "y": 750, "seen": 27, "decoded_ms": 177.684307}, {"seq": 28, "x": 2377, "y": 850, "seen": 28, "decoded_ms": 174.644202}], "p50_ms": 173.26692100000002, "p95_ms": 506.013562, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
@@ -0,0 +1,11 @@
{
"pid": 293859,
"requested_seconds": 360.0,
"samples": 65960,
"timestamp_after_pre_read_clock": 1,
"timestamp_after_post_read_clock": 0,
"max_age_ms": 1109.787402,
"survived": true,
"last_sequence": 6538,
"elapsed_seconds": 360.003
}
@@ -0,0 +1 @@
{"pid": 293859, "worker": 293861, "pause_seconds": 2.6, "exited": true, "check_seconds": 2.9042484689998673}
Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

@@ -0,0 +1 @@
{"pid": 299860, "ppid": 235482, "args": "/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.72 Desktop --resolution 2560x1440 --fps 30 --bitrate 30000 --display-mode fullscreen --video-codec H.264 --video-decoder hardware --absolute-mouse --capture-system-keys always --keep-awake --no-vsync --no-frame-pacing --audio-on-host ", "sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03", "native": true, "shape": [24, 24, 3, 1]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Whitespace-only changes.
@@ -0,0 +1 @@
{"host": "192.168.1.72", "pid": 256179, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 3, "hy": 1, "x": 181, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 3, "hy": 1}, "geometry_matches": true, "max_pixel_difference": 162, "visible_pixels": 254, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 12, "x": 547, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 101, "visible_pixels": 240, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 913, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 175, "visible_pixels": 301, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 1280, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 129, "visible_pixels": 433, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 78, "visible_pixels": 281, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [250, 650], "elapsed_ms": 2.727055, "same_shape": true}, "clicks": [{"seq": 3, "x": 182, "y": 750, "seen": 3, "decoded_ms": 177.207219}, {"seq": 4, "x": 548, "y": 850, "seen": 4, "decoded_ms": 177.766631}, {"seq": 5, "x": 914, "y": 750, "seen": 5, "decoded_ms": 199.373737}, {"seq": 6, "x": 1280, "y": 850, "seen": 6, "decoded_ms": 182.446665}, {"seq": 7, "x": 1645, "y": 750, "seen": 7, "decoded_ms": 174.159044}, {"seq": 8, "x": 2011, "y": 850, "seen": 8, "decoded_ms": 211.839356}, {"seq": 9, "x": 2377, "y": 750, "seen": 9, "decoded_ms": 172.112732}, {"seq": 10, "x": 182, "y": 850, "seen": 10, "decoded_ms": 218.076291}, {"seq": 11, "x": 548, "y": 750, "seen": 11, "decoded_ms": 158.319669}, {"seq": 12, "x": 914, "y": 850, "seen": 12, "decoded_ms": 190.78387}, {"seq": 13, "x": 1280, "y": 750, "seen": 13, "decoded_ms": 475.373962}, {"seq": 14, "x": 1645, "y": 850, "seen": 14, "decoded_ms": 183.265657}, {"seq": 15, "x": 2011, "y": 750, "seen": 15, "decoded_ms": 190.493082}, {"seq": 16, "x": 2377, "y": 850, "seen": 16, "decoded_ms": 174.658874}, {"seq": 17, "x": 182, "y": 750, "seen": 17, "decoded_ms": 202.290292}, {"seq": 18, "x": 548, "y": 850, "seen": 18, "decoded_ms": 201.035264}, {"seq": 19, "x": 914, "y": 750, "seen": 19, "decoded_ms": 169.113849}, {"seq": 20, "x": 1280, "y": 850, "seen": 20, "decoded_ms": 389.791156}, {"seq": 21, "x": 1645, "y": 750, "seen": 21, "decoded_ms": 145.709303}, {"seq": 22, "x": 2011, "y": 850, "seen": 22, "decoded_ms": 142.136716}, {"seq": 23, "x": 2377, "y": 750, "seen": 23, "decoded_ms": 150.834416}, {"seq": 24, "x": 182, "y": 850, "seen": 24, "decoded_ms": 218.919785}, {"seq": 25, "x": 548, "y": 750, "seen": 25, "decoded_ms": 145.115185}, {"seq": 26, "x": 914, "y": 850, "seen": 26, "decoded_ms": 174.238963}, {"seq": 27, "x": 1280, "y": 750, "seen": 27, "decoded_ms": 476.883703}, {"seq": 28, "x": 1645, "y": 850, "seen": 28, "decoded_ms": 165.254265}, {"seq": 29, "x": 2011, "y": 750, "seen": 29, "decoded_ms": 182.507335}, {"seq": 30, "x": 2377, "y": 850, "seen": 30, "decoded_ms": 168.10207}], "p50_ms": 180.106648, "p95_ms": 475.373962, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
Whitespace-only changes.
@@ -0,0 +1 @@
{"host": "192.168.1.72", "pid": 293859, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 3, "hy": 1, "x": 181, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 3, "hy": 1}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 254, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 12, "x": 547, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 240, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 913, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 301, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 1280, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 433, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 281, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [350, 700], "elapsed_ms": 249.623897, "same_shape": true, "steps": [{"target": [250, 650], "position": [250, 650], "query_ms": 2.255624}, {"target": [280, 670], "position": [280, 670], "query_ms": 2.353619}, {"target": [300, 690], "position": [300, 690], "query_ms": 2.202252}, {"target": [350, 700], "position": [350, 700], "query_ms": 2.288873}]}, "clicks": [{"seq": 1, "x": 182, "y": 750, "seen": 1, "decoded_ms": 186.428878}, {"seq": 2, "x": 548, "y": 850, "seen": 2, "decoded_ms": 181.712898}, {"seq": 3, "x": 914, "y": 750, "seen": 3, "decoded_ms": 207.490698}, {"seq": 4, "x": 1280, "y": 850, "seen": 4, "decoded_ms": 533.830025}, {"seq": 5, "x": 1645, "y": 750, "seen": 5, "decoded_ms": 190.340809}, {"seq": 6, "x": 2011, "y": 850, "seen": 6, "decoded_ms": 204.401567}, {"seq": 7, "x": 2377, "y": 750, "seen": 7, "decoded_ms": 166.595545}, {"seq": 8, "x": 182, "y": 850, "seen": 8, "decoded_ms": 195.011611}, {"seq": 9, "x": 548, "y": 750, "seen": 9, "decoded_ms": 181.070161}, {"seq": 10, "x": 914, "y": 850, "seen": 10, "decoded_ms": 191.989545}, {"seq": 11, "x": 1280, "y": 750, "seen": 11, "decoded_ms": 450.587655}, {"seq": 12, "x": 1645, "y": 850, "seen": 12, "decoded_ms": 166.558247}, {"seq": 13, "x": 2011, "y": 750, "seen": 13, "decoded_ms": 174.993944}, {"seq": 14, "x": 2377, "y": 850, "seen": 14, "decoded_ms": 195.413266}, {"seq": 15, "x": 182, "y": 750, "seen": 15, "decoded_ms": 242.226344}, {"seq": 16, "x": 548, "y": 850, "seen": 16, "decoded_ms": 206.124429}, {"seq": 17, "x": 914, "y": 750, "seen": 17, "decoded_ms": 181.78767}, {"seq": 18, "x": 1280, "y": 850, "seen": 18, "decoded_ms": 463.825494}, {"seq": 19, "x": 1645, "y": 750, "seen": 19, "decoded_ms": 194.360157}, {"seq": 20, "x": 2011, "y": 850, "seen": 20, "decoded_ms": 177.975533}, {"seq": 21, "x": 2377, "y": 750, "seen": 21, "decoded_ms": 229.660945}, {"seq": 22, "x": 182, "y": 850, "seen": 22, "decoded_ms": 159.611781}, {"seq": 23, "x": 548, "y": 750, "seen": 23, "decoded_ms": 149.18704}, {"seq": 24, "x": 914, "y": 850, "seen": 24, "decoded_ms": 179.366438}, {"seq": 25, "x": 1280, "y": 750, "seen": 25, "decoded_ms": 522.420223}, {"seq": 26, "x": 1645, "y": 850, "seen": 26, "decoded_ms": 154.151825}, {"seq": 27, "x": 2011, "y": 750, "seen": 27, "decoded_ms": 154.669796}, {"seq": 28, "x": 2377, "y": 850, "seen": 28, "decoded_ms": 145.580813}], "p50_ms": 188.3848435, "p95_ms": 522.420223, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

@@ -0,0 +1,7 @@
{
"agent": 39181,
"client": 293859,
"pause_seconds": 1.1,
"local_pointer": "x:650 y:900 screen:0 window:48234511",
"survived": true
}
@@ -0,0 +1,5 @@
{
"bridge_frozen_seconds": 1.2,
"checked_after_seconds": 3.289,
"remaining_clients": []
}
Whitespace-only changes.
@@ -0,0 +1 @@
{"host": "192.168.1.72", "pid": 272099, "shapes": [{"name": "default", "source": {"w": 24, "h": 24, "hx": 3, "hy": 1, "x": 181, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 182, "y": 600, "w": 24, "h": 24, "hx": 3, "hy": 1}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 254, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "text", "source": {"w": 24, "h": 24, "hx": 11, "hy": 12, "x": 547, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 548, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 240, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "ew-resize", "source": {"w": 24, "h": 24, "hx": 12, "hy": 12, "x": 913, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 914, "y": 600, "w": 24, "h": 24, "hx": 12, "hy": 12}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 301, "video": {"reference": 15726330, "nonuniform_pixels": 0}}, {"name": "wait", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 1280, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1280, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 433, "video": {"reference": 15791866, "nonuniform_pixels": 0}}, {"name": "none", "source": {"w": 1, "h": 1, "hx": 0, "hy": 0, "x": 1645, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 1645, "y": 600, "w": 1, "h": 1, "hx": 0, "hy": 0}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 0, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "crosshair", "source": {"w": 24, "h": 24, "hx": 11, "hy": 11, "x": 2011, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2011, "y": 600, "w": 24, "h": 24, "hx": 11, "hy": 11}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 281, "video": {"reference": 15726074, "nonuniform_pixels": 0}}, {"name": "custom", "source": {"w": 24, "h": 24, "hx": 7, "hy": 9, "x": 2377, "y": 599, "sw": 2560, "sh": 1440}, "local": {"x": 2377, "y": 600, "w": 24, "h": 24, "hx": 7, "hy": 9}, "geometry_matches": true, "max_pixel_difference": 0, "visible_pixels": 576, "video": {"reference": 15726074, "nonuniform_pixels": 0}}], "paused_client": {"position": [350, 700], "elapsed_ms": 263.611753, "same_shape": true, "steps": [{"target": [250, 650], "position": [250, 650], "query_ms": 16.220678}, {"target": [280, 670], "position": [280, 670], "query_ms": 2.214575}, {"target": [300, 690], "position": [300, 690], "query_ms": 2.215451}, {"target": [350, 700], "position": [350, 700], "query_ms": 2.380822}]}, "clicks": [{"seq": 2, "x": 182, "y": 750, "seen": 2, "decoded_ms": 192.980398}, {"seq": 3, "x": 548, "y": 850, "seen": 3, "decoded_ms": 249.189423}, {"seq": 4, "x": 914, "y": 750, "seen": 4, "decoded_ms": 193.088898}, {"seq": 5, "x": 1280, "y": 850, "seen": 5, "decoded_ms": 374.099277}, {"seq": 6, "x": 1645, "y": 750, "seen": 6, "decoded_ms": 166.521582}, {"seq": 7, "x": 2011, "y": 850, "seen": 7, "decoded_ms": 207.861192}, {"seq": 8, "x": 2377, "y": 750, "seen": 8, "decoded_ms": 246.000709}, {"seq": 9, "x": 182, "y": 850, "seen": 9, "decoded_ms": 181.998527}, {"seq": 10, "x": 548, "y": 750, "seen": 10, "decoded_ms": 181.410831}, {"seq": 11, "x": 914, "y": 850, "seen": 11, "decoded_ms": 221.952754}, {"seq": 12, "x": 1280, "y": 750, "seen": 12, "decoded_ms": 507.168335}, {"seq": 13, "x": 1645, "y": 850, "seen": 13, "decoded_ms": 182.568433}, {"seq": 14, "x": 2011, "y": 750, "seen": 14, "decoded_ms": 164.879833}, {"seq": 15, "x": 2377, "y": 850, "seen": 15, "decoded_ms": 212.375805}, {"seq": 16, "x": 182, "y": 750, "seen": 16, "decoded_ms": 188.116394}, {"seq": 17, "x": 548, "y": 850, "seen": 17, "decoded_ms": 203.54084}, {"seq": 18, "x": 914, "y": 750, "seen": 18, "decoded_ms": 183.044427}, {"seq": 19, "x": 1280, "y": 850, "seen": 19, "decoded_ms": 316.917585}, {"seq": 20, "x": 1645, "y": 750, "seen": 20, "decoded_ms": 161.788239}, {"seq": 21, "x": 2011, "y": 850, "seen": 21, "decoded_ms": 180.879436}, {"seq": 22, "x": 2377, "y": 750, "seen": 22, "decoded_ms": 172.71762}, {"seq": 23, "x": 182, "y": 850, "seen": 23, "decoded_ms": 175.79901}, {"seq": 24, "x": 548, "y": 750, "seen": 24, "decoded_ms": 185.907366}, {"seq": 25, "x": 914, "y": 850, "seen": 25, "decoded_ms": 207.759418}, {"seq": 26, "x": 1280, "y": 750, "seen": 26, "decoded_ms": 468.920931}, {"seq": 27, "x": 1645, "y": 850, "seen": 27, "decoded_ms": 189.784407}, {"seq": 28, "x": 2011, "y": 750, "seen": 28, "decoded_ms": 186.12174}, {"seq": 29, "x": 2377, "y": 850, "seen": 29, "decoded_ms": 182.798273}], "p50_ms": 188.9504005, "p95_ms": 468.920931, "method": "XTest click-down to unique decoded sequence; excludes USB and scanout"}
@@ -0,0 +1,9 @@
{
"host": "192.168.1.72",
"target": [
400,
600
],
"readback": "X=400\nY=600\nSCREEN=0\nWINDOW=48234511\n",
"passed": true
}
@@ -0,0 +1,8 @@
{
"host": "192.168.1.72",
"target": [
400,
600
],
"readback": "X=1500\nY=900\nSCREEN=0\nWINDOW=48234511\n"
}
@@ -0,0 +1 @@
{"pid": 299142, "args": "/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.70 Desktop --resolution 2560x1440 --fps 45 --bitrate 30000 --display-mode fullscreen --video-codec H.264 --video-decoder hardware --absolute-mouse --capture-system-keys always --keep-awake --no-vsync --no-frame-pacing --audio-on-host ", "sha256": "5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03", "native": false}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

@@ -1,5 +1,7 @@
# 独立光标二次开发设计与接入条件
> 2026-11-05更新:Ubuntu/麒麟已部署单座席X11受限方案,通过认证SSH同步形状,在新连接前启动无光标捕获并在整次会话结束后恢复服务,见[实施记录](linux-native-cursor-20261105.md)。以下是11月3日Windows正式控制协议设计与当时现场状态;该Windows协议仍未实现,不能混同Linux新方案。
状态:已通过受控的 Windows 光标导出 + 盒子 X11 本地指针原型验证;尚未实现 Sunshine/Moonlight 的自动独立光标通道,不应标记为生产功能已部署。测试已结束,当前恢复单个 Windows 远程白色光标。实测结果见 [独立光标可行性验证](independent-cursor-feasibility-20261103.md)。
## 已核实的实际路径
@@ -1,5 +1,7 @@
# 独立光标原型可行性实测(2026-11-03)
> 2026-11-05更新:Linux两台主机已接入不同于本原型的认证SSH原生光标方案,见[实施记录](linux-native-cursor-20261105.md)。本页保留11月3日Windows受控实验;Windows仍走原视频光标,正式双方控制协议尚未实现。
## 结论和当前现场
本地独立移动和 Windows 光标静态形状显示可行,值得继续开发 Sunshine + Moonlight 的正式同步协议。本轮完成了真实盒子和 VM100 的受控原型,不是仅做源码调研。但是自动形状同步、视频帧交接、断线回退尚未成为产品功能,不能默认启用,也不能称鼠标或窗口零延迟。
@@ -2,6 +2,8 @@
> 后续更新:麒麟已升级真实2560×1440/30FPS配置与30Mbps,动态短测约23FPS,见[麒麟2K调优记录](kylin-quality-20261104.md)。以下1080p参数属于初次接入记录。
> 2026-11-05:两套Linux均为真实2K/30FPS请求配置与30Mbps,已启用同步原生形状的独立光标,指针位置不再等待视频;Ubuntu恢复原生Adwaita/default加速,关闭动画保留。详见[独立光标实施记录](linux-native-cursor-20261105.md)。视频动态性能和物理输入延迟仍有边界,较早画质实验见[Ubuntu调优记录](ubuntu-quality-20261105.md)。
两台 Linux 已接入现场盒子的正常桌面选择流程。实际验证了连接、画面、键盘、鼠标、退出和再次连接;最终回到桌面列表。Windows 也完成了原配置连接回归。
## 现场配置
+66
View File
@@ -0,0 +1,66 @@
# Ubuntu / 麒麟原生独立光标实施(2026-11-05)
本轮按用户要求保留系统原生光标,降低指针移动对视频更新的依赖。两台Linux都使用X11、真实2560×1440、30FPS请求、30Mbps和原有软件编码;Windows仍为原硬编路径。Ubuntu光标主题已从此前的DMZ-White恢复Adwaita/24px,鼠标加速恢复default,关闭动画的性能设置保留。
## 调研与取舍
当前Moonlight v6.2.0的普通本地光标快捷键不会同步服务端形状,不能用于本任务。[上游使用说明](https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide)和[维护者说明](https://github.com/moonlight-stream/moonlight-qt/issues/1386)与固定版本源码一致。Linux Sunshine X11捕获会通过XFixes读取光标后混入视频;[v0.21.0捕获源码](https://github.com/LizardByte/Sunshine/blob/v0.21.0/src/platform/linux/x11grab.cpp)已实际下载核对。
继续调编码参数无法消除指针位置对软件编码、网络、解码的等待。换SPICE/VNC/RDP虽然可采用协议原生光标,但会改变现有桌面入口、音视频和部署路径;本轮没有把更换协议当成已验证优化。两台又有不同Sunshine版本,因此采用**已认证SSH光标通道 + 精确版本固定的进程级捕获适配器 + SDL主线程原生光标**,保留现有Moonlight视频和可靠鼠标输入通道。
这与11月3日的正式控制协议设计不同:不做运行中切换,而是在新视频连接前启动无光标捕获,退出时整个捕获会话结束后才恢复普通服务。这样无需用未关联的视频帧ACK或固定延时来猜测旧光标何时消失。SSH使用每台主机独立的受限强制命令密钥和固定主机公钥;没有新增未认证UDP端口。实现、安装、生命周期和回退详见[模块说明](../terminal/native-cursor/README.md)。
[SDL光标API](https://wiki.libsdl.org/SDL2/SDL_CreateColorCursor)提供真实彩色形状和热点。[SDL2 X11源码](https://github.com/libsdl-org/SDL/blob/release-2.30.0/src/video/x11/SDL_x11mouse.c)还暴露两个适配点:Xcursor像素直接复制,必须预乘alpha;启用指针边界时窗口内warp可能不执行,因此主动位置校正使用窗口范围内的global warp。没有更换SDL系统库。
## 实现及边界
- 0005客户端补丁由构建器在现有补丁后应用;Linux主机JSON才启用功能,Windows无该JSON。profile中的客户端SHA256在远端租约前校验,旧备份客户端实测被拒绝且麒麟原服务PID未改变。fork监督进程后exec保留kiosk预期的Moonlight PID,实际测试从正常列表进入。
- 主机guard核验私有Sunshine副本和adapter SHA256;adapter仅在该进程的XFixes捕获结果中清除光标像素。原系统二进制、主题、原配置保留;租约结束删除/run中的临时服务drop-in并恢复原服务。
- 形状、热点、动画、透明隐藏、单屏几何、序号和随机租约均有边界检查;原子状态文件位于盒子0700私有目录。SDL资源只在主线程创建/切换/释放。
- 最新本地输入位置先由服务器实际指针确认;本地静止500ms且没有按钮按下时才接受服务器主动warp,避免拿迟到的输入回显回拉光标。
- 当前仅支持已配置的单座席X11/2560×1440/绝对鼠标、最多128×128像素形状。短暂停顿保留上次形状,本地位置继续移动;未知形状、源分辨率改变、同步状态超过2秒或SSH故障会结束当前串流,恢复普通服务后可从列表重连。主机租约4秒,不允许无限期保留旧状态。
- 不宣称通用多座席、相对鼠标游戏、Wayland、任意显示设备或运行中平滑降级已完成。启用模式内阻止切换为相对鼠标或固定本地箭头的快捷键;其他主机保持原逻辑。
- 原视频保留2K H.264 4:2:0及30Mbps,没有用降分辨率提高指针反馈。独立指针解决位置移动,点击后的页面和拖窗内容继续受视频链路限制。
## 联调与证据
原始证据位于[evidence/native-cursor-20261105/](evidence/native-cursor-20261105/)。最终SHA为 `5d4330…` 的两系统复测为 `ubuntu-release-probe.json`、`kylin-release-probe.json`,点击与源事件核对见 `release-click-verification.json` 及对应 `*-release-source.log`。较早 `ubuntu-v4-final-probe.json`、`kylin-final-probe.json` 等保留作修正历史,不混同最终二进制证据。
| 验证项目 | Ubuntu | 麒麟 |
| --- | --- | --- |
| 原生箭头、文字、横向缩放、忙碌、隐藏、十字、自定义共7种 | 几何/热点一致,采样像素最大差0 | 几何/热点一致,采样像素最大差0 |
| 光标附近100×100均匀视频区域 | 七种均无非均匀像素;隐藏0可见像素 | 七种均无非均匀像素;隐藏0可见像素 |
| 唯一编号点击与源事件核对 | 28/28,最大坐标误差1.055px | 28/28,最大误差0.829px |
| 合成按下至编号出现在解码画面 | P50 188.38ms / P95 522.42ms | P50 134.37ms / P95 165.56ms |
| 稳定会话短按/双击补测 | 16次40ms按住全部收到,双击计数+2 | 16次40ms按住全部收到,双击计数+2 |
| 暂停Moonlight进程时移动本地指针 | 约250ms内4个位置读回正确,形状保留 | 约250ms内4个位置读回正确,形状保留 |
| 服务端主动warp至(400,600) | 本地读回(400,600) | 本地读回(400,600) |
| 主机agent / guard被SIGKILL后恢复原服务 | 2.724s / 0.771s检查恢复 | 3.220s / 0.893s检查恢复 |
这些点击数字测量的是**页面/视频响应**,不表示本地指针移动延迟;坐标查询耗时也不是物理显示延迟。暂停视频生产者仍可移动本地指针证明了指针位置不再等待视频更新。没有测量物理USB输入、面板扫描或与Windows做相同仪器对照,因此不能宣称物理零延迟或定量等同Windows。
Ubuntu冻结盒子bridge后状态超时退出,3.289s检查无残留客户端且原服务恢复;麒麟实际改为1920×1080后自动结束会话,3.555s检查恢复原服务和2560×1440,之后从正常入口重连2K通过。焦点探针仅确认原生形状重新出现;kiosk键盘抓取下没有证据证明XSetInputFocus(None)触发了真正SDL失焦,**完整失焦场景尚未验收**。详见对应 `*-recovery.json`、`ubuntu-stale-state.json`、`kylin-resolution-change.json`、`kylin-focus.json` 和warp记录。
4项本地协议/边界/旧客户端拒绝单测通过;固定基线按顺序应用0001~0005(0002在common-c子模块)的检查通过,构建器规范化补丁CRLF后仍严格检查上下文。现场客户端保留原先源码修改并加入0005,本轮未把其他任务的0004重新构建部署;不得仅据构建器清洁应用成功宣称现场已包含0004。
最终2秒等待策略的Ubuntu持续测试(`ubuntu-final-soak.json`)完成360.003秒、65,960次实际状态读取,接收器持续存活。观察到1次“包时间戳晚于读前时间、但不晚于读后时间”的真实并发更新,支持修正校验顺序;读后未来时间0次。测试中主动暂停主机代理1.1秒,本地指针移动至(650,900),会话存活并恢复同步(`ubuntu-short-stall.json`);最大状态年龄1109.79ms包含这次故障注入,不是鼠标移动延迟。之后同一会话的七种光标和28点击复测全部匹配;再冻结bridge 2.6秒,2.904秒检查客户端已退出,原Sunshine路径和guard inactive核对通过,见 `ubuntu-final-stale-state.json`、`final-stale-recovery-services.json`。这仍是有界回归,长期稳定性未验收。
## 故障与修正
1. Ubuntu系统Sunshine带file capabilities,加载器忽略LD_PRELOAD。没有清除原文件能力;只在租约内使用相同SHA256、无额外能力的私有副本,正常退出恢复原文件。适配器映射和dlsym receipt均确认后才返回READY。
2. 麒麟KYSEC拒绝未认证新脚本;仅认证host.py、私有Sunshine副本和adapter。没有关闭全局安全策略。
3. GTK blank cursor实测是1×1、热点(1,1)。初版按可见光标边界拒绝并安全结束会话;修正为全透明图像的规范1×1/热点(0,0)。可见图像仍严格拒绝越界热点。
4. 第一轮原生边缘像素差异大,发现SDL2 X11复制未预乘像素;修正后逐像素复测,不把初版当通过。初版28次点击P50约180ms/P95约475ms,只是探索数据。
5. Ubuntu两次“强制暂停客户端→恢复→第一点击”探针首击未回显(40ms和80ms各一次),根因未确定。最终将正常输入测量与SIGSTOP实验分开,在稳定会话核对28个唯一编号及源端事件,再单独暂停客户端;稳定短按/双击补测也通过。没有证据把失败归因于GTK/窗口焦点,不得据后续通过抹去失败,或宣称任意高速输入绝不丢失。
6. 主动warp第一轮未移动,确认是SDL2在pointer barrier存在时忽略窗口内warp;改为有界global warp后再复测。
7. 最终解锁Ubuntu时弹出历史Sunshine崩溃提示。`/var/crash`中报告为11:12:07、原`/usr/bin/sunshine`、SIGTRAP;同秒服务日志为“10 seconds passed, yet Sunshine's still running: Forcing shutdown”,发生在故障恢复测试期间原服务停止的阶段,随后11:12:14恢复。该停止看门狗问题未作上游修复;不能据后续恢复称所有服务退出都无异常。原报告保留,未向外发送;元数据与日志在证据目录。
8. 普通Ubuntu桌面连续运行约4分42秒后触发“stale or mismatched lease”退出。复核发现客户端先取当前时间再打开状态文件,可与写入进程原子更新文件竞态,合法新时间戳可能大于先前读数;修正为读取完成后再取校验时间。该轮错误未区分未来时间/实际过期,不能断言该竞态是退出的唯一原因。此前详细形状/点击证据来自修正前构建。
9. 仅修正读时间顺序的构建先完成6分钟/65,658次状态读取,最大状态年龄308.10ms、无未来时间;但之后会话9分32秒时发生真实heartbeat timeout。故障发生前有持续忙碌动画和桌面应用启动,尚未确定是X11读取、调度还是SSH发送阻塞,不能只凭CPU软件编码推断唯一原因。最终将客户端/bridge等待上限调整为2秒,主机租约为4秒,并增加超过100ms的X11读取耗时诊断;本地指针位置仍立即显示,状态最多保留2秒后结束整次会话。修正版需按新阈值验证短暂停顿容忍和持续失效恢复;不把前一轮6分钟通过当成长时间稳定证明。
## 配置、备份与回退
现场客户端SHA256 `5d4330f703d7f8e053bc3795ca42bdaf231c9e9dd8ebb00a612013adbcccee03`,两个Linux profile均固定该值。私有副本:Ubuntu SHA256 `61e1ecbc8db2df786b9760c652aa4b9885dff36c57d4af09616344314cb7c34c`;麒麟 `1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71`。adapter `cff4b7532aee69f0f205fa42946f45a89c1330486adc6358b90d13d2ccf1a0ea`。精确manifest在证据目录。未重启VM/盒子、未改PVE显卡或Windows服务端。
最终正常入口Windows为2K45/RKMPP/EGL、无原生光标环境;麒麟为2K30、原生形状恢复后正常退出,原服务active且无临时drop-in。盒子随后保留Ubuntu2K30会话,PID299860/父235482、原生Adwaita/24及default加速,实际视频源仍2K。两台GTK源程序和盒子持续探针均停止;活动Ubuntu会话的guard按设计保持active。见 `windows-final.json`、`kylin-normal-client.json`、`ubuntu-normal-client.json`、`final-services.json` 及普通桌面截图。XWD截图不含盒子原生指针,不能根据截图中没有箭头判定无光标。
盒子备份 `~/vdi-terminal/backups/native-cursor-20261105/` 有旧客户端和wrapper。回退先退出Linux会话并确认guard inactive/原Sunshine active,再把盒子对应 `~/.config/vdi-terminal/native-cursor/<IP>.json` 改名为.disabled;重连即使用原视频光标。若要恢复旧客户端,必须先停用两个JSON。Ubuntu主题原值备份仍在 `~/vdi-terminal/backups/ubuntu-quality-20261105/gsettings.json`;本轮恢复原生主题符合最新用户要求。
+9
View File
@@ -1,5 +1,11 @@
# 任意 U 盘透传实现(2026-11-04)
2026-11-05 补充:空的 usb-redir 端点移除已验证,但用户照片显示安全删除菜单还列出
显卡、VirtIO 网卡/串口和 SATA 控制器。仅移除 USB 端点不能保证托盘图标消失。
固定 PCI 设备属性修复准备与实际验收范围见 `fixed-pci-tray-20261105.md`。
该修复随后已在用户授权正常关机再开机后应用:四项 PCI 设备能力从 6 变为 0,
设备状态、GPU、系统盘、网络和 Sunshine 正常;真实 U 盘插拔与托盘外观待现场确认。
在已验证的真实 USB 透传链路上,将盒子端策略从固定 VID/PID/序列号扩展为
“任意纯 USB Mass Storage 设备”。键盘、鼠标、Hub 和显示设备仍由盒子本地使用。
@@ -11,6 +17,9 @@
- 盒子检查分区是否本地挂载、是否有 holder 或作为 swap;发现本地占用时等待,不强制卸载。
- QEMU 使用 `0x08:-1:-1:-1:1` 过滤器,允许任意 USB Mass Storage,仍拒绝其他 USB 类别。
- PVE QMP watcher 在 VM/PVE 重启后重新建立运行时 `usb-redir` 设备。
- 盒子同时提供仅回环的存储就绪状态端口。PVE watcher 只在状态为 `ready` 时创建
`usb-redir`,连续 3 次确认状态为空后删除端点;因此物理 U 盘拔出后,Windows
的安全删除硬件图标也会消失。插拔瞬间使用防抖,避免短暂枚举波动造成误移除。
## 部署入口