功能:支持真实 2K 串流与重启持久化并优化渲染
变更详情:真2K串流与重启持久化,优化客户端渲染和帧率配置 三端统一2560x1440,盒子使用297MHz HDMI时序并保留1080p回退;Windows脚本动态定位VDD恢复2K。串流默认2K20/30Mbps,使用GLES2,设置页支持20/24FPS及自定义值。保持Windows正常光标,记录NVENC阶段性实测和GPU重启后Code43未恢复的限制。
This commit is contained in:
parent
4f9565a768
commit
e47b01beec
16
MEMORY.md
16
MEMORY.md
@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
完整对话总结在:`D:\DAIMA\vdi-cloud\PROJECT_CONTEXT.md`
|
完整对话总结在:`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`
|
- 项目目录:`D:\DAIMA\vdi-cloud`
|
||||||
- 终端盒子:RK3528 PRO DDR4,Armbian Ubuntu Noble,aarch64,IP `192.168.1.188`
|
- 终端盒子:RK3528 PRO DDR4,Armbian Ubuntu Noble,aarch64,IP `192.168.1.188`
|
||||||
@ -26,7 +40,7 @@
|
|||||||
- **当前 VM 100 未挂 GPU**:另一并行会话在改配置(hv_vendor_id 方案),本会话已停手避撞,
|
- **当前 VM 100 未挂 GPU**:另一并行会话在改配置(hv_vendor_id 方案),本会话已停手避撞,
|
||||||
两个会话不要同时改 VM 100 配置!
|
两个会话不要同时改 VM 100 配置!
|
||||||
- Sunshine 配对 2026.914 改为"Web UI 批准式":`moonlight pair --pin NNNN` +
|
- 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 会卡死配对
|
删 sunshine_state.json = 全部失配;盒子 Moonlight 配置目录缓存旧 uniqueid 会卡死配对
|
||||||
|
|
||||||
## 必须遵守
|
## 必须遵守
|
||||||
|
|||||||
@ -290,7 +290,7 @@ Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器
|
|||||||
`GET /api/csrf-token`(Basic Auth)→
|
`GET /api/csrf-token`(Basic Auth)→
|
||||||
`POST /api/pin` body `{"pairing_id":"<GET /api/pin 里拿>","pin":"4321"}`,
|
`POST /api/pin` body `{"pairing_id":"<GET /api/pin 里拿>","pin":"4321"}`,
|
||||||
头带 `X-CSRF-Token` + Basic Auth(无 /api/login;POST 必须 CSRF 头)。
|
头带 `X-CSRF-Token` + Basic Auth(无 /api/login;POST 必须 CSRF 头)。
|
||||||
- Web 凭据已重置为 `vdiadmin / VdiPass2026`(写在本页,勿提交正式环境)。
|
- Sunshine Web 管理凭据由环境管理员保管,不在仓库记录明文密码。
|
||||||
- **旧版缓存 uniqueid 会让 pair 卡 "Found unexpected PC" 死循环**:
|
- **旧版缓存 uniqueid 会让 pair 卡 "Found unexpected PC" 死循环**:
|
||||||
盒子 `~/.config/Moonlight Game Streaming Project/` 整目录删除后重配即可。
|
盒子 `~/.config/Moonlight Game Streaming Project/` 整目录删除后重配即可。
|
||||||
- Sunshine 重启不再换 uniqueid(持久化已恢复);**若再出现"每次重启换 uniqueid",
|
- Sunshine 重启不再换 uniqueid(持久化已恢复);**若再出现"每次重启换 uniqueid",
|
||||||
@ -298,14 +298,40 @@ Desktop Duplication 的脏帧节奏,叠加 QEMU 显示栈),不是编码器
|
|||||||
- NVIDIA 设备半坏时 Sunshine 捕获初始化会崩循环(服务恢复 5-6 分钟拉起一次),
|
- NVIDIA 设备半坏时 Sunshine 捕获初始化会崩循环(服务恢复 5-6 分钟拉起一次),
|
||||||
表象是"每几分钟换一次 UUID + 配对全部失败"。根因仍是驱动 D3D 问题。
|
表象是"每几分钟换一次 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`。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# host/port Sunshine 服务端地址与端口(当前联调环境为 VM 100)
|
# host/port Sunshine 服务端地址与端口(当前联调环境为 VM 100)
|
||||||
# app 应用名。用名称而不是 app id:Sunshine 重装或升级后 app id 会变。
|
# app 应用名。用名称而不是 app id:Sunshine 重装或升级后 app id 会变。
|
||||||
# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 1920x1080@60Hz)
|
# width/height 分辨率,需与服务端显示模式匹配(当前虚拟显示器 2560x1440@30Hz)
|
||||||
# fps 目标帧率
|
# fps 目标帧率
|
||||||
# bitrate 码率,单位 Kbps
|
# bitrate 码率,单位 Kbps
|
||||||
# codec 视频编码:H.264 / HEVC / AV1 / auto
|
# codec 视频编码:H.264 / HEVC / AV1 / auto
|
||||||
@ -13,19 +13,19 @@
|
|||||||
# RK3528 当前没有可用的 V4L2 request/m2m 设备,auto 会选到
|
# RK3528 当前没有可用的 V4L2 request/m2m 设备,auto 会选到
|
||||||
# h264_v4l2m2m 并报 "Could not find a valid device",故用 software。
|
# h264_v4l2m2m 并报 "Could not find a valid device",故用 software。
|
||||||
# vsync 1=开启垂直同步(--vsync),0=关闭(--no-vsync)
|
# vsync 1=开启垂直同步(--vsync),0=关闭(--no-vsync)
|
||||||
# 2026-10-29 A/B 实测:关闭 vsync/frame-pacing 对队列与渲染延迟
|
# 同步开关 A/B 未见明确延迟收益,保留开启以避免撕裂。
|
||||||
# 无改善(队列 47.2ms vs 基线 44.1ms),延迟瓶颈是服务端软编码
|
# 连续动画确认服务端可发送约59FPS;当前2K软件呈现选择20FPS档,
|
||||||
# 帧率(~21.6FPS,每帧约46ms),保持开启以获得更平滑的画面。
|
# NVENC重启故障及各档性能记录见 docs/moonlight-kiosk-20261028.md。
|
||||||
# frame-pacing 1=开启帧率匀速(--frame-pacing),0=关闭(--no-frame-pacing)
|
# frame-pacing 1=开启帧率匀速(--frame-pacing),0=关闭(--no-frame-pacing)
|
||||||
# performance-overlay 1=显示性能叠图(调试用)
|
# performance-overlay 1=显示性能叠图(调试用)
|
||||||
# hang-timeout 串流假死判定时间(秒),0 表示关闭该检测
|
# hang-timeout 串流假死判定时间(秒),0 表示关闭该检测
|
||||||
host=192.168.1.70
|
host=192.168.1.70
|
||||||
port=47984
|
port=47984
|
||||||
app=Desktop
|
app=Desktop
|
||||||
width=1920
|
width=2560
|
||||||
height=1080
|
height=1440
|
||||||
fps=60
|
fps=20
|
||||||
bitrate=20000
|
bitrate=30000
|
||||||
codec=H.264
|
codec=H.264
|
||||||
decoder=software
|
decoder=software
|
||||||
display-mode=fullscreen
|
display-mode=fullscreen
|
||||||
|
|||||||
28
terminal/kiosk-shell/scripts/vdi-kiosk-start
Executable file
28
terminal/kiosk-shell/scripts/vdi-kiosk-start
Executable 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
|
||||||
@ -30,9 +30,9 @@ struct DesktopInfo {
|
|||||||
// 连接参数(kUnassigned 时为空)
|
// 连接参数(kUnassigned 时为空)
|
||||||
QString endpoint; // 主机地址(IP / 域名 / VPN 隧道地址)
|
QString endpoint; // 主机地址(IP / 域名 / VPN 隧道地址)
|
||||||
QString app; // Sunshine 应用名
|
QString app; // Sunshine 应用名
|
||||||
int width = 1920;
|
int width = 2560;
|
||||||
int height = 1080;
|
int height = 1440;
|
||||||
int fps = 60;
|
int fps = 20;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 一次会话的连接凭据;当前为本地构造,后续由平台 connect-ticket 接口下发
|
// 一次会话的连接凭据;当前为本地构造,后续由平台 connect-ticket 接口下发
|
||||||
@ -40,9 +40,9 @@ struct ConnectionTicket {
|
|||||||
QString desktopId;
|
QString desktopId;
|
||||||
QString endpoint;
|
QString endpoint;
|
||||||
QString app;
|
QString app;
|
||||||
int width = 1920;
|
int width = 2560;
|
||||||
int height = 1080;
|
int height = 1440;
|
||||||
int fps = 60;
|
int fps = 20;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AuthResult {
|
struct AuthResult {
|
||||||
|
|||||||
@ -63,6 +63,8 @@ DesktopSettingsDialog::DesktopSettingsDialog(std::vector<DesktopInfo> desktops,
|
|||||||
comboFps_ = new QComboBox(this);
|
comboFps_ = new QComboBox(this);
|
||||||
comboFps_->addItem(QStringLiteral("60"), 60);
|
comboFps_->addItem(QStringLiteral("60"), 60);
|
||||||
comboFps_->addItem(QStringLiteral("30"), 30);
|
comboFps_->addItem(QStringLiteral("30"), 30);
|
||||||
|
comboFps_->addItem(QStringLiteral("24"), 24);
|
||||||
|
comboFps_->addItem(QStringLiteral("20"), 20);
|
||||||
form->addRow(QStringLiteral("帧率"), comboFps_);
|
form->addRow(QStringLiteral("帧率"), comboFps_);
|
||||||
|
|
||||||
root->addLayout(form);
|
root->addLayout(form);
|
||||||
@ -109,7 +111,11 @@ void DesktopSettingsDialog::loadCurrent() {
|
|||||||
QStringLiteral("%1x%2").arg(d.width).arg(d.height));
|
QStringLiteral("%1x%2").arg(d.width).arg(d.height));
|
||||||
comboResolution_->setCurrentIndex(resIndex >= 0 ? resIndex : 0);
|
comboResolution_->setCurrentIndex(resIndex >= 0 ? resIndex : 0);
|
||||||
int fpsIndex = comboFps_->findData(d.fps);
|
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;
|
loading_ = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,9 @@ std::vector<DesktopInfo> defaultDesktops(const QString& migratedServer) {
|
|||||||
win.os = QStringLiteral("Windows 桌面");
|
win.os = QStringLiteral("Windows 桌面");
|
||||||
win.endpoint = migratedServer; // 兼容老盒子:迁移 client.conf 的 server
|
win.endpoint = migratedServer; // 兼容老盒子:迁移 client.conf 的 server
|
||||||
win.app = QStringLiteral("Desktop");
|
win.app = QStringLiteral("Desktop");
|
||||||
win.width = 1920;
|
win.width = 2560;
|
||||||
win.height = 1080;
|
win.height = 1440;
|
||||||
win.fps = 60;
|
win.fps = 20;
|
||||||
list.push_back(win);
|
list.push_back(win);
|
||||||
|
|
||||||
DesktopInfo ubuntu;
|
DesktopInfo ubuntu;
|
||||||
@ -50,9 +50,9 @@ DesktopInfo fromJson(const QJsonObject& obj) {
|
|||||||
d.os = obj.value(QStringLiteral("os")).toString();
|
d.os = obj.value(QStringLiteral("os")).toString();
|
||||||
d.endpoint = obj.value(QStringLiteral("endpoint")).toString();
|
d.endpoint = obj.value(QStringLiteral("endpoint")).toString();
|
||||||
d.app = obj.value(QStringLiteral("app")).toString();
|
d.app = obj.value(QStringLiteral("app")).toString();
|
||||||
d.width = obj.value(QStringLiteral("width")).toInt(1920);
|
d.width = obj.value(QStringLiteral("width")).toInt(2560);
|
||||||
d.height = obj.value(QStringLiteral("height")).toInt(1080);
|
d.height = obj.value(QStringLiteral("height")).toInt(1440);
|
||||||
d.fps = obj.value(QStringLiteral("fps")).toInt(60);
|
d.fps = obj.value(QStringLiteral("fps")).toInt(20);
|
||||||
d.state = d.endpoint.isEmpty() ? DesktopState::kUnassigned : DesktopState::kAvailable;
|
d.state = d.endpoint.isEmpty() ? DesktopState::kUnassigned : DesktopState::kAvailable;
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,7 +123,12 @@ bool MoonlightSession::activateStreamWindow() {
|
|||||||
{QStringLiteral("search"), QStringLiteral("--onlyvisible"),
|
{QStringLiteral("search"), QStringLiteral("--onlyvisible"),
|
||||||
QStringLiteral("--pid"), QString::number(process_->processId()),
|
QStringLiteral("--pid"), QString::number(process_->processId()),
|
||||||
QStringLiteral("--class"), QStringLiteral("com.moonlight_stream.Moonlight")});
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
const QString windowId = QString::fromUtf8(finder.readAllStandardOutput())
|
const QString windowId = QString::fromUtf8(finder.readAllStandardOutput())
|
||||||
@ -154,8 +159,10 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb
|
|||||||
process_->setProgram(QStringLiteral("moonlight-qt"));
|
process_->setProgram(QStringLiteral("moonlight-qt"));
|
||||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||||
environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0"));
|
environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0"));
|
||||||
|
environment.insert(QStringLiteral("SDL_RENDER_DRIVER"), QStringLiteral("opengles2"));
|
||||||
process_->setProcessEnvironment(environment);
|
process_->setProcessEnvironment(environment);
|
||||||
|
|
||||||
|
const int bitrateKbps = ticket.width >= 2560 ? 30000 : 20000;
|
||||||
QStringList args;
|
QStringList args;
|
||||||
args << QStringLiteral("stream")
|
args << QStringLiteral("stream")
|
||||||
<< ticket.endpoint
|
<< ticket.endpoint
|
||||||
@ -163,7 +170,7 @@ bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callb
|
|||||||
<< QStringLiteral("--resolution")
|
<< QStringLiteral("--resolution")
|
||||||
<< QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height)
|
<< QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height)
|
||||||
<< QStringLiteral("--fps") << QString::number(ticket.fps)
|
<< QStringLiteral("--fps") << QString::number(ticket.fps)
|
||||||
<< QStringLiteral("--bitrate") << QString::number(20000)
|
<< QStringLiteral("--bitrate") << QString::number(bitrateKbps)
|
||||||
<< QStringLiteral("--display-mode") << QStringLiteral("fullscreen")
|
<< QStringLiteral("--display-mode") << QStringLiteral("fullscreen")
|
||||||
<< QStringLiteral("--video-codec") << options_.videoCodec
|
<< QStringLiteral("--video-codec") << options_.videoCodec
|
||||||
<< QStringLiteral("--video-decoder") << options_.decoder
|
<< QStringLiteral("--video-decoder") << options_.decoder
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=VDI Cloud Desktop (Kiosk)
|
Name=VDI Cloud Desktop (Kiosk)
|
||||||
Comment=产品唯一自启入口:看门狗守护 kiosk-shell(登录 -> 桌面列表 -> 会话)
|
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
|
Terminal=false
|
||||||
X-GNOME-Autostart-enabled=true
|
X-GNOME-Autostart-enabled=true
|
||||||
X-GNOME-Autostart-Delay=2
|
X-GNOME-Autostart-Delay=2
|
||||||
|
|||||||
@ -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/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"/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"/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
|
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'
|
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-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
|
scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SRC/vdi-moonlight.desktop" "$BOX":~/.config/autostart/vdi-moonlight.desktop
|
||||||
|
|
||||||
echo "==> 在盒子上编译"
|
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 "==> 安装二进制"
|
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/'
|
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'
|
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 "==> 启动受管栈并验证"
|
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 "==> 抓屏确认当前界面(应为桌面列表)"
|
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"
|
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
81
tools/set-2k-display.ps1
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user