mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 12:53:43 +01:00
On a host whose GPU can encode H.264 (a Windows driver through d3d12 on WSL 2 does), KRDP in
the demo guest encoded on the guest's CPUs. The host side was fine (vainfo on the host lists
H.264 and HEVC EncSlice), and virgl can pass a guest's VA-API video to it, but a chain of seven
problems stood between the two. Each is a patch, and the guest takes them all:
QEMU (nix/dev/qemu-virgl-video.patch)
QEMU never asks virglrenderer for video: virglrenderer turns it on only for an embedder that
passes VIRGL_RENDERER_USE_VIDEO and a get_drm_fd callback to open the host's video device on.
The flag is defined only with VIRGL_RENDERER_UNSTABLE_APIS, which the build sets.
virglrenderer 1.3.0, four patches in this order
virglrenderer-chroma-format.patch
the guest's Mesa and virglrenderer number the chroma format of a codec differently: 4:2:0
was read as 4:2:2, and the host's encoder refused every codec it was asked for
virglrenderer-video-memory-upload.patch
the d3d12 driver has no dma-bufs: vaExportSurfaceHandle fails, nothing was uploaded and
the encoder returned an empty bitstream. The planes of the guest's picture are read back
(glReadPixels) and written to the surface through a derived image (vaDeriveImage, with
vaCreateImage and vaPutImage as a fallback)
virglrenderer-h264-packed-sps.patch
Mesa's VA-API takes the level of the stream from a packed SPS, which virglrenderer never
sent: level 0 reached UNREACHABLE in the d3d12 encoder and crashed QEMU
virglrenderer-ubo0-constants.patch
a constant buffer 0 bound as a buffer was never read by the shader, so the colour matrix
of an RGB to YUV conversion was zero and the picture was a solid green
Mesa 26.2.5 in the guest, two patches (hardware.graphics.package)
mesa-virgl-rgb-yuv-csc.patch
the graphics compositor never loaded an RGB to YUV matrix, and multiplied by the YUV to
RGB one of its initial state
mesa-virgl-fence-leak.patch
a surface kept the out-fence (a sync_file) of its last flush and the next flush stored a
new one over it: one file descriptor leaked per converted frame, 44 a second under motion,
and KRDP froze when it reached the limit of 1024, about 28 seconds in
KRDP (krdp.nix) asks for H264Baseline and the guest's driver lists Main and High, so ffmpeg's
h264_vaapi found no profile it could use: it is built with H264Main.
What was checked: a luma ramp and a colour-bar pattern encoded through the guest to the 4090
decode to the right values; the real desktop shows over RDP in Windows' own client with the
right colours; under es2gears motion the descriptors of krdpserver stay flat (3 sync_file, 78
of 1024) where they were 949 of 1024 after 28 seconds.
run-kde sets what the host needs for Mesa and for virglrenderer's libva to find the Windows
driver, on WSL 2 with the GPU on and unless it is set already: GALLIUM_DRIVER=d3d12,
LD_LIBRARY_PATH with /usr/lib/wsl/lib first, LIBVA_DRIVER_NAME=d3d12 and LIBVA_DRIVERS_PATH
(/run/opengl-driver/lib/dri, for the d3d12 video driver, which it looks for). --doctor checks
for the libraries and for that driver, and the tests have a scenario for each.
Known: the software decoder warns "illegal short term buffer state detected" on the stream; the
d3d12 encoder writes Constrained Baseline whatever profile is asked for. There is no binary
cache: the first run builds QEMU and Mesa, which takes about 20 minutes. QEMU, virglrenderer and
Mesa are the runner's and the guest's only, and the VM suites build the Mesa as well.
Not measured yet: whether it is smoother than the CPU encoder under the same load.
711 lines
26 KiB
Bash
711 lines
26 KiB
Bash
#!/usr/bin/env bash
|
|
# run-kde: start the komorebi demo guest (nix/dev/run-kde.nix) in a virtual machine, so that anyone
|
|
# can try tiling by hand. docs/trying-it.md is the guide, docs/dev/utm.md says what the guest takes
|
|
# from its runner; this is the program.
|
|
#
|
|
# nix run .#run-kde -- [options] or just run-kde [options]
|
|
#
|
|
# The flake wraps this file with the guest's runner baked in (RUN_KDE_RUNNER_BIN, RUN_KDE_GUEST) and
|
|
# the tools it needs on its PATH. Everything it reads from the machine it reads through a variable
|
|
# that a test can set (RUN_KDE_*, below), so that `--dry-run` gives the same answer on a machine
|
|
# that has none of it: the check `run-kde-dry-run` runs it in a sandbox.
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: run-kde [options]
|
|
|
|
Starts a NixOS guest with a Plasma session in which komorebi tiles a few windows, and shows it in
|
|
a window (QEMU through WSLg, where there is one) or over RDP. The first run builds komorebi from
|
|
source and downloads Plasma: there is no binary cache.
|
|
|
|
--doctor check every prerequisite and say how to fix what is missing; exit 1 if
|
|
one is, and always when there is no KVM
|
|
--dry-run print what would be started, and start nothing
|
|
--build-only build the guest and stop
|
|
--reset delete the state directory (the disk, the password and the key) and stop
|
|
--shell no window: this terminal is the guest's console (--display none --gpu off)
|
|
--rdp on WSL 2, open Windows' own RDP client (mstsc) when the guest is up
|
|
--display KIND gtk, vnc, rdp or none (default: gtk where there is a window, else rdp)
|
|
--gpu MODE auto, on or off (default: auto): on renders in the guest with the host's GPU
|
|
(virtio-vga-gl); off renders on the guest's CPUs (virtio-vga)
|
|
--tcg allow software emulation when there is no KVM (too slow to try anything)
|
|
--memory MB the guest's memory (default: what this machine has, 4096 to 8192)
|
|
--cores N the guest's cores (default: what this machine has, up to 6)
|
|
--size GB the size of the guest's disk when it is made (default: 20, sparse)
|
|
--resolution WxH the size of the guest's screen (default: 1280x800)
|
|
--ssh-port PORT the host's port for ssh into the guest (default: 2223)
|
|
--rdp-port PORT the host's port for RDP into the guest (default: 3389)
|
|
-h, --help this text
|
|
|
|
The repository the guest mounts at /repo is the current checkout, or KOMOREBI_REPO.
|
|
EOF
|
|
}
|
|
|
|
# what the flake bakes in, and a guess for a run of the file by hand
|
|
guest=${RUN_KDE_GUEST:-run-kde-$(uname -m)}
|
|
runner_bin=${RUN_KDE_RUNNER_BIN:-}
|
|
|
|
# what this machine says about itself: each can be set by a test
|
|
kvm_device=${RUN_KDE_KVM_DEVICE:-/dev/kvm}
|
|
fuse_device=${RUN_KDE_FUSE_DEVICE:-/dev/fuse}
|
|
virtiofsd_bin=${RUN_KDE_VIRTIOFSD:-}
|
|
dxg_device=${RUN_KDE_DXG_DEVICE:-/dev/dxg}
|
|
dri_dir=${RUN_KDE_DRI_DIR:-/dev/dri}
|
|
meminfo=${RUN_KDE_MEMINFO:-/proc/meminfo}
|
|
cpuinfo=${RUN_KDE_CPUINFO:-/proc/cpuinfo}
|
|
osrelease=${RUN_KDE_OSRELEASE:-/proc/sys/kernel/osrelease}
|
|
sys_module_dir=${RUN_KDE_SYS_MODULE_DIR:-/sys/module}
|
|
store_dir=${RUN_KDE_STORE_DIR:-/nix}
|
|
wslconfig=${RUN_KDE_WSLCONFIG:-}
|
|
wsl_lib_dir=${RUN_KDE_WSL_LIB_DIR:-/usr/lib/wsl/lib}
|
|
va_drivers_dir=${RUN_KDE_VA_DRIVERS_DIR:-/run/opengl-driver/lib/dri}
|
|
|
|
mode=run
|
|
display=auto
|
|
gpu=auto
|
|
tcg=0
|
|
rdp=0
|
|
shell=0
|
|
memory=
|
|
cores=
|
|
size=20
|
|
resolution=1280x800
|
|
ssh_port=2223
|
|
rdp_port=3389
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--doctor) mode=doctor ;;
|
|
--dry-run) mode=dry-run ;;
|
|
--build-only) mode=build-only ;;
|
|
--reset) mode=reset ;;
|
|
--shell) shell=1 ;;
|
|
--rdp) rdp=1 ;;
|
|
--tcg) tcg=1 ;;
|
|
--display | --gpu | --memory | --cores | --size | --resolution | --ssh-port | --rdp-port)
|
|
[ $# -ge 2 ] || {
|
|
echo "run-kde: $1 needs a value" >&2
|
|
exit 2
|
|
}
|
|
case $1 in
|
|
--display) display=$2 ;;
|
|
--gpu) gpu=$2 ;;
|
|
--memory) memory=$2 ;;
|
|
--cores) cores=$2 ;;
|
|
--size) size=$2 ;;
|
|
--resolution) resolution=$2 ;;
|
|
--ssh-port) ssh_port=$2 ;;
|
|
--rdp-port) rdp_port=$2 ;;
|
|
esac
|
|
shift
|
|
;;
|
|
-h | --help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "run-kde: unknown option $1 (see --help)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
case $display in auto | gtk | vnc | rdp | none) ;; *)
|
|
echo "run-kde: --display is gtk, vnc, rdp or none" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
case $gpu in auto | on | off) ;; *)
|
|
echo "run-kde: --gpu is auto, on or off" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
for number in "$size" "$ssh_port" "$rdp_port" ${memory:+"$memory"} ${cores:+"$cores"}; do
|
|
case $number in '' | *[!0-9]*)
|
|
echo "run-kde: expected a number, got $number" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
case $resolution in [0-9]*x[0-9]*) ;; *)
|
|
echo "run-kde: --resolution is WIDTHxHEIGHT" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
if [ "$shell" = 1 ]; then
|
|
display=none
|
|
gpu=off
|
|
fi
|
|
|
|
state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/komorebi-run-kde
|
|
|
|
if [ "$mode" = reset ]; then
|
|
if [ -e "$state_dir" ]; then
|
|
rm -rf "$state_dir"
|
|
echo "run-kde: deleted $state_dir"
|
|
else
|
|
echo "run-kde: there is no $state_dir"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# what the machine is
|
|
|
|
wsl=0
|
|
if grep -qi microsoft "$osrelease" 2> /dev/null || [ -n "${WSL_DISTRO_NAME:-}" ]; then
|
|
wsl=1
|
|
fi
|
|
|
|
kvm=missing
|
|
if [ -e "$kvm_device" ]; then
|
|
if [ -r "$kvm_device" ] && [ -w "$kvm_device" ]; then kvm=ok; else kvm=denied; fi
|
|
fi
|
|
|
|
virt=none
|
|
if grep -qw svm "$cpuinfo" 2> /dev/null; then
|
|
virt=svm
|
|
elif grep -qw vmx "$cpuinfo" 2> /dev/null; then
|
|
virt=vmx
|
|
fi
|
|
|
|
have_window=0
|
|
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ -n "${DISPLAY:-}" ]; then have_window=1; fi
|
|
|
|
mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' "$meminfo" 2> /dev/null || true)
|
|
mem_mb=${mem_mb:-0}
|
|
cpus=${RUN_KDE_CPUS:-$(nproc 2> /dev/null || echo 1)}
|
|
|
|
# a device the host's GPU shows up as: WSL has /dev/dxg, a Linux machine a render node
|
|
gpu_device=
|
|
if [ "$wsl" = 1 ]; then
|
|
if [ -e "$dxg_device" ]; then gpu_device=$dxg_device; fi
|
|
else
|
|
for node in "$dri_dir"/renderD*; do
|
|
if [ -e "$node" ]; then
|
|
gpu_device=$node
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
host_renderer() {
|
|
if [ "${RUN_KDE_HOST_RENDERER+set}" = set ]; then
|
|
printf '%s\n' "$RUN_KDE_HOST_RENDERER"
|
|
elif [ -n "${DISPLAY:-}" ] && command -v glxinfo > /dev/null 2>&1; then
|
|
glxinfo -B 2> /dev/null | sed -n 's/^OpenGL renderer string: //p' | head -n 1 || true
|
|
elif [ -n "${WAYLAND_DISPLAY:-}" ] && command -v eglinfo > /dev/null 2>&1; then
|
|
eglinfo -B 2> /dev/null | sed -n 's/^OpenGL core profile renderer: //p' | head -n 1 || true
|
|
fi
|
|
}
|
|
|
|
free_gb() {
|
|
if [ -n "${RUN_KDE_FREE_GB:-}" ]; then
|
|
echo "$RUN_KDE_FREE_GB"
|
|
else
|
|
df -Pk "$1" 2> /dev/null | awk 'NR == 2 { print int($4 / 1048576) }' || echo 0
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# what the guest gets
|
|
|
|
guest_memory=$memory
|
|
if [ -z "$guest_memory" ]; then
|
|
guest_memory=$((mem_mb - 2048))
|
|
[ "$guest_memory" -ge 4096 ] || guest_memory=4096
|
|
[ "$guest_memory" -le 8192 ] || guest_memory=8192
|
|
fi
|
|
guest_cores=$cores
|
|
if [ -z "$guest_cores" ]; then
|
|
guest_cores=$cpus
|
|
[ "$guest_cores" -le 6 ] || guest_cores=6
|
|
fi
|
|
|
|
if [ "$display" = auto ]; then
|
|
if [ "$have_window" = 1 ]; then display=gtk; else display=rdp; fi
|
|
fi
|
|
|
|
renderer=$(host_renderer)
|
|
gpu_on=0
|
|
case $gpu in
|
|
off) gpu_why="turned off with --gpu off" ;;
|
|
on)
|
|
gpu_on=1
|
|
gpu_why="turned on with --gpu on"
|
|
;;
|
|
auto)
|
|
if [ "$display" != gtk ]; then
|
|
gpu_why="there is no window to show it in (the display is $display)"
|
|
elif [ -z "$gpu_device" ]; then
|
|
if [ "$wsl" = 1 ]; then
|
|
gpu_why="there is no $dxg_device, so WSL has no GPU"
|
|
else
|
|
gpu_why="there is no render node in $dri_dir"
|
|
fi
|
|
else
|
|
case $renderer in
|
|
*llvmpipe* | *softpipe* | *swrast*)
|
|
gpu_why="the host's OpenGL is software ($renderer)"
|
|
;;
|
|
*)
|
|
gpu_on=1
|
|
gpu_why="$gpu_device is there${renderer:+ and the OpenGL of the host is $renderer}"
|
|
;;
|
|
esac
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# the virtiofsd that the guest's runner starts for each of its shares (the runner names it by its
|
|
# store path), else the one on PATH
|
|
find_virtiofsd() {
|
|
local named=
|
|
if [ -n "$virtiofsd_bin" ]; then
|
|
printf '%s\n' "$virtiofsd_bin"
|
|
return 0
|
|
fi
|
|
if [ -n "$runner_bin" ] && [ -r "$runner_bin" ]; then
|
|
named=$(grep -o '/nix/store/[a-z0-9]*-virtiofsd-[^/ ]*/bin/virtiofsd' "$runner_bin" 2> /dev/null | head -n 1 || true)
|
|
fi
|
|
if [ -n "$named" ] && [ -x "$named" ]; then
|
|
printf '%s\n' "$named"
|
|
else
|
|
command -v virtiofsd || true
|
|
fi
|
|
}
|
|
|
|
# What the host's GPU needs of the process that uses it, on WSL 2. Mesa has to be told to take the
|
|
# d3d12 driver of Windows, whose libraries are in a directory of their own, and the libva that
|
|
# virglrenderer takes the host's video encoder from has to be told which driver that is. Without
|
|
# the first OpenGL is llvmpipe, and without the second the guest's VA-API lists no encoder, so KRDP
|
|
# encodes H.264 on the guest's CPUs. What a person has set is left as it is.
|
|
host_env=()
|
|
if [ "$wsl" = 1 ] && [ "$gpu_on" = 1 ]; then
|
|
host_env+=("GALLIUM_DRIVER=${GALLIUM_DRIVER:-d3d12}")
|
|
if [ -d "$wsl_lib_dir" ]; then
|
|
case ":${LD_LIBRARY_PATH:-}:" in
|
|
*":$wsl_lib_dir:"*) ;;
|
|
*) host_env+=("LD_LIBRARY_PATH=$wsl_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}") ;;
|
|
esac
|
|
fi
|
|
if [ -e "$va_drivers_dir/d3d12_drv_video.so" ]; then
|
|
host_env+=("LIBVA_DRIVER_NAME=${LIBVA_DRIVER_NAME:-d3d12}")
|
|
host_env+=("LIBVA_DRIVERS_PATH=${LIBVA_DRIVERS_PATH:-$va_drivers_dir}")
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the checks
|
|
|
|
errors=0
|
|
warnings=0
|
|
verbose=0
|
|
if [ "$mode" = doctor ]; then verbose=1; fi
|
|
|
|
ok() {
|
|
if [ "$verbose" = 1 ]; then printf ' ✓ %s\n' "$1"; fi
|
|
}
|
|
bad() {
|
|
errors=$((errors + 1))
|
|
printf ' ✗ %s\n' "$1"
|
|
shift
|
|
for line in "$@"; do printf ' %s\n' "$line"; done
|
|
}
|
|
warn() {
|
|
warnings=$((warnings + 1))
|
|
printf ' ! %s\n' "$1"
|
|
shift
|
|
for line in "$@"; do printf ' %s\n' "$line"; done
|
|
}
|
|
|
|
# The guest mounts its /nix/store and its shared directories by virtiofs: for each of them the
|
|
# runner starts a virtiofsd as this user, and QEMU hands the guest's virtio-fs device its socket.
|
|
# `wsl2-probe` (docs/dev/wsl2-probe.md, P6) runs the same chain with a guest; its fixes are these.
|
|
check_virtiofs() {
|
|
local virtiofsd tmp pid up line lines
|
|
# virtiofsd does not open /dev/fuse (it speaks FUSE to the guest over its socket): a warning
|
|
if [ ! -e "$fuse_device" ]; then
|
|
fuse_fix=("on NixOS, boot.kernelModules = [ \"fuse\" ];")
|
|
if [ "$wsl" = 1 ]; then
|
|
fuse_fix+=("the kernel of WSL 2 has FUSE built in, so a kernel= line in %UserProfile%\\.wslconfig under [wsl2]"
|
|
"that names another kernel is the likely cause")
|
|
fi
|
|
warn "FUSE: $fuse_device does not exist; virtiofsd does not use it, so the guest can still start" "${fuse_fix[@]}"
|
|
elif [ -r "$fuse_device" ] && [ -w "$fuse_device" ]; then
|
|
ok "FUSE: $fuse_device can be opened"
|
|
else
|
|
warn "FUSE: $fuse_device is there and this user cannot open it; virtiofsd does not use it, so the guest can still start" \
|
|
"on NixOS, services.udev.extraRules = ''KERNEL==\"fuse\", MODE=\"0666\"'';" \
|
|
"for now: sudo chmod 666 $fuse_device"
|
|
fi
|
|
|
|
virtiofsd=$(find_virtiofsd)
|
|
if [ -z "$virtiofsd" ]; then
|
|
bad "virtiofsd: the program is not there, and the guest mounts its /nix/store through it" \
|
|
"start run-kde as nix run .#run-kde, which brings the virtiofsd of nixpkgs"
|
|
return 0
|
|
fi
|
|
# the options of nixpkgs' runner, for a directory that is empty
|
|
tmp=$(mktemp -d)
|
|
RUST_LOG=error "$virtiofsd" --socket-path="$tmp/socket" --shared-dir="$tmp" --readonly \
|
|
--sandbox=none --seccomp=none --cache=always --no-announce-submounts \
|
|
--translate-uid=host:65534:0:1 --translate-gid=host:65534:0:1 > "$tmp/log" 2>&1 < /dev/null &
|
|
pid=$!
|
|
up=0
|
|
for _ in $(seq 1 50); do
|
|
if [ -S "$tmp/socket" ]; then
|
|
up=1
|
|
break
|
|
fi
|
|
kill -0 "$pid" 2> /dev/null || break
|
|
sleep 0.1
|
|
done
|
|
kill "$pid" 2> /dev/null || true
|
|
wait "$pid" 2> /dev/null || true
|
|
if [ "$up" = 1 ]; then
|
|
ok "virtiofsd: starts as this user and serves a directory"
|
|
else
|
|
lines=()
|
|
while IFS= read -r line; do lines+=("virtiofsd said: $line"); done < <(head -n 6 "$tmp/log")
|
|
bad "virtiofsd: does not start as this user, and the guest mounts its /nix/store through it" \
|
|
"${lines[@]}" \
|
|
"virtiofsd ran with the options of the guest's runner and could not serve a directory: the lines above say why." \
|
|
"A socket path is 107 bytes at most, so a long \$TMPDIR is one cause: TMPDIR=/tmp run-kde" \
|
|
"Say what virtiofsd printed when you ask for help: the guest cannot start without it"
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
check_everything() {
|
|
if [ "$wsl" = 1 ]; then
|
|
ok "this is WSL 2 ($(cat "$osrelease" 2> /dev/null || echo unknown))"
|
|
else
|
|
ok "this is Linux, not WSL 2 ($(cat "$osrelease" 2> /dev/null || echo unknown))"
|
|
fi
|
|
|
|
if command -v nix > /dev/null 2>&1; then
|
|
ok "nix is installed"
|
|
else
|
|
bad "nix is not installed" "install Nix with flakes enabled: https://nixos.org/download"
|
|
fi
|
|
|
|
case $kvm in
|
|
ok) ok "KVM: $kvm_device can be used" ;;
|
|
denied)
|
|
bad "KVM: $kvm_device is there and this user cannot open it" \
|
|
"put this user in the group kvm: on NixOS, users.users.<you>.extraGroups = [ \"kvm\" ];" \
|
|
"then open a new login (on WSL: run \`wsl --shutdown\` in PowerShell and start it again)"
|
|
;;
|
|
missing)
|
|
kvm_fix=()
|
|
if [ "$virt" = none ]; then
|
|
kvm_fix+=("the CPU's virtualization flags (svm or vmx) are not visible here, so nested virtualization is off")
|
|
if [ "$wsl" = 1 ]; then
|
|
kvm_fix+=("in %UserProfile%\\.wslconfig, under [wsl2], set: nestedVirtualization=true")
|
|
fi
|
|
kvm_fix+=("and turn on SVM (AMD) or VT-x (Intel) in the firmware setup of the machine")
|
|
else
|
|
kvm_fix+=("the CPU has $virt, so the kernel has no KVM or has not loaded it")
|
|
if [ "$virt" = svm ] && [ ! -e "$sys_module_dir/kvm_amd" ]; then
|
|
kvm_fix+=("kvm_amd is not loaded: modprobe kvm_amd (on NixOS, boot.kernelModules = [ \"kvm-amd\" ];)")
|
|
elif [ "$virt" = vmx ] && [ ! -e "$sys_module_dir/kvm_intel" ]; then
|
|
kvm_fix+=("kvm_intel is not loaded: modprobe kvm_intel (on NixOS, boot.kernelModules = [ \"kvm-intel\" ];)")
|
|
fi
|
|
if [ "$wsl" = 1 ]; then
|
|
kvm_fix+=("in %UserProfile%\\.wslconfig, under [wsl2], set: nestedVirtualization=true, then run \`wsl --shutdown\`")
|
|
fi
|
|
fi
|
|
if [ "$tcg" = 1 ]; then
|
|
warn "KVM: $kvm_device does not exist; --tcg lets the guest start without it, and Plasma is too slow to try anything that way" "${kvm_fix[@]}"
|
|
else
|
|
bad "KVM: $kvm_device does not exist, and the guest needs it" "${kvm_fix[@]}"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
check_virtiofs
|
|
|
|
if [ "$mem_mb" -lt 5120 ]; then
|
|
bad "memory: this machine has $mem_mb MB, and the guest needs at least 4096 MB of it" \
|
|
"in %UserProfile%\\.wslconfig, under [wsl2], set: memory=12GB (WSL gets half of the machine's memory by default)"
|
|
elif [ "$mem_mb" -lt 10240 ] && [ -z "$memory" ]; then
|
|
warn "memory: this machine has $mem_mb MB, so the guest gets $guest_memory MB (8192 is comfortable)" \
|
|
"in %UserProfile%\\.wslconfig, under [wsl2], set: memory=12GB"
|
|
else
|
|
ok "memory: this machine has $mem_mb MB, and the guest gets $guest_memory MB"
|
|
fi
|
|
ok "cores: this machine has $cpus, and the guest gets $guest_cores"
|
|
|
|
store_free=$(free_gb "$store_dir")
|
|
state_parent=${state_dir%/*}
|
|
[ -d "$state_parent" ] || state_parent=$HOME
|
|
state_free=$(free_gb "$state_parent")
|
|
if [ "$store_free" -lt 12 ]; then
|
|
bad "disk: $store_free GB are free for the Nix store ($store_dir), and the guest's closure is about 11 GB" \
|
|
"free some room (nix-collect-garbage -d), or give WSL's disk more: \`wsl --manage <distro> --resize <size>\`"
|
|
elif [ "$store_free" -lt 25 ]; then
|
|
warn "disk: $store_free GB are free for the Nix store ($store_dir): the first run needs about 16 GB" \
|
|
"nix-collect-garbage -d frees room"
|
|
else
|
|
ok "disk: $store_free GB are free for the Nix store ($store_dir)"
|
|
fi
|
|
if [ "$state_free" -lt "$size" ]; then
|
|
warn "disk: $state_free GB are free for the guest's disk in $state_dir, which can grow to $size GB"
|
|
fi
|
|
|
|
if [ "$have_window" = 1 ]; then
|
|
window=
|
|
if [ -n "${WAYLAND_DISPLAY:-}" ]; then window="WAYLAND_DISPLAY=$WAYLAND_DISPLAY"; fi
|
|
if [ -n "${DISPLAY:-}" ]; then window="${window:+$window }DISPLAY=$DISPLAY"; fi
|
|
ok "a window can be shown ($window)"
|
|
elif [ "$wsl" = 1 ]; then
|
|
warn "WSLg is not there (neither WAYLAND_DISPLAY nor DISPLAY is set), so the guest is reached over RDP" \
|
|
"WSLg comes with WSL 2 on Windows 11: run \`wsl --update\` in PowerShell, and do not set guiApplications=false in .wslconfig"
|
|
else
|
|
warn "there is no window to show the guest in (neither WAYLAND_DISPLAY nor DISPLAY is set), so the guest is reached over RDP"
|
|
fi
|
|
|
|
if [ "$wsl" = 1 ]; then
|
|
if command -v mstsc.exe > /dev/null 2>&1 && command -v cmdkey.exe > /dev/null 2>&1; then
|
|
ok "Windows interop: mstsc.exe and cmdkey.exe can be run, for --rdp"
|
|
else
|
|
warn "Windows interop: mstsc.exe or cmdkey.exe cannot be run, which --rdp needs" \
|
|
"on NixOS-WSL, wsl.interop.includePath = true; (and wsl.interop.register = true;)"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$gpu_device" ]; then
|
|
ok "GPU: $gpu_device is there"
|
|
elif [ "$wsl" = 1 ]; then
|
|
warn "GPU: there is no $dxg_device, so the guest renders on its CPUs" \
|
|
"install the Windows GPU driver with WSL support; on NixOS-WSL, wsl.useWindowsDriver = true;"
|
|
else
|
|
warn "GPU: there is no render node in $dri_dir, so the guest renders on its CPUs"
|
|
fi
|
|
if [ -n "$gpu_device" ]; then
|
|
case $renderer in
|
|
'') warn "GPU: the host's OpenGL renderer is not known (glxinfo and eglinfo say nothing)" ;;
|
|
*llvmpipe* | *softpipe* | *swrast*)
|
|
warn "GPU: the host's OpenGL is software ($renderer), so the guest renders on its CPUs" \
|
|
"on NixOS-WSL, wsl.useWindowsDriver = true; gives Mesa the Windows driver (d3d12)"
|
|
;;
|
|
*) ok "GPU: the host's OpenGL renderer is $renderer" ;;
|
|
esac
|
|
fi
|
|
if [ "$wsl" = 1 ] && [ -n "$gpu_device" ]; then
|
|
if [ -d "$wsl_lib_dir" ]; then
|
|
ok "GPU: the Windows driver is in $wsl_lib_dir"
|
|
else
|
|
warn "GPU: there is no $wsl_lib_dir, so Mesa cannot take the Windows driver (d3d12)" \
|
|
"run \`wsl --update\` in PowerShell, and install the Windows GPU driver with WSL support"
|
|
fi
|
|
if [ -e "$va_drivers_dir/d3d12_drv_video.so" ]; then
|
|
ok "GPU: the video driver of Mesa for it (d3d12) is in $va_drivers_dir"
|
|
else
|
|
warn "GPU: there is no d3d12 video driver in $va_drivers_dir, so KRDP encodes H.264 on the guest's CPUs" \
|
|
"on NixOS-WSL, wsl.useWindowsDriver = true; with a Mesa that is built with video codecs"
|
|
fi
|
|
fi
|
|
if [ "$gpu_on" = 1 ]; then
|
|
ok "the guest will use the GPU: $gpu_why"
|
|
else
|
|
ok "the guest will not use the GPU: $gpu_why"
|
|
fi
|
|
|
|
if [ "$wsl" = 1 ]; then
|
|
if [ -z "$wslconfig" ] && command -v cmd.exe > /dev/null 2>&1 && command -v wslpath > /dev/null 2>&1; then
|
|
profile=$(cmd.exe /C 'echo %USERPROFILE%' 2> /dev/null | tr -d '\r' || true)
|
|
if [ -n "$profile" ]; then wslconfig=$(wslpath -u "$profile")/.wslconfig; fi
|
|
fi
|
|
if [ -n "$wslconfig" ] && [ -r "$wslconfig" ]; then
|
|
if grep -qi '^ *networkingMode *= *mirrored' "$wslconfig"; then
|
|
warn "network: .wslconfig has networkingMode=mirrored, which may not forward localhost to the guest's RDP" \
|
|
"ssh and the window do not depend on it; if RDP does not connect, remove that line and run \`wsl --shutdown\`"
|
|
else
|
|
ok "network: $wslconfig does not set mirrored networking"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
if [ "$mode" = doctor ] || [ "$mode" = run ] || [ "$mode" = build-only ]; then
|
|
if [ "$mode" = doctor ]; then echo "run-kde: what the guest needs, and what this machine has"; fi
|
|
check_everything
|
|
if [ "$errors" -gt 0 ]; then
|
|
echo "run-kde: $errors thing(s) to fix first" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$mode" = doctor ]; then
|
|
echo "run-kde: $errors error(s), $warnings warning(s)"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the guest
|
|
|
|
repo=${KOMOREBI_REPO:-}
|
|
if [ -z "$repo" ]; then
|
|
repo=$(git rev-parse --show-toplevel 2> /dev/null || pwd)
|
|
fi
|
|
if [ ! -e "$repo/flake.nix" ]; then
|
|
echo "run-kde: $repo is not a checkout of the repository (there is no flake.nix): run it from one, or set KOMOREBI_REPO" >&2
|
|
exit 1
|
|
fi
|
|
|
|
disk=$state_dir/disk.qcow2
|
|
|
|
# the arguments of the runner, and the environment it reads (docs/dev/utm.md says what each does)
|
|
if [ "$gpu_on" = 1 ]; then gpu_device_name=virtio-vga-gl; else gpu_device_name=virtio-vga; fi
|
|
width=${resolution%x*}
|
|
height=${resolution#*x}
|
|
qemu_opts=()
|
|
if [ "$tcg" != 1 ] || [ "$kvm" = ok ]; then qemu_opts+=(-cpu host); fi
|
|
case $display in
|
|
gtk)
|
|
if [ "$gpu_on" = 1 ]; then qemu_opts+=(-display "gtk,gl=on"); else qemu_opts+=(-display gtk); fi
|
|
;;
|
|
vnc) qemu_opts+=(-display none -vnc 127.0.0.1:0) ;;
|
|
rdp | none)
|
|
if [ "$gpu_on" = 1 ]; then qemu_opts+=(-display egl-headless); else qemu_opts+=(-display none); fi
|
|
;;
|
|
esac
|
|
qemu_opts+=(-vga none -device "$gpu_device_name,xres=$width,yres=$height")
|
|
net_opts="hostfwd=tcp:127.0.0.1:$ssh_port-:22,hostfwd=tcp:127.0.0.1:$rdp_port-:3389"
|
|
runner_args=(-m "$guest_memory" -smp "$guest_cores")
|
|
|
|
if [ "$wsl" = 1 ]; then host_kind="WSL 2"; else host_kind=Linux; fi
|
|
if [ "$kvm" = ok ]; then kvm_word=yes; else kvm_word=no; fi
|
|
if [ "$gpu_on" = 1 ]; then
|
|
gpu_word=on
|
|
gpu_label="GPU (virgl)"
|
|
else
|
|
gpu_word=off
|
|
gpu_label="software (llvmpipe)"
|
|
fi
|
|
|
|
if [ "$mode" = dry-run ]; then
|
|
echo "run-kde would start $guest:"
|
|
echo " host: $host_kind, KVM $kvm_word"
|
|
echo " display: $display"
|
|
echo " gpu: $gpu_word, because $gpu_why"
|
|
echo " environment:"
|
|
echo " NIX_DISK_IMAGE=$disk"
|
|
echo " KOMOREBI_REPO=$repo"
|
|
echo " RUN_KDE_STATE=$state_dir"
|
|
echo " RUN_KDE_MEMORY=$guest_memory"
|
|
echo " QEMU_NET_OPTS=$net_opts"
|
|
echo " QEMU_OPTS=${qemu_opts[*]}"
|
|
for var in "${host_env[@]}"; do echo " $var"; done
|
|
echo " command: ${runner_bin:-<the runner, built from $repo>} ${runner_args[*]}"
|
|
exit 0
|
|
fi
|
|
|
|
# the guest's runner, built when it is not there. RUN_KDE_QEMU_OPTS adds to the QEMU options.
|
|
if [ -z "$runner_bin" ] || [ ! -x "$runner_bin" ]; then
|
|
echo "run-kde: building the guest ($guest). There is no binary cache for komorebi: the first run"
|
|
echo "run-kde: builds it from source and downloads about 4 GB of Plasma (11 GB in the Nix store), which takes a while."
|
|
runner=$(nix build --no-link --print-out-paths -L "$repo#nixosConfigurations.$guest.config.system.build.vm")
|
|
set -- "$runner"/bin/run-*-vm
|
|
runner_bin=$1
|
|
if [ ! -x "$runner_bin" ]; then
|
|
echo "run-kde: $runner has no runner in bin" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
if [ "$mode" = build-only ]; then
|
|
echo "run-kde: the guest is built: $runner_bin"
|
|
exit 0
|
|
fi
|
|
|
|
# the state: what a run keeps, and what the guest takes from it. Nothing of it is in the Nix store.
|
|
umask 077
|
|
mkdir -p "$state_dir"
|
|
chmod 700 "$state_dir"
|
|
if [ ! -s "$state_dir/rdp-password" ]; then
|
|
head -c 15 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$state_dir/rdp-password"
|
|
fi
|
|
if [ ! -s "$state_dir/ssh_key" ]; then
|
|
rm -f "$state_dir/ssh_key" "$state_dir/ssh_key.pub"
|
|
ssh-keygen -q -t ed25519 -N '' -C run-kde -f "$state_dir/ssh_key"
|
|
fi
|
|
if [ ! -e "$disk" ]; then
|
|
echo "run-kde: making the guest's disk: $size GB, which takes room as the guest writes to it"
|
|
raw=$state_dir/disk.raw
|
|
qemu-img create -q -f raw "$raw" "${size}G"
|
|
mkfs.ext4 -q -L nixos "$raw"
|
|
qemu-img convert -f raw -O qcow2 "$raw" "$disk"
|
|
rm -f "$raw"
|
|
fi
|
|
password=$(cat "$state_dir/rdp-password")
|
|
|
|
case $display in
|
|
gtk) sees="a window of QEMU, with the guest's screen in it" ;;
|
|
vnc) sees="nothing here: connect a VNC client to 127.0.0.1:5900, or RDP below" ;;
|
|
rdp) sees="nothing here: connect over RDP, below" ;;
|
|
none) sees="nothing but this terminal, which is the guest's console" ;;
|
|
esac
|
|
cat <<EOF
|
|
|
|
run-kde: starting $guest
|
|
You see $sees
|
|
Graphics $gpu_label: $gpu_why
|
|
the guest says what KWin draws with in its cheat sheet, and here once it is up
|
|
RDP 127.0.0.1:$rdp_port user komorebi password $password
|
|
ssh ssh -i $state_dir/ssh_key -p $ssh_port komorebi@127.0.0.1
|
|
Repository $repo is /repo in the guest
|
|
Quit run \`sudo poweroff\` in the guest, or press Ctrl-a x in this terminal
|
|
State $state_dir (run-kde --reset deletes it)
|
|
The guest takes about a minute to start. This terminal is the guest's console.
|
|
|
|
EOF
|
|
|
|
export NIX_DISK_IMAGE=$disk
|
|
export KOMOREBI_REPO=$repo
|
|
export RUN_KDE_STATE=$state_dir
|
|
export RUN_KDE_MEMORY=$guest_memory
|
|
export QEMU_NET_OPTS=$net_opts
|
|
export QEMU_OPTS="${qemu_opts[*]}${RUN_KDE_QEMU_OPTS:+ $RUN_KDE_QEMU_OPTS}"
|
|
for var in "${host_env[@]}"; do
|
|
# shellcheck disable=SC2163 # the variable named by what `var` holds, as NAME=value
|
|
export "$var"
|
|
done
|
|
# virtiofsd says a great deal about being run by a user and not by root, none of it a problem
|
|
export RUST_LOG=${RUST_LOG:-error}
|
|
|
|
# Windows' own RDP client, once the guest has a session: its credentials are made for this run and
|
|
# removed again at the end
|
|
rdp_helper=
|
|
cleanup() {
|
|
if [ -n "$rdp_helper" ]; then
|
|
kill "$rdp_helper" 2> /dev/null || true
|
|
cmdkey.exe /delete:TERMSRV/localhost > /dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
if [ "$rdp" = 1 ]; then
|
|
if [ "$wsl" = 1 ] && command -v mstsc.exe > /dev/null 2>&1 && command -v cmdkey.exe > /dev/null 2>&1; then
|
|
trap cleanup EXIT
|
|
(
|
|
for _ in $(seq 1 300); do
|
|
if ssh -i "$state_dir/ssh_key" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=3 \
|
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
|
|
-p "$ssh_port" komorebi@127.0.0.1 'systemctl --user is-active --quiet krdp' 2> /dev/null; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
cmdkey.exe /generic:TERMSRV/localhost /user:komorebi "/pass:$password" > /dev/null
|
|
mstsc.exe "/v:localhost:$rdp_port"
|
|
) &
|
|
rdp_helper=$!
|
|
else
|
|
echo "run-kde: --rdp needs WSL 2 with mstsc.exe and cmdkey.exe (run-kde --doctor says how); use an RDP client of your own with the address above" >&2
|
|
fi
|
|
fi
|
|
|
|
"$runner_bin" "${runner_args[@]}"
|