优化:完善麒麟2K显示与逐主机视频范围协商
44
docs/evidence/kylin-mouse-20261104/benchmark-source.py
Normal 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()
|
||||
@ -0,0 +1,3 @@
|
||||
49316 00:00:10 31.4
|
||||
|
||||
49316 00:00:35 62.7
|
||||
|
After Width: | Height: | Size: 364 KiB |
@ -0,0 +1,3 @@
|
||||
48825 00:00:10 29.7
|
||||
|
||||
48825 00:00:35 62.4
|
||||
|
After Width: | Height: | Size: 402 KiB |
@ -0,0 +1,3 @@
|
||||
48246 00:01:04 66.2
|
||||
|
||||
48246 00:01:29 74.2
|
||||
BIN
docs/evidence/kylin-mouse-20261104/bgra-limited-2k45-valid.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
107
docs/evidence/kylin-mouse-20261104/color-comparison.json
Normal 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
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
1008
docs/evidence/kylin-mouse-20261104/cpu-sampling-raw.txt
Normal file
@ -0,0 +1,3 @@
|
||||
49732 00:01:03 3.0
|
||||
|
||||
49732 00:01:15 3.5
|
||||
@ -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}
|
||||
BIN
docs/evidence/kylin-mouse-20261104/final-full-2k30.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
@ -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
|
||||
}
|
||||
BIN
docs/evidence/kylin-mouse-20261104/final-kiosk-list.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
@ -0,0 +1,3 @@
|
||||
49732 00:01:58 5.5
|
||||
|
||||
49732 00:02:09 6.0
|
||||
@ -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}
|
||||
BIN
docs/evidence/kylin-mouse-20261104/final-limited-2k30.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
BIN
docs/evidence/kylin-mouse-20261104/final-normal-desktop.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
21
docs/evidence/kylin-mouse-20261104/final-normal-host.txt
Normal 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
|
||||
桌面
|
||||
@ -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."
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
45143 00:19:31 99.0
|
||||
|
||||
45143 00:19:51 99.2
|
||||
BIN
docs/evidence/kylin-mouse-20261104/fullrange-2k45.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
@ -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}
|
||||
|
After Width: | Height: | Size: 376 KiB |
32
docs/evidence/kylin-mouse-20261104/inputecho-probe.py
Normal 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}))
|
||||
48
docs/evidence/kylin-mouse-20261104/inputecho-source.py
Normal 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()
|
||||
@ -0,0 +1,3 @@
|
||||
45143 00:20:22 97.2
|
||||
|
||||
45143 00:20:43 97.4
|
||||
BIN
docs/evidence/kylin-mouse-20261104/limitedrange-2k45.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
@ -0,0 +1,3 @@
|
||||
47776 00:00:09 44.8
|
||||
|
||||
47776 00:00:28 70.9
|
||||
BIN
docs/evidence/kylin-mouse-20261104/limitedrange-thread2-2k45.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
19
docs/evidence/kylin-mouse-20261104/restored-host-state.txt
Normal 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>,)
|
||||
@ -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"}
|
||||
10
docs/evidence/kylin-mouse-20261104/sampling-hotspots.json
Normal 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."
|
||||
}
|
||||
59
docs/evidence/kylin-mouse-20261104/trial-summary.json
Normal 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."
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{"192.168.1.71": "0", "192.168.1.70": "unset", "192.168.1.72": "unset", "explicit override": "1", "invalid rejected": 78}
|
||||
@ -0,0 +1,3 @@
|
||||
169194 00:06:35 9.7
|
||||
|
||||
169194 00:06:59 10.3
|
||||
BIN
docs/evidence/kylin-quality-20261104/1080p30-baseline.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
@ -0,0 +1,3 @@
|
||||
179730 00:00:09 8.7
|
||||
|
||||
179730 00:00:34 26.2
|
||||
BIN
docs/evidence/kylin-quality-20261104/1080p45-compositor.png
Normal file
|
After Width: | Height: | Size: 686 KiB |
@ -0,0 +1,3 @@
|
||||
180224 00:00:11 46.2
|
||||
|
||||
180224 00:00:36 74.1
|
||||
|
After Width: | Height: | Size: 685 KiB |
@ -0,0 +1,3 @@
|
||||
180438 00:00:09 42.7
|
||||
|
||||
180438 00:00:35 74.5
|
||||
|
After Width: | Height: | Size: 771 KiB |
@ -0,0 +1,3 @@
|
||||
180841 00:00:10 42.4
|
||||
|
||||
180841 00:00:37 74.1
|
||||
|
After Width: | Height: | Size: 688 KiB |
23
docs/evidence/kylin-quality-20261104/2k-display-probe.txt
Normal 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*
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"F8_received": 2,
|
||||
"alternating_clicks_correct": 48,
|
||||
"native_width": 2560,
|
||||
"native_height": 1440,
|
||||
"coordinate_error_bound_px": 1
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
16379 00:01:07 47.7
|
||||
|
||||
16379 00:01:35 57.3
|
||||
BIN
docs/evidence/kylin-quality-20261104/2k30-damage-thread1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,3 @@
|
||||
25194 00:00:09 30.2
|
||||
|
||||
25194 00:00:34 58.6
|
||||
BIN
docs/evidence/kylin-quality-20261104/2k30-damage-thread2.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
@ -0,0 +1,3 @@
|
||||
42266 00:00:59 35.0
|
||||
|
||||
42266 00:01:21 43.1
|
||||
|
After Width: | Height: | Size: 376 KiB |
@ -0,0 +1,3 @@
|
||||
42266 00:00:09 43.6
|
||||
|
||||
42266 00:00:30 73.2
|
||||
|
After Width: | Height: | Size: 376 KiB |
@ -0,0 +1,3 @@
|
||||
25194 00:01:01 0.8
|
||||
|
||||
25194 00:01:20 1.1
|
||||
|
After Width: | Height: | Size: 378 KiB |
@ -0,0 +1,3 @@
|
||||
16379 00:00:12 46.0
|
||||
|
||||
16379 00:00:37 71.3
|
||||
BIN
docs/evidence/kylin-quality-20261104/2k30-ultrafast-thread1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
@ -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()
|
||||
44
docs/evidence/kylin-quality-20261104/benchmark-source.py
Normal 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()
|
||||
@ -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}
|
||||
BIN
docs/evidence/kylin-quality-20261104/final-desktop.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
27
docs/evidence/kylin-quality-20261104/final-host-state.txt
Normal 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
|
||||
20
docs/evidence/kylin-quality-20261104/final-kiosk-check.json
Normal 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
|
||||
}
|
||||
]
|
||||
9
docs/evidence/kylin-quality-20261104/final-sunshine.conf
Normal 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
|
||||
20
docs/evidence/kylin-quality-20261104/final-sunshine.service
Normal 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
|
||||
@ -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
|
||||
104
docs/evidence/kylin-quality-20261104/trial-summary.json
Normal 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)"
|
||||
}
|
||||
]
|
||||
10
docs/evidence/kylin-quality-20261104/vm101-change-plan.json
Normal 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
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"vga": {
|
||||
"before": null,
|
||||
"after": "std,memory=64"
|
||||
}
|
||||
}
|
||||
22
docs/evidence/kylin-quality-20261104/vm101-execution.json
Normal 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"
|
||||
}
|
||||
]
|
||||
44
docs/evidence/kylin-quality-20261104/vram-diagnostic.txt
Normal 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
|
||||
52
docs/kylin-mouse-20261104.md
Normal 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同等的视频响应,需要进一步实现并验证转换并行化/硬件编码;若目标是指针本身不等待视频,则需要实现自动形状同步、显隐、热点和可靠交接的独立光标通道。现有受控原型尚不满足这一要求,不能用普通本地箭头叠加冒充完成。
|
||||
65
docs/kylin-quality-20261104.md
Normal 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)。
|
||||
@ -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" "$@"
|
||||
|
||||