验证:补充2K刷新率与单显示器验收
This commit is contained in:
1 parent
a98d9ddeee
commit
b29b59c62f
21 files changed
+1195
-4
No files matched your search
@@ -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,不能称零延迟。
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -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␍
|
||||||
|
␍
|
||||||
|
␍
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
@@ -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␍
|
||||||
|
␍
|
||||||
|
␍
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
@@ -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'
|
$ErrorActionPreference = 'Stop'
|
||||||
Add-Type @'
|
Add-Type @'
|
||||||
using System;
|
using System;
|
||||||
@@ -45,12 +46,12 @@ public static class VdiDisplayMode {
|
|||||||
if (!EnumDisplaySettings(name, -1, ref m)) return "unavailable";
|
if (!EnumDisplaySettings(name, -1, ref m)) return "unavailable";
|
||||||
return m.width + "x" + m.height + "@" + m.hz;
|
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++) {
|
for (int i = 0; ; i++) {
|
||||||
var m = new Mode();
|
var m = new Mode();
|
||||||
m.size = (short)Marshal.SizeOf(typeof(Mode));
|
m.size = (short)Marshal.SizeOf(typeof(Mode));
|
||||||
if (!EnumDisplaySettings(name, i, ref m)) return -99;
|
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;
|
m.fields = 0x80000 | 0x100000 | 0x400000;
|
||||||
int test = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 2, IntPtr.Zero);
|
int test = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 2, IntPtr.Zero);
|
||||||
if (test != 0) return test;
|
if (test != 0) return test;
|
||||||
@@ -61,13 +62,18 @@ public static class VdiDisplayMode {
|
|||||||
}
|
}
|
||||||
'@
|
'@
|
||||||
$log = 'C:/ProgramData/VDI/set-2k-result.txt'
|
$log = 'C:/ProgramData/VDI/set-2k-result.txt'
|
||||||
|
$expectedMode = "2560x1440@$RefreshRate"
|
||||||
try {
|
try {
|
||||||
for ($i = 0; $i -lt 12; $i++) {
|
for ($i = 0; $i -lt 12; $i++) {
|
||||||
$dev = [VdiDisplayMode]::Find()
|
$dev = [VdiDisplayMode]::Find()
|
||||||
if ($dev) {
|
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)
|
$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
|
"$(Get-Date -Format o) device=$dev result=0 mode=$mode" | Set-Content $log
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user