验证:补充2K刷新率与单显示器验收
This commit is contained in:
parent
a98d9ddeee
commit
b29b59c62f
55
docs/2k60-validation-20261103.md
Normal file
55
docs/2k60-validation-20261103.md
Normal file
@ -0,0 +1,55 @@
|
||||
# GPU修复后刷新率与鼠标画面优化验收(2026-11-03)
|
||||
|
||||
## 结论
|
||||
|
||||
Windows虚拟屏已调整为2560×1440、60Hz,登录脚本已备份并更新;客户端默认已设为45FPS,并通过正常选择桌面入口重连。当前盒子的Xorg/EGL路径在请求60FPS时实际只能渲染约46FPS,因此不宣称稳定2K60。45FPS短测实际44.22FPS,持续测试43.14FPS;相对30FPS基线29.83FPS提高约45%~48%,排队约1.3~1.6ms。
|
||||
|
||||
## GPU复核
|
||||
|
||||
本轮实际查询NVIDIA错误码0,驱动582.78,vBIOS 84.04.2b.00.13。动态串流期间Sunshine实际创建h264_nvenc,NVIDIA有1个编码会话,检查时编码引擎约24%。运维报告的重启测试作为其提供的验收记录,本轮未自行重启VM、宿主或GPU,也未修改直通和ROM配置。
|
||||
|
||||
## 同一修正测试源对照
|
||||
|
||||
修正后的Windows测试窗口实测约59FPS,避免了原WinForms定时器将源限制到约32FPS的问题。所有测试保持2560×1440、H.264、30Mbps、RKMPP硬解;默认客户端为已有两个输入补丁的版本,第三补丁调度未启用。除同步实验外,均关闭VSync与帧步调。
|
||||
|
||||
| 档位 | 接收/渲染FPS | 主机处理平均 | 解码平均 | 帧队列平均 | 客户端队列丢弃指标 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| VDD30 / 客户端30 | 29.95 / 29.83 | 7.5ms | 6.81ms | 0.60ms | 0.41% |
|
||||
| VDD60 / 客户端60、最新帧队列 | 58.82 / 46.11 | 7.5ms | 7.37ms | 7.54ms | 21.61% |
|
||||
| VDD60 / 客户端60、原队列 | 58.81 / 46.10 | 7.6ms | 7.13ms | 36.13ms | 10.28% |
|
||||
| VDD60 / 客户端60、精确尺寸nearest实验 | 58.20 / 42.01 | 7.5ms | 6.94ms | 7.66ms | 27.77% |
|
||||
| VDD60 / 客户端60、VSync开/帧步调关 | 57.88 / 24.39 | 7.5ms | 7.26ms | 8.12ms | 57.80% |
|
||||
| VDD60 / 客户端45、最新帧队列 | 44.84 / 44.22 | 7.6ms | 8.54ms | 1.30ms | 1.38% |
|
||||
| VDD60 / 客户端45、持续测试 | 43.98 / 43.14 | 7.6ms | 7.32ms | 1.55ms | 1.91% |
|
||||
|
||||
上表“客户端队列丢弃指标”来自上游标成`Frames dropped due to network jitter`的统计,但实际字段是`pacerDroppedFrames`,本项目最新帧队列也计入其中。不能把它全部归因于网络,原队列溢出统计也并不完整;应结合接收/解码/渲染FPS及帧队列时间评估。上述会话真正的网络丢帧字段均为0%,网络延迟约2~7ms;这些是现场顺序测试,不是严格实验室基准。
|
||||
|
||||
60FPS会话采样盒子图形核心约89%~95%负载、800MHz;DDR频率已在786MHz最高公布档位,MATE合成器关闭。测试温度约62~65℃,没有通过提高频率/电压或更改CPU/GPU调速器来制造结果。原队列没有提高帧率却增加排队,nearest采样和VSync实验均未显示收益,未部署。
|
||||
|
||||
nearest实验保留在`terminal/moonlight/experiments/0004-vdi-egl-exact-size-sampling.patch`供复查,已从盒子正常源码撤回,不进入默认构建流程。正常二进制SHA256仍为`35de7b1a4ade583823b23b5f2e012307f6d0a3848c8eb97bd6d4de9660afa00f`。
|
||||
|
||||
## 配置和回退
|
||||
|
||||
- Windows实际活动VDD编号已变为DISPLAY3,工具动态查找,未写死设备号。旧DISPLAY2备份不能直接作为新设备的恢复依据,因此探测器增加`--backup-tag`区分验收批次。
|
||||
- `tools/set-2k-display.ps1`默认60Hz,支持`-RefreshRate 30`回退;已更新原VDISet2KDisplay登录任务使用的脚本,未修改任务动作或用户。
|
||||
- Windows部署前原脚本和任务XML备份:`C:\ProgramData\VDI\backups\display-refresh-20261103-142920`。部署工具按本次新生成的结果日志和任务退出码确认成功,随后独立Query确认60Hz。本轮没有重启验证登录触发。
|
||||
- `tools/run-mouse-display-probe.py Apply --hz 30 --backup-tag nvenc-20261103`可临时恢复30Hz;要保持30Hz应恢复原登录脚本,或将登录任务脚本参数设为`-RefreshRate 30`。
|
||||
- `tools/set-terminal-fps.py --fps 45 --apply`只更改盒子Windows桌面票据FPS,自动备份和原子写入,不更改其他桌面;回退用`--fps 30 --apply`。
|
||||
|
||||
部署工具前两次因Windows任务调度器LastRunTime与墙上时钟不同步而误判成功任务为失败,已自动恢复旧脚本;随后改为新结果日志证明执行新鲜度,部署成功。受配置切换影响的`nvenc45-sustained`不纳入持续测试结论,重新执行`nvenc45-sustained-final`。
|
||||
|
||||
## 持续测试和现场收尾
|
||||
|
||||
`nvenc45-sustained-final`在视频就绪后注入20秒鼠标移动和32次点击,再持续120秒,正常退出。接收43.98/渲染43.14FPS,主机平均7.6ms、解码7.32ms、排队1.55ms、呈现1.55ms,实际网络丢帧0%。这是约两分半钟的动态会话结果,不是长期稳定性保证。
|
||||
|
||||
三组Windows点击记录共256次左右交替点击,脚本校验全部符合预期,包含被排除的持续性能试验中的32次;点击正确性可核验,但其帧率数据不纳入持续性能结论。修正测试源在保存的状态中约59.4~59.8FPS,足够进行本轮对照。
|
||||
|
||||
已执行`set-terminal-fps.py --fps 45 --apply`,原30帧票据备份于盒子`~/vdi-terminal/backups/fps-20261103-143333-297521/desktops.json`。只改变Windows卡片FPS,其余桌面、分辨率和编码配置保持原值。
|
||||
|
||||
已停止并删除VDIInputLatencyBench临时任务;最终Windows查询确认`benchmark_task_present=False`。通过正常kiosk页面点击Windows连接,实际新进程PID31694带`--resolution 2560x1440 --fps 45`,RKMPP/DRM PRIME/EGL路径正常。再次查询错误码0、NVENC会话1、编码占用18%,Sunshine本次实际创建h264_nvenc。显示守护active,正常客户端二进制未换成实验版本。
|
||||
|
||||
证据目录:`docs/evidence/2k60-20261103/`,包含逐会话日志、点击记录、最终GPU/编码查询和页面截图。实际拖窗与鼠标的主观体验仍需用户现场确认;本轮自动点击和统计不能代替物理输入到屏幕测量。
|
||||
|
||||
## 后续边界
|
||||
|
||||
独立光标尚未实现,仍保留单个Windows远程白色光标。下一阶段要进一步达到稳定2K60,需要验证更低开销的显示路径及正式Weston/Wayland架构,或评估终端硬件;当前不能通过提高FPS参数或安装所谓流畅插件绕过呈现瓶颈。独立光标双方协议与单光标交接设计见`docs/independent-cursor-design-20261103.md`。本轮未测量物理输入到屏幕的P50/P95,不能称零延迟。
|
||||
199
docs/evidence/2k60-20261103/clicks-first-three.txt
Normal file
199
docs/evidence/2k60-20261103/clicks-first-three.txt
Normal file
@ -0,0 +1,199 @@
|
||||
width=2560 height=1440 paints=25330 elapsed_ms=425839 clicks=96
|
||||
|
||||
ms,x,y,target
|
||||
|
||||
42057,639,600,L
|
||||
|
||||
42058,1920,600,R
|
||||
|
||||
42093,639,600,L
|
||||
|
||||
42094,1920,600,R
|
||||
|
||||
42095,639,600,L
|
||||
|
||||
42096,1920,600,R
|
||||
|
||||
42097,639,600,L
|
||||
|
||||
42098,1920,600,R
|
||||
|
||||
42099,639,600,L
|
||||
|
||||
42100,1920,600,R
|
||||
|
||||
42101,639,600,L
|
||||
|
||||
42102,1920,600,R
|
||||
|
||||
42103,639,600,L
|
||||
|
||||
42104,1920,600,R
|
||||
|
||||
42105,639,600,L
|
||||
|
||||
42106,1920,600,R
|
||||
|
||||
42107,639,600,L
|
||||
|
||||
42141,1920,600,R
|
||||
|
||||
42142,639,600,L
|
||||
|
||||
42143,1920,600,R
|
||||
|
||||
42144,639,600,L
|
||||
|
||||
42145,1920,600,R
|
||||
|
||||
42147,639,600,L
|
||||
|
||||
42148,1920,600,R
|
||||
|
||||
42148,639,600,L
|
||||
|
||||
42149,1920,600,R
|
||||
|
||||
42150,639,600,L
|
||||
|
||||
42151,1920,600,R
|
||||
|
||||
42152,639,600,L
|
||||
|
||||
42153,1920,600,R
|
||||
|
||||
42154,639,600,L
|
||||
|
||||
42154,1920,600,R
|
||||
|
||||
112701,639,600,L
|
||||
|
||||
112702,1920,600,R
|
||||
|
||||
112704,639,600,L
|
||||
|
||||
112706,1920,600,R
|
||||
|
||||
112707,639,600,L
|
||||
|
||||
112709,1920,600,R
|
||||
|
||||
112711,639,600,L
|
||||
|
||||
112712,1920,600,R
|
||||
|
||||
112732,639,600,L
|
||||
|
||||
112733,1920,600,R
|
||||
|
||||
112735,639,600,L
|
||||
|
||||
112736,1920,600,R
|
||||
|
||||
112737,639,600,L
|
||||
|
||||
112759,1920,600,R
|
||||
|
||||
112762,639,600,L
|
||||
|
||||
112763,1920,600,R
|
||||
|
||||
112765,639,600,L
|
||||
|
||||
112766,1920,600,R
|
||||
|
||||
112768,639,600,L
|
||||
|
||||
112769,1920,600,R
|
||||
|
||||
112788,639,600,L
|
||||
|
||||
112790,1920,600,R
|
||||
|
||||
112791,639,600,L
|
||||
|
||||
112792,1920,600,R
|
||||
|
||||
112793,639,600,L
|
||||
|
||||
112815,1920,600,R
|
||||
|
||||
112818,639,600,L
|
||||
|
||||
112820,1920,600,R
|
||||
|
||||
112822,639,600,L
|
||||
|
||||
112823,1920,600,R
|
||||
|
||||
112824,639,600,L
|
||||
|
||||
112825,1920,600,R
|
||||
|
||||
302705,639,600,L
|
||||
|
||||
302706,1920,600,R
|
||||
|
||||
302707,639,600,L
|
||||
|
||||
302710,1920,600,R
|
||||
|
||||
302711,639,600,L
|
||||
|
||||
302712,1920,600,R
|
||||
|
||||
302713,639,600,L
|
||||
|
||||
302732,1920,600,R
|
||||
|
||||
302733,639,600,L
|
||||
|
||||
302734,1920,600,R
|
||||
|
||||
302735,639,600,L
|
||||
|
||||
302754,1920,600,R
|
||||
|
||||
302777,639,600,L
|
||||
|
||||
302778,1920,600,R
|
||||
|
||||
302779,639,600,L
|
||||
|
||||
302780,1920,600,R
|
||||
|
||||
302782,639,600,L
|
||||
|
||||
302783,1920,600,R
|
||||
|
||||
302784,639,600,L
|
||||
|
||||
302785,1920,600,R
|
||||
|
||||
302786,639,600,L
|
||||
|
||||
302805,1920,600,R
|
||||
|
||||
302809,639,600,L
|
||||
|
||||
302812,1920,600,R
|
||||
|
||||
302813,639,600,L
|
||||
|
||||
302814,1920,600,R
|
||||
|
||||
302815,639,600,L
|
||||
|
||||
302817,1920,600,R
|
||||
|
||||
302819,639,600,L
|
||||
|
||||
302820,1920,600,R
|
||||
|
||||
302821,639,600,L
|
||||
|
||||
302838,1920,600,R
|
||||
|
||||
{"LastTaskResult":267009}
|
||||
|
||||
|
||||
199
docs/evidence/2k60-20261103/clicks-second-series-after45.txt
Normal file
199
docs/evidence/2k60-20261103/clicks-second-series-after45.txt
Normal file
@ -0,0 +1,199 @@
|
||||
width=2560 height=1440 paints=29513 elapsed_ms=493665 clicks=96
|
||||
|
||||
ms,x,y,target
|
||||
|
||||
145117,639,600,L
|
||||
|
||||
145118,1920,600,R
|
||||
|
||||
145122,639,600,L
|
||||
|
||||
145123,1920,600,R
|
||||
|
||||
145144,639,600,L
|
||||
|
||||
145145,1920,600,R
|
||||
|
||||
145148,639,600,L
|
||||
|
||||
145150,1920,600,R
|
||||
|
||||
145151,639,600,L
|
||||
|
||||
145153,1920,600,R
|
||||
|
||||
145175,639,600,L
|
||||
|
||||
145180,1920,600,R
|
||||
|
||||
145183,639,600,L
|
||||
|
||||
145185,1920,600,R
|
||||
|
||||
145186,639,600,L
|
||||
|
||||
145188,1920,600,R
|
||||
|
||||
145190,639,600,L
|
||||
|
||||
145191,1920,600,R
|
||||
|
||||
145193,639,600,L
|
||||
|
||||
145213,1920,600,R
|
||||
|
||||
145215,639,600,L
|
||||
|
||||
145217,1920,600,R
|
||||
|
||||
145219,639,600,L
|
||||
|
||||
145220,1920,600,R
|
||||
|
||||
145221,639,600,L
|
||||
|
||||
145223,1920,600,R
|
||||
|
||||
145225,639,600,L
|
||||
|
||||
145245,1920,600,R
|
||||
|
||||
145246,639,600,L
|
||||
|
||||
145247,1920,600,R
|
||||
|
||||
145248,639,600,L
|
||||
|
||||
145250,1920,600,R
|
||||
|
||||
324992,639,600,L
|
||||
|
||||
324993,1920,600,R
|
||||
|
||||
324994,639,600,L
|
||||
|
||||
324995,1920,600,R
|
||||
|
||||
325013,639,600,L
|
||||
|
||||
325015,1920,600,R
|
||||
|
||||
325016,639,600,L
|
||||
|
||||
325017,1920,600,R
|
||||
|
||||
325017,639,600,L
|
||||
|
||||
325034,1920,600,R
|
||||
|
||||
325038,639,600,L
|
||||
|
||||
325040,1920,600,R
|
||||
|
||||
325042,639,600,L
|
||||
|
||||
325044,1920,600,R
|
||||
|
||||
325045,639,600,L
|
||||
|
||||
325047,1920,600,R
|
||||
|
||||
325065,639,600,L
|
||||
|
||||
325067,1920,600,R
|
||||
|
||||
325069,639,600,L
|
||||
|
||||
325070,1920,600,R
|
||||
|
||||
325072,639,600,L
|
||||
|
||||
325074,1920,600,R
|
||||
|
||||
325075,639,600,L
|
||||
|
||||
325092,1920,600,R
|
||||
|
||||
325095,639,600,L
|
||||
|
||||
325097,1920,600,R
|
||||
|
||||
325100,639,600,L
|
||||
|
||||
325102,1920,600,R
|
||||
|
||||
325103,639,600,L
|
||||
|
||||
325104,1920,600,R
|
||||
|
||||
325121,639,600,L
|
||||
|
||||
325123,1920,600,R
|
||||
|
||||
434053,639,600,L
|
||||
|
||||
434055,1920,600,R
|
||||
|
||||
434058,639,600,L
|
||||
|
||||
434061,1920,600,R
|
||||
|
||||
434063,639,600,L
|
||||
|
||||
434063,1920,600,R
|
||||
|
||||
434079,639,600,L
|
||||
|
||||
434081,1920,600,R
|
||||
|
||||
434084,639,600,L
|
||||
|
||||
434085,1920,600,R
|
||||
|
||||
434086,639,600,L
|
||||
|
||||
434088,1920,600,R
|
||||
|
||||
434089,639,600,L
|
||||
|
||||
434105,1920,600,R
|
||||
|
||||
434108,639,600,L
|
||||
|
||||
434109,1920,600,R
|
||||
|
||||
434110,639,600,L
|
||||
|
||||
434112,1920,600,R
|
||||
|
||||
434129,639,600,L
|
||||
|
||||
434131,1920,600,R
|
||||
|
||||
434132,639,600,L
|
||||
|
||||
434148,1920,600,R
|
||||
|
||||
434152,639,600,L
|
||||
|
||||
434155,1920,600,R
|
||||
|
||||
434157,639,600,L
|
||||
|
||||
434158,1920,600,R
|
||||
|
||||
434160,639,600,L
|
||||
|
||||
434162,1920,600,R
|
||||
|
||||
434163,639,600,L
|
||||
|
||||
434164,1920,600,R
|
||||
|
||||
434166,639,600,L
|
||||
|
||||
434167,1920,600,R
|
||||
|
||||
{"LastTaskResult":267009}
|
||||
|
||||
|
||||
135
docs/evidence/2k60-20261103/clicks-second-series-before45.txt
Normal file
135
docs/evidence/2k60-20261103/clicks-second-series-before45.txt
Normal file
@ -0,0 +1,135 @@
|
||||
width=2560 height=1440 paints=24646 elapsed_ms=412170 clicks=64
|
||||
|
||||
ms,x,y,target
|
||||
|
||||
145117,639,600,L
|
||||
|
||||
145118,1920,600,R
|
||||
|
||||
145122,639,600,L
|
||||
|
||||
145123,1920,600,R
|
||||
|
||||
145144,639,600,L
|
||||
|
||||
145145,1920,600,R
|
||||
|
||||
145148,639,600,L
|
||||
|
||||
145150,1920,600,R
|
||||
|
||||
145151,639,600,L
|
||||
|
||||
145153,1920,600,R
|
||||
|
||||
145175,639,600,L
|
||||
|
||||
145180,1920,600,R
|
||||
|
||||
145183,639,600,L
|
||||
|
||||
145185,1920,600,R
|
||||
|
||||
145186,639,600,L
|
||||
|
||||
145188,1920,600,R
|
||||
|
||||
145190,639,600,L
|
||||
|
||||
145191,1920,600,R
|
||||
|
||||
145193,639,600,L
|
||||
|
||||
145213,1920,600,R
|
||||
|
||||
145215,639,600,L
|
||||
|
||||
145217,1920,600,R
|
||||
|
||||
145219,639,600,L
|
||||
|
||||
145220,1920,600,R
|
||||
|
||||
145221,639,600,L
|
||||
|
||||
145223,1920,600,R
|
||||
|
||||
145225,639,600,L
|
||||
|
||||
145245,1920,600,R
|
||||
|
||||
145246,639,600,L
|
||||
|
||||
145247,1920,600,R
|
||||
|
||||
145248,639,600,L
|
||||
|
||||
145250,1920,600,R
|
||||
|
||||
324992,639,600,L
|
||||
|
||||
324993,1920,600,R
|
||||
|
||||
324994,639,600,L
|
||||
|
||||
324995,1920,600,R
|
||||
|
||||
325013,639,600,L
|
||||
|
||||
325015,1920,600,R
|
||||
|
||||
325016,639,600,L
|
||||
|
||||
325017,1920,600,R
|
||||
|
||||
325017,639,600,L
|
||||
|
||||
325034,1920,600,R
|
||||
|
||||
325038,639,600,L
|
||||
|
||||
325040,1920,600,R
|
||||
|
||||
325042,639,600,L
|
||||
|
||||
325044,1920,600,R
|
||||
|
||||
325045,639,600,L
|
||||
|
||||
325047,1920,600,R
|
||||
|
||||
325065,639,600,L
|
||||
|
||||
325067,1920,600,R
|
||||
|
||||
325069,639,600,L
|
||||
|
||||
325070,1920,600,R
|
||||
|
||||
325072,639,600,L
|
||||
|
||||
325074,1920,600,R
|
||||
|
||||
325075,639,600,L
|
||||
|
||||
325092,1920,600,R
|
||||
|
||||
325095,639,600,L
|
||||
|
||||
325097,1920,600,R
|
||||
|
||||
325100,639,600,L
|
||||
|
||||
325102,1920,600,R
|
||||
|
||||
325103,639,600,L
|
||||
|
||||
325104,1920,600,R
|
||||
|
||||
325121,639,600,L
|
||||
|
||||
325123,1920,600,R
|
||||
|
||||
{"LastTaskResult":267009}
|
||||
|
||||
|
||||
135
docs/evidence/2k60-20261103/clicks-sustained.txt
Normal file
135
docs/evidence/2k60-20261103/clicks-sustained.txt
Normal file
@ -0,0 +1,135 @@
|
||||
width=2560 height=1440 paints=26163 elapsed_ms=440146 clicks=64
|
||||
|
||||
ms,x,y,target
|
||||
|
||||
36986,639,600,L
|
||||
|
||||
36987,1920,600,R
|
||||
|
||||
36989,639,600,L
|
||||
|
||||
37005,1920,600,R
|
||||
|
||||
37023,639,600,L
|
||||
|
||||
37026,1920,600,R
|
||||
|
||||
37030,639,600,L
|
||||
|
||||
37031,1920,600,R
|
||||
|
||||
37033,639,600,L
|
||||
|
||||
37034,1920,600,R
|
||||
|
||||
37036,639,600,L
|
||||
|
||||
37037,1920,600,R
|
||||
|
||||
37037,639,600,L
|
||||
|
||||
37038,1920,600,R
|
||||
|
||||
37056,639,600,L
|
||||
|
||||
37058,1920,600,R
|
||||
|
||||
37060,639,600,L
|
||||
|
||||
37062,1920,600,R
|
||||
|
||||
37082,639,600,L
|
||||
|
||||
37084,1920,600,R
|
||||
|
||||
37085,639,600,L
|
||||
|
||||
37086,1920,600,R
|
||||
|
||||
37087,639,600,L
|
||||
|
||||
37088,1920,600,R
|
||||
|
||||
37106,639,600,L
|
||||
|
||||
37110,1920,600,R
|
||||
|
||||
37112,639,600,L
|
||||
|
||||
37113,1920,600,R
|
||||
|
||||
37114,639,600,L
|
||||
|
||||
37114,1920,600,R
|
||||
|
||||
37115,639,600,L
|
||||
|
||||
37116,1920,600,R
|
||||
|
||||
264310,639,600,L
|
||||
|
||||
264311,1920,600,R
|
||||
|
||||
264314,639,600,L
|
||||
|
||||
264315,1920,600,R
|
||||
|
||||
264316,639,600,L
|
||||
|
||||
264317,1920,600,R
|
||||
|
||||
264334,639,600,L
|
||||
|
||||
264337,1920,600,R
|
||||
|
||||
264338,639,600,L
|
||||
|
||||
264339,1920,600,R
|
||||
|
||||
264340,639,600,L
|
||||
|
||||
264358,1920,600,R
|
||||
|
||||
264361,639,600,L
|
||||
|
||||
264362,1920,600,R
|
||||
|
||||
264363,639,600,L
|
||||
|
||||
264364,1920,600,R
|
||||
|
||||
264368,639,600,L
|
||||
|
||||
264370,1920,600,R
|
||||
|
||||
264388,639,600,L
|
||||
|
||||
264392,1920,600,R
|
||||
|
||||
264393,639,600,L
|
||||
|
||||
264394,1920,600,R
|
||||
|
||||
264396,639,600,L
|
||||
|
||||
264415,1920,600,R
|
||||
|
||||
264418,639,600,L
|
||||
|
||||
264420,1920,600,R
|
||||
|
||||
264422,639,600,L
|
||||
|
||||
264424,1920,600,R
|
||||
|
||||
264425,639,600,L
|
||||
|
||||
264427,1920,600,R
|
||||
|
||||
264428,639,600,L
|
||||
|
||||
264429,1920,600,R
|
||||
|
||||
{"LastTaskResult":267009}
|
||||
|
||||
|
||||
BIN
docs/evidence/2k60-20261103/desktop-selection.png
Normal file
BIN
docs/evidence/2k60-20261103/desktop-selection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
11
docs/evidence/2k60-20261103/final-host.txt
Normal file
11
docs/evidence/2k60-20261103/final-host.txt
Normal file
@ -0,0 +1,11 @@
|
||||
{"Name":"NVIDIA GeForce GTX 965M","ConfigManagerErrorCode":0}
|
||||
NVIDIA GeForce GTX 965M, 84.04.2b.00.13, 1, 18 %
|
||||
|
||||
Capture size : 2560x1440
|
||||
[2026-11-03 14:34:22.850]: Info: Requested frame rate [45fps]
|
||||
[2026-11-03 14:34:22.851]: Info: Creating encoder [h264_nvenc]
|
||||
2026-11-03T14:29:21.7186993+08:00 device=\\.\DISPLAY3 result=0 mode=2560x1440@60
|
||||
benchmark_task_present=False
|
||||
|
||||
|
||||
|
||||
BIN
docs/evidence/2k60-20261103/normal-windows-45fps.png
Normal file
BIN
docs/evidence/2k60-20261103/normal-windows-45fps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 MiB |
BIN
docs/evidence/single-display-20261103/after-desktop.png
Normal file
BIN
docs/evidence/single-display-20261103/after-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
19
docs/evidence/single-display-20261103/after-health.txt
Normal file
19
docs/evidence/single-display-20261103/after-health.txt
Normal file
@ -0,0 +1,19 @@
|
||||
Stopped scoped encoder-session monitor
|
||||
[{"Name":"Microsoft Basic Display Driver","ConfigManagerErrorCode":0},{"Name":"Virtual Display Driver","ConfigManagerErrorCode":0},{"Name":"NVIDIA GeForce GTX 965M","ConfigManagerErrorCode":0},{"Name":"Microsoft Basic Display Adapter","ConfigManagerErrorCode":0}]
|
||||
NVIDIA GeForce GTX 965M, 582.78, 10 %
|
||||
|
||||
[2026-11-03 16:57:40.500]: Error: Couldn't get default audio endpoint [0x80070490]
|
||||
[2026-11-03 16:57:40.500]: Error: Unable to initialize audio capture. The stream will not have audio.
|
||||
Capture size : 2560x1440
|
||||
[2026-11-03 16:57:41.165]: Info: Desktop resolution [2560x1440]
|
||||
[2026-11-03 16:57:41.165]: Info: Display refresh rate [60Hz]
|
||||
[2026-11-03 16:57:41.166]: Info: Creating encoder [h264_nvenc]
|
||||
Capture size : 2560x1440
|
||||
[2026-11-03 17:14:13.305]: Info: Desktop resolution [2560x1440]
|
||||
[2026-11-03 17:14:13.306]: Info: Display refresh rate [60Hz]
|
||||
[2026-11-03 17:14:13.308]: Info: Creating encoder [h264_nvenc]
|
||||
probe_task_present=False
|
||||
backup_exists=True
|
||||
|
||||
|
||||
|
||||
7
docs/evidence/single-display-20261103/after-query.txt
Normal file
7
docs/evidence/single-display-20261103/after-query.txt
Normal file
@ -0,0 +1,7 @@
|
||||
[{"width":0,"height":0,"description":"Microsoft Basic Display Adapter","primary":false,"active":false,"y":0,"hz":0,"device":"\\\\.\\DISPLAY1","x":0},{"width":0,"height":0,"description":"NVIDIA GeForce GTX 965M","primary":false,"active":false,"y":0,"hz":0,"device":"\\\\.\\DISPLAY2","x":0},{"width":2560,"height":1440,"description":"Virtual Display Driver","primary":true,"active":true,"y":0,"hz":60,"device":"\\\\.\\DISPLAY4","x":0}]
|
||||
|
||||
|
||||
|
||||
task_result=0
|
||||
|
||||
|
||||
12
docs/evidence/single-display-20261103/after-terminal.txt
Normal file
12
docs/evidence/single-display-20261103/after-terminal.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Screen 0: minimum 320 x 200, current 2560 x 1440, maximum 16384 x 16384
|
||||
HDMI-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 597mm x 336mm
|
||||
1920x1440 75.00
|
||||
2048x1152 60.00
|
||||
1920x1080 100.00 60.00 60.00 50.00 59.94
|
||||
1920x1080i 60.00 50.00 59.94
|
||||
1600x1200 60.00
|
||||
1680x1050 59.88
|
||||
1600x900 60.00
|
||||
1280x1024 60.02
|
||||
46934 /home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.70 Desktop --resolution 2560x1440 --fps 45 --bitrate 30000 --display-mode fullscreen --video-codec H.264 --video-decoder hardware --absolute-mouse --keep-awake --no-vsync --no-frame-pacing --audio-on-host
|
||||
active
|
||||
7
docs/evidence/single-display-20261103/apply.txt
Normal file
7
docs/evidence/single-display-20261103/apply.txt
Normal file
@ -0,0 +1,7 @@
|
||||
[{"width":0,"height":0,"description":"Microsoft Basic Display Adapter","primary":false,"active":false,"y":0,"hz":0,"device":"\\\\.\\DISPLAY1","x":0},{"width":0,"height":0,"description":"NVIDIA GeForce GTX 965M","primary":false,"active":false,"y":0,"hz":0,"device":"\\\\.\\DISPLAY2","x":0},{"width":2560,"height":1440,"description":"Virtual Display Driver","primary":true,"active":true,"y":0,"hz":60,"device":"\\\\.\\DISPLAY4","x":0}]
|
||||
|
||||
|
||||
|
||||
task_result=0
|
||||
|
||||
|
||||
7
docs/evidence/single-display-20261103/before.txt
Normal file
7
docs/evidence/single-display-20261103/before.txt
Normal file
@ -0,0 +1,7 @@
|
||||
[{"width":2560,"height":1440,"description":"Microsoft Basic Display Adapter","primary":false,"active":true,"y":1440,"hz":64,"device":"\\\\.\\DISPLAY1","x":-1024},{"width":0,"height":0,"description":"NVIDIA GeForce GTX 965M","primary":false,"active":false,"y":0,"hz":0,"device":"\\\\.\\DISPLAY2","x":0},{"width":2560,"height":1440,"description":"Virtual Display Driver","primary":true,"active":true,"y":0,"hz":60,"device":"\\\\.\\DISPLAY4","x":0}]
|
||||
|
||||
|
||||
|
||||
task_result=0
|
||||
|
||||
|
||||
35
docs/single-display-20261103.md
Normal file
35
docs/single-display-20261103.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Windows仅保留2K主屏(2026-11-03)
|
||||
|
||||
用户条件:只有在不影响当前2K屏幕使用时才调整。已完成并读回:Windows仅一个活动桌面输出,保留Virtual Display Driver主屏2560×1440@60,盒子原Moonlight进程46934继续2K45/RKMPP/EGL,截图正常。GTX965M错误码0,Sunshine布局变化后仍创建h264_nvenc,最终编码占用10%。没有重启Windows/盒子、禁用显卡设备、卸载驱动或修改PVE/GPU直通。
|
||||
|
||||
## 调整前后
|
||||
|
||||
| 输出 | 原布局 | 现布局 |
|
||||
| --- | --- | --- |
|
||||
| DISPLAY4 / Virtual Display Driver | 主屏,2560×1440@60,坐标0,0 | 保留原模式和主屏身份,唯一活动输出 |
|
||||
| DISPLAY1 / Microsoft Basic Display Adapter | 扩展屏,2560×1440@64,坐标-1024,1440 | 从Windows桌面布局停用 |
|
||||
| DISPLAY2 / NVIDIA GTX965M直接输出 | 原本未激活 | 保持未激活;GPU本身仍正常用于硬件编码 |
|
||||
|
||||
这里的“停用”仅指显示输出不参与Windows扩展桌面。设备管理器仍保留显示适配器,Windows设置也可能列出未连接的输出,不能将此操作说成卸载了多余显卡或移除了所有显示设备条目。
|
||||
|
||||
本轮VDD编号已是DISPLAY4,早先DISPLAY3记录不能用于写死设备号。工具按唯一活动VDD动态选择,并在修改前强制核对它已是2560×1440@60主屏,不满足条件不修改。
|
||||
|
||||
## 实现与验证
|
||||
|
||||
新增`tools/set-single-vdi-display.ps1`,Query只查询;Apply先保存设备身份、模式、主屏、布局为XML,使用ChangeDisplaySettingsEx逐项暂存,最后一次提交停用其他活动输出。申请为当前用户保存布局;不添加持续守护或额外登录任务。读回若不是唯一VDD主屏或模式改变,立即尝试恢复本次原布局。已有备份拒绝覆盖。
|
||||
|
||||
新增`tools/run-single-vdi-display.py`,通过已有VM100代理,在当前已登录用户会话运行一次性任务。任务已删除,最终`probe_task_present=False`。初次Query编译因缺少System.Xml引用失败,未执行显示修改;补齐引用后查询、应用、独立再次查询均成功。
|
||||
|
||||
当前状态已经验证;未通过重启/重新登录检验持久性,以避免中断用户使用。原2K登录脚本仍动态选择VDD,未改分辨率策略。盒子原配置、显示守护和串流参数未修改。
|
||||
|
||||
复核时误使用了会持续运行的`nvidia-smi encodersessions`,读取超时后已按进程及父命令精确停止本次监视子进程,再以一次性查询确认GPU健康;没有终止Sunshine或用户应用。Sunshine日志中的16:57音频端点错误早于本次显示调整,不是本次产生的新故障。
|
||||
|
||||
## 回退和证据
|
||||
|
||||
Windows原布局备份:`C:\ProgramData\VDI\single-display-20261103\before.xml`。
|
||||
|
||||
如需恢复,使用`python tools/run-single-vdi-display.py Restore`。恢复前核对显示设备名称、ID和注册表Key,身份变更时拒绝套用旧布局;本轮没有为演示回退再次打断当前单屏状态。
|
||||
|
||||
证据目录`docs/evidence/single-display-20261103/`包含before、apply、独立after-query、显卡/编码健康查询及实际2560×1440桌面截图。
|
||||
|
||||
API依据:[Microsoft ChangeDisplaySettingsEx](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw)。
|
||||
@ -0,0 +1,49 @@
|
||||
--- a/app/streaming/video/ffmpeg-renderers/eglvid.h
|
||||
+++ b/app/streaming/video/ffmpeg-renderers/eglvid.h
|
||||
@@ -57,6 +57,8 @@
|
||||
int m_GlesMajorVersion;
|
||||
int m_GlesMinorVersion;
|
||||
bool m_HasExtUnpackSubimage;
|
||||
+ bool m_VdiNearestOneToOne;
|
||||
+ int m_VdiTextureFilter;
|
||||
|
||||
#define NV12_PARAM_YUVMAT 0
|
||||
#define NV12_PARAM_OFFSET 1
|
||||
--- a/app/streaming/video/ffmpeg-renderers/eglvid.cpp
|
||||
+++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp
|
||||
@@ -75,7 +75,9 @@
|
||||
m_eglClientWaitSync(nullptr),
|
||||
m_GlesMajorVersion(0),
|
||||
m_GlesMinorVersion(0),
|
||||
- m_HasExtUnpackSubimage(false)
|
||||
+ m_HasExtUnpackSubimage(false),
|
||||
+ m_VdiNearestOneToOne(qEnvironmentVariableIntValue("VDI_EGL_NEAREST_1TO1") == 1),
|
||||
+ m_VdiTextureFilter(GL_LINEAR)
|
||||
{
|
||||
SDL_assert(backendRenderer);
|
||||
SDL_assert(backendRenderer->canExportEGL());
|
||||
@@ -812,6 +814,24 @@
|
||||
dst.w = drawableWidth;
|
||||
dst.h = drawableHeight;
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
+
|
||||
+ // At exact pixel size no resampling is needed. Keep linear filtering for
|
||||
+ // scaled video and all other backends. Never change overlay filtering.
|
||||
+ if (m_VdiNearestOneToOne && m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||
+ const int filter = dst.w == frame->width && dst.h == frame->height ? GL_NEAREST : GL_LINEAR;
|
||||
+ if (filter != m_VdiTextureFilter) {
|
||||
+ for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
+ glActiveTexture(GL_TEXTURE0 + i);
|
||||
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, filter);
|
||||
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, filter);
|
||||
+ }
|
||||
+ m_VdiTextureFilter = filter;
|
||||
+ EGL_LOG(Info, "VDI video texture filter: %s (%dx%d -> %dx%d)",
|
||||
+ filter == GL_NEAREST ? "nearest" : "linear",
|
||||
+ frame->width, frame->height, dst.w, dst.h);
|
||||
+ }
|
||||
+ }
|
||||
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||
|
||||
glUseProgram(m_ShaderProgram);
|
||||
75
tools/deploy-vdi-display-refresh.py
Normal file
75
tools/deploy-vdi-display-refresh.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Back up and deploy the VDD login refresh script in VM100, without rebooting."""
|
||||
import argparse
|
||||
import base64
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.parse_args()
|
||||
root = Path(__file__).resolve().parent
|
||||
term = runpy.run_path(str(root / "pve_term_exec.py"))
|
||||
vm = runpy.run_path(str(root / "pve_vm.py"))
|
||||
ticket, csrf = term["login"]()
|
||||
state = vm["api"]("/api2/json/nodes/starhe/qemu/100/status/current", ticket=ticket)
|
||||
if state.get("status") != "running":
|
||||
sys.exit("VM100 is not running; nothing changed")
|
||||
payload = base64.b64encode((root / "set-2k-display.ps1").read_bytes()).decode()
|
||||
command = r"""
|
||||
$ErrorActionPreference='Stop'
|
||||
$target='C:\ProgramData\VDI\set-2k-display.ps1'
|
||||
$task='VDISet2KDisplay'
|
||||
$entry=Get-ScheduledTask -TaskName $task
|
||||
if($entry.State -eq 'Running') { throw 'Display task is already running; inspect before deployment' }
|
||||
if(-not (Test-Path -LiteralPath $target)) { throw 'Original display script is missing' }
|
||||
if(($entry.Actions | Select-Object -First 1).Arguments -notlike '*set-2k-display.ps1*') {
|
||||
throw 'Unexpected display task action; do not change it'
|
||||
}
|
||||
$backup='C:\ProgramData\VDI\backups\display-refresh-'+(Get-Date -Format 'yyyyMMdd-HHmmss')
|
||||
New-Item -ItemType Directory -Path $backup | Out-Null
|
||||
Copy-Item -LiteralPath $target -Destination "$backup\set-2k-display.ps1"
|
||||
Export-ScheduledTask -TaskName $task | Set-Content -Encoding UTF8 "$backup\VDISet2KDisplay.xml"
|
||||
if(Test-Path 'C:\ProgramData\VDI\set-2k-result.txt') {
|
||||
Copy-Item 'C:\ProgramData\VDI\set-2k-result.txt' "$backup\previous-result.txt"
|
||||
}
|
||||
$temporary=$target+'.new'
|
||||
try {
|
||||
[IO.File]::WriteAllBytes($temporary,[Convert]::FromBase64String('__PAYLOAD__'))
|
||||
Move-Item -LiteralPath $temporary -Destination $target -Force
|
||||
# Prove freshness by a newly created result file; scheduler LastRunTime can
|
||||
# lag wall-clock time after VM clock correction and is not a reliable token.
|
||||
Remove-Item -LiteralPath 'C:\ProgramData\VDI\set-2k-result.txt' -ErrorAction SilentlyContinue
|
||||
Start-ScheduledTask -TaskName $task
|
||||
Start-Sleep -Seconds 2
|
||||
for($i=0;$i -lt 20;$i++) {
|
||||
if((Get-ScheduledTask -TaskName $task).State -ne 'Running') { break }
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
$info=Get-ScheduledTaskInfo -TaskName $task
|
||||
if((Get-ScheduledTask -TaskName $task).State -eq 'Running' -or
|
||||
$info.LastTaskResult -ne 0) {
|
||||
throw 'New display task did not complete successfully'
|
||||
}
|
||||
$log=Get-Content 'C:\ProgramData\VDI\set-2k-result.txt' -Raw
|
||||
if($log -notmatch 'result=0 mode=2560x1440@60') { throw '60Hz read-back missing in display task log' }
|
||||
@{backup=$backup; task_result=$info.LastTaskResult; result=$log.Trim()} | ConvertTo-Json -Compress
|
||||
} catch {
|
||||
if(Test-Path 'C:\ProgramData\VDI\set-2k-result.txt') {
|
||||
Copy-Item 'C:\ProgramData\VDI\set-2k-result.txt' "$backup\failed-result.txt"
|
||||
Get-Content "$backup\failed-result.txt"
|
||||
}
|
||||
if($info) { $info | Select-Object LastTaskResult,LastRunTime | ConvertTo-Json -Compress }
|
||||
Stop-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
|
||||
Copy-Item -LiteralPath "$backup\set-2k-display.ps1" -Destination $target -Force
|
||||
Start-ScheduledTask -TaskName $task
|
||||
throw
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporary -ErrorAction SilentlyContinue
|
||||
}
|
||||
""".replace("__PAYLOAD__", payload)
|
||||
out, error = vm["agent"](ticket, csrf, ["powershell.exe", "-NoProfile", "-Command", command])
|
||||
print(out)
|
||||
if error or '"task_result":0' not in out:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
55
tools/run-single-vdi-display.py
Normal file
55
tools/run-single-vdi-display.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Query or detach extra Windows desktop outputs in VM100's interactive session."""
|
||||
import argparse
|
||||
import base64
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=["Query","Apply","Restore"])
|
||||
args=parser.parse_args()
|
||||
root=Path(__file__).resolve().parent
|
||||
t=runpy.run_path(str(root/"pve_term_exec.py")); v=runpy.run_path(str(root/"pve_vm.py"))
|
||||
ticket,csrf=t["login"]()
|
||||
if v["api"]("/api2/json/nodes/starhe/qemu/100/status/current",ticket=ticket).get("status")!="running":
|
||||
sys.exit("VM100 is not running; nothing changed")
|
||||
payload=base64.b64encode((root/"set-single-vdi-display.ps1").read_bytes()).decode()
|
||||
command=r"""
|
||||
$ErrorActionPreference='Stop'
|
||||
$folder='C:\ProgramData\VDI\single-display-20261103'
|
||||
$task='VDISingleDisplayProbe'
|
||||
if(Get-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue) { throw 'Task already exists' }
|
||||
$user=(Get-CimInstance Win32_ComputerSystem).UserName
|
||||
if(-not $user) { throw 'No interactive user' }
|
||||
New-Item -ItemType Directory -Force $folder | Out-Null
|
||||
$acl=Get-Acl $folder
|
||||
$acl.SetAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($user,'Modify','ContainerInherit,ObjectInherit','None','Allow')))
|
||||
Set-Acl $folder $acl
|
||||
[IO.File]::WriteAllBytes("$folder\single.ps1",[Convert]::FromBase64String('__PAYLOAD__'))
|
||||
Remove-Item "$folder\result.txt" -ErrorAction SilentlyContinue
|
||||
$run="try { & '$folder\single.ps1' -Action __ACTION__ | Out-File -Encoding utf8 -Width 500 '$folder\result.txt' } catch { `$_ | Out-String | Out-File -Encoding utf8 -Width 500 '$folder\result.txt'; exit 1 }"
|
||||
$encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($run))
|
||||
$a=New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand $encoded"
|
||||
$p=New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
|
||||
$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2)
|
||||
Register-ScheduledTask -TaskName $task -Action $a -Principal $p -Settings $s | Out-Null
|
||||
try {
|
||||
Start-ScheduledTask -TaskName $task
|
||||
Start-Sleep -Seconds 2
|
||||
for($i=0;$i-lt 30;$i++) {
|
||||
if((Get-ScheduledTask -TaskName $task).State-ne 'Running') { break }
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if((Get-ScheduledTask -TaskName $task).State-eq 'Running') { throw 'Probe timed out' }
|
||||
Get-Content "$folder\result.txt" -Raw
|
||||
'task_result='+(Get-ScheduledTaskInfo -TaskName $task).LastTaskResult
|
||||
} finally {
|
||||
Stop-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName $task -Confirm:$false
|
||||
}
|
||||
""".replace("__PAYLOAD__",payload).replace("__ACTION__",args.action)
|
||||
out,err=v["agent"](ticket,csrf,["powershell.exe","-NoProfile","-Command",command])
|
||||
print(out)
|
||||
if err or "task_result=0" not in out:
|
||||
sys.exit(err or "Display operation did not succeed")
|
||||
@ -1,3 +1,4 @@
|
||||
param([ValidateSet(30, 60)][int]$RefreshRate = 60)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type @'
|
||||
using System;
|
||||
@ -45,12 +46,12 @@ public static class VdiDisplayMode {
|
||||
if (!EnumDisplaySettings(name, -1, ref m)) return "unavailable";
|
||||
return m.width + "x" + m.height + "@" + m.hz;
|
||||
}
|
||||
public static int Apply(string name) {
|
||||
public static int Apply(string name, int refreshRate) {
|
||||
for (int i = 0; ; i++) {
|
||||
var m = new Mode();
|
||||
m.size = (short)Marshal.SizeOf(typeof(Mode));
|
||||
if (!EnumDisplaySettings(name, i, ref m)) return -99;
|
||||
if (m.width == 2560 && m.height == 1440 && m.hz == 30) {
|
||||
if (m.width == 2560 && m.height == 1440 && m.hz == refreshRate) {
|
||||
m.fields = 0x80000 | 0x100000 | 0x400000;
|
||||
int test = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 2, IntPtr.Zero);
|
||||
if (test != 0) return test;
|
||||
@ -61,13 +62,18 @@ public static class VdiDisplayMode {
|
||||
}
|
||||
'@
|
||||
$log = 'C:/ProgramData/VDI/set-2k-result.txt'
|
||||
$expectedMode = "2560x1440@$RefreshRate"
|
||||
try {
|
||||
for ($i = 0; $i -lt 12; $i++) {
|
||||
$dev = [VdiDisplayMode]::Find()
|
||||
if ($dev) {
|
||||
$result = [VdiDisplayMode]::Apply($dev)
|
||||
if ([VdiDisplayMode]::Current($dev) -eq $expectedMode) {
|
||||
"$(Get-Date -Format o) device=$dev result=0 mode=$expectedMode (already correct)" | Set-Content $log
|
||||
exit 0
|
||||
}
|
||||
$result = [VdiDisplayMode]::Apply($dev, $RefreshRate)
|
||||
$mode = [VdiDisplayMode]::Current($dev)
|
||||
if ($result -eq 0 -and $mode -eq '2560x1440@30') {
|
||||
if ($result -eq 0 -and $mode -eq $expectedMode) {
|
||||
"$(Get-Date -Format o) device=$dev result=0 mode=$mode" | Set-Content $log
|
||||
exit 0
|
||||
}
|
||||
|
||||
134
tools/set-single-vdi-display.ps1
Normal file
134
tools/set-single-vdi-display.ps1
Normal file
@ -0,0 +1,134 @@
|
||||
param(
|
||||
[ValidateSet('Query','Apply','Restore')][string]$Action='Query',
|
||||
[string]$BackupPath='C:\ProgramData\VDI\single-display-20261103\before.xml'
|
||||
)
|
||||
$ErrorActionPreference='Stop'
|
||||
# Run as the logged-in desktop user. Detach desktop outputs, never disable GPUs.
|
||||
Add-Type -ReferencedAssemblies System.Xml,System.Core,System -TypeDefinition @'
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Serialization;
|
||||
public static class VdiSingleDisplay {
|
||||
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
|
||||
public struct Device {
|
||||
public int cb;
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string Name;
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Description;
|
||||
public int Flags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Id;
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string Key;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
|
||||
public struct Mode {
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string name;
|
||||
public short spec,driver,size,extra;
|
||||
public int fields,x,y,orientation,fixedOutput;
|
||||
public short color,duplex,yres,tt,collate;
|
||||
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)] public string form;
|
||||
public short logPixels;
|
||||
public int bpp,width,height,flags,hz,icmMethod,icmIntent,media,dither,reserved1,reserved2,panWidth,panHeight;
|
||||
}
|
||||
public class Record { public Device device; public Mode mode; public bool hasMode; }
|
||||
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern bool EnumDisplayDevices(string name,uint index,ref Device device,uint flags);
|
||||
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern bool EnumDisplaySettings(string name,int index,ref Mode mode);
|
||||
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern int ChangeDisplaySettingsEx(string name,ref Mode mode,IntPtr hwnd,uint flags,IntPtr param);
|
||||
[DllImport("user32.dll",CharSet=CharSet.Unicode,EntryPoint="ChangeDisplaySettingsExW")]
|
||||
static extern int Commit(string name,IntPtr mode,IntPtr hwnd,uint flags,IntPtr param);
|
||||
public static Record[] Query() {
|
||||
var result=new List<Record>();
|
||||
for(uint i=0;;i++) {
|
||||
Device d=new Device(); d.cb=Marshal.SizeOf(typeof(Device));
|
||||
if(!EnumDisplayDevices(null,i,ref d,0)) return result.ToArray();
|
||||
if((d.Flags&8)!=0) continue;
|
||||
Mode m=new Mode(); m.size=(short)Marshal.SizeOf(typeof(Mode));
|
||||
bool ok=EnumDisplaySettings(d.Name,-1,ref m);
|
||||
result.Add(new Record {device=d,mode=m,hasMode=ok});
|
||||
}
|
||||
}
|
||||
static void Check(int code,string operation) {
|
||||
if(code!=0) throw new InvalidOperationException(operation+" failed: "+code+" (no reboot will be requested)");
|
||||
}
|
||||
public static string Target(Record[] records) {
|
||||
string found=null;
|
||||
foreach(var r in records) if((r.device.Flags&1)!=0 && r.device.Description.IndexOf("Virtual Display Driver",StringComparison.OrdinalIgnoreCase)>=0) {
|
||||
if(found!=null) throw new InvalidOperationException("Multiple active VDD outputs; refuse ambiguous selection");
|
||||
if(!r.hasMode || r.mode.width<640 || r.mode.height<480) throw new InvalidOperationException("Invalid active VDD mode");
|
||||
found=r.device.Name;
|
||||
}
|
||||
if(found==null) throw new InvalidOperationException("No active VDD; do not detach anything");
|
||||
return found;
|
||||
}
|
||||
public static void Save(Record[] records,string path) {
|
||||
using(var stream=new FileStream(path,FileMode.CreateNew,FileAccess.Write)) new XmlSerializer(typeof(Record[])).Serialize(stream,records);
|
||||
}
|
||||
public static Record[] Load(string path) {
|
||||
using(var stream=File.OpenRead(path)) return (Record[])new XmlSerializer(typeof(Record[])).Deserialize(stream);
|
||||
}
|
||||
static void Stage(Record record,bool active,bool primary) {
|
||||
Mode m=record.mode; m.size=(short)Marshal.SizeOf(typeof(Mode)); m.extra=0;
|
||||
if(active) {
|
||||
if(!record.hasMode) throw new InvalidOperationException("Missing original mode");
|
||||
m.fields=0x20|0x40000|0x80000|0x100000|0x400000;
|
||||
} else {
|
||||
m.width=m.height=0; m.fields=0x20|0x80000|0x100000;
|
||||
}
|
||||
Check(ChangeDisplaySettingsEx(record.device.Name,ref m,IntPtr.Zero,0x10000001u|(primary?0x10u:0u),IntPtr.Zero),"Stage "+record.device.Name);
|
||||
}
|
||||
public static void Apply(Record[] before,string keep) {
|
||||
foreach(var r0 in before) if(r0.device.Name==keep) {
|
||||
Record r=new Record {device=r0.device,mode=r0.mode,hasMode=r0.hasMode};
|
||||
Mode m=r.mode; m.x=m.y=0; r.mode=m; Stage(r,true,true);
|
||||
}
|
||||
foreach(var r in before) if(r.device.Name!=keep && (r.device.Flags&1)!=0) Stage(r,false,false);
|
||||
Check(Commit(null,IntPtr.Zero,IntPtr.Zero,0,IntPtr.Zero),"Commit single display");
|
||||
}
|
||||
public static void Restore(Record[] before) {
|
||||
Record[] now=Query();
|
||||
foreach(var old in before) {
|
||||
bool matched=false;
|
||||
foreach(var r in now) if(r.device.Name==old.device.Name && r.device.Id==old.device.Id && r.device.Key==old.device.Key) matched=true;
|
||||
if(!matched) throw new InvalidOperationException("Display identity changed; refuse stale restore: "+old.device.Name);
|
||||
}
|
||||
foreach(var r in before) if((r.device.Flags&4)!=0) Stage(r,true,true);
|
||||
foreach(var r in before) if((r.device.Flags&4)==0) {
|
||||
bool currentlyActive=false;
|
||||
foreach(var current in now) if(current.device.Name==r.device.Name) currentlyActive=(current.device.Flags&1)!=0;
|
||||
if((r.device.Flags&1)!=0 || currentlyActive) Stage(r,(r.device.Flags&1)!=0,false);
|
||||
}
|
||||
Check(Commit(null,IntPtr.Zero,IntPtr.Zero,0,IntPtr.Zero),"Restore topology");
|
||||
}
|
||||
}
|
||||
'@
|
||||
function Report {
|
||||
@([VdiSingleDisplay]::Query() | ForEach-Object {
|
||||
@{device=$_.device.Name; description=$_.device.Description; active=(($_.device.Flags-band 1)-ne 0);
|
||||
primary=(($_.device.Flags-band 4)-ne 0); width=$_.mode.width; height=$_.mode.height; hz=$_.mode.hz; x=$_.mode.x; y=$_.mode.y}
|
||||
}) | ConvertTo-Json -Depth 4 -Compress
|
||||
}
|
||||
if($Action -eq 'Query') { Report; exit 0 }
|
||||
if($Action -eq 'Restore') { [VdiSingleDisplay]::Restore([VdiSingleDisplay]::Load($BackupPath)); Report; exit 0 }
|
||||
$before=[VdiSingleDisplay]::Query()
|
||||
$keep=[VdiSingleDisplay]::Target($before)
|
||||
$target=@($before | Where-Object {$_.device.Name -eq $keep})[0]
|
||||
if($target.mode.width -ne 2560 -or $target.mode.height -ne 1440 -or $target.mode.hz -ne 60 -or ($target.device.Flags-band 4)-eq 0) {
|
||||
throw 'Required existing 2560x1440@60 VDD primary is absent; nothing changed'
|
||||
}
|
||||
$active=@($before | Where-Object {($_.device.Flags-band 1)-ne 0})
|
||||
if($active.Count -eq 1 -and ($active[0].device.Flags-band 4)-ne 0) { Report; exit 0 }
|
||||
New-Item -ItemType Directory -Force (Split-Path $BackupPath) | Out-Null
|
||||
[VdiSingleDisplay]::Save($before,$BackupPath)
|
||||
try {
|
||||
[VdiSingleDisplay]::Apply($before,$keep)
|
||||
Start-Sleep -Seconds 2
|
||||
$actual=@([VdiSingleDisplay]::Query() | Where-Object {($_.device.Flags-band 1)-ne 0})
|
||||
if($actual.Count -ne 1 -or $actual[0].device.Name -ne $keep -or ($actual[0].device.Flags-band 4)-eq 0) { throw 'Single VDD primary read-back failed' }
|
||||
$old=@($before | Where-Object {$_.device.Name -eq $keep})[0]
|
||||
if($actual[0].mode.width-ne $old.mode.width -or $actual[0].mode.height-ne $old.mode.height -or $actual[0].mode.hz-ne $old.mode.hz) { throw 'VDD mode changed unexpectedly' }
|
||||
Report
|
||||
} catch {
|
||||
$failure=$_
|
||||
[VdiSingleDisplay]::Restore($before)
|
||||
throw $failure
|
||||
}
|
||||
51
tools/set-terminal-fps.py
Normal file
51
tools/set-terminal-fps.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Query or change only the Windows desktop FPS in the box's local POC config."""
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--fps", type=int, choices=[30, 45, 60], required=True)
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
args = parser.parse_args()
|
||||
program = r'''
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import json, os, shutil, stat, tempfile
|
||||
path=Path.home()/'.config/vdi-terminal/desktops.json'
|
||||
original=path.read_bytes()
|
||||
data=json.loads(original)
|
||||
matches=[d for d in data['desktops'] if d.get('id')=='desk-win10-01']
|
||||
if len(matches)!=1 or matches[0].get('endpoint')!='192.168.1.70':
|
||||
raise RuntimeError('Unexpected Windows desktop identity; inspect before changing')
|
||||
desktop=matches[0]
|
||||
before=desktop.get('fps')
|
||||
backup=None
|
||||
if __APPLY__:
|
||||
backup=Path.home()/'vdi-terminal/backups'/('fps-'+datetime.now().strftime('%Y%m%d-%H%M%S-%f'))
|
||||
backup.mkdir(parents=True)
|
||||
shutil.copy2(path,backup/'desktops.json')
|
||||
desktop['fps']=__FPS__
|
||||
name=None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(dir=path.parent,prefix='.desktops-',delete=False) as stream:
|
||||
name=stream.name
|
||||
stream.write((json.dumps(data,ensure_ascii=False,indent=2)+'\n').encode())
|
||||
os.chmod(name,stat.S_IMODE(path.stat().st_mode))
|
||||
if path.read_bytes()!=original:
|
||||
raise RuntimeError('Desktop configuration changed concurrently; left it intact')
|
||||
os.replace(name,path)
|
||||
name=None
|
||||
if json.loads(path.read_bytes())!=data:
|
||||
raise RuntimeError('Desktop configuration read-back mismatch')
|
||||
finally:
|
||||
if name is not None: os.unlink(name)
|
||||
print(json.dumps({'desktop':desktop['id'],'before':before,'fps':desktop['fps'],
|
||||
'requested':__FPS__,'applied':__APPLY__,
|
||||
'backup':str(backup) if backup else None}))
|
||||
'''.replace("__FPS__", str(args.fps)).replace("__APPLY__", str(args.apply))
|
||||
result = subprocess.run(["ssh", "-o", "ConnectTimeout=8", "armbian@192.168.1.188", "python3 -"],
|
||||
input=program, capture_output=True, text=True, encoding="utf-8")
|
||||
print(result.stdout, end="")
|
||||
if result.stderr:
|
||||
print(result.stderr, end="", file=sys.stderr)
|
||||
sys.exit(result.returncode)
|
||||
Loading…
Reference in New Issue
Block a user