Graphics in Qemu
Performance Impact of SPICE+VirGL
I have been using libvirt (virt-manager) for years, until recently I tried to run Linux VMs with Qemu directly using virgl.
Running a Linux VM with Qemu's SDL display is much smoother than SPICE+VirGL solution provided by virt-manager, it made me realize that SPICE actually has an impact on graphics performance in VMs.
SDL
I copied Qemu parameters from Quickemu project, which works for both Linux and Windows.
If you use SDL under wayland, you probably need to tell SDL to use wayland backend:
export SDL_VIDEODRIVER=wayland
There's another feature that you are going to lose if you use SDL instead of SPICE: copy/paste between host and guests. This can be partially solved with file system sharing
File System Sharing
We can use virtio-9p:
-fsdev local,id=fsdev0,path="$HOME/Public",security_model=mapped-xattr \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=host_share
Then mount it in guest:
sudo mount -t 9p -o trans=virtio host_share /mnt
You can also write it in /etc/fstab
so it automatically mounts on start up
See https://superuser.com/questions/502205/libvirt-9p-kvm-mount-in-fstab-fails-to-mount-at-boot-time
/data /data 9p trans=virtio,rw,_netdev 0 0
You can also add needed modules to your initramfs
9p
9pnet
9pnet_virtio
And write to fstab like this:
# src_mnt is the mount tag
src_mnt /src 9p trans=virtio 0 0
Snapshot
Leaving the original qcow2 disk image untouched, you can:
qemu-img create -f qcow2 -b "$vdisk_file" -F qcow2 current.img
And use current.img
in your qemu
command
When you need to delete the snapshot (to merge it into the original disk image):
merge_current_snapshot() {
info "Merging current snapshot to $vdisk_file"
qemu-img rebase -b "$vdisk_file" -f qcow2 -F qcow2 "$current"
qemu-img commit "$current"
}
Networking
Use libvirt
This requires you to install and enable libvirt service, the benefit of this is you don't need to configure NAT and stuff by yourself, just use what's provided by libvirtd.
Remember to sudo systemctl enable --now libvirtd
, assuming your default network interface is virbr0
:
-device virtio-net-pci,netdev=n1 \
-netdev tap,id=n1,br=virbr0,"helper=/usr/lib/qemu/qemu-bridge-helper",vhost=on
qemu-bridge-helper
can help with creating TAP devices, you don't need root to run qemu
as the helper has SUID.
NOTE qemu uses the same MAC address for all guests by default, if you want them to communicate with each other you will need to change that. To generate a random MAC address:
printf -v mac "52:54:%02x:%02x:%02x:%02x" $((RANDOM & 0xff)) $((RANDOM & 0xff)) $((RANDOM & 0xff)) $((RANDOM & 0xff))
According to Arch Wiki, you can also generate a fixed unique MAC address for each guest, with this script you can make sure the MAC address doesn't change during relaunch
#!/usr/bin/env python
# usage: qemu-mac-hasher.py <VMName>
import sys
import zlib
crc = str(hex(zlib.crc32(sys.argv[1].encode("utf-8")))).replace("x", "")[-8:]
print("52:54:%s%s:%s%s:%s%s:%s%s" % tuple(crc))
We can invoke the Python code in our bash wrapper:
vm_name="ubuntu22.04"
macaddr="$(python3 -c "import zlib;crc=str(hex(zlib.crc32('$vm_name'.encode('utf-8'))).replace('x', '')[-8:]);print('52:54:%s%s:%s%s:%s%s:%s%s'%tuple(crc))")"
How to get guest IP when it starts
This is extremely useful when you want to connect to your Windows VM via RDP (which I recommend doing since the performance is really promising)
fetch_guest_ip() {
info "Trying to obtain IP address of guest $vm_name..."
while true; do
guest_ip=$(ip neigh show dev virbr0 | grep "$mac" | grep -o -P "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
[[ -n "$guest_ip" ]] && {
info "IP of $vm_name is $guest_ip"
break
}
sleep 1
done
[[ "$is_windows" -eq 1 ]] && {
info "Connecting to $guest_ip via RDP..."
# use /h:1080 /w:1920 if you don't want full screen
# change `scale` to 180 if it still looks small
wlfreerdp /u:user /p:password /v:"$guest_ip" /scale:140 +clipboard +aero /f
}
}
Example Wrapper Scripts
Initialization
self="$0"
vdisk_file="$1"
vm_name="$(echo "$vdisk_file" | cut -d '.' -f1)"
current="current.img"
[[ -f macaddr.txt ]] || {
printf -v mac "52:54:%02x:%02x:%02x:%02x" $((RANDOM & 0xff)) $((RANDOM & 0xff)) $((RANDOM & 0xff)) $((RANDOM & 0xff))
echo -ne "$mac" | tee macaddr.txt
}
is_windows=0
echo "$vm_name" | grep -i windows >/dev/null && is_windows=1
guest_ip=""
mac=$(cat macaddr.txt)
qemu_bridge_helper="/usr/libexec/qemu-bridge-helper"
[[ -f "$qemu_bridge_helper" ]] || qemu_bridge_helper="/usr/lib/qemu/qemu-bridge-helper"
[[ -f "$qemu_bridge_helper" ]] || err "$qemu_bridge_helper not found"
err() {
echo -e "\e[31m$1\e[0m"
exit 1
}
info() {
echo -e "\e[36m$1\e[0m"
}
[[ -d share ]] || mkdir share
ip link | grep virbr0 || {
info "libvirtd not running?"
sudo systemctl status libvirtd
sudo systemctl restart libvirtd
}
[[ "$#" -ge 1 ]] || err "$self <disk.qcow2> <snapshot/origin/win>"
command -v qemu-system-x86_64 >/dev/null || err "qemu-system-x86_64 not found"
export SDL_VIDEODRIVER=wayland
spice_port=$((3000 + RANDOM % 5000))
addr="/run/user/1000/spice-$spice_port.sock"
spice_addr="spice+unix://$addr"
Linux VM With UEFI
uefi_with_virgl_sdl() {
disk="$current"
[[ -n "$1" ]] && disk="$1"
qemu-system-x86_64 -name "$vm_name",process="$vm_name" \
-pidfile "$vm_name.pid" \
-enable-kvm \
-machine q35,smm=off,vmport=off \
-cpu host,kvm=on,topoext \
-smp cores=4,threads=2,sockets=1 \
-m 4G \
-device virtio-balloon \
-vga none \
-display sdl,gl=on \
-device virtio-vga-gl,xres=1920,yres=1080 \
-audiodev pa,id=audio0 \
-device intel-hda \
-device hda-duplex,audiodev=audio0 \
-rtc base=localtime,clock=host,driftfix=slew \
-device virtio-rng-pci,rng=rng0 \
-object rng-random,id=rng0,filename=/dev/urandom \
-device qemu-xhci,id=spicepass \
-chardev spicevmc,id=usbredirchardev1,name=usbredir \
-device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
-chardev spicevmc,id=usbredirchardev2,name=usbredir \
-device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
-chardev spicevmc,id=usbredirchardev3,name=usbredir \
-device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
-device pci-ohci,id=smartpass \
-device usb-ccid \
-chardev spicevmc,id=ccid,name=smartcard \
-device ccid-card-passthru,chardev=ccid \
-device usb-ehci,id=input \
-device usb-kbd,bus=input.0 \
-k en-us \
-device usb-tablet,bus=input.0 \
-nic tap,id=n1,br=virbr0,model=virtio-net-pci,helper="$qemu_bridge_helper",vhost=on,mac="$mac" \
-device virtio-blk-pci,drive=SystemDisk \
-drive id=SystemDisk,if=none,format=qcow2,file="$disk" \
-fsdev local,id=fsdev0,path="$PWD/share",security_model=mapped-xattr \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=host_share \
-monitor unix:"$vm_name-monitor.socket",server,nowait \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
-serial unix:"$vm_name-serial.socket",server,nowait
# -drive media=cdrom,index=0,file="kali-linux-2023-W22-installer-amd64.iso" \
}
Linux VM Without UEFI
start_with_virgl_sdl() {
disk="$current"
[[ -n "$1" ]] && disk="$1"
qemu-system-x86_64 -name "$vm_name",process="$vm_name" \
-pidfile "$vm_name.pid" \
-enable-kvm \
-machine q35,smm=off,vmport=off \
-cpu host,kvm=on,topoext \
-smp cores=4,threads=2,sockets=1 \
-m 4G \
-device virtio-balloon \
-vga none \
-device virtio-vga-gl,xres=1920,yres=1080 \
-display sdl,gl=on \
-audiodev pa,id=audio0 \
-device intel-hda \
-device hda-duplex,audiodev=audio0 \
-rtc base=localtime,clock=host,driftfix=slew \
-device virtio-rng-pci,rng=rng0 \
-object rng-random,id=rng0,filename=/dev/urandom \
-device qemu-xhci,id=spicepass \
-chardev spicevmc,id=usbredirchardev1,name=usbredir \
-device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
-chardev spicevmc,id=usbredirchardev2,name=usbredir \
-device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
-chardev spicevmc,id=usbredirchardev3,name=usbredir \
-device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
-device pci-ohci,id=smartpass \
-device usb-ccid \
-chardev spicevmc,id=ccid,name=smartcard \
-device ccid-card-passthru,chardev=ccid \
-device usb-ehci,id=input \
-device usb-kbd,bus=input.0 \
-k en-us \
-device usb-tablet,bus=input.0 \
-nic tap,id=n1,br=virbr0,model=virtio-net-pci,helper="$qemu_bridge_helper",vhost=on,mac="$mac" \
-device virtio-blk-pci,drive=SystemDisk \
-drive id=SystemDisk,if=none,format=qcow2,file="$disk" \
-fsdev local,id=fsdev0,path="$PWD/share",security_model=mapped-xattr \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=host_share \
-monitor unix:"$vm_name-monitor.socket",server,nowait \
-serial unix:"$vm_name-serial.socket",server,nowait
# -drive media=cdrom,index=0,file="kali-linux-2023-W22-installer-amd64.iso" \
}
Windows VMs
start_windows_headless_uefi() {
disk="$current"
[[ -n "$1" ]] && disk="$1"
qemu-system-x86_64 -name "$vm_name",process="$vm_name" \
-pidfile "$vm_name.pid" \
-enable-kvm \
-machine q35,smm=off,vmport=off \
-cpu host,kvm=on,+hypervisor,+invtsc,l3-cache=on,migratable=no,hv_passthrough,topoext \
-smp cores=4,threads=2,sockets=1 \
-m 4G \
-device virtio-balloon \
-device virtio-vga,xres=1920,yres=1080 \
-display none \
-spice port="$spice_port",disable-ticketing=on \
-audiodev pa,id=audio0 \
-device intel-hda \
-device hda-duplex,audiodev=audio0 \
-rtc base=localtime,clock=host,driftfix=slew \
-device virtio-rng-pci,rng=rng0 \
-object rng-random,id=rng0,filename=/dev/urandom \
-device qemu-xhci,id=spicepass \
-chardev spicevmc,id=usbredirchardev1,name=usbredir \
-device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \
-chardev spicevmc,id=usbredirchardev2,name=usbredir \
-device usb-redir,chardev=usbredirchardev2,id=usbredirdev2 \
-chardev spicevmc,id=usbredirchardev3,name=usbredir \
-device usb-redir,chardev=usbredirchardev3,id=usbredirdev3 \
-device pci-ohci,id=smartpass \
-device usb-ccid \
-chardev spicevmc,id=ccid,name=smartcard \
-device ccid-card-passthru,chardev=ccid \
-device usb-ehci,id=input \
-device usb-kbd,bus=input.0 \
-k en-us \
-device usb-tablet,bus=input.0 \
-nic tap,id=n1,br=virbr0,model=virtio-net-pci,helper="$qemu_bridge_helper",vhost=on,mac="$mac" \
-device virtio-blk-pci,drive=SystemDisk \
-drive id=SystemDisk,if=none,format=qcow2,file="$disk" \
-fsdev local,id=fsdev0,path="$PWD/share",security_model=mapped-xattr \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=host_share \
-monitor unix:"$vm_name-monitor.socket",server,nowait \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
-serial unix:"$vm_name-serial.socket",server,nowait
# -drive media=cdrom,index=0,file="windows.iso" \
# -drive media=cdrom,index=1,file="virtio-win.iso" \
}
Comments
comments powered by Disqus