功能:支持真实 2K 串流与重启持久化并优化渲染

变更详情:真2K串流与重启持久化,优化客户端渲染和帧率配置

三端统一2560x1440,盒子使用297MHz HDMI时序并保留1080p回退;Windows脚本动态定位VDD恢复2K。串流默认2K20/30Mbps,使用GLES2,设置页支持20/24FPS及自定义值。保持Windows正常光标,记录NVENC阶段性实测和GPU重启后Code43未恢复的限制。
This commit is contained in:
宋一鸣 2026-11-02 16:40:22 +08:00
parent 4f9565a768
commit e47b01beec
11 changed files with 198 additions and 36 deletions

View File

@ -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 会卡死配对
## 必须遵守

View File

@ -290,7 +290,7 @@ Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器
`GET /api/csrf-token`(Basic Auth)→
`POST /api/pin` body `{"pairing_id":"<GET /api/pin 里拿>","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`。
---

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -63,6 +63,8 @@ DesktopSettingsDialog::DesktopSettingsDialog(std::vector<DesktopInfo> 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;
}

View File

@ -23,9 +23,9 @@ std::vector<DesktopInfo> 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;
}

View File

@ -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

View File

@ -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

View File

@ -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"

81
tools/set-2k-display.ps1 Normal file
View File

@ -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
}