From e47b01beec14500a71e8ab8f0cf2995308d0ecf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E4=B8=80=E9=B8=A3?= <1000000008@qq.com> Date: Mon, 2 Nov 2026 16:40:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=202K=20=E4=B8=B2=E6=B5=81=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E6=8C=81=E4=B9=85=E5=8C=96=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更详情:真2K串流与重启持久化,优化客户端渲染和帧率配置 三端统一2560x1440,盒子使用297MHz HDMI时序并保留1080p回退;Windows脚本动态定位VDD恢复2K。串流默认2K20/30Mbps,使用GLES2,设置页支持20/24FPS及自定义值。保持Windows正常光标,记录NVENC阶段性实测和GPU重启后Code43未恢复的限制。 --- MEMORY.md | 16 +++- docs/moonlight-kiosk-20261028.md | 42 ++++++++-- terminal/kiosk-shell/etc/moonlight.conf | 16 ++-- terminal/kiosk-shell/scripts/vdi-kiosk-start | 28 +++++++ terminal/kiosk-shell/src/desktop_session.h | 12 +-- .../src/desktop_settings_dialog.cpp | 8 +- terminal/kiosk-shell/src/desktop_store.cpp | 12 +-- .../kiosk-shell/src/moonlight_session.cpp | 11 ++- terminal/kiosk-shell/vdi-kiosk.desktop | 2 +- tools/deploy_kiosk.sh | 6 +- tools/set-2k-display.ps1 | 81 +++++++++++++++++++ 11 files changed, 198 insertions(+), 36 deletions(-) create mode 100755 terminal/kiosk-shell/scripts/vdi-kiosk-start create mode 100644 tools/set-2k-display.ps1 diff --git a/MEMORY.md b/MEMORY.md index 96ffc2a..e346314 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -2,6 +2,20 @@ 完整对话总结在:`D:\DAIMA\vdi-cloud\PROJECT_CONTEXT.md` +## 2026-11-02 最新实测(优先于下文旧状态) +- 已部署真实2K:盒子HDMI 2560x1440约60Hz、Windows VDD 2560x1440@30、Moonlight 2560x1440@20/30Mbps。 +- RK3528旧PHY拒绝EDID的241.70MHz,实测用297MHz固定时钟和扩展消隐成功: + `297.00 2560 2608 2640 2720 1440 1443 1448 1820 +HSync -VSync`;盒子重启已验证,link Good。 +- Windows VDD版本11.30.4.434,生效XML在 `C:\VirtualDisplayDriver\vdd_settings.xml`。 + 登录任务VDISet2KDisplay动态定位Virtual Display Driver(不能写死DISPLAY编号),脚本在 + `C:\ProgramData\VDI\set-2k-display.ps1`,仓库副本 `tools/set-2k-display.ps1`;VM重启后执行成功。 +- 鼠标保留白色Windows远程光标,已删除本地黑色光标自动开启和透明光标任务;GLES2、关闭MATE合成器保留。 +- **GPU并非稳定完成**:运维曾打通582.78/NVENC,连续动画可发送59FPS;但本次VM重启后Code43复发, + nvidia-smi失败、Sunshine当前实际为libx264软件编码。客户机重启设备/隔离VDD/VM停启均无效; + 无效VDIRecoverNvidia任务已删除。未改宿主VFIO模块、未重启宿主。ROM为gtx965m-pci.rom。 +- NVENC正常时2K20实测19.67FPS、网络丢帧0%、抖动丢帧0.94%;这些数据不能作为当前软编性能保证。 +- 详细最新状态见 `docs/moonlight-kiosk-20261028.md` 的“当前现场(2026-11-02 15:45)”。 + ## 当前状态 - 项目目录:`D:\DAIMA\vdi-cloud` - 终端盒子:RK3528 PRO DDR4,Armbian Ubuntu Noble,aarch64,IP `192.168.1.188` @@ -26,7 +40,7 @@ - **当前 VM 100 未挂 GPU**:另一并行会话在改配置(hv_vendor_id 方案),本会话已停手避撞, 两个会话不要同时改 VM 100 配置! - Sunshine 配对 2026.914 改为"Web UI 批准式":`moonlight pair --pin NNNN` + - `GET /api/csrf-token` + `POST /api/pin {pairing_id,pin}`;Web 凭据 vdiadmin/VdiPass2026; + `GET /api/csrf-token` + `POST /api/pin {pairing_id,pin}`;Web 凭据由环境管理员保管; 删 sunshine_state.json = 全部失配;盒子 Moonlight 配置目录缓存旧 uniqueid 会卡死配对 ## 必须遵守 diff --git a/docs/moonlight-kiosk-20261028.md b/docs/moonlight-kiosk-20261028.md index ed95309..8b3c248 100644 --- a/docs/moonlight-kiosk-20261028.md +++ b/docs/moonlight-kiosk-20261028.md @@ -290,7 +290,7 @@ Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器 `GET /api/csrf-token`(Basic Auth)→ `POST /api/pin` body `{"pairing_id":"","pin":"4321"}`, 头带 `X-CSRF-Token` + Basic Auth(无 /api/login;POST 必须 CSRF 头)。 -- Web 凭据已重置为 `vdiadmin / VdiPass2026`(写在本页,勿提交正式环境)。 +- Sunshine Web 管理凭据由环境管理员保管,不在仓库记录明文密码。 - **旧版缓存 uniqueid 会让 pair 卡 "Found unexpected PC" 死循环**: 盒子 `~/.config/Moonlight Game Streaming Project/` 整目录删除后重配即可。 - Sunshine 重启不再换 uniqueid(持久化已恢复);**若再出现"每次重启换 uniqueid", @@ -298,14 +298,40 @@ Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器 - NVIDIA 设备半坏时 Sunshine 捕获初始化会崩循环(服务恢复 5-6 分钟拉起一次), 表象是"每几分钟换一次 UUID + 配对全部失败"。根因仍是驱动 D3D 问题。 -## 当前现场(2026-10-30 14:50) +## 2026-11-02:GPU/NVENC与客户端渲染阶段性实测(最终状态见下节) + +- GTX 965M 直通、582.78 驱动、DXGI/D3D 和 NVENC 已打通;Sunshine 实际使用 + `h264_nvenc`,连续 60Hz 动画下服务端发送约 59 FPS、网络丢帧 0%、服务端处理约 4.5ms。 +- Virtual Display Driver 已从 8.35.11.365 升级到官方签名的 11.30.4.434; + 保持 1920x1080@60,并绑定 `NVIDIA GeForce GTX 965M`。 +- 鼠标最终采用 Windows 远程光标方案:白色 Windows 光标在 NVENC/30FPS 稳定链路下已不卡顿; + 已删除 kiosk 自动开启盒子本地黑色光标的逻辑,并移除 Windows 透明光标登录任务、恢复系统光标, + 避免黑色光标带动白色光标的双光标现象。 +- 客户端瓶颈实测定位在 RK3528 的 Xorg/MATE 合成和 SDL OpenGL 呈现:关闭 MATE + compositor 后,1080p60 连续动画渲染从 16.3 FPS 提到 28.8 FPS;强制 + `SDL_RENDER_DRIVER=opengles2` 后提高到 37.4 FPS。 +- 900p60 约 43.4 FPS、720p60 约 45.4 FPS,画质损失大但仍不到60;最终产品默认采用 + **1920x1080@30**:实测输入30.04、解码30.04、渲染29.90 FPS,网络丢帧0%、 + 抖动丢帧0.46%、客户端渲染4.69ms,体感比“名义60实际持续丢帧”稳定。 +- 新增 `vdi-kiosk-start`,开机固定盒子输出1920x1080@60后启动受管 kiosk; + `vdi-kiosk.desktop` 已改走该入口。保留 VSync/Frame Pacing(关闭无收益)。 + +## 当前现场(2026-11-02 15:45,优先于上面的历史实验结论) + +- 真实2K三段已验证:Windows MttVDD 2560x1440@30、Moonlight 2560x1440@20(30000 Kbps)、盒子 HDMI 2560x1440@60。1920x1440不是本项目验收所说的2K。 +- 显示器 EDID 首选时序为2560x1440、241.70MHz;当前RK3528 vendor内核拒绝此时钟。改用已支持的297MHz和加长垂直消隐后成功: + `297.00 2560 2608 2640 2720 1440 1443 1448 1820 +HSync -VSync`,实际约59.994Hz,link-status Good。 + 此模式不是显示器原厂时序,已在现场显示器测试;其他显示器必须重新验证,启动器失败回退1080p,不能把回退结果报成2K。 +- `vdi-kiosk-start` 开机建立上述模式并启动单一kiosk链;盒子真实重启后仍为2560x1440,单链自启验收通过。保留GLES2和关闭MATE合成器。 +- VDD配置路径经25.7.23源码核查为 `C:\VirtualDisplayDriver\vdd_settings.xml`,包含1080p60和1440p30/60;旧安装目录的XML不应再作为生效证据。 +- Windows计划任务 `VDISet2KDisplay` 以已登录test用户的交互令牌运行,执行 `C:\ProgramData\VDI\set-2k-display.ps1`。脚本动态查找活动的Virtual Display Driver,不能写死DISPLAY编号,也不能在Guest Agent的Session 0里切换交互桌面。 + 对应源码归档为 `tools/set-2k-display.ps1`;重启VM后日志 `set-2k-result.txt` 返回result=0、mode=2560x1440@30。Windows 125% DPI下逻辑坐标2048x1152不代表物理像素不足2K。 +- NVIDIA正常期间的连续动画测试:2K30输入29.92/渲染21.52 FPS、抖动丢帧27.75%;2K24渲染22.15、丢帧8.14%;2K20输入20.31/渲染19.67、网络丢帧0%、抖动丢帧0.94%。所以现场选择2K20,未达到2K30/60满帧。 +- **重启验收发现GPU回归故障,当前NVENC未恢复**:驱动582.78、ROM `gtx965m-pci.rom`、Proxmox hostpci0配置仍保留;VM重启后Code43。设备重启可暂时令PnP显示OK,但NVML仍报Not Found,Sunshine实际使用 `libx264 [software]`。 + 客户机内重复重启设备、暂时停用VDD/Sunshine、VM完整关机再启动均未恢复;之前NVENC成功及2K20性能数据不能作为当前性能保证。 +- 已删除无效的 `VDIRecoverNvidia` 自动恢复任务和脚本。宿主GPU只有bus reset,`vfio_pci.disable_idle_d3=Y`,VM关机后仍D0;这些证据不足以断言唯一根因,未修改运维的VFIO模块/ROM配置,也未重启宿主机或影响VM101/102。 +- 当前盒子可正常进入2K20 Windows桌面,白色Windows光标保留、本地黑色光标自动切换已删除;GPU恢复需要与运维协同,验收必须检查nvidia-smi、DXGI和Sunshine实际编码器,不能只看设备管理器。 -- VM 100:**GPU 未挂载**(另一并行会话用 `hv_vendor_id=NVidiaFIX` 方案接手了配置, - 为避免两个会话互相覆盖,本会话已停止改 VM 配置);VM 内 566.36 驱动已装好待用。 -- 盒子:kiosk 单链已恢复、配对已修复、串流实测 ~23 FPS(软编基线)可正常进出。 -- 宿主机 /root 下留有 nv582.78.exe、DisplayDriver.zip、DD566.zip、gtx965m.rom; - 8018 端口有临时 HTTP 服务(http8018svc)供 VM 拉文件,用完可 - `systemctl stop http8018svc`。 --- diff --git a/terminal/kiosk-shell/etc/moonlight.conf b/terminal/kiosk-shell/etc/moonlight.conf index baf0cb3..a10f5f6 100644 --- a/terminal/kiosk-shell/etc/moonlight.conf +++ b/terminal/kiosk-shell/etc/moonlight.conf @@ -5,7 +5,7 @@ # # host/port Sunshine 服务端地址与端口(当前联调环境为 VM 100) # app 应用名。用名称而不是 app id:Sunshine 重装或升级后 app id 会变。 -# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 1920x1080@60Hz) +# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 2560x1440@30Hz) # fps 目标帧率 # bitrate 码率,单位 Kbps # codec 视频编码:H.264 / HEVC / AV1 / auto @@ -13,19 +13,19 @@ # RK3528 当前没有可用的 V4L2 request/m2m 设备,auto 会选到 # h264_v4l2m2m 并报 "Could not find a valid device",故用 software。 # vsync 1=开启垂直同步(--vsync),0=关闭(--no-vsync) -# 2026-10-29 A/B 实测:关闭 vsync/frame-pacing 对队列与渲染延迟 -# 无改善(队列 47.2ms vs 基线 44.1ms),延迟瓶颈是服务端软编码 -# 帧率(~21.6FPS,每帧约46ms),保持开启以获得更平滑的画面。 +# 同步开关 A/B 未见明确延迟收益,保留开启以避免撕裂。 +# 连续动画确认服务端可发送约59FPS;当前2K软件呈现选择20FPS档, +# NVENC重启故障及各档性能记录见 docs/moonlight-kiosk-20261028.md。 # frame-pacing 1=开启帧率匀速(--frame-pacing),0=关闭(--no-frame-pacing) # performance-overlay 1=显示性能叠图(调试用) # hang-timeout 串流假死判定时间(秒),0 表示关闭该检测 host=192.168.1.70 port=47984 app=Desktop -width=1920 -height=1080 -fps=60 -bitrate=20000 +width=2560 +height=1440 +fps=20 +bitrate=30000 codec=H.264 decoder=software display-mode=fullscreen diff --git a/terminal/kiosk-shell/scripts/vdi-kiosk-start b/terminal/kiosk-shell/scripts/vdi-kiosk-start new file mode 100755 index 0000000..4b46850 --- /dev/null +++ b/terminal/kiosk-shell/scripts/vdi-kiosk-start @@ -0,0 +1,28 @@ +#!/bin/sh + +export DISPLAY="${DISPLAY:-:0}" +export XAUTHORITY="${XAUTHORITY:-/home/armbian/.Xauthority}" + +# Match the local output to the 2K Moonlight stream. +# This kernel rejects the monitor's 241.70 MHz timing but accepts the 297 MHz +# PLL rate. Longer vertical blanking produces 2560x1440 at approximately 60 Hz. +# HDMI can appear a few seconds after the desktop autostart entry runs. +: >/tmp/vdi-display-mode.log +mode="2560x1440_60_fixed297" +for _ in $(seq 1 20); do + if xrandr --query 2>>/tmp/vdi-display-mode.log | grep -q '^HDMI-1 connected'; then + xrandr --newmode "$mode" 297.00 \ + 2560 2608 2640 2720 1440 1443 1448 1820 \ + +HSync -VSync >>/tmp/vdi-display-mode.log 2>&1 || true + xrandr --addmode HDMI-1 "$mode" >>/tmp/vdi-display-mode.log 2>&1 || true + if xrandr --output HDMI-1 --mode "$mode" >>/tmp/vdi-display-mode.log 2>&1; then + break + fi + xrandr --output HDMI-1 --mode 1920x1080 --rate 60 \ + >>/tmp/vdi-display-mode.log 2>&1 || true + fi + sleep 1 +done + +exec /home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- \ + /home/armbian/vdi-terminal/bin/kiosk-shell diff --git a/terminal/kiosk-shell/src/desktop_session.h b/terminal/kiosk-shell/src/desktop_session.h index 7b5bc2c..0cfb495 100644 --- a/terminal/kiosk-shell/src/desktop_session.h +++ b/terminal/kiosk-shell/src/desktop_session.h @@ -30,9 +30,9 @@ struct DesktopInfo { // 连接参数(kUnassigned 时为空) QString endpoint; // 主机地址(IP / 域名 / VPN 隧道地址) QString app; // Sunshine 应用名 - int width = 1920; - int height = 1080; - int fps = 60; + int width = 2560; + int height = 1440; + int fps = 20; }; // 一次会话的连接凭据;当前为本地构造,后续由平台 connect-ticket 接口下发 @@ -40,9 +40,9 @@ struct ConnectionTicket { QString desktopId; QString endpoint; QString app; - int width = 1920; - int height = 1080; - int fps = 60; + int width = 2560; + int height = 1440; + int fps = 20; }; struct AuthResult { diff --git a/terminal/kiosk-shell/src/desktop_settings_dialog.cpp b/terminal/kiosk-shell/src/desktop_settings_dialog.cpp index bb53357..6185615 100644 --- a/terminal/kiosk-shell/src/desktop_settings_dialog.cpp +++ b/terminal/kiosk-shell/src/desktop_settings_dialog.cpp @@ -63,6 +63,8 @@ DesktopSettingsDialog::DesktopSettingsDialog(std::vector desktops, comboFps_ = new QComboBox(this); comboFps_->addItem(QStringLiteral("60"), 60); comboFps_->addItem(QStringLiteral("30"), 30); + comboFps_->addItem(QStringLiteral("24"), 24); + comboFps_->addItem(QStringLiteral("20"), 20); form->addRow(QStringLiteral("帧率"), comboFps_); root->addLayout(form); @@ -109,7 +111,11 @@ void DesktopSettingsDialog::loadCurrent() { QStringLiteral("%1x%2").arg(d.width).arg(d.height)); comboResolution_->setCurrentIndex(resIndex >= 0 ? resIndex : 0); int fpsIndex = comboFps_->findData(d.fps); - comboFps_->setCurrentIndex(fpsIndex >= 0 ? fpsIndex : 0); + if (fpsIndex < 0) { + comboFps_->addItem(QString::number(d.fps), d.fps); + fpsIndex = comboFps_->count() - 1; + } + comboFps_->setCurrentIndex(fpsIndex); loading_ = false; break; } diff --git a/terminal/kiosk-shell/src/desktop_store.cpp b/terminal/kiosk-shell/src/desktop_store.cpp index e53c830..335187a 100644 --- a/terminal/kiosk-shell/src/desktop_store.cpp +++ b/terminal/kiosk-shell/src/desktop_store.cpp @@ -23,9 +23,9 @@ std::vector defaultDesktops(const QString& migratedServer) { win.os = QStringLiteral("Windows 桌面"); win.endpoint = migratedServer; // 兼容老盒子:迁移 client.conf 的 server win.app = QStringLiteral("Desktop"); - win.width = 1920; - win.height = 1080; - win.fps = 60; + win.width = 2560; + win.height = 1440; + win.fps = 20; list.push_back(win); DesktopInfo ubuntu; @@ -50,9 +50,9 @@ DesktopInfo fromJson(const QJsonObject& obj) { d.os = obj.value(QStringLiteral("os")).toString(); d.endpoint = obj.value(QStringLiteral("endpoint")).toString(); d.app = obj.value(QStringLiteral("app")).toString(); - d.width = obj.value(QStringLiteral("width")).toInt(1920); - d.height = obj.value(QStringLiteral("height")).toInt(1080); - d.fps = obj.value(QStringLiteral("fps")).toInt(60); + d.width = obj.value(QStringLiteral("width")).toInt(2560); + d.height = obj.value(QStringLiteral("height")).toInt(1440); + d.fps = obj.value(QStringLiteral("fps")).toInt(20); d.state = d.endpoint.isEmpty() ? DesktopState::kUnassigned : DesktopState::kAvailable; return d; } diff --git a/terminal/kiosk-shell/src/moonlight_session.cpp b/terminal/kiosk-shell/src/moonlight_session.cpp index 9d1c3dc..33a09f5 100644 --- a/terminal/kiosk-shell/src/moonlight_session.cpp +++ b/terminal/kiosk-shell/src/moonlight_session.cpp @@ -123,7 +123,12 @@ bool MoonlightSession::activateStreamWindow() { {QStringLiteral("search"), QStringLiteral("--onlyvisible"), QStringLiteral("--pid"), QString::number(process_->processId()), QStringLiteral("--class"), QStringLiteral("com.moonlight_stream.Moonlight")}); - if (!finder.waitForFinished(500) || finder.exitCode() != 0) { + if (!finder.waitForFinished(500)) { + finder.kill(); + finder.waitForFinished(); + return false; + } + if (finder.exitCode() != 0) { return false; } const QString windowId = QString::fromUtf8(finder.readAllStandardOutput()) @@ -154,8 +159,10 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb process_->setProgram(QStringLiteral("moonlight-qt")); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0")); + environment.insert(QStringLiteral("SDL_RENDER_DRIVER"), QStringLiteral("opengles2")); process_->setProcessEnvironment(environment); + const int bitrateKbps = ticket.width >= 2560 ? 30000 : 20000; QStringList args; args << QStringLiteral("stream") << ticket.endpoint @@ -163,7 +170,7 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb << QStringLiteral("--resolution") << QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height) << QStringLiteral("--fps") << QString::number(ticket.fps) - << QStringLiteral("--bitrate") << QString::number(20000) + << QStringLiteral("--bitrate") << QString::number(bitrateKbps) << QStringLiteral("--display-mode") << QStringLiteral("fullscreen") << QStringLiteral("--video-codec") << options_.videoCodec << QStringLiteral("--video-decoder") << options_.decoder diff --git a/terminal/kiosk-shell/vdi-kiosk.desktop b/terminal/kiosk-shell/vdi-kiosk.desktop index 876212e..de73953 100644 --- a/terminal/kiosk-shell/vdi-kiosk.desktop +++ b/terminal/kiosk-shell/vdi-kiosk.desktop @@ -2,7 +2,7 @@ Type=Application Name=VDI Cloud Desktop (Kiosk) Comment=产品唯一自启入口:看门狗守护 kiosk-shell(登录 -> 桌面列表 -> 会话) -Exec=/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- /home/armbian/vdi-terminal/bin/kiosk-shell +Exec=/home/armbian/vdi-terminal/bin/vdi-kiosk-start Terminal=false X-GNOME-Autostart-enabled=true X-GNOME-Autostart-Delay=2 diff --git a/tools/deploy_kiosk.sh b/tools/deploy_kiosk.sh index c3f355e..13192e6 100644 --- a/tools/deploy_kiosk.sh +++ b/tools/deploy_kiosk.sh @@ -19,14 +19,14 @@ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'rm -rf ~ scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/CMakeLists.txt" "$BOX":~/vdi-build/repo/kiosk-shell/ scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC"/src/*.cpp "$SRC"/src/*.h "$BOX":~/vdi-build/repo/kiosk-shell/src/ scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC"/tests/*.cpp "$BOX":~/vdi-build/repo/kiosk-shell/tests/ -scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/scripts/vdi-moonlight" "$BOX":~/vdi-terminal/bin/vdi-moonlight +scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/scripts/vdi-moonlight" "$SRC/scripts/vdi-kiosk-start" "$BOX":~/vdi-terminal/bin/ scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/etc/moonlight.conf" "$BOX":/tmp/vdi-moonlight.conf ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'mkdir -p ~/.config/vdi-terminal; test -f ~/.config/vdi-terminal/moonlight.conf || cp /tmp/vdi-moonlight.conf ~/.config/vdi-terminal/moonlight.conf; rm -f /tmp/vdi-moonlight.conf' scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/vdi-kiosk.desktop" "$BOX":~/.config/autostart/vdi-kiosk.desktop scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/vdi-moonlight.desktop" "$BOX":~/.config/autostart/vdi-moonlight.desktop echo "==> 在盒子上编译" -ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'sed -i "s/\r$//" ~/vdi-build/repo/kiosk-shell/src/*.cpp ~/vdi-build/repo/kiosk-shell/src/*.h ~/vdi-build/repo/kiosk-shell/tests/*.cpp ~/vdi-terminal/bin/vdi-moonlight; chmod +x ~/vdi-terminal/bin/vdi-moonlight; cd ~/vdi-build/repo/kiosk-shell && (cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON > /tmp/cmake.log 2>&1 || (tail -30 /tmp/cmake.log; exit 1)) && cmake --build build && QT_QPA_PLATFORM=offscreen ctest --test-dir build --output-on-failure' +ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'sed -i "s/\r$//" ~/vdi-build/repo/kiosk-shell/src/*.cpp ~/vdi-build/repo/kiosk-shell/src/*.h ~/vdi-build/repo/kiosk-shell/tests/*.cpp ~/vdi-terminal/bin/vdi-moonlight ~/vdi-terminal/bin/vdi-kiosk-start; chmod +x ~/vdi-terminal/bin/vdi-moonlight ~/vdi-terminal/bin/vdi-kiosk-start; cd ~/vdi-build/repo/kiosk-shell && (cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON > /tmp/cmake.log 2>&1 || (tail -30 /tmp/cmake.log; exit 1)) && cmake --build build && QT_QPA_PLATFORM=offscreen ctest --test-dir build --output-on-failure' echo "==> 安装二进制" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'monitor_pids=$(pgrep -f "^/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- " 2>/dev/null || true); test -z "$monitor_pids" || kill $monitor_pids 2>/dev/null || true; pair_pids=$(pgrep -f "^bash -c tail -f .*(pin|pair)_fifo" 2>/dev/null || true); test -z "$pair_pids" || kill $pair_pids 2>/dev/null || true; tail_pids=$(pgrep -f "^tail -f .*(pin|pair)_fifo" 2>/dev/null || true); test -z "$tail_pids" || kill $tail_pids 2>/dev/null || true; rm -f ~/pair_fifo /tmp/pin_fifo; for name in vdi-moonlight moonlight-qt kiosk-shell; do pids=$(pidof "$name" 2>/dev/null || true); test -z "$pids" || kill $pids 2>/dev/null || true; done; for i in $(seq 1 40); do pgrep -f "^/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- " >/dev/null 2>&1 || break; sleep 0.2; done; monitor_pids=$(pgrep -f "^/home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- " 2>/dev/null || true); test -z "$monitor_pids" || kill -9 $monitor_pids 2>/dev/null || true; cp ~/vdi-build/repo/kiosk-shell/build/kiosk-shell ~/vdi-terminal/bin/kiosk-shell && cp ~/vdi-build/repo/kiosk-shell/build/vdi-kiosk-monitor ~/vdi-terminal/bin/vdi-kiosk-monitor && chmod +x ~/vdi-terminal/bin/kiosk-shell ~/vdi-terminal/bin/vdi-kiosk-monitor && ls -la ~/vdi-terminal/bin/' @@ -35,7 +35,7 @@ echo "==> 收敛自启项(只保留 kiosk-shell 一条链)" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'mkdir -p ~/.config/autostart; rm -f ~/.config/autostart/vdi-kiosk.desktop.bak; sed -i "s/^Hidden=.*/Hidden=true/;s/^X-GNOME-Autostart-enabled=.*/X-GNOME-Autostart-enabled=false/" ~/.config/autostart/vdi-moonlight.desktop 2>/dev/null || true; grep -H . ~/.config/autostart/*.desktop' echo "==> 启动受管栈并验证" -ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'rm -f /tmp/vdi-kiosk.heartbeat; nohup env DISPLAY=:0 XAUTHORITY=/home/armbian/.Xauthority /home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- /home/armbian/vdi-terminal/bin/kiosk-shell > /tmp/vdi-monitor.log 2>&1 & sleep 8; echo "--- 进程 ---"; pgrep -af "vdi-kiosk-monitor|kiosk-shell|moonlight-qt" | grep -v pgrep; echo "--- 看门狗日志 ---"; tail -5 /tmp/vdi-monitor.log; echo "--- 心跳 ---"; cat /tmp/vdi-kiosk.heartbeat 2>/dev/null; echo' +ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'rm -f /tmp/vdi-kiosk.heartbeat; nohup env DISPLAY=:0 XAUTHORITY=/home/armbian/.Xauthority /home/armbian/vdi-terminal/bin/vdi-kiosk-start > /tmp/vdi-monitor.log 2>&1 & sleep 8; echo "--- 进程 ---"; pgrep -af "vdi-kiosk-monitor|kiosk-shell|moonlight-qt" | grep -v pgrep; echo "--- 看门狗日志 ---"; tail -5 /tmp/vdi-monitor.log; echo "--- 心跳 ---"; cat /tmp/vdi-kiosk.heartbeat 2>/dev/null; echo' echo "==> 抓屏确认当前界面(应为桌面列表)" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'DISPLAY=:0 xwd -root -silent' 2>/dev/null | python "$REPO/tools/xwd2png.py" /tmp/kiosk-login.png && echo "captured /tmp/kiosk-login.png" diff --git a/tools/set-2k-display.ps1 b/tools/set-2k-display.ps1 new file mode 100644 index 0000000..d965154 --- /dev/null +++ b/tools/set-2k-display.ps1 @@ -0,0 +1,81 @@ +$ErrorActionPreference = 'Stop' +Add-Type @' +using System; +using System.Runtime.InteropServices; +public static class VdiDisplayMode { + [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; + } + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + static extern bool EnumDisplayDevices(string name, uint index, ref Device dev, 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 window, uint flags, IntPtr param); + + public static string Find() { + for (uint i = 0; ; i++) { + var d = new Device(); + d.cb = Marshal.SizeOf(typeof(Device)); + if (!EnumDisplayDevices(null, i, ref d, 0)) return null; + if ((d.Flags & 1) != 0 && d.Description.IndexOf("Virtual Display Driver", StringComparison.OrdinalIgnoreCase) >= 0) + return d.Name; + } + } + public static string Current(string name) { + var m = new Mode(); + m.size = (short)Marshal.SizeOf(typeof(Mode)); + if (!EnumDisplaySettings(name, -1, ref m)) return "unavailable"; + return m.width + "x" + m.height + "@" + m.hz; + } + public static int Apply(string name) { + 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) { + m.fields = 0x80000 | 0x100000 | 0x400000; + int test = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 2, IntPtr.Zero); + if (test != 0) return test; + return ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 1, IntPtr.Zero); + } + } + } +} +'@ +$log = 'C:/ProgramData/VDI/set-2k-result.txt' +try { + for ($i = 0; $i -lt 12; $i++) { + $dev = [VdiDisplayMode]::Find() + if ($dev) { + $result = [VdiDisplayMode]::Apply($dev) + $mode = [VdiDisplayMode]::Current($dev) + if ($result -eq 0 -and $mode -eq '2560x1440@30') { + "$(Get-Date -Format o) device=$dev result=0 mode=$mode" | Set-Content $log + exit 0 + } + } + Start-Sleep 5 + } + throw "2K mode not applied: device=$dev result=$result mode=$mode" +} catch { + "$(Get-Date -Format o) ERROR $_" | Set-Content $log + exit 1 +}