功能:完善2K串流、显示恢复与输入优化
This commit is contained in:
1 parent
e47b01beec
commit
938ec97cce
95 files changed
+5788
-72
No files matched your search
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# Temporary benchmark stream only. Refuses to replace an existing user stream.
|
||||
# Run after launching the scoped Windows input benchmark, with VDD mode matched.
|
||||
set -euo pipefail
|
||||
task_root="${VDI_RKMPP_BUILD_ROOT:-$HOME/vdi-build/rkmpp-experiment}"
|
||||
label="${1:?label required}"
|
||||
fps="${2:?FPS required}"
|
||||
priority="${3:-0}"
|
||||
[[ "$label" =~ ^[a-zA-Z0-9-]+$ ]] || exit 2
|
||||
[[ "$fps" = 30 || "$fps" = 60 ]] || exit 2
|
||||
[[ "$priority" = 0 || "$priority" = 1 ]] || exit 2
|
||||
exec 9>"$task_root/mouse-bench.lock"
|
||||
flock -n 9 || { echo 'Another benchmark is active'; exit 1; }
|
||||
if pgrep -f '^/home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight.* stream ' >/dev/null; then
|
||||
echo 'A stream is already active; exit it normally before benchmarking' >&2
|
||||
exit 1
|
||||
fi
|
||||
export DISPLAY=:0 XAUTHORITY="$HOME/.Xauthority"
|
||||
export LD_LIBRARY_PATH="$task_root/runtime/lib:$task_root/deps/usr/lib/aarch64-linux-gnu"
|
||||
export QML2_IMPORT_PATH="$task_root/deps/usr/lib/aarch64-linux-gnu/qt5/qml"
|
||||
export QT_PLUGIN_PATH="$task_root/deps/usr/lib/aarch64-linux-gnu/qt5/plugins"
|
||||
export H264_DECODER_HINT=h264_rkmpp HEVC_DECODER_HINT=hevc_rkmpp
|
||||
export VDI_LATEST_FRAME=1 VDI_INPUT_TIMING=1 VDI_INPUT_PRIORITY="$priority"
|
||||
export GL_IS_SLOW=0 SDL_RENDER_DRIVER=opengles2 SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR=1
|
||||
client="${VDI_RKMPP_CLIENT:-$task_root/build/moonlight/app/moonlight}"
|
||||
mkdir -p "$task_root/mouse-bench"
|
||||
log="$task_root/mouse-bench/$label.log"
|
||||
test ! -e "$log" || { echo 'Label already used; preserve existing evidence'; exit 1; }
|
||||
child=""
|
||||
cleanup() {
|
||||
if [ -n "$child" ] && kill -0 "$child" 2>/dev/null; then
|
||||
kill -TERM "$child" 2>/dev/null || true
|
||||
for ((j=0;j<5;j++)); do
|
||||
kill -0 "$child" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
kill -KILL "$child" 2>/dev/null || true
|
||||
wait "$child" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
"$client" stream 192.168.1.70 Desktop --resolution 2560x1440 --fps "$fps" \
|
||||
--bitrate 30000 --display-mode fullscreen --video-codec H.264 \
|
||||
--video-decoder hardware --absolute-mouse --keep-awake --no-vsync \
|
||||
--no-frame-pacing --audio-on-host > "$log" 2>&1 &
|
||||
child=$!
|
||||
ready=0
|
||||
for ((i=0;i<35;i++)); do
|
||||
if grep -q 'egl_overlay.frag' "$log"; then ready=1; break; fi
|
||||
kill -0 "$child" || { tail -20 "$log"; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
test "$ready" = 1 || { echo 'No video readiness'; exit 1; }
|
||||
echo "Ready: $label; injecting motion"
|
||||
python3 "$task_root/mouse-bench/inject-mouse-sweep.py"
|
||||
python3 "$task_root/mouse-bench/inject-input-burst.py"
|
||||
sleep 12
|
||||
# Do not send the exit chord into an unrelated foreground application.
|
||||
focus_pid=$(xdotool getwindowfocus getwindowpid 2>/dev/null || true)
|
||||
if [ "$focus_pid" != "$child" ]; then
|
||||
echo 'Stream lost foreground; terminate only benchmark child' >&2
|
||||
exit 1
|
||||
fi
|
||||
xdotool key ctrl+alt+shift+q
|
||||
for ((i=0;i<15;i++)); do
|
||||
kill -0 "$child" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
kill -0 "$child" 2>/dev/null && { echo 'Stream exit timed out'; exit 1; }
|
||||
wait "$child"
|
||||
child=""
|
||||
grep -E 'VDI input|VDI main-thread|frame rate|Host processing|Frames dropped|Average .*time|Average network|queue delay' "$log"
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/bin/bash
|
||||
# Run on the ARM64 terminal. All sources, libraries and output stay in TASK_ROOT.
|
||||
# No apt install, system library replacement, desktop restart or sudo is needed.
|
||||
set -euo pipefail
|
||||
TASK_ROOT="${VDI_RKMPP_BUILD_ROOT:-$HOME/vdi-build/rkmpp-experiment}"
|
||||
SDK="$TASK_ROOT/deps/usr"
|
||||
PREFIX="$TASK_ROOT/runtime"
|
||||
JOBS="${VDI_BUILD_JOBS:-2}"
|
||||
mkdir -p "$TASK_ROOT"/{src,debs,build} "$PREFIX"
|
||||
|
||||
clone_source() {
|
||||
local name="$1" ref="$2" url="$3"
|
||||
if [ ! -d "$TASK_ROOT/src/$name/.git" ]; then
|
||||
git clone --depth=1 --branch "$ref" "$url" "$TASK_ROOT/src/$name"
|
||||
fi
|
||||
test "$(git -C "$TASK_ROOT/src/$name" rev-parse HEAD)" = "$4" || {
|
||||
echo "Unexpected $name source revision; inspect before building" >&2; exit 1;
|
||||
}
|
||||
}
|
||||
clone_source mpp develop https://github.com/rockchip-linux/mpp.git 7916c3cc000fefc0bdf96636b7d54fe174724f44
|
||||
clone_source ffmpeg 6.1 https://github.com/nyanmisaka/ffmpeg-rockchip.git 797387de1c9caa9170100d7217ee093d1897f750
|
||||
clone_source moonlight v6.2.0 https://github.com/moonlight-stream/moonlight-qt.git de2467e433821664cdd2224aad8c89a625be1ad9
|
||||
git -C "$TASK_ROOT/src/moonlight" submodule update --init --recursive --depth=1
|
||||
|
||||
# Versioned VDI interaction patches. Explicit directory supports copying this
|
||||
# builder to the box without the rest of the repository. Refuse unknown drift.
|
||||
PATCH_DIR="${VDI_MOONLIGHT_PATCH_DIR:-$(cd "$(dirname "$0")/../terminal/moonlight/patches" && pwd)}"
|
||||
apply_vdi_patch() {
|
||||
local source="$1" patch="$2"
|
||||
test -r "$patch" || { echo "Missing VDI patch: $patch" >&2; exit 1; }
|
||||
if git -C "$source" apply --reverse --check "$patch" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
git -C "$source" apply --check "$patch"
|
||||
git -C "$source" apply "$patch"
|
||||
}
|
||||
apply_vdi_patch "$TASK_ROOT/src/moonlight" "$PATCH_DIR/0001-vdi-input-and-latest-frame.patch"
|
||||
apply_vdi_patch "$TASK_ROOT/src/moonlight/moonlight-common-c/moonlight-common-c" \
|
||||
"$PATCH_DIR/0002-vdi-absolute-click-barrier.patch"
|
||||
apply_vdi_patch "$TASK_ROOT/src/moonlight" "$PATCH_DIR/0003-vdi-input-priority.patch"
|
||||
|
||||
# Moonlight 6.2.0 uses std::optional in drm.h but relies on a transitive
|
||||
# include from Qt 6. Our Qt 5 build needs the standard header explicitly.
|
||||
export VDI_MOONLIGHT_SOURCE="$TASK_ROOT/src/moonlight"
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
header = Path(os.environ['VDI_MOONLIGHT_SOURCE']) / 'app/streaming/video/ffmpeg-renderers/drm.h'
|
||||
text = header.read_text()
|
||||
if '#include <optional>' not in text:
|
||||
assert '#include <set>' in text
|
||||
header.write_text(text.replace('#include <set>', '#include <optional>\n#include <set>', 1))
|
||||
PY
|
||||
|
||||
# Download only the required headers and private runtime libraries from the
|
||||
# configured distribution repository. apt does not install or upgrade anything.
|
||||
cd "$TASK_ROOT/debs"
|
||||
apt-get download libdrm-dev libopus-dev libsdl2-dev libsdl2-2.0-0 \
|
||||
libsdl2-ttf-dev libsdl2-ttf-2.0-0 libudev-dev libxkbcommon-dev \
|
||||
libegl-dev libgles-dev libgles1 libgbm-dev libwayland-dev libwayland-bin wayland-protocols \
|
||||
qtdeclarative5-dev qtdeclarative5-dev-tools qt5-qmltooling-plugins \
|
||||
qtquickcontrols2-5-dev libqt5quickcontrols2-5 libqt5quicktemplates2-5 \
|
||||
libqt5svg5-dev libqt5svg5 libqt5qmlworkerscript5 libqt5quickparticles5 \
|
||||
libqt5quickshapes5 libqt5quicktest5 libx264-dev \
|
||||
qml-module-qtquick2 qml-module-qtquick-window2 qml-module-qtquick-layouts \
|
||||
qml-module-qtqml-models2 qml-module-qtquick-controls2 qml-module-qtquick-templates2 \
|
||||
libharfbuzz-dev libfreetype-dev libpng-dev zlib1g-dev libbrotli-dev \
|
||||
libglib2.0-dev libgraphite2-dev libffi-dev
|
||||
for package in ./*.deb; do dpkg-deb -x "$package" "$TASK_ROOT/deps"; done
|
||||
|
||||
# Relocate pkg-config prefixes, preserving the system /usr and its libraries.
|
||||
export VDI_PRIVATE_SDK="$SDK"
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
sdk = Path(os.environ['VDI_PRIVATE_SDK'])
|
||||
for pc in sdk.rglob('*.pc'):
|
||||
text = pc.read_text()
|
||||
if str(sdk) not in text:
|
||||
pc.write_text(text.replace('/usr', str(sdk)))
|
||||
for pri in sdk.rglob('qt_lib_*.pri'):
|
||||
pri.write_text(pri.read_text().replace('$$QT_MODULE_LIB_BASE', str(sdk / 'lib/aarch64-linux-gnu'))
|
||||
.replace('$$QT_MODULE_INCLUDE_BASE', str(sdk / 'include/aarch64-linux-gnu/qt5'))
|
||||
.replace('$$QT_MODULE_BIN_BASE', str(sdk / 'lib/qt5/bin')))
|
||||
# A -dev symlink can target a different distro patch version than is installed.
|
||||
# Link to the existing matching SONAME; never replace the system runtime.
|
||||
for link in (sdk / 'lib/aarch64-linux-gnu').glob('*.so'):
|
||||
if link.is_symlink() and not link.exists():
|
||||
target = Path(os.readlink(link)).name
|
||||
if '.so.' not in target:
|
||||
continue
|
||||
soname = target.split('.so.')[0] + '.so.' + target.split('.so.')[1].split('.')[0]
|
||||
system_lib = Path('/lib/aarch64-linux-gnu') / soname
|
||||
if system_lib.exists():
|
||||
link.unlink()
|
||||
link.symlink_to(system_lib)
|
||||
PY
|
||||
export PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig:$SDK/lib/aarch64-linux-gnu/pkgconfig:$SDK/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
|
||||
export LD_LIBRARY_PATH="$PREFIX/lib:$SDK/lib/aarch64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export QMAKEPATH="$SDK/lib/aarch64-linux-gnu/qt5"
|
||||
export QML2_IMPORT_PATH="$SDK/lib/aarch64-linux-gnu/qt5/qml"
|
||||
export CPLUS_INCLUDE_PATH="$SDK/include/aarch64-linux-gnu:$SDK/include${CPLUS_INCLUDE_PATH:+:$CPLUS_INCLUDE_PATH}"
|
||||
export C_INCLUDE_PATH="$SDK/include/aarch64-linux-gnu:$SDK/include${C_INCLUDE_PATH:+:$C_INCLUDE_PATH}"
|
||||
|
||||
if [ "${VDI_BUILD_STAGE:-all}" != moonlight ]; then
|
||||
echo '=== build MPP ==='
|
||||
cmake -S "$TASK_ROOT/src/mpp" -B "$TASK_ROOT/build/mpp" \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||
-DRKPLATFORM=ON -DHAVE_DRM=ON -DBUILD_SHARED_LIBS=ON -DBUILD_TEST=OFF
|
||||
cmake --build "$TASK_ROOT/build/mpp" -j "$JOBS"
|
||||
cmake --install "$TASK_ROOT/build/mpp"
|
||||
|
||||
echo '=== build FFmpeg with RKMPP ==='
|
||||
mkdir -p "$TASK_ROOT/build/ffmpeg"
|
||||
cd "$TASK_ROOT/build/ffmpeg"
|
||||
"$TASK_ROOT/src/ffmpeg/configure" --prefix="$PREFIX" \
|
||||
--enable-gpl --enable-version3 --enable-shared --disable-static \
|
||||
--enable-libdrm --enable-rkmpp --disable-rkrga --enable-libx264 \
|
||||
--disable-doc --disable-debug --disable-autodetect --disable-network \
|
||||
--disable-everything --enable-avcodec --enable-avformat --enable-avfilter \
|
||||
--enable-swscale --enable-swresample --enable-ffmpeg --enable-ffprobe \
|
||||
--enable-decoder=h264,hevc,h264_rkmpp,hevc_rkmpp,rawvideo,wrapped_avframe \
|
||||
--enable-parser=h264,hevc --enable-demuxer=h264,hevc \
|
||||
--enable-muxer=h264,null,rawvideo --enable-protocol=file \
|
||||
--enable-encoder=libx264,wrapped_avframe,rawvideo --enable-indev=lavfi \
|
||||
--enable-filter=testsrc2,null,format,hwdownload,scale \
|
||||
--extra-cflags="-I$PREFIX/include -I$SDK/include" \
|
||||
--extra-ldflags="-L$PREFIX/lib -L$SDK/lib/aarch64-linux-gnu -Wl,-rpath,$PREFIX/lib -Wl,-rpath,$SDK/lib/aarch64-linux-gnu"
|
||||
make -j "$JOBS"
|
||||
make install
|
||||
"$PREFIX/bin/ffmpeg" -hide_banner -decoders | grep rkmpp
|
||||
fi
|
||||
|
||||
echo '=== build Moonlight ==='
|
||||
mkdir -p "$TASK_ROOT/build/moonlight"
|
||||
cd "$TASK_ROOT/build/moonlight"
|
||||
qmake "$TASK_ROOT/src/moonlight/moonlight-qt.pro" \
|
||||
CONFIG+=release CONFIG+=recheck CONFIG+=disable-libva CONFIG+=disable-libvdpau \
|
||||
CONFIG+=disable-libplacebo CONFIG+=disable-discord \
|
||||
"QMAKE_LIBDIR+=$SDK/lib/aarch64-linux-gnu" \
|
||||
"QMAKE_LIBDIR+=$PREFIX/lib" \
|
||||
"QMAKE_RPATHDIR+=$SDK/lib/aarch64-linux-gnu" \
|
||||
"QMAKE_RPATHDIR+=$PREFIX/lib" \
|
||||
"QMAKE_INCDIR+=$SDK/include/aarch64-linux-gnu/qt5" \
|
||||
"QMAKE_INCDIR+=$SDK/include/aarch64-linux-gnu" \
|
||||
"QMAKE_INCDIR+=$SDK/include"
|
||||
make qmake_all
|
||||
if ! grep -q -- '-DHAVE_EGL' app/Makefile.Release; then
|
||||
echo 'EGL renderer missing; refusing to build a client without the DRM PRIME display path' >&2
|
||||
exit 1
|
||||
fi
|
||||
# qmake/make does not invalidate old objects when feature defines change.
|
||||
# In particular a stale ffmpeg.o can silently omit EGL despite a successful
|
||||
# configure check. Clean the app whenever its compiler/link flags change.
|
||||
flags_hash="$(grep -E '^(DEFINES|INCPATH|LIBS|CXXFLAGS)[[:space:]]*=' app/Makefile.Release | sha256sum | cut -d ' ' -f 1)"
|
||||
if [ "$(cat .vdi-build-flags.sha256 2>/dev/null || true)" != "$flags_hash" ]; then
|
||||
make -C app -f Makefile.Release clean
|
||||
printf '%s\n' "$flags_hash" > .vdi-build-flags.sha256
|
||||
fi
|
||||
make -j "$JOBS"
|
||||
echo '=== complete; experimental binary only, production unchanged ==='
|
||||
echo "$TASK_ROOT/build/moonlight/app/moonlight"
|
||||
git -C "$TASK_ROOT/src/mpp" rev-parse HEAD
|
||||
git -C "$TASK_ROOT/src/ffmpeg" rev-parse HEAD
|
||||
git -C "$TASK_ROOT/src/moonlight" rev-parse HEAD
|
||||
sha256sum "$TASK_ROOT"/debs/*.deb > "$TASK_ROOT/dependency-sha256.txt"
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Install only X11 display recovery; keep the kiosk/Moonlight session running.
|
||||
set -euo pipefail
|
||||
|
||||
BOX="${VDI_BOX:-armbian@192.168.1.188}"
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$REPO/terminal/kiosk-shell"
|
||||
STAGE=$(ssh -o BatchMode=yes -o ConnectTimeout=8 "$BOX" 'mktemp -d /tmp/vdi-display-deploy.XXXXXX')
|
||||
scp -q "$SRC/scripts/vdi-display-guard" "$SRC/scripts/vdi-kiosk-start" \
|
||||
"$SRC/etc/vdi-display-guard.service" "$BOX:$STAGE/"
|
||||
ssh "$BOX" bash -s -- "$STAGE" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
stage=$1
|
||||
backup="$HOME/vdi-terminal/backups/display-guard-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup" "$HOME/vdi-terminal/bin" "$HOME/.config/systemd/user"
|
||||
for file in "$HOME/vdi-terminal/bin/vdi-kiosk-start" \
|
||||
"$HOME/vdi-terminal/bin/vdi-display-guard" \
|
||||
"$HOME/.config/systemd/user/vdi-display-guard.service"; do
|
||||
test ! -f "$file" || cp -p "$file" "$backup/"
|
||||
done
|
||||
sed -i 's/\r$//' "$stage/"*
|
||||
python3 -c 'import ast, sys; ast.parse(open(sys.argv[1]).read())' "$stage/vdi-display-guard"
|
||||
sh -n "$stage/vdi-kiosk-start"
|
||||
install -m 0755 "$stage/vdi-display-guard" "$stage/vdi-kiosk-start" "$HOME/vdi-terminal/bin/"
|
||||
install -m 0644 "$stage/vdi-display-guard.service" "$HOME/.config/systemd/user/"
|
||||
|
||||
# Correct only this monitor's known stale layout; preserve all other layouts.
|
||||
python3 - "$backup" <<'PY'
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
path = Path.home() / '.config/monitors.xml'
|
||||
if path.exists():
|
||||
tree = ET.parse(path)
|
||||
changed = False
|
||||
for output in tree.findall('.//output'):
|
||||
if (output.get('name') == 'HDMI-1'
|
||||
and output.findtext('vendor') == 'GWR'
|
||||
and output.findtext('product') == '0x2708'
|
||||
and output.findtext('width') == '1920'
|
||||
and output.findtext('height') == '1440'
|
||||
and output.findtext('rate') == '75'):
|
||||
for name, value in [('width', '2560'), ('height', '1440'), ('rate', '60')]:
|
||||
output.find(name).text = value
|
||||
changed = True
|
||||
if changed:
|
||||
shutil.copy2(path, Path(sys.argv[1]) / 'monitors.xml')
|
||||
temp = path.with_name('monitors.xml.vdi-new')
|
||||
tree.write(temp, encoding='utf-8', xml_declaration=True)
|
||||
temp.replace(path)
|
||||
print('Updated known stale MATE layout to 2560x1440@60')
|
||||
PY
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable vdi-display-guard.service
|
||||
systemctl --user restart vdi-display-guard.service
|
||||
systemctl --user is-active vdi-display-guard.service
|
||||
echo "Backup: $backup"
|
||||
# Only remove the three files uploaded by this deployment.
|
||||
rm -- "$stage/vdi-display-guard" "$stage/vdi-kiosk-start" "$stage/vdi-display-guard.service"
|
||||
rmdir -- "$stage"
|
||||
REMOTE
|
||||
+11
-2
@@ -15,21 +15,30 @@ echo "==> 检查盒子连通性"
|
||||
ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'echo "box ok: $(hostname)"'
|
||||
|
||||
echo "==> 同步源码到盒子"
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'rm -rf ~/vdi-build/repo/kiosk-shell/src ~/vdi-build/repo/kiosk-shell/tests && mkdir -p ~/vdi-build/repo/kiosk-shell/src ~/vdi-build/repo/kiosk-shell/tests'
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'rm -rf ~/vdi-build/repo/kiosk-shell/src ~/vdi-build/repo/kiosk-shell/tests && mkdir -p ~/vdi-build/repo/kiosk-shell/src ~/vdi-build/repo/kiosk-shell/tests ~/vdi-build/repo/kiosk-shell/scripts'
|
||||
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 "$SRC/tests/test_display_guard.py" "$BOX":~/vdi-build/repo/kiosk-shell/tests/
|
||||
scp -q "$SRC/scripts/vdi-display-guard" "$BOX":~/vdi-build/repo/kiosk-shell/scripts/
|
||||
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/scripts/vdi-moonlight-rkmpp" "$BOX":~/vdi-terminal/bin/
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BOX" 'sed -i "s/\r$//" ~/vdi-terminal/bin/vdi-moonlight-rkmpp; chmod +x ~/vdi-terminal/bin/vdi-moonlight-rkmpp'
|
||||
scp -q "$SRC/etc/70-vdi-rockchip-video.rules" "$BOX":/tmp/70-vdi-rockchip-video.rules
|
||||
ssh "$BOX" 'sed -i "s/\r$//" /tmp/70-vdi-rockchip-video.rules; sudo -n install -m 0644 /tmp/70-vdi-rockchip-video.rules /etc/udev/rules.d/70-vdi-rockchip-video.rules && sudo -n udevadm control --reload-rules && for device in /dev/mpp_service /dev/rga /dev/dma_heap/*; do test ! -e "$device" || { sudo -n chgrp video "$device" && sudo -n chmod 0660 "$device"; } || exit 1; done'
|
||||
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 "==> 安装 HDMI 热插拔恢复服务"
|
||||
bash "$REPO/tools/deploy_display_guard.sh"
|
||||
|
||||
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 ~/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/'
|
||||
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 moonlight 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/'
|
||||
|
||||
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'
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Generate a deterministic 1440p H.264 clip without any display/capture dependency.
|
||||
// Link against the isolated FFmpeg libavcodec/libavutil built for the terminal.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
static int write_packets(AVCodecContext *encoder, AVPacket *packet, FILE *output) {
|
||||
int result;
|
||||
while ((result = avcodec_receive_packet(encoder, packet)) == 0) {
|
||||
if (fwrite(packet->data, 1, packet->size, output) != (size_t)packet->size) return -1;
|
||||
av_packet_unref(packet);
|
||||
}
|
||||
return result == AVERROR(EAGAIN) || result == AVERROR_EOF ? 0 : result;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) return 2;
|
||||
const AVCodec *codec = avcodec_find_encoder_by_name("libx264");
|
||||
AVCodecContext *encoder = avcodec_alloc_context3(codec);
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
FILE *output = fopen(argv[1], "wb");
|
||||
if (!codec || !encoder || !frame || !packet || !output) return 1;
|
||||
encoder->width = frame->width = 2560;
|
||||
encoder->height = frame->height = 1440;
|
||||
encoder->pix_fmt = frame->format = AV_PIX_FMT_YUV420P;
|
||||
encoder->time_base = (AVRational){1, 30};
|
||||
encoder->framerate = (AVRational){30, 1};
|
||||
encoder->max_b_frames = 0;
|
||||
encoder->thread_count = 2;
|
||||
av_opt_set(encoder->priv_data, "preset", "ultrafast", 0);
|
||||
av_opt_set(encoder->priv_data, "tune", "zerolatency", 0);
|
||||
if (avcodec_open2(encoder, codec, NULL) < 0 || av_frame_get_buffer(frame, 32) < 0) return 1;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (av_frame_make_writable(frame) < 0) return 1;
|
||||
for (int y = 0; y < frame->height; y++) {
|
||||
memset(frame->data[0] + y * frame->linesize[0], 32 + (y + i * 4) % 190, frame->width);
|
||||
}
|
||||
for (int plane = 1; plane < 3; plane++) {
|
||||
for (int y = 0; y < frame->height / 2; y++) {
|
||||
memset(frame->data[plane] + y * frame->linesize[plane], 128, frame->width / 2);
|
||||
}
|
||||
}
|
||||
frame->pts = i;
|
||||
if (avcodec_send_frame(encoder, frame) < 0 || write_packets(encoder, packet, output) < 0) return 1;
|
||||
}
|
||||
if (avcodec_send_frame(encoder, NULL) < 0 || write_packets(encoder, packet, output) < 0) return 1;
|
||||
if (fclose(output) != 0) return 1;
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&encoder);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"""X11 test only: inject 32 alternating left/right clicks into the bench window."""
|
||||
import ctypes
|
||||
|
||||
x11 = ctypes.CDLL("libX11.so.6")
|
||||
xtest = ctypes.CDLL("libXtst.so.6")
|
||||
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
|
||||
x11.XOpenDisplay.restype = ctypes.c_void_p
|
||||
x11.XFlush.argtypes = [ctypes.c_void_p]
|
||||
x11.XCloseDisplay.argtypes = [ctypes.c_void_p]
|
||||
xtest.XTestFakeMotionEvent.argtypes = [ctypes.c_void_p, ctypes.c_int,
|
||||
ctypes.c_int, ctypes.c_int, ctypes.c_ulong]
|
||||
xtest.XTestFakeButtonEvent.argtypes = [ctypes.c_void_p, ctypes.c_uint,
|
||||
ctypes.c_int, ctypes.c_ulong]
|
||||
display = x11.XOpenDisplay(None)
|
||||
if not display:
|
||||
raise RuntimeError("X11 unavailable")
|
||||
try:
|
||||
for index in range(32):
|
||||
x = 640 if index % 2 == 0 else 1920
|
||||
assert xtest.XTestFakeMotionEvent(display, -1, x, 600, 0)
|
||||
assert xtest.XTestFakeButtonEvent(display, 1, 1, 0)
|
||||
assert xtest.XTestFakeButtonEvent(display, 1, 0, 0)
|
||||
x11.XFlush(display)
|
||||
finally:
|
||||
x11.XCloseDisplay(display)
|
||||
print("Injected 32 clicks, expected L,R alternating; final pointer on R")
|
||||
@@ -0,0 +1,31 @@
|
||||
"""X11 benchmark only: 20 seconds of 125 Hz motion, no buttons or keys."""
|
||||
import ctypes
|
||||
import math
|
||||
import time
|
||||
|
||||
x11 = ctypes.CDLL("libX11.so.6")
|
||||
xtest = ctypes.CDLL("libXtst.so.6")
|
||||
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
|
||||
x11.XOpenDisplay.restype = ctypes.c_void_p
|
||||
x11.XFlush.argtypes = [ctypes.c_void_p]
|
||||
x11.XCloseDisplay.argtypes = [ctypes.c_void_p]
|
||||
xtest.XTestFakeMotionEvent.argtypes = [ctypes.c_void_p, ctypes.c_int,
|
||||
ctypes.c_int, ctypes.c_int, ctypes.c_ulong]
|
||||
display = x11.XOpenDisplay(None)
|
||||
if not display:
|
||||
raise RuntimeError("X11 unavailable")
|
||||
start = time.monotonic()
|
||||
count = 0
|
||||
try:
|
||||
while time.monotonic() - start < 20:
|
||||
elapsed = time.monotonic() - start
|
||||
x = round(1280 + 1000 * math.sin(elapsed * 4))
|
||||
y = round(720 + 450 * math.cos(elapsed * 3))
|
||||
if not xtest.XTestFakeMotionEvent(display, -1, x, y, 0):
|
||||
raise RuntimeError("Motion injection failed")
|
||||
x11.XFlush(display)
|
||||
count += 1
|
||||
time.sleep(max(0, start + count / 125 - time.monotonic()))
|
||||
finally:
|
||||
x11.XCloseDisplay(display)
|
||||
print(f"Injected {count} motion events in {time.monotonic()-start:.2f}s")
|
||||
@@ -0,0 +1,85 @@
|
||||
# Temporary interactive test window; exits after ten minutes. No system settings.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$benchDir = 'C:\ProgramData\VDI\input-bench-20261103'
|
||||
New-Item -ItemType Directory -Force $benchDir | Out-Null
|
||||
Set-Content -LiteralPath "$benchDir\pid.txt" -Value $PID
|
||||
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
public class VDIInputBenchForm : Form {
|
||||
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
||||
[DllImport("winmm.dll")] public static extern uint timeBeginPeriod(uint period);
|
||||
[DllImport("winmm.dll")] public static extern uint timeEndPeriod(uint period);
|
||||
readonly Stopwatch watch = Stopwatch.StartNew();
|
||||
readonly Timer timer = new Timer();
|
||||
readonly string folder;
|
||||
int paints, clicks;
|
||||
long lastReport;
|
||||
long lastFrameIndex = -1;
|
||||
public VDIInputBenchForm(string path) {
|
||||
folder = path;
|
||||
Text = "VDI Input Latency Bench";
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
Bounds = Screen.PrimaryScreen.Bounds;
|
||||
TopMost = true;
|
||||
DoubleBuffered = true;
|
||||
File.WriteAllText(Path.Combine(folder,"clicks.csv"), "ms,x,y,target\n");
|
||||
// A WinForms 16ms timer can quantize to ~31ms at the default timer
|
||||
// resolution and accidentally cap the *source* at 32FPS. Pace by an
|
||||
// absolute monotonic frame index, not accumulated timer ticks.
|
||||
timer.Interval = 1;
|
||||
timer.Tick += delegate {
|
||||
if(watch.Elapsed.TotalMinutes >= 10) { Close(); return; }
|
||||
long frameIndex = watch.ElapsedTicks * 60 / Stopwatch.Frequency;
|
||||
if(frameIndex != lastFrameIndex) { lastFrameIndex = frameIndex; Invalidate(); }
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
protected override void OnMouseDown(MouseEventArgs e) {
|
||||
base.OnMouseDown(e);
|
||||
string target = e.X < Width/2 ? "L" : "R";
|
||||
File.AppendAllText(Path.Combine(folder,"clicks.csv"),
|
||||
watch.ElapsedMilliseconds+","+e.X+","+e.Y+","+target+"\n");
|
||||
++clicks;
|
||||
Invalidate();
|
||||
}
|
||||
protected override void OnPaint(PaintEventArgs e) {
|
||||
base.OnPaint(e);
|
||||
long ms = watch.ElapsedMilliseconds;
|
||||
e.Graphics.Clear(Color.FromArgb(16,24,39));
|
||||
using(Brush left = new SolidBrush(Color.FromArgb(35,85,130)))
|
||||
using(Brush right = new SolidBrush(Color.FromArgb(50,115,90))) {
|
||||
e.Graphics.FillRectangle(left,50,180,Width/2-100,Height-260);
|
||||
e.Graphics.FillRectangle(right,Width/2+50,180,Width/2-100,Height-260);
|
||||
}
|
||||
int x = (int)((ms*0.6)%(Width+100))-100;
|
||||
e.Graphics.FillRectangle(Brushes.Gold,x,0,60,Height);
|
||||
using(Font font = new Font("Consolas",24)) {
|
||||
e.Graphics.DrawString("VDI test: moving bars + ordered clicks | "+clicks+" clicks | "+ms+" ms",
|
||||
font,Brushes.White,70,40);
|
||||
e.Graphics.DrawString("LEFT",font,Brushes.White,Width/4-50,Height/2);
|
||||
e.Graphics.DrawString("RIGHT",font,Brushes.White,Width*3/4-50,Height/2);
|
||||
}
|
||||
++paints;
|
||||
if(ms-lastReport>=1000) {
|
||||
File.WriteAllText(Path.Combine(folder,"status.txt"),
|
||||
"width="+Width+" height="+Height+" paints="+paints+" elapsed_ms="+ms+" clicks="+clicks);
|
||||
lastReport=ms;
|
||||
}
|
||||
}
|
||||
protected override void OnFormClosed(FormClosedEventArgs e) {
|
||||
timer.Stop(); timer.Dispose(); base.OnFormClosed(e);
|
||||
}
|
||||
}
|
||||
'@
|
||||
[VDIInputBenchForm]::SetProcessDPIAware() | Out-Null
|
||||
$timerResult=[VDIInputBenchForm]::timeBeginPeriod(1)
|
||||
try {
|
||||
[System.Windows.Forms.Application]::Run((New-Object VDIInputBenchForm($benchDir)))
|
||||
} finally {
|
||||
if($timerResult -eq 0) { [VDIInputBenchForm]::timeEndPeriod(1) | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace VdiDiagnostics {
|
||||
public class AdapterResult {
|
||||
public string Name;
|
||||
public uint VendorId;
|
||||
public int DeviceResult;
|
||||
}
|
||||
|
||||
// Enumerate the actual DXGI adapters and create a D3D11 device on each NVIDIA
|
||||
// adapter. A WMI/PnP model name alone does not prove that D3D is operational.
|
||||
public static class D3DProbe {
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct AdapterDesc {
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string Description;
|
||||
public uint VendorId, DeviceId, SubSysId, Revision;
|
||||
public UIntPtr DedicatedVideoMemory, DedicatedSystemMemory, SharedSystemMemory;
|
||||
public long AdapterLuid;
|
||||
}
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int EnumAdapters(IntPtr factory, uint index, out IntPtr adapter);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate int GetDesc(IntPtr adapter, out AdapterDesc desc);
|
||||
[DllImport("dxgi.dll")]
|
||||
private static extern int CreateDXGIFactory1(ref Guid iid, out IntPtr factory);
|
||||
[DllImport("d3d11.dll")]
|
||||
private static extern int D3D11CreateDevice(IntPtr adapter, uint driverType,
|
||||
IntPtr software, uint flags, IntPtr levels, uint levelCount, uint sdkVersion,
|
||||
out IntPtr device, out uint featureLevel, out IntPtr context);
|
||||
|
||||
public static AdapterResult[] Check() {
|
||||
var results = new List<AdapterResult>();
|
||||
var iid = new Guid("770aae78-f26f-4dba-a829-253c83d1b387");
|
||||
IntPtr factory;
|
||||
Marshal.ThrowExceptionForHR(CreateDXGIFactory1(ref iid, out factory));
|
||||
try {
|
||||
var table = Marshal.ReadIntPtr(factory);
|
||||
var enumerate = (EnumAdapters)Marshal.GetDelegateForFunctionPointer(
|
||||
Marshal.ReadIntPtr(table, 7 * IntPtr.Size), typeof(EnumAdapters));
|
||||
for (uint i = 0; i < 32; i++) {
|
||||
IntPtr adapter;
|
||||
int hr = enumerate(factory, i, out adapter);
|
||||
if (hr == unchecked((int)0x887A0002)) break;
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
try {
|
||||
var descMethod = (GetDesc)Marshal.GetDelegateForFunctionPointer(
|
||||
Marshal.ReadIntPtr(Marshal.ReadIntPtr(adapter), 8 * IntPtr.Size), typeof(GetDesc));
|
||||
AdapterDesc desc;
|
||||
Marshal.ThrowExceptionForHR(descMethod(adapter, out desc));
|
||||
if (desc.VendorId != 0x10de) continue;
|
||||
IntPtr device, context;
|
||||
uint level;
|
||||
hr = D3D11CreateDevice(adapter, 0, IntPtr.Zero, 0, IntPtr.Zero,
|
||||
0, 7, out device, out level, out context);
|
||||
if (context != IntPtr.Zero) Marshal.Release(context);
|
||||
if (device != IntPtr.Zero) Marshal.Release(device);
|
||||
results.Add(new AdapterResult { Name = desc.Description,
|
||||
VendorId = desc.VendorId, DeviceResult = hr });
|
||||
} finally { Marshal.Release(adapter); }
|
||||
}
|
||||
} finally { Marshal.Release(factory); }
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Start/status/stop only the temporary VDI input benchmark through VM100's agent."""
|
||||
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=["start", "status", "stop"])
|
||||
args = parser.parse_args()
|
||||
tools_dir = Path(__file__).resolve().parent
|
||||
term = runpy.run_path(str(tools_dir / "pve_term_exec.py"))
|
||||
vm = runpy.run_path(str(tools_dir / "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; no benchmark task changed")
|
||||
folder = r"C:\ProgramData\VDI\input-bench-20261103"
|
||||
prefix = "$ErrorActionPreference='Stop'; $folder='" + folder + "'; "
|
||||
if args.action == "start":
|
||||
encoded = base64.b64encode((tools_dir / "input-latency-bench.ps1").read_bytes()).decode()
|
||||
command = prefix + r"""
|
||||
New-Item -ItemType Directory -Force $folder | Out-Null
|
||||
if (Get-ScheduledTask -TaskName VDIInputLatencyBench -ErrorAction SilentlyContinue) { throw 'Benchmark task already exists; stop it first' }
|
||||
[IO.File]::WriteAllBytes("$folder\bench.ps1",[Convert]::FromBase64String('""" + encoded + r"""'))
|
||||
$action=New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File $folder\bench.ps1"
|
||||
$principal=New-ScheduledTaskPrincipal -UserId test -LogonType Interactive -RunLevel Limited
|
||||
$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
Register-ScheduledTask -TaskName VDIInputLatencyBench -Action $action -Principal $principal -Settings $settings | Out-Null
|
||||
Start-ScheduledTask -TaskName VDIInputLatencyBench
|
||||
'Benchmark launched'
|
||||
"""
|
||||
elif args.action == "status":
|
||||
command = prefix + r"""
|
||||
Get-Content "$folder\status.txt" -ErrorAction SilentlyContinue
|
||||
Get-Content "$folder\clicks.csv" -ErrorAction SilentlyContinue
|
||||
Get-ScheduledTaskInfo -TaskName VDIInputLatencyBench | Select-Object LastTaskResult | ConvertTo-Json -Compress
|
||||
"""
|
||||
else:
|
||||
command = prefix + r"""
|
||||
Stop-ScheduledTask -TaskName VDIInputLatencyBench -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName VDIInputLatencyBench -Confirm:$false -ErrorAction SilentlyContinue
|
||||
if (Test-Path "$folder\pid.txt") {
|
||||
$benchPid=[int](Get-Content "$folder\pid.txt")
|
||||
$process=Get-CimInstance Win32_Process -Filter "ProcessId=$benchPid"
|
||||
if ($process.CommandLine -like '*input-bench-20261103*') { Stop-Process -Id $benchPid -ErrorAction SilentlyContinue }
|
||||
}
|
||||
'Benchmark stopped; other applications untouched'
|
||||
"""
|
||||
out, error = vm["agent"](ticket, csrf, ["powershell.exe", "-NoProfile", "-Command", command])
|
||||
print(out)
|
||||
if error:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Run the scoped VDD mode probe in VM100's already logged-in user 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", "List", "Apply", "Restore"])
|
||||
parser.add_argument("--hz", type=int, default=60)
|
||||
args = parser.parse_args()
|
||||
if not 24 <= args.hz <= 240:
|
||||
parser.error("--hz must be in 24..240")
|
||||
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; no task or display configuration changed")
|
||||
payload = base64.b64encode((root / "set-vdi-display-mode.ps1").read_bytes()).decode()
|
||||
command = r"""
|
||||
$ErrorActionPreference='Stop'
|
||||
$folder='C:\ProgramData\VDI\mouse-optimization'
|
||||
$task='VDIMouseDisplayProbe'
|
||||
if(Get-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue) { throw 'Probe 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 -LiteralPath $folder
|
||||
$rule=New-Object Security.AccessControl.FileSystemAccessRule($user,'Modify','ContainerInherit,ObjectInherit','None','Allow')
|
||||
$acl.SetAccessRule($rule)
|
||||
Set-Acl -LiteralPath $folder -AclObject $acl
|
||||
[IO.File]::WriteAllBytes("$folder\mode.ps1",[Convert]::FromBase64String('__VDI_PAYLOAD__'))
|
||||
$run="try { & '$folder\mode.ps1' -Action __VDI_ACTION__ -RefreshRate __VDI_HZ__ | Out-File -Encoding utf8 -Width 300 '$folder\mode-result.txt' } catch { `$_ | Out-String | Out-File -Encoding utf8 -Width 300 '$folder\mode-result.txt'; exit 1 }"
|
||||
$encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($run))
|
||||
$action=New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand $encoded"
|
||||
$principal=New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
|
||||
$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 1)
|
||||
Register-ScheduledTask -TaskName $task -Action $action -Principal $principal -Settings $settings | Out-Null
|
||||
try {
|
||||
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
|
||||
}
|
||||
if((Get-ScheduledTask -TaskName $task).State -eq 'Running') { throw 'Probe timed out' }
|
||||
Get-Content "$folder\mode-result.txt"
|
||||
'task_result='+(Get-ScheduledTaskInfo -TaskName $task).LastTaskResult
|
||||
} finally {
|
||||
Stop-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName $task -Confirm:$false
|
||||
}
|
||||
""".replace("__VDI_ACTION__", args.action).replace("__VDI_HZ__", str(args.hz)).replace("__VDI_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,52 @@
|
||||
param([switch]$Restore)
|
||||
# Run in the logged-in desktop user session, not the Guest Agent's Session 0.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$directory = Join-Path $env:LOCALAPPDATA 'VDI'
|
||||
New-Item -ItemType Directory -Force $directory | Out-Null
|
||||
$backup = Join-Path $directory 'desktop-responsiveness-before-20261103.json'
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class VDIVisualResponse {
|
||||
[StructLayout(LayoutKind.Sequential)] struct Animation { public uint size; public int enabled; }
|
||||
[DllImport("user32.dll", EntryPoint="SystemParametersInfoW", SetLastError=true)]
|
||||
static extern bool SpiAnimation(uint action, uint size, ref Animation data, uint flags);
|
||||
[DllImport("user32.dll", EntryPoint="SystemParametersInfoW", SetLastError=true)]
|
||||
static extern bool SpiGet(uint action, uint unused, out uint value, uint flags);
|
||||
[DllImport("user32.dll", EntryPoint="SystemParametersInfoW", SetLastError=true)]
|
||||
static extern bool SpiSet(uint action, uint value, IntPtr unused, uint flags);
|
||||
static void Check(bool result) { if(!result) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
||||
public static int AnimationEnabled() {
|
||||
Animation data = new Animation { size=8 };
|
||||
Check(SpiAnimation(0x0048,8,ref data,0)); return data.enabled;
|
||||
}
|
||||
public static uint MenuDelay() { uint value; Check(SpiGet(0x006A,0,out value,0)); return value; }
|
||||
public static void Set(int animation, uint delay) {
|
||||
Animation data = new Animation { size=8, enabled=animation };
|
||||
Check(SpiAnimation(0x0049,8,ref data,3));
|
||||
Check(SpiSet(0x006B,delay,IntPtr.Zero,3));
|
||||
}
|
||||
}
|
||||
'@
|
||||
$before = @{ animation=[VDIVisualResponse]::AnimationEnabled(); menuDelay=[VDIVisualResponse]::MenuDelay() }
|
||||
if ($Restore) {
|
||||
$target = Get-Content -LiteralPath $backup -Raw | ConvertFrom-Json
|
||||
} else {
|
||||
if (-not (Test-Path -LiteralPath $backup)) {
|
||||
$before | ConvertTo-Json | Set-Content -LiteralPath $backup -Encoding UTF8
|
||||
}
|
||||
$target = @{ animation=0; menuDelay=100 }
|
||||
}
|
||||
try {
|
||||
[VDIVisualResponse]::Set([int]$target.animation,[uint32]$target.menuDelay)
|
||||
$after = @{ animation=[VDIVisualResponse]::AnimationEnabled(); menuDelay=[VDIVisualResponse]::MenuDelay() }
|
||||
if ($after.animation -ne $target.animation -or $after.menuDelay -ne $target.menuDelay) {
|
||||
throw 'Desktop preferences were not retained'
|
||||
}
|
||||
@{ before=$before; after=$after; backup=$backup; restored=[bool]$Restore } |
|
||||
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $directory 'desktop-responsiveness-result.json') -Encoding UTF8
|
||||
} catch {
|
||||
[VDIVisualResponse]::Set([int]$before.animation,[uint32]$before.menuDelay)
|
||||
throw
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
param(
|
||||
[ValidateSet('Query', 'List', 'Apply', 'Restore')][string]$Action = 'Query',
|
||||
[ValidateRange(640, 7680)][int]$Width = 2560,
|
||||
[ValidateRange(480, 4320)][int]$Height = 1440,
|
||||
[ValidateRange(24, 240)][int]$RefreshRate = 60,
|
||||
[string]$BackupPath = 'C:\ProgramData\VDI\mouse-optimization\display-before.json'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Run in the logged-in desktop session. Apply is temporary (no registry update).
|
||||
# Never assumes DISPLAY1, and refuses ambiguous active virtual displays.
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class VdiModeProbe {
|
||||
[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() {
|
||||
string found = null;
|
||||
for (uint i = 0; ; i++) {
|
||||
var d = new Device(); d.cb = Marshal.SizeOf(typeof(Device));
|
||||
if (!EnumDisplayDevices(null, i, ref d, 0)) return found;
|
||||
if ((d.Flags & 1) == 0 || d.Description.IndexOf("Virtual Display Driver", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
||||
if (found != null) throw new InvalidOperationException("Multiple active virtual displays; select manually");
|
||||
found = d.Name;
|
||||
}
|
||||
}
|
||||
public static Mode Current(string name) {
|
||||
var m = new Mode(); m.size = (short)Marshal.SizeOf(typeof(Mode));
|
||||
if (!EnumDisplaySettings(name, -1, ref m)) throw new InvalidOperationException("Cannot query mode");
|
||||
return m;
|
||||
}
|
||||
public static Mode[] List(string name) {
|
||||
var modes = new List<Mode>();
|
||||
for(int i=0; ; ++i) {
|
||||
var m = new Mode(); m.size = (short)Marshal.SizeOf(typeof(Mode));
|
||||
if (!EnumDisplaySettings(name, i, ref m)) return modes.ToArray();
|
||||
modes.Add(m);
|
||||
}
|
||||
}
|
||||
public static void Apply(string name, int w, int h, int hz, int bpp) {
|
||||
foreach(var m0 in List(name)) {
|
||||
var m = m0;
|
||||
if(m.width != w || m.height != h || m.hz != hz || m.bpp != bpp) continue;
|
||||
m.fields = 0x80000 | 0x100000 | 0x400000 | 0x40000;
|
||||
int result = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 2, IntPtr.Zero);
|
||||
if(result != 0) throw new InvalidOperationException("Mode test failed: " + result);
|
||||
result = ChangeDisplaySettingsEx(name, ref m, IntPtr.Zero, 0, IntPtr.Zero);
|
||||
if(result != 0) throw new InvalidOperationException("Mode apply failed: " + result);
|
||||
var actual = Current(name);
|
||||
if(actual.width != w || actual.height != h || actual.hz != hz || actual.bpp != bpp)
|
||||
throw new InvalidOperationException("Mode read-back mismatch");
|
||||
return;
|
||||
}
|
||||
throw new InvalidOperationException("Requested mode not advertised");
|
||||
}
|
||||
}
|
||||
'@
|
||||
$device = [VdiModeProbe]::Find()
|
||||
if (-not $device) { throw 'No active Virtual Display Driver in this session' }
|
||||
$current = [VdiModeProbe]::Current($device)
|
||||
if ($Action -eq 'List') {
|
||||
[VdiModeProbe]::List($device) | Where-Object { $_.width -eq $Width -and $_.height -eq $Height } |
|
||||
Select-Object width,height,hz,bpp | ConvertTo-Json -Compress
|
||||
exit 0
|
||||
}
|
||||
if ($Action -eq 'Apply') {
|
||||
if (-not (Test-Path -LiteralPath $BackupPath)) {
|
||||
New-Item -ItemType Directory -Force (Split-Path $BackupPath) | Out-Null
|
||||
@{device=$device; width=$current.width; height=$current.height; hz=$current.hz; bpp=$current.bpp} |
|
||||
ConvertTo-Json | Set-Content -LiteralPath $BackupPath
|
||||
}
|
||||
try {
|
||||
[VdiModeProbe]::Apply($device,$Width,$Height,$RefreshRate,$current.bpp)
|
||||
} catch {
|
||||
[VdiModeProbe]::Apply($device,$current.width,$current.height,$current.hz,$current.bpp)
|
||||
throw
|
||||
}
|
||||
}
|
||||
if ($Action -eq 'Restore') {
|
||||
$before = Get-Content -LiteralPath $BackupPath -Raw | ConvertFrom-Json
|
||||
if ($before.device -ne $device) { throw 'Display identity changed; inspect before restoring' }
|
||||
[VdiModeProbe]::Apply($device,$before.width,$before.height,$before.hz,$before.bpp)
|
||||
}
|
||||
$current = [VdiModeProbe]::Current($device)
|
||||
@{device=$device; width=$current.width; height=$current.height; hz=$current.hz; bpp=$current.bpp; action=$Action} |
|
||||
ConvertTo-Json -Compress
|
||||
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>VDI stream motion test</title>
|
||||
<style>
|
||||
html, body { margin: 0; overflow: hidden; background: #101827; color: white; font: 24px monospace; }
|
||||
canvas { display: block; width: 100vw; height: 100vh; }
|
||||
#stats { position: fixed; right: 24px; top: 20px; background: #000c; padding: 12px; }
|
||||
</style>
|
||||
<canvas id="scene"></canvas><div id="stats"></div>
|
||||
<script>
|
||||
// Deterministic moving bars + checkerboard: creates continuous desktop damage.
|
||||
// Read the source RAF rate separately from the Moonlight stream/render rate.
|
||||
const canvas = document.getElementById('scene');
|
||||
const context = canvas.getContext('2d', { alpha: false });
|
||||
const stats = document.getElementById('stats');
|
||||
let count = 0, last = performance.now(), sourceFps = 0;
|
||||
function render(now) {
|
||||
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
|
||||
canvas.width = innerWidth; canvas.height = innerHeight;
|
||||
}
|
||||
const width = canvas.width, height = canvas.height;
|
||||
context.fillStyle = '#101827'; context.fillRect(0, 0, width, height);
|
||||
const offset = Math.floor(now * 0.12) % 128;
|
||||
for (let y = height / 2; y < height; y += 128) {
|
||||
for (let x = -128; x < width; x += 128) {
|
||||
context.fillStyle = (Math.round((x + 128) / 128) + Math.floor(y / 128)) % 2 ? '#28517a' : '#132c46';
|
||||
context.fillRect(x + offset, y, 126, 126);
|
||||
}
|
||||
}
|
||||
const x = (now * 0.6) % (width + 100) - 100;
|
||||
context.fillStyle = '#ffcf40'; context.fillRect(x, 0, 80, height);
|
||||
context.fillStyle = '#ffffff'; context.fillRect(width - x, 0, 8, height);
|
||||
count++;
|
||||
if (now - last >= 1000) { sourceFps = count * 1000 / (now - last); count = 0; last = now; }
|
||||
stats.textContent = `${width}x${height} | source RAF ${sourceFps.toFixed(1)} FPS | ${Math.floor(now)} ms`;
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Run against the actual patched source and private runtime on the ARM64 box.
|
||||
set -euo pipefail
|
||||
b="${VDI_RKMPP_BUILD_ROOT:-$HOME/vdi-build/rkmpp-experiment}"
|
||||
sdk="$b/deps/usr"
|
||||
export LD_LIBRARY_PATH="$b/runtime/lib:$sdk/lib/aarch64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export PKG_CONFIG_PATH="$b/runtime/lib/pkgconfig:$sdk/lib/aarch64-linux-gnu/pkgconfig:$sdk/share/pkgconfig"
|
||||
export CPLUS_INCLUDE_PATH="$sdk/include/aarch64-linux-gnu:$sdk/include"
|
||||
export C_INCLUDE_PATH="$sdk/include/aarch64-linux-gnu:$sdk/include"
|
||||
sanitize=()
|
||||
if [ "${VDI_TEST_SANITIZE:-0}" = 1 ]; then
|
||||
sanitize=(-fsanitize=address -g)
|
||||
fi
|
||||
g++ "${sanitize[@]}" -std=c++17 -fPIC -UNDEBUG "$b/latency-tests/test_latency.cpp" \
|
||||
-I"$b/src/moonlight/app/streaming/input" \
|
||||
-I"$b/src/moonlight/app/streaming/video/ffmpeg-renderers/pacer" \
|
||||
-I"$b/runtime/include" $(pkg-config --cflags --libs Qt5Core sdl2) \
|
||||
-L"$b/runtime/lib" -lavutil -o "$b/latency-tests/test_latency"
|
||||
"$b/latency-tests/test_latency"
|
||||
|
||||
g++ "${sanitize[@]}" -std=c++17 -fPIC -UNDEBUG "$b/latency-tests/test_input_priority.cpp" \
|
||||
-I"$b/src/moonlight/app/streaming/input" $(pkg-config --cflags --libs sdl2) \
|
||||
-o "$b/latency-tests/test_input_priority"
|
||||
"$b/latency-tests/test_input_priority"
|
||||
gcc "${sanitize[@]}" -std=gnu11 -ffunction-sections -fdata-sections -Wl,--gc-sections -UNDEBUG \
|
||||
-I"$b/src/moonlight/moonlight-common-c/moonlight-common-c/src" \
|
||||
-I"$b/src/moonlight/moonlight-common-c/moonlight-common-c/enet/include" \
|
||||
"$b/latency-tests/test_absolute_input.c" \
|
||||
"$b/build/moonlight/moonlight-common-c/libmoonlight-common-c.a" \
|
||||
-lpthread -lcrypto -o "$b/latency-tests/test_absolute_input"
|
||||
"$b/latency-tests/test_absolute_input"
|
||||
@@ -0,0 +1,105 @@
|
||||
param(
|
||||
[string]$SunshineLogPath = "",
|
||||
[int]$MaxLogAgeMinutes = 30
|
||||
)
|
||||
|
||||
# 只读验收脚本:不重启 VM、设备、VFIO 或 Sunshine,不修改任何配置。
|
||||
# 退出码 0 才表示当前状态满足“可继续做串流性能验收”的前置条件。
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ok = $true
|
||||
|
||||
function Report([string]$Name, [bool]$Passed, [string]$Detail) {
|
||||
$state = if ($Passed) { "PASS" } else { "FAIL" }
|
||||
Write-Output ("[{0}] {1}: {2}" -f $state, $Name, $Detail)
|
||||
}
|
||||
|
||||
$nvidiaCommand = Get-Command nvidia-smi -ErrorAction SilentlyContinue
|
||||
if ($null -eq $nvidiaCommand) {
|
||||
Report "nvidia-smi" $false "命令不存在"
|
||||
$ok = $false
|
||||
} else {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$gpuText = & $nvidiaCommand.Source --query-gpu=name,driver_version,pstate --format=csv,noheader 2>&1
|
||||
$queryExitCode = $LASTEXITCODE
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($queryExitCode -ne 0 -or [string]::IsNullOrWhiteSpace(($gpuText -join ""))) {
|
||||
Report "nvidia-smi" $false (($gpuText -join " ").Trim())
|
||||
$ok = $false
|
||||
} else {
|
||||
Report "nvidia-smi" $true (($gpuText -join " ").Trim())
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$displayAdapters = @(Get-CimInstance Win32_VideoController |
|
||||
Where-Object { $_.Name -match "NVIDIA|GeForce|Quadro|Tesla" })
|
||||
$healthy = @($displayAdapters | Where-Object { $_.ConfigManagerErrorCode -eq 0 }).Count -gt 0
|
||||
Report "NVIDIA PnP" $healthy (($displayAdapters | ForEach-Object { "$($_.Name): code=$($_.ConfigManagerErrorCode)" }) -join "; ")
|
||||
$ok = $ok -and $healthy
|
||||
if (-not ('VdiDiagnostics.D3DProbe' -as [type])) {
|
||||
Add-Type -Path (Join-Path $PSScriptRoot 'probe-nvidia-d3d.cs')
|
||||
}
|
||||
$dxgi = @([VdiDiagnostics.D3DProbe]::Check())
|
||||
$healthy = @($dxgi | Where-Object { $_.DeviceResult -eq 0 }).Count -gt 0
|
||||
$detail = if ($dxgi.Count) { ($dxgi | ForEach-Object { '{0}: HRESULT=0x{1:X8}' -f $_.Name,$_.DeviceResult }) -join '; ' } else { 'DXGI did not enumerate an NVIDIA adapter' }
|
||||
Report "NVIDIA DXGI + D3D11" $healthy $detail
|
||||
$ok = $ok -and $healthy
|
||||
} catch {
|
||||
Report "NVIDIA DXGI + D3D11" $false $_.Exception.Message
|
||||
$ok = $false
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SunshineLogPath)) {
|
||||
$candidates = @(
|
||||
"C:\Program Files\Sunshine\config\sunshine.log",
|
||||
"C:\ProgramData\Sunshine\config\sunshine.log"
|
||||
)
|
||||
$SunshineLogPath = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SunshineLogPath) -or -not (Test-Path -LiteralPath $SunshineLogPath)) {
|
||||
Report "Sunshine encoder" $false "找不到 sunshine.log;不能证明实际使用 h264_nvenc"
|
||||
$ok = $false
|
||||
} else {
|
||||
# Encoder discovery/probing can mention both NVENC and x264. Inspect only
|
||||
# the most recent actual creation, with a timestamp, never the entire history.
|
||||
$pattern = '\[(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)\].*Creating encoder \[(?<encoder>[^\]]+)\]'
|
||||
$matches = [regex]::Matches((Get-Content -LiteralPath $SunshineLogPath -Raw), $pattern)
|
||||
$healthy = $false
|
||||
$detail = '没有实际编码器创建记录;请连接Moonlight并播放测试动画'
|
||||
if ($matches.Count -gt 0) {
|
||||
$entry = $matches[$matches.Count - 1]
|
||||
$encoder = $entry.Groups['encoder'].Value
|
||||
$createdAt = [datetime]::Parse($entry.Groups['time'].Value, [Globalization.CultureInfo]::InvariantCulture)
|
||||
$ageMinutes = ((Get-Date) - $createdAt).TotalMinutes
|
||||
$healthy = $encoder -eq 'h264_nvenc' -and $ageMinutes -ge 0 -and $ageMinutes -le $MaxLogAgeMinutes
|
||||
$detail = '{0}, created {1}, age {2:N1} min' -f $encoder,$createdAt,$ageMinutes
|
||||
}
|
||||
Report "Sunshine active encoder" $healthy $detail
|
||||
$ok = $ok -and $healthy
|
||||
}
|
||||
|
||||
if ($nvidiaCommand) {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$activity = & $nvidiaCommand.Source --query-gpu=encoder.stats.sessionCount,utilization.encoder --format=csv,noheader,nounits 2>&1
|
||||
$queryExitCode = $LASTEXITCODE
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$healthy = $false
|
||||
if ($queryExitCode -eq 0) {
|
||||
foreach ($row in $activity) {
|
||||
if ([string]$row -match '^\s*(\d+),\s*(\d+)\s*$') {
|
||||
if ([int]$Matches[1] -gt 0 -and [int]$Matches[2] -gt 0) { $healthy = $true }
|
||||
}
|
||||
}
|
||||
}
|
||||
Report "NVENC session/utilization" $healthy ($activity -join '; ')
|
||||
$ok = $ok -and $healthy
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
Write-Output "GPU/NVENC 前置验收通过:可以继续做 Moonlight 帧率与延迟测试。"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Output "GPU/NVENC 前置验收失败:不要把当前状态作为硬编码性能结果。"
|
||||
exit 1
|
||||
Reference in new issue
Block a user