优化:完善麒麟2K显示与逐主机视频范围协商

This commit is contained in:
宋一鸣 2026-11-04 19:12:52 +08:00
parent b1082f8465
commit e35abf6dc0
78 changed files with 2040 additions and 0 deletions

View File

@ -0,0 +1,44 @@
import gi,time,json,math
gi.require_version('Gtk','3.0')
from gi.repository import Gtk,Gdk,GLib
started=time.monotonic(); draws=0; ticks=0
win=Gtk.Window(title='VDI Kylin quality benchmark')
win.connect('destroy',Gtk.main_quit)
area=Gtk.DrawingArea();win.add(area)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.KEY_PRESS_MASK)
area.set_can_focus(True)
def event(w,e):
print(json.dumps({'event':e.type.value_nick,'keyval':getattr(e,'keyval',None),'button':getattr(e,'button',None),'x':getattr(e,'x',None),'y':getattr(e,'y',None),'width':area.get_allocated_width(),'height':area.get_allocated_height(),'t':time.monotonic()}),flush=True)
return False
win.connect('key-press-event',event);area.connect('button-press-event',event)
def draw(w,cr):
global draws
draws+=1; width=w.get_allocated_width();height=w.get_allocated_height()
cr.set_source_rgb(.96,.97,.99);cr.paint()
cr.select_font_face('sans');cr.set_font_size(30);cr.set_source_rgb(.08,.12,.18)
cr.move_to(60,65);cr.show_text('VDI Kylin - resolution / text / motion / input verification')
cr.set_font_size(22);cr.move_to(60,105);cr.show_text(f'Native desktop: {width} x {height} Temporary test window')
for row,size in enumerate([12,14,16,18,20,24]):
cr.set_font_size(size);cr.move_to(60,155+row*45);cr.set_source_rgb(.1,.1,.1)
cr.show_text(f'{size}px Cloud desktop 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz')
for i in range(600):
cr.set_source_rgb(0,0,0) if i%2 else cr.set_source_rgb(1,1,1)
cr.rectangle(60+i,445,1,80);cr.fill()
for i,color in enumerate([(1,0,0),(0,.65,0),(0,.2,1),(.6,0,.6)]):
cr.set_source_rgb(*color);cr.set_font_size(18);cr.move_to(60,565+i*28);cr.show_text('Color text 0123456789 - desktop quality test')
elapsed=time.monotonic()-started
for i in range(5):
x=70+((elapsed*220+i*173)%(max(200,width-400)))
cr.set_source_rgb(.1+i*.14,.35,.8-i*.1);cr.rectangle(x,height-280+i*35,120,28);cr.fill()
cr.set_source_rgb(.1,.15,.2);cr.set_font_size(16);cr.move_to(60,height-30)
cr.show_text(f'Source draw rate: {draws/max(.1,elapsed):.2f} FPS; elapsed {elapsed:.1f}s. Close after testing.')
return False
def tick():
global ticks
ticks+=1;area.queue_draw_area(0,max(0,area.get_allocated_height()-300),area.get_allocated_width(),300);return True
def report():
elapsed=time.monotonic()-started
print(json.dumps({'event':'source-stats','seconds':elapsed,'draws':draws,'ticks':ticks,'draw_fps':draws/elapsed}),flush=True);return True
area.connect('draw',draw);win.fullscreen();win.show_all();area.grab_focus()
GLib.timeout_add(16,tick);GLib.timeout_add_seconds(5,report)
print('READY',flush=True);Gtk.main()

View File

@ -0,0 +1,3 @@
49316 00:00:10 31.4
49316 00:00:35 62.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

View File

@ -0,0 +1,3 @@
48825 00:00:10 29.7
48825 00:00:35 62.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@ -0,0 +1,3 @@
48246 00:01:04 66.2
48246 00:01:29 74.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

View File

@ -0,0 +1,107 @@
{
"static-text": {
"mean_abs_rgb_difference": [
1.1931075392269421,
0.3489188672024493,
0.5770053578262534
],
"full_rgb_extrema": [
[
0,
255
],
[
0,
255
],
[
0,
255
]
],
"limited_rgb_extrema": [
[
0,
255
],
[
0,
255
],
[
0,
255
]
]
},
"background": {
"mean_abs_rgb_difference": [
1.25989,
0.3125,
0.5
],
"full_rgb_extrema": [
[
245,
246
],
[
247,
248
],
[
254,
255
]
],
"limited_rgb_extrema": [
[
244,
245
],
[
248,
249
],
[
253,
254
]
]
},
"stripes": {
"mean_abs_rgb_difference": [
0.8629375,
0.40147916666666666,
0.9251041666666666
],
"full_rgb_extrema": [
[
0,
255
],
[
0,
255
],
[
0,
255
]
],
"limited_rgb_extrema": [
[
0,
255
],
[
0,
255
],
[
0,
255
]
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
49732 00:01:03 3.0
49732 00:01:15 3.5

View File

@ -0,0 +1 @@
{"method": "synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout", "samples_ms": [155.707363, 160.165062, 161.74993, 151.641546, 160.943381, 149.877411, 186.379217, 189.858565, 143.675298, 136.966855, 181.810169, 145.660997, 157.279633, 168.322978, 135.966703, 141.853499, 139.798088, 147.296667, 174.832486, 173.398907, 138.442264, 138.570433, 135.947427, 150.334109], "timeouts": [], "p50_ms": 150.9878275, "p95_ms": 186.379217}

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

View File

@ -0,0 +1,11 @@
{
"pid": 135145,
"ppid": 92843,
"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 --keep-awake --no-vsync --no-frame-pacing --audio-on-host",
"color_range_override": "0",
"normal_kiosk_entry": true,
"hardware_decode_ready": true,
"egl_renderer": true,
"server_range": "MPEG",
"temporary_benchmark_stopped": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,3 @@
49732 00:01:58 5.5
49732 00:02:09 6.0

View File

@ -0,0 +1 @@
{"method": "synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout", "samples_ms": [150.01172, 140.54207, 146.622806, 140.651155, 144.384812, 147.6731, 147.065551, 138.139572, 143.82567, 141.994567, 141.890732, 146.535279, 140.382497, 136.103407, 176.574946, 173.033478, 135.687185, 141.140538, 168.252132, 152.002315, 168.699252, 141.007818, 135.567293, 143.323084], "timeouts": [], "p50_ms": 143.574377, "p95_ms": 173.033478}

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

@ -0,0 +1,21 @@
[2026:11:04:18:47:42]: Info:
[2026:11:04:18:47:42]: Info: // Ignore any errors mentioned above, they are not relevant. //
[2026:11:04:18:47:42]: Info:
[2026:11:04:18:47:42]: Info: Found H.264 encoder: libx264 [software]
Source ID 249 was not found when attempting to remove it
Source ID 248 was not found when attempting to remove it
[2026:11:04:18:47:42]: Info: CLIENT CONNECTED
[2026:11:04:18:47:42]: Info: Detecting monitors
[2026:11:04:18:47:42]: Info: Detected monitor 0: Virtual-1, connected: true
[2026:11:04:18:47:42]: Info: Screencasting with X11
[2026:11:04:18:47:42]: Info: Configuring selected monitor (0) to stream
[2026:11:04:18:47:42]: Info: Streaming display: Virtual-1 with res 2560x1440 offset by 0x0
[2026:11:04:18:47:42]: Info: SDR color coding [Rec. 601]
[2026:11:04:18:47:42]: Info: Color depth: 8-bit
[2026:11:04:18:47:42]: Info: Color range: [MPEG]
[2026:11:04:18:47:42]: Info: [libx264 @ 0x7f5d64028940] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[2026:11:04:18:47:42]: Info: [libx264 @ 0x7f5d64028940] profile Constrained Baseline, level 5.0, 4:2:0, 8-bit
[2026:11:04:18:47:43]: Info: Found default monitor by name: vdi_audio.monitor
MainPID=0
ActiveState=inactive
桌面

View File

@ -0,0 +1,71 @@
{
"final-full-2k30": {
"method": "synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout",
"samples_ms": [
155.707363,
160.165062,
161.74993,
151.641546,
160.943381,
149.877411,
186.379217,
189.858565,
143.675298,
136.966855,
181.810169,
145.660997,
157.279633,
168.322978,
135.966703,
141.853499,
139.798088,
147.296667,
174.832486,
173.398907,
138.442264,
138.570433,
135.947427,
150.334109
],
"timeouts": [],
"p50_ms": 150.9878275,
"p95_ms": 186.379217,
"render_fps": 23.35,
"decode_ms": 9.54
},
"final-limited-2k30": {
"method": "synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout",
"samples_ms": [
150.01172,
140.54207,
146.622806,
140.651155,
144.384812,
147.6731,
147.065551,
138.139572,
143.82567,
141.994567,
141.890732,
146.535279,
140.382497,
136.103407,
176.574946,
173.033478,
135.687185,
141.140538,
168.252132,
152.002315,
168.699252,
141.007818,
135.567293,
143.323084
],
"timeouts": [],
"p50_ms": 143.574377,
"p95_ms": 173.033478,
"render_fps": 27.25,
"decode_ms": 6.96
},
"interpretation": "Short sequential synthetic input-to-decoded-video proxy, not physical end-to-end latency. Similar ~140-150ms distributions and varying network; no demonstrated large latency improvement or Windows equivalence."
}

View File

@ -0,0 +1,3 @@
45143 00:19:31 99.0
45143 00:19:51 99.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

View File

@ -0,0 +1 @@
{"method": "synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout", "samples_ms": [183.940229, 161.718547, 160.71209, 138.491578, 141.028294, 141.934718, 154.77252, 163.688624, 125.041352, 179.385106, 179.373316, 178.694809, 185.66681, 177.159603, 189.758573, 151.004259, 149.251196, 165.416771, 157.880119, 159.972861, 179.910412, 189.615124, 185.924098, 140.941751], "timeouts": [], "p50_ms": 162.7035855, "p95_ms": 189.615124}

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -0,0 +1,32 @@
import ctypes,ctypes.util,subprocess,os,time,json,statistics
os.environ.update(DISPLAY=':0',XAUTHORITY='/home/armbian/.Xauthority')
x=ctypes.CDLL(ctypes.util.find_library('X11'));x.XOpenDisplay.argtypes=[ctypes.c_char_p];x.XOpenDisplay.restype=ctypes.c_void_p
x.XDefaultRootWindow.argtypes=[ctypes.c_void_p];x.XDefaultRootWindow.restype=ctypes.c_ulong
x.XGetImage.argtypes=[ctypes.c_void_p,ctypes.c_ulong,ctypes.c_int,ctypes.c_int,ctypes.c_uint,ctypes.c_uint,ctypes.c_ulong,ctypes.c_int];x.XGetImage.restype=ctypes.c_void_p
x.XGetPixel.argtypes=[ctypes.c_void_p,ctypes.c_int,ctypes.c_int];x.XGetPixel.restype=ctypes.c_ulong
x.XDestroyImage.argtypes=[ctypes.c_void_p];x.XCloseDisplay.argtypes=[ctypes.c_void_p]
d=x.XOpenDisplay(b':0');assert d;root=x.XDefaultRootWindow(d)
prefix='/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.71 '
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
ids=subprocess.check_output(['xdotool','search','--onlyvisible','--pid',items[0][0],'--class','Moonlight'],text=True).split();assert ids
subprocess.run(['xdotool','windowactivate','--sync',ids[-1]],check=True)
def pixel():
im=x.XGetImage(d,root,2250,130,1,1,0xffffffff,2);assert im
v=x.XGetPixel(im,0,0);x.XDestroyImage(im)
return v&0xffffff
samples=[];errors=[]
for i in range(24):
v=pixel();assert v<0x202020 or v>0xe0e0e0,hex(v)
start=time.monotonic_ns()
subprocess.run(['xdotool','key','--clearmodifiers','--delay','0','F9'],check=True,stdout=subprocess.DEVNULL)
while time.monotonic_ns()-start<2000000000:
cur=pixel()
if (v<0x202020 and cur>0xe0e0e0) or (v>0xe0e0e0 and cur<0x202020):
samples.append((time.monotonic_ns()-start)/1e6);break
time.sleep(.002)
else:errors.append(i)
time.sleep(.12)
x.XCloseDisplay(d)
vals=sorted(samples)
print(json.dumps({'method':'synthetic F9 injection to decoded video marker in X11 root; includes xdotool startup and polling, excludes physical USB and panel scanout','samples_ms':samples,'timeouts':errors,'p50_ms':statistics.median(vals) if vals else None,'p95_ms':vals[min(len(vals)-1,int(len(vals)*.95))] if vals else None}))

View File

@ -0,0 +1,48 @@
import gi,time,json,math
gi.require_version('Gtk','3.0')
from gi.repository import Gtk,Gdk,GLib
started=time.monotonic(); draws=0; ticks=0; marker=False
win=Gtk.Window(title='VDI Kylin quality benchmark')
win.connect('destroy',Gtk.main_quit)
area=Gtk.DrawingArea();win.add(area)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.KEY_PRESS_MASK)
area.set_can_focus(True)
def event(w,e):
global marker
if e.type==Gdk.EventType.KEY_PRESS and getattr(e,'keyval',None)==Gdk.KEY_F9:
marker=not marker; area.queue_draw_area(2180,80,220,140)
print(json.dumps({'event':e.type.value_nick,'keyval':getattr(e,'keyval',None),'button':getattr(e,'button',None),'x':getattr(e,'x',None),'y':getattr(e,'y',None),'width':area.get_allocated_width(),'height':area.get_allocated_height(),'t':time.monotonic()}),flush=True)
return False
win.connect('key-press-event',event);area.connect('button-press-event',event)
def draw(w,cr):
global draws
draws+=1; width=w.get_allocated_width();height=w.get_allocated_height()
cr.set_source_rgb(.96,.97,.99);cr.paint()
cr.set_source_rgb(*( (1,1,1) if marker else (0,0,0)));cr.rectangle(2180,80,220,140);cr.fill()
cr.select_font_face('sans');cr.set_font_size(30);cr.set_source_rgb(.08,.12,.18)
cr.move_to(60,65);cr.show_text('VDI Kylin - resolution / text / motion / input verification')
cr.set_font_size(22);cr.move_to(60,105);cr.show_text(f'Native desktop: {width} x {height} Temporary test window')
for row,size in enumerate([12,14,16,18,20,24]):
cr.set_font_size(size);cr.move_to(60,155+row*45);cr.set_source_rgb(.1,.1,.1)
cr.show_text(f'{size}px Cloud desktop 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz')
for i in range(600):
cr.set_source_rgb(0,0,0) if i%2 else cr.set_source_rgb(1,1,1)
cr.rectangle(60+i,445,1,80);cr.fill()
for i,color in enumerate([(1,0,0),(0,.65,0),(0,.2,1),(.6,0,.6)]):
cr.set_source_rgb(*color);cr.set_font_size(18);cr.move_to(60,565+i*28);cr.show_text('Color text 0123456789 - desktop quality test')
elapsed=time.monotonic()-started
for i in range(5):
x=70+((elapsed*220+i*173)%(max(200,width-400)))
cr.set_source_rgb(.1+i*.14,.35,.8-i*.1);cr.rectangle(x,height-280+i*35,120,28);cr.fill()
cr.set_source_rgb(.1,.15,.2);cr.set_font_size(16);cr.move_to(60,height-30)
cr.show_text(f'Source draw rate: {draws/max(.1,elapsed):.2f} FPS; elapsed {elapsed:.1f}s. Close after testing.')
return False
def tick():
global ticks
ticks+=1;area.queue_draw_area(0,max(0,area.get_allocated_height()-300),area.get_allocated_width(),300);return True
def report():
elapsed=time.monotonic()-started
print(json.dumps({'event':'source-stats','seconds':elapsed,'draws':draws,'ticks':ticks,'draw_fps':draws/elapsed}),flush=True);return True
area.connect('draw',draw);win.fullscreen();win.show_all();area.grab_focus()
GLib.timeout_add(16,tick);GLib.timeout_add_seconds(5,report)
print('READY',flush=True);Gtk.main()

View File

@ -0,0 +1,3 @@
45143 00:20:22 97.2
45143 00:20:43 97.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

View File

@ -0,0 +1,3 @@
47776 00:00:09 44.8
47776 00:00:28 70.9

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

View File

@ -0,0 +1,19 @@
MainPID=49732
ActiveState=active
sunshine_name = VDI-Kylin
capture = x11
encoder = software
sw_preset = ultrafast
sw_tune = zerolatency
upnp = disabled
origin_web_ui_allowed = pc
hevc_mode = 1
audio_sink = vdi_audio
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192
Virtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm
1280x800 74.99 +
3840x2160 60.00 50.00 59.94
1920x1440 60.00
2560x1080 50.00
1856x1392 60.00
(<false>,)

View File

@ -0,0 +1 @@
{"removed_unused_trials": ["sunshine-v0.21.0-vdi-bgra", "sunshine-v0.21.0-vdi-bgra-fullchroma", "sunshine-v0.21.0-vdi-bgra-bilinear"], "original_sha256": "1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71"}

View File

@ -0,0 +1,10 @@
{
"source": "software CPU clock perf_event_open 10ms period, 4 seconds; top80 addresses only",
"build_sha256": "1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71",
"top80_groups_approx": {
"static_ffmpeg_conversion_approx": 140,
"other": 36,
"static_x264_approx": 34
},
"warning": "Approximate stripped ELF ranges; not exact function timing. Scalar color-range loop disassembled at 0x8e8490 and RGB chroma loop at 0x8fb330."
}

View File

@ -0,0 +1,59 @@
{
"trials": [
{
"label": "fullrange-2k45",
"render_fps": 21.42,
"decode_ms": 7.48,
"queue_ms": 0.15,
"render_ms": 2.07,
"network_drop_pct": 0.0,
"jitter_drop_pct": 1.05
},
{
"label": "limitedrange-2k45",
"render_fps": 28.81,
"decode_ms": 6.9,
"queue_ms": 0.23,
"render_ms": 2.02,
"network_drop_pct": 0.0,
"jitter_drop_pct": 0.81
},
{
"label": "limitedrange-thread2-2k45",
"render_fps": 25.67,
"decode_ms": 7.09,
"queue_ms": 0.12,
"render_ms": 2.01,
"network_drop_pct": 0.0,
"jitter_drop_pct": 0.99
},
{
"label": "bgra-limited-2k45-valid",
"render_fps": 27.17,
"decode_ms": 6.85,
"queue_ms": 0.11,
"render_ms": 2.01,
"network_drop_pct": 0.0,
"jitter_drop_pct": 0.78
},
{
"label": "bgra-fullchroma-limited-2k45",
"render_fps": 25.61,
"decode_ms": 10.38,
"queue_ms": 0.13,
"render_ms": 2.03,
"network_drop_pct": 0.0,
"jitter_drop_pct": 1.72
},
{
"label": "bgra-bilinear-limited-2k45",
"render_fps": 30.79,
"decode_ms": 7.44,
"queue_ms": 0.13,
"render_ms": 1.93,
"network_drop_pct": 0.0,
"jitter_drop_pct": 1.12
}
],
"note": "2K45 request, 30Mbps H264, same partial-motion 60fps source. Short sequential tests, network variation. Only original binary + limited range retained; all binary experiments and 2 threads reverted. Inputecho baseline incomplete after stream deadline and subsequent Windows connection; exclude from paired latency claims."
}

View File

@ -0,0 +1 @@
{"192.168.1.71": "0", "192.168.1.70": "unset", "192.168.1.72": "unset", "explicit override": "1", "invalid rejected": 78}

View File

@ -0,0 +1,3 @@
169194 00:06:35 9.7
169194 00:06:59 10.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

View File

@ -0,0 +1,3 @@
179730 00:00:09 8.7
179730 00:00:34 26.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

View File

@ -0,0 +1,3 @@
180224 00:00:11 46.2
180224 00:00:36 74.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

View File

@ -0,0 +1,3 @@
180438 00:00:09 42.7
180438 00:00:35 74.5

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 KiB

View File

@ -0,0 +1,3 @@
180841 00:00:10 42.4
180841 00:00:37 74.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

View File

@ -0,0 +1,23 @@
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192
Virtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm
1280x800 74.99 +
3840x2160 60.00 50.00 59.94
1920x1440 60.00
2560x1080 50.00
1856x1392 60.00
1792x1344 60.00
2048x1152 60.00
1920x1200 59.88
1920x1080 60.00 50.00
1600x1200 60.00
1680x1050 59.95
1400x1050 59.98
1280x1024 60.02
1440x900 59.89
1280x960 60.00
1360x768 60.02
1280x768 59.87
1024x768 60.00
800x600 60.32
640x480 60.00 59.94
2560x1440_60.00 59.95*

View File

@ -0,0 +1,7 @@
{
"F8_received": 2,
"alternating_clicks_correct": 48,
"native_width": 2560,
"native_height": 1440,
"coordinate_error_bound_px": 1
}

View File

@ -0,0 +1,3 @@
16379 00:01:07 47.7
16379 00:01:35 57.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,3 @@
25194 00:00:09 30.2
25194 00:00:34 58.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

View File

@ -0,0 +1,3 @@
42266 00:00:59 35.0
42266 00:01:21 43.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -0,0 +1,3 @@
42266 00:00:09 43.6
42266 00:00:30 73.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -0,0 +1,3 @@
25194 00:01:01 0.8
25194 00:01:20 1.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

View File

@ -0,0 +1,3 @@
16379 00:00:12 46.0
16379 00:00:37 71.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,44 @@
import gi,time,json,math
gi.require_version('Gtk','3.0')
from gi.repository import Gtk,Gdk,GLib
started=time.monotonic(); draws=0; ticks=0
win=Gtk.Window(title='VDI Kylin quality benchmark')
win.connect('destroy',Gtk.main_quit)
area=Gtk.DrawingArea();win.add(area)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.KEY_PRESS_MASK)
area.set_can_focus(True)
def event(w,e):
print(json.dumps({'event':e.type.value_nick,'keyval':getattr(e,'keyval',None),'button':getattr(e,'button',None),'x':getattr(e,'x',None),'y':getattr(e,'y',None),'width':area.get_allocated_width(),'height':area.get_allocated_height(),'t':time.monotonic()}),flush=True)
return False
win.connect('key-press-event',event);area.connect('button-press-event',event)
def draw(w,cr):
global draws
draws+=1; width=w.get_allocated_width();height=w.get_allocated_height()
cr.set_source_rgb(.96,.97,.99);cr.paint()
cr.select_font_face('sans');cr.set_font_size(30);cr.set_source_rgb(.08,.12,.18)
cr.move_to(60,65);cr.show_text('VDI Kylin - resolution / text / motion / input verification')
cr.set_font_size(22);cr.move_to(60,105);cr.show_text(f'Native desktop: {width} x {height} Temporary test window')
for row,size in enumerate([12,14,16,18,20,24]):
cr.set_font_size(size);cr.move_to(60,155+row*45);cr.set_source_rgb(.1,.1,.1)
cr.show_text(f'{size}px Cloud desktop 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz')
for i in range(600):
cr.set_source_rgb(0,0,0) if i%2 else cr.set_source_rgb(1,1,1)
cr.rectangle(60+i,445,1,80);cr.fill()
for i,color in enumerate([(1,0,0),(0,.65,0),(0,.2,1),(.6,0,.6)]):
cr.set_source_rgb(*color);cr.set_font_size(18);cr.move_to(60,565+i*28);cr.show_text('Color text 0123456789 - desktop quality test')
elapsed=time.monotonic()-started
for i in range(5):
x=70+((elapsed*220+i*173)%(max(200,width-400)))
cr.set_source_rgb(.1+i*.14,.35,.8-i*.1);cr.rectangle(x,height-280+i*35,120,28);cr.fill()
cr.set_source_rgb(.1,.15,.2);cr.set_font_size(16);cr.move_to(60,height-30)
cr.show_text(f'Source draw rate: {draws/max(.1,elapsed):.2f} FPS; elapsed {elapsed:.1f}s. Close after testing.')
return False
def tick():
global ticks
ticks+=1;area.queue_draw_area(0,max(0,area.get_allocated_height()-300),area.get_allocated_width(),300);return True
def report():
elapsed=time.monotonic()-started
print(json.dumps({'event':'source-stats','seconds':elapsed,'draws':draws,'ticks':ticks,'draw_fps':draws/elapsed}),flush=True);return True
area.connect('draw',draw);win.fullscreen();win.show_all();area.grab_focus()
GLib.timeout_add(16,tick);GLib.timeout_add_seconds(5,report)
print('READY',flush=True);Gtk.main()

View File

@ -0,0 +1,44 @@
import gi,time,json,math
gi.require_version('Gtk','3.0')
from gi.repository import Gtk,Gdk,GLib
started=time.monotonic(); draws=0; ticks=0
win=Gtk.Window(title='VDI Kylin quality benchmark')
win.connect('destroy',Gtk.main_quit)
area=Gtk.DrawingArea();win.add(area)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.KEY_PRESS_MASK)
area.set_can_focus(True)
def event(w,e):
print(json.dumps({'event':e.type.value_nick,'keyval':getattr(e,'keyval',None),'button':getattr(e,'button',None),'x':getattr(e,'x',None),'y':getattr(e,'y',None),'width':area.get_allocated_width(),'height':area.get_allocated_height(),'t':time.monotonic()}),flush=True)
return False
win.connect('key-press-event',event);area.connect('button-press-event',event)
def draw(w,cr):
global draws
draws+=1; width=w.get_allocated_width();height=w.get_allocated_height()
cr.set_source_rgb(.96,.97,.99);cr.paint()
cr.select_font_face('sans');cr.set_font_size(30);cr.set_source_rgb(.08,.12,.18)
cr.move_to(60,65);cr.show_text('VDI Kylin - resolution / text / motion / input verification')
cr.set_font_size(22);cr.move_to(60,105);cr.show_text(f'Native desktop: {width} x {height} Temporary test window')
for row,size in enumerate([12,14,16,18,20,24]):
cr.set_font_size(size);cr.move_to(60,155+row*45);cr.set_source_rgb(.1,.1,.1)
cr.show_text(f'{size}px Cloud desktop 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz')
for i in range(600):
cr.set_source_rgb(0,0,0) if i%2 else cr.set_source_rgb(1,1,1)
cr.rectangle(60+i,445,1,80);cr.fill()
for i,color in enumerate([(1,0,0),(0,.65,0),(0,.2,1),(.6,0,.6)]):
cr.set_source_rgb(*color);cr.set_font_size(18);cr.move_to(60,565+i*28);cr.show_text('Color text 0123456789 - desktop quality test')
elapsed=time.monotonic()-started
for i in range(5):
x=70+((elapsed*220+i*173)%(max(200,width-400)))
cr.set_source_rgb(.1+i*.14,.35,.8-i*.1);cr.rectangle(x,height-280+i*35,120,28);cr.fill()
cr.set_source_rgb(.1,.15,.2);cr.set_font_size(16);cr.move_to(60,height-30)
cr.show_text(f'Source draw rate: {draws/max(.1,elapsed):.2f} FPS; elapsed {elapsed:.1f}s. Close after testing.')
return False
def tick():
global ticks
ticks+=1;area.queue_draw();return True
def report():
elapsed=time.monotonic()-started
print(json.dumps({'event':'source-stats','seconds':elapsed,'draws':draws,'ticks':ticks,'draw_fps':draws/elapsed}),flush=True);return True
area.connect('draw',draw);win.fullscreen();win.show_all();area.grab_focus()
GLib.timeout_add(16,tick);GLib.timeout_add_seconds(5,report)
print('READY',flush=True);Gtk.main()

View File

@ -0,0 +1 @@
{"updated": {"app": "Desktop", "endpoint": "192.168.1.71", "fps": 30, "height": 1440, "id": "desk-kylin-01", "name": "麒麟", "os": "银河麒麟桌面", "width": 2560}, "other_desktop_objects_unchanged": true}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

@ -0,0 +1,27 @@
active
enabled
inactive
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 8192 x 8192
Virtual-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 325mm x 203mm
1280x800 74.99 +
3840x2160 60.00 50.00 59.94
1920x1440 60.00
(<false>,)
[2026:11:04:17:36:00]: Info: [libx264 @ 0x7fa2e81c4300] kb/s:3306.24
[2026:11:04:17:36:00]: Info: // Testing for available encoders, this may generate errors. You can safely ignore those errors. //
[2026:11:04:17:36:00]: Info:
[2026:11:04:17:36:00]: Info: // Ignore any errors mentioned above, they are not relevant. //
[2026:11:04:17:36:00]: Info:
[2026:11:04:17:36:00]: Info: Found H.264 encoder: libx264 [software]
[2026:11:04:17:36:00]: Info: CLIENT CONNECTED
[2026:11:04:17:36:00]: Info: Detecting monitors
[2026:11:04:17:36:00]: Info: Detected monitor 0: Virtual-1, connected: true
[2026:11:04:17:36:00]: Info: Screencasting with X11
[2026:11:04:17:36:00]: Info: Configuring selected monitor (0) to stream
[2026:11:04:17:36:00]: Info: Streaming display: Virtual-1 with res 2560x1440 offset by 0x0
[2026:11:04:17:36:00]: Info: SDR color coding [Rec. 601]
[2026:11:04:17:36:00]: Info: Color depth: 8-bit
[2026:11:04:17:36:00]: Info: Color range: [JPEG]
[2026:11:04:17:36:00]: Info: [libx264 @ 0x7fa2fc00e6c0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[2026:11:04:17:36:00]: Info: [libx264 @ 0x7fa2fc00e6c0] profile Constrained Baseline, level 5.0, 4:2:0, 8-bit
[2026:11:04:17:36:00]: Info: Found default monitor by name: vdi_audio.monitor

View File

@ -0,0 +1,20 @@
[
{
"attempt": 1,
"normal_kiosk": true,
"width": 2560,
"height": 1440,
"configured_fps": 30,
"bitrate_kbps": 30000,
"hardware_decode_ready": true
},
{
"attempt": 2,
"normal_kiosk": true,
"width": 2560,
"height": 1440,
"configured_fps": 30,
"bitrate_kbps": 30000,
"hardware_decode_ready": true
}
]

View File

@ -0,0 +1,9 @@
sunshine_name = VDI-Kylin
capture = x11
encoder = software
sw_preset = ultrafast
sw_tune = zerolatency
upnp = disabled
origin_web_ui_allowed = pc
hevc_mode = 1
audio_sink = vdi_audio

View File

@ -0,0 +1,20 @@
[Unit]
Description=VDI Linux desktop Sunshine host
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Environment=DISPLAY=:0 XAUTHORITY=/home/test1/.Xauthority
ExecStartPre=/bin/sh -c '/usr/bin/xrandr --query | grep -q 2560x1440_60.00 || /usr/bin/xrandr --newmode 2560x1440_60.00 241.50 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync'
ExecStartPre=/usr/bin/xrandr --addmode Virtual-1 2560x1440_60.00
ExecStartPre=/usr/bin/xrandr --output Virtual-1 --mode 2560x1440_60.00
ExecStartPre=/bin/sh -c 'pactl list short sinks | grep -qw vdi_audio || pactl load-module module-null-sink sink_name=vdi_audio sink_properties=device.description=VDI-Audio'
ExecStartPre=/usr/bin/pactl set-default-sink vdi_audio
ExecStart=/opt/vdi-sunshine-0.21.0/usr/bin/sunshine /home/test1/.config/sunshine/sunshine.conf
ExecStartPost=-/usr/bin/gdbus call --session --dest org.ukui.KWin --object-path /Compositor --method org.ukui.kwin.Compositing.suspend
ExecStopPost=-/usr/bin/gdbus call --session --dest org.ukui.KWin --object-path /Compositor --method org.ukui.kwin.Compositing.resume
Restart=on-failure
RestartSec=5
[Install]
WantedBy=graphical-session.target

View File

@ -0,0 +1,9 @@
sunshine_name = VDI-Kylin
capture = x11
encoder = software
sw_preset = ultrafast
sw_tune = zerolatency
upnp = disabled
origin_web_ui_allowed = pc
hevc_mode = 1
audio_sink = vdi_audio

View File

@ -0,0 +1,104 @@
[
{
"trial": "1080p30-baseline",
"valid_for_performance": true,
"Rendering frame rate": "28.63 FPS",
"Average decoding time": "4.95 ms",
"Average frame queue delay": "0.17 ms",
"Average rendering time (including monitor V-sync latency)": "1.69 ms",
"Average network latency": "24 ms (variance: 27 ms)"
},
{
"trial": "1080p45-compositor",
"valid_for_performance": true,
"Rendering frame rate": "29.56 FPS",
"Average decoding time": "5.10 ms",
"Average frame queue delay": "0.25 ms",
"Average rendering time (including monitor V-sync latency)": "1.68 ms",
"Average network latency": "11 ms (variance: 7 ms)"
},
{
"trial": "1080p45-no-compositor-retry",
"valid_for_performance": true,
"Rendering frame rate": "31.86 FPS",
"Average decoding time": "5.98 ms",
"Average frame queue delay": "0.16 ms",
"Average rendering time (including monitor V-sync latency)": "1.64 ms",
"Average network latency": "4 ms (variance: 4 ms)"
},
{
"trial": "1080p45-superfast-threads2",
"valid_for_performance": true,
"Rendering frame rate": "27.84 FPS",
"Average decoding time": "7.18 ms",
"Average frame queue delay": "0.18 ms",
"Average rendering time (including monitor V-sync latency)": "1.65 ms",
"Average network latency": "5 ms (variance: 2 ms)"
},
{
"trial": "1080p45-ultrafast-threads2",
"valid_for_performance": true,
"Rendering frame rate": "32.44 FPS",
"Average decoding time": "4.82 ms",
"Average frame queue delay": "0.23 ms",
"Average rendering time (including monitor V-sync latency)": "1.60 ms",
"Average network latency": "2 ms (variance: 1 ms)"
},
{
"trial": "2k30-damage-thread1",
"valid_for_performance": false,
"excluded_reason": "lock screen or interrupted/unverified scene",
"Rendering frame rate": "21.81 FPS",
"Average decoding time": "7.74 ms",
"Average frame queue delay": "0.21 ms",
"Average rendering time (including monitor V-sync latency)": "1.95 ms",
"Average network latency": "8 ms (variance: 1 ms)"
},
{
"trial": "2k30-damage-thread2",
"valid_for_performance": false,
"excluded_reason": "lock screen or interrupted/unverified scene",
"Rendering frame rate": "19.38 FPS",
"Average decoding time": "8.69 ms",
"Average frame queue delay": "0.21 ms",
"Average rendering time (including monitor V-sync latency)": "2.01 ms",
"Average network latency": "4 ms (variance: 1 ms)"
},
{
"trial": "2k30-desktop-no-compositor-valid",
"valid_for_performance": true,
"Rendering frame rate": "22.97 FPS",
"Average decoding time": "9.04 ms",
"Average frame queue delay": "0.12 ms",
"Average rendering time (including monitor V-sync latency)": "2.11 ms",
"Average network latency": "9 ms (variance: 6 ms)"
},
{
"trial": "2k30-desktop-thread1-valid",
"valid_for_performance": true,
"Rendering frame rate": "18.89 FPS",
"Average decoding time": "14.13 ms",
"Average frame queue delay": "0.09 ms",
"Average rendering time (including monitor V-sync latency)": "2.12 ms",
"Average network latency": "10 ms (variance: 8 ms)"
},
{
"trial": "2k30-desktop-thread2-valid",
"valid_for_performance": true,
"Rendering frame rate": "18.85 FPS",
"Average decoding time": "8.96 ms",
"Average frame queue delay": "0.29 ms",
"Average rendering time (including monitor V-sync latency)": "2.12 ms",
"Average network latency": "5 ms (variance: 1 ms)"
},
{
"trial": "2k30-ultrafast-thread1",
"valid_for_performance": false,
"excluded_reason": "lock screen or interrupted/unverified scene",
"Rendering frame rate": "14.12 FPS",
"Average decoding time": "12.11 ms",
"Average frame queue delay": "0.12 ms",
"Average rendering time (including monitor V-sync latency)": "2.10 ms",
"Average network latency": "4 ms (variance: 2 ms)"
}
]

View File

@ -0,0 +1,10 @@
{
"vmid": 101,
"name": "kylin-test",
"before_vga": null,
"apply_vga": "std,memory=64",
"requires": "graceful shutdown to stopped, then start",
"rollback_vga": "delete explicit vga field (restore default)",
"other_fields": "preserve",
"reboot_other_machines": false
}

View File

@ -0,0 +1,23 @@
{
"ostype": "l26",
"cores": 2,
"scsi0": "local-lvm:vm-101-disk-0,discard=on,iothread=1,size=50G",
"scsihw": "virtio-scsi-single",
"onboot": 1,
"sockets": 1,
"digest": "5e371a9964bee0ea2acf3265409281aae89c9ffc",
"memory": "4096",
"boot": "order=scsi0;ide2;net0",
"net0": "virtio=BC:24:11:25:29:40,bridge=vmbr0,firewall=1",
"vmgenid": "cc8ff198-c228-4ac6-8c37-918d4978b79b",
"balloon": 0,
"name": "kylin-test",
"cpu": "host",
"numa": 0,
"bios": "seabios",
"ide2": "none,media=cdrom",
"agent": "1",
"smbios1": "uuid=8ca270ad-7f1a-4da0-97f2-99a94eef9400",
"meta": "creation-qemu=11.0.3,ctime=1793090566",
"machine": "q35"
}

View File

@ -0,0 +1,6 @@
{
"vga": {
"before": null,
"after": "std,memory=64"
}
}

View File

@ -0,0 +1,22 @@
[
{
"phase": "precheck",
"status": "running",
"vga": null,
"digest": "5e371a9964bee0ea2acf3265409281aae89c9ffc"
},
{
"phase": "graceful_shutdown_requested"
},
{
"phase": "stopped"
},
{
"phase": "vram_configured",
"vga": "std,memory=64"
},
{
"phase": "started",
"vga": "std,memory=64"
}
]

View File

@ -0,0 +1,44 @@
use_type: 1
use_tt: 0
size: 4096
0x0000000000000000-0x000000000000012c: 300: free
0x000000000000012c-0x0000000000000915: 2025: used
0x0000000000000915-0x0000000000001000: 1771: free
total: 4096, used 2025 free 2071
framebuffer[39]:
allocated by = Xorg
refcount=2
format=XR24 little-endian (0x34325258)
modifier=0x0
size=1920x1080
layers:
size[0]=1920x1080
pitch[0]=7680
offset[0]=0
obj[0]:
name=0
refcount=2
start=001007d4
size=8294400
imported=no
placement=(none)
bus.offset=fc12c000
framebuffer[36]:
allocated by = [fbcon]
refcount=1
format=XR24 little-endian (0x34325258)
modifier=0x0
size=1280x800
layers:
size[0]=1280x800
pitch[0]=5120
offset[0]=0
obj[0]:
name=0
refcount=3
start=00100000
size=4096000
imported=no
placement=(none)
name size handles refcount
Y

View File

@ -0,0 +1,52 @@
# 麒麟鼠标掉帧继续排查(2026-11-04)
本轮保留的改动是**仅麒麟主机协商 limited 视频范围**,避免旧版 Sunshine 的额外全范围转换。真实桌面和视频仍为2560×1440、H.264、30Mbps,正常卡片仍请求30FPS。同场景45FPS请求的对照中,实际渲染由21.42提高至28.81FPS(约35%);尚未达到Windows约43~44FPS的既有水平,也未证明物理鼠标到屏幕延迟与Windows相同。
## 现状与改动
- Sunshine恢复官方v0.21.0原文件,SHA256为`1c3099bf924db3aee759b3d7c05e012569ef8650c400d1479144398795decc71`。保持X11、ultrafast、zerolatency、原单线程、暂停UKUI合成器,无额外capability。
- 盒子`vdi-moonlight-rkmpp`支持逐主机色彩范围文件:`${XDG_CONFIG_HOME:-$HOME/.config}/vdi-terminal/host-color-range/<host>`,内容只允许`0`(limited)或`1`(full)。仅配置`192.168.1.71`为`0`。显式`COLOR_RANGE_OVERRIDE`环境变量优先,无配置的主机沿用Moonlight默认行为。
- 服务端实际日志从`Color range: [JPEG]`变为`[MPEG]`;客户端解码后仍扩展成正常屏幕RGB,不是将黑白截成灰色。未改客户端二进制、分辨率、码率、Windows/Ubuntu卡片或PVE配置。
- wrapper已部署,语法检查、麒麟/Windows/Ubuntu隔离、显式环境覆盖和非法配置拒绝检查通过,见[配置验证](evidence/kylin-mouse-20261104/wrapper-profile-check.json)。用户明确允许切回麒麟后,已从正常kiosk列表连接,确认父进程为kiosk、2560×1440/30FPS/30Mbps、`COLOR_RANGE_OVERRIDE=0`,实际2K RKMPP/EGL解码和服务端`[MPEG]`日志通过。最终保留麒麟桌面连接,见[正常入口](evidence/kylin-mouse-20261104/final-kiosk-connection.json)及[最终桌面](evidence/kylin-mouse-20261104/final-normal-desktop.png)。
备份:盒子`~/vdi-terminal/backups/kylin-mouse-20261104/`有原wrapper和desktops.json;麒麟同目录有原sunshine.conf。回退删除麒麟色彩范围文件,下次连接恢复原协商;必要时恢复wrapper备份。没有修改其他主机的配置文件。
## 测试证据
局部运动GTK源约60FPS,所有以下有效轮次均核对真实未锁屏测试窗口,并发送F8和24次交替点击。每轮请求2560×1440/45FPS/30Mbps,短时顺序测试;网络延迟和抖动有波动,不作为长期保证。
| 服务端 / 协商 | 实际渲染FPS | 解码ms | 队列ms | 采用情况 |
| --- | ---: | ---: | ---: | --- |
| 原程序 / full |21.42|7.48|0.15|对照|
| 原程序 / limited |28.81|6.90|0.23|保留limited协商|
| 原程序 / limited / 两线程 |25.67|7.09|0.12|已撤销|
| BGRA兼容副本 / limited |27.17|6.85|0.11|已撤销|
| BGRA + full-chroma副本 / limited |25.61|10.38|0.13|已撤销|
| BGRA + bilinear副本 / limited |30.79|7.44|0.13|收益不足以保留二进制维护和色度采样变化,已撤销|
数据及日志索引见[测试汇总](evidence/kylin-mouse-20261104/trial-summary.json)。这轮没有把请求45FPS写成真实达到45FPS。原full与limited静态区域的RGB平均绝对差约0.3~1.3/255,黑白条纹仍包含0和255,小字号与彩色文字已视觉核对;这不是逐像素无损承诺。见[色彩核对](evidence/kylin-mouse-20261104/color-comparison.json)、[limited实际截图](evidence/kylin-mouse-20261104/limitedrange-2k45.png)。
原始进程线程观察显示Sunshine有一个线程接近占满单核,而盒子呈现队列通常约0.1~0.2ms。通过Linux软件CPU-clock采样,旧版静态FFmpeg的逐像素色彩转换出现在热点中;ELF已剥离符号,地址区间只能粗略分类,不能当作精确函数耗时。记录见[采样说明](evidence/kylin-mouse-20261104/sampling-hotspots.json)。
## 未采用的兼容实验
仅对精确SHA256匹配的原文件创建三个独立副本,未覆盖原文件。根据v0.21.0源码与反汇编,两个`sws_getContext`调用的BGR0立即数由121改为BGRA的28;后续分别测试附加`SWS_FULL_CHR_H_INP`或改为`SWS_BILINEAR`。只对独立副本做单文件KYSEC认证。实验服务drop-in已删除,三个副本确认没有进程使用后删除,原版SHA和活动服务已读回确认,见[清理记录](evidence/kylin-mouse-20261104/restored-original.json)、[恢复状态](evidence/kylin-mouse-20261104/restored-host-state.txt)。未保留任何实验二进制作为正常入口。
依据:[Sunshine v0.21.0 video.cpp](https://github.com/LizardByte/Sunshine/blob/v0.21.0/src/video.cpp)、[FFmpeg n6.0转换初始化](https://github.com/FFmpeg/FFmpeg/blob/n6.0/libswscale/x86/swscale.c)、[FFmpeg色彩范围转换](https://github.com/FFmpeg/FFmpeg/blob/n6.0/libswscale/swscale.c)。
## 回显延迟边界及后续
增加了F9翻转画面黑白标记的探针:盒子在发出合成按键前计时,轮询实际解码视频所在X11根窗口,看到标记翻转后结束。该方法包含工具启动、串流往返和轮询,排除物理USB输入和显示面板扫描。
用户授权切回麒麟后,使用同一个未锁屏的动态源、原版服务端、相同2560×1440/30FPS/30Mbps完成顺序对照,每组24次,均无超时:
| 配置 | 实际渲染FPS | 解码ms | 合成输入回显P50 ms | P95 ms |
| --- | ---: | ---: | ---: | ---: |
| full原协商 |23.35|9.54|150.99|186.38|
| limited保留配置 |27.25|6.96|143.57|173.03|
见[完整对照数据](evidence/kylin-mouse-20261104/final-response-comparison.json)。视频更新约提高17%;回显分布重叠、网络延迟也不同,24个样本不足以证明显著或大幅的延迟下降,更不能宣称达到Windows几乎无延迟。该探针也不能替代物理输入到屏幕的测量。
此前full/30FPS的一组24次P50约163ms、P95约190ms,后续超出临时串流期限,盒子随后已连接Windows,完整收尾未完成;保留原始JSON和部分日志用于审计,不混入上述完整对照。临时GTK和临时串流服务已停止,最终使用正常kiosk串流。
麒麟2vCPU的软件捕获/颜色转换/编码链路仍然限制视频更新。若目标是Windows同等的视频响应,需要进一步实现并验证转换并行化/硬件编码;若目标是指针本身不等待视频,则需要实现自动形状同步、显隐、热点和可靠交接的独立光标通道。现有受控原型尚不满足这一要求,不能用普通本地箭头叠加冒充完成。

View File

@ -0,0 +1,65 @@
# 麒麟真实 2K 与响应优化(2026-11-04)
后续鼠标掉帧排查已保留逐主机limited协商,同场景原程序对照约21→29FPS,尚未达到Windows水平,见[继续排查记录](kylin-mouse-20261104.md)。下文为本轮之前的真实2K落地记录。
麒麟已从 1080p 升级为真实 **2560×1440** 桌面与视频,盒子麒麟卡片配置为 **30 FPS、30Mbps、H.264**。分辨率与码率已与 Windows 对齐;实际帧率尚未达到 Windows 的水平。保留暂停 UKUI 桌面合成器的优化,局部运动短测由约 19 FPS 提高至约 23 FPS。物理鼠标到屏幕的端到端延迟未测量。
## 最终部署
| 项目 | 生效配置 |
| --- | --- |
| PVE VM 101 kylin-test | `vga=std,memory=64`,其他字段保持原值 |
| 麒麟 Virtual-1 | 2560×1440,约59.95Hz,自定义模式 `2560x1440_60.00` |
| Sunshine | v0.21.0,X11 捕获,libx264 ultrafast / zerolatency,原单线程策略 |
| 盒子麒麟对象 | `desk-kylin-01`,192.168.1.71 / Desktop,2560×1440 / 30 FPS |
| 客户端 | 原 RKMPP 硬解、DRM PRIME、EGL/GLES、latest-frame 队列、无VSync/无frame-pacing |
| 视频码率 | 30Mbps,沿用现有产品对2K的规则;Windows同为30Mbps |
用户明确授权后,已正常关闭 VM 101 至 stopped,修改显存并重新启动。配置差异检查确认只有 `vga` 变化;未重启 PVE、Windows 或 Ubuntu。麒麟内部 PCI BAR 已识别为64MB。见 [执行记录](evidence/kylin-quality-20261104/vm101-execution.json)、[配置差异](evidence/kylin-quality-20261104/vm101-config-change.json)。
服务 `~/.config/systemd/user/vdi-sunshine.service` 增加启动前创建、添加和切换2K模式的步骤,时序为:
```text
241.50 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync
```
已通过“切回1080p、删除自定义模式、启动服务”的实测,确认能重新创建并进入2K。新服务配置尚未通过第二次整机重启验收。麒麟 test1 图形会话在本次开机后存在;未改变其登录或锁屏策略。
服务启动后经 `org.ukui.KWin /Compositor` 暂停桌面合成,停止服务时恢复合成;读回最终 `active=false`。这针对当前UKUI/Xorg现场环境,调用失败不阻止 Sunshine 本体启动。原 `kwinrc` 未修改。见 [最终服务](evidence/kylin-quality-20261104/final-sunshine.service)、[最终配置](evidence/kylin-quality-20261104/final-sunshine.conf)。
## 实际验证
临时 GTK 窗口展示小字号文字、单像素条纹、彩色文字与局部运动块;有效轮次的源绘制约59FPS。只在确认 Moonlight 窗口焦点后注入输入。测试后窗口已关闭。
| 有效2K动态桌面轮次 | 实际渲染 FPS | 解码 ms | 帧队列 ms | 呈现 ms |
| --- | ---: | ---: | ---: | ---: |
| X11 / 单线程 / 合成开启 | 18.89 | 14.13 | 0.09 | 2.12 |
| X11 / 两线程 / 合成开启 | 18.85 | 8.96 | 0.29 | 2.12 |
| X11 / 单线程 / 合成暂停(采用) | 22.97 | 9.04 | 0.12 | 2.11 |
这是约20秒保持加连接与输入阶段的短测,网络延迟和抖动并不完全相同,不能视为长期性能保证。客户端帧队列很短,未证明物理输入延迟下降;2K实际23FPS也不能宣传为稳定30或45FPS。
- 实际桌面与视频尺寸均为2560×1440,已核对盒子截屏,文字不再由1080p放大。见 [2K动态图](evidence/kylin-quality-20261104/2k30-desktop-no-compositor-valid.png)。
- 当前有效测试进程记录中,48次跨屏交替点击全部落在预期坐标(误差不超过1px),收到F8。见 [输入结果](evidence/kylin-quality-20261104/2k-input-result.json) 和 [原始事件](evidence/kylin-quality-20261104/2k-valid-source-and-input.log)。
- 从正常 kiosk 麒麟卡片连接、退出回列表、再次连接,确认2K参数与RKMPP/EGL路径;最终保持正常麒麟桌面连接供用户体验。见 [重连结果](evidence/kylin-quality-20261104/final-kiosk-check.json)、[正常入口日志](evidence/kylin-quality-20261104/final-kiosk-client.log)、[最终桌面](evidence/kylin-quality-20261104/final-desktop.png)。
- 只修改盒子的麒麟对象,Windows、Ubuntu对象经结构比较保持原值;没有修改产品源码、客户端二进制和全局渲染配置。
部分早期2K轮次在长时间暂停后采到了锁屏画面,已经从有效性能结论中排除:`2k30-ultrafast-thread1`、`2k30-damage-thread1`、`2k30-damage-thread2`。这些原始文件保留用于审计。当前有效轮次均为文件名含 `desktop-...-valid` 的三轮,截图与输入记录已核对。测试源码也改为只重绘运动区域,不能直接将新旧不同负载的数字相减作为提升幅度。
曾探测KMS捕获,但此虚拟显卡上未初始化成功,已恢复X11;最终 Sunshine 实体二进制与入口的 `getcap` 均为空,未保留该探测使用的额外 capability。两线程、较慢压缩预设未带来适合本机的收益,均未保留。
## 原因、边界与回退
原默认 Standard VGA 只有16MB。Linux5.15 DRM VRAM helper限制单帧缓冲不超显存一半,2560×1440×4字节的单帧约14.06MiB,双缓冲约28.13MiB,因此原2K切换失败;增加到64MB后解除限制。参考 [内核实现](https://github.com/torvalds/linux/blob/v5.15/drivers/gpu/drm/drm_gem_vram_helper.c)。
麒麟目前只有2个虚拟CPU且使用软件编码。这次完成了真实2K、码率对齐、合成开销优化和基础输入/重连验证;45FPS、长时间高动态负载、物理输入延迟、音质、生产多用户隔离仍未验收。
备份位置:
- 麒麟 `/home/test1/vdi-terminal/backups/kylin-quality-20261104/`:原 sunshine.conf、vdi-sunshine.service、kwinrc、xrandr-before.txt。
- 盒子 `/home/armbian/vdi-terminal/backups/kylin-quality-20261104/desktops.json`。
- PVE [原配置快照](evidence/kylin-quality-20261104/vm101-config-before.json),原 `vga` 字段未设置。
回退先正常退出串流;恢复盒子备份(若其他桌面后续有修改,只恢复麒麟对象);在麒麟恢复原 Sunshine 配置与服务,`systemctl --user daemon-reload` 后重启服务。当前服务停止会恢复合成;原服务将显示器设回1080p。64MB显存可保留;如需完全恢复原PVE设置,删除显式 `vga` 字段并在维护窗口正常关机再启动。
编码参数含义参考 [Sunshine v0.21.0 官方说明](https://docs.lizardbyte.dev/projects/sunshine/v0.21.0/about/advanced_usage.html#min-threads)。

View File

@ -17,4 +17,22 @@ export HEVC_DECODER_HINT=hevc_rkmpp
# Only affects unpaced rendering in the patched client. Leave the last presented
# frame and decoder references intact; replace only waiting decoded frames.
export VDI_LATEST_FRAME="${VDI_LATEST_FRAME:-1}"
# A host can negotiate limited range to avoid Sunshine's software full-range
# conversion. The decoder still expands the negotiated range for display.
# Keep this per host: hardware-encoded desktops need no override.
if [ "${1:-}" = stream ] && [ -z "${COLOR_RANGE_OVERRIDE+x}" ]; then
case "${2:-}" in
''|*[!a-zA-Z0-9._-]*) ;;
*)
range_file="${XDG_CONFIG_HOME:-$HOME/.config}/vdi-terminal/host-color-range/$2"
if [ -f "$range_file" ]; then
color_range=$(cat "$range_file")
case "$color_range" in
0|1) export COLOR_RANGE_OVERRIDE="$color_range" ;;
*) echo 'Invalid host color range (expected 0 or 1)' >&2; exit 78 ;;
esac
fi
;;
esac
fi
exec "$client" "$@"