From: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC qemu-server 05/13] add experimental kyber-gl display
Date: Tue, 25 Aug 2026 13:34:30 +0200 [thread overview]
Message-ID: <20260825113442.947620-6-alexandre.derumier@groupe-cyllene.com> (raw)
In-Reply-To: <20260825113442.947620-1-alexandre.derumier@groupe-cyllene.com>
kyber-gl display is mapping on vhost-user-gpu-pci.
virgl in a helper process rather than inside QEMU: it exports a dmabuf, so QEMU
emits ScanoutDMABUF2 and the encoder imports it instead of reading pixels back.
Currently experimental because of detected bugs:
- tried vhost-user-gpu instead vhost-user-gpu-pci, but deadlock intermittently
on QEMU 11.0.2, blocking the main thread under the BQL,
so the guest freezes rather than just the stream.
- Modes whose stride is not a multiple of 256 - 800x600 and 1360x768 - fail
to import on radeonsi and are never streamed. The rest import once the
retry is made with an explicit DRM_FORMAT_MOD_LINEAR, QEMU labelling its
scanouts DRM_FORMAT_MOD_INVALID; those two need a copy instead.
- The picture is flipped vertically through the kernel and plymouth phase,
while GRUB and the desktop are both correct, so the origin changes twice
within a session and y0_top does not track it.
Signed-off-by: Alexandre Derumier <alexandre.derumier@groupe-cyllene.com>
---
src/PVE/API2/Qemu.pm | 9 ++-
src/PVE/QemuServer.pm | 29 ++++++--
src/PVE/QemuServer/Kyber.pm | 4 +-
src/PVE/QemuServer/Makefile | 1 +
src/PVE/QemuServer/VhostUserGPU.pm | 103 +++++++++++++++++++++++++++++
src/test/cfg2cmd/kyber-gl.conf | 3 +
src/test/cfg2cmd/kyber-gl.conf.cmd | 29 ++++++++
7 files changed, 169 insertions(+), 9 deletions(-)
create mode 100644 src/PVE/QemuServer/VhostUserGPU.pm
create mode 100644 src/test/cfg2cmd/kyber-gl.conf
create mode 100644 src/test/cfg2cmd/kyber-gl.conf.cmd
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index bae345b..79aab86 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -3371,9 +3371,10 @@ __PACKAGE__->register_method({
my $conf = PVE::QemuConfig->load_config($vmid, $node);
my $vga = PVE::QemuServer::parse_vga($conf->{vga} // '');
+ my $vga_type = $vga->{type} // '';
die "VM $vmid is not configured for the Kyber console"
- . " - set its display to 'kyber' and restart it\n"
- if ($vga->{type} // '') ne 'kyber';
+ . " - set its display to 'kyber' or 'kyber-gl' and restart it\n"
+ if $vga_type !~ /^kyber(?:-gl)?$/;
die "VM $vmid is not running\n" if !PVE::QemuServer::Helpers::vm_running_locally($vmid);
@@ -3397,7 +3398,9 @@ __PACKAGE__->register_method({
# Only 'vnc' adds the vdagent chardev the guest needs to share a clipboard.
my $clipboard = ($vga->{clipboard} // '') eq 'vnc';
- PVE::QemuServer::Kyber::write_env($vmid, $secret, $port, $clipboard);
+ my $dmabuf = $vga_type eq 'kyber-gl';
+
+ PVE::QemuServer::Kyber::write_env($vmid, $secret, $port, $clipboard, $dmabuf);
PVE::QemuServer::Kyber::restart_controller($vmid);
}
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 33a5e40..652f3ea 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -97,6 +97,7 @@ use PVE::QemuServer::RNG qw(parse_rng print_rng_device_commandline print_rng_obj
use PVE::QemuServer::RunState;
use PVE::QemuServer::StateFile;
use PVE::QemuServer::USB;
+use PVE::QemuServer::VhostUserGPU;
use PVE::QemuServer::Virtiofs qw(max_virtiofs start_all_virtiofsd);
use PVE::QemuServer::VolumeChain;
use PVE::QemuServer::DBusDisplay;
@@ -171,7 +172,7 @@ my $vga_fmt = {
optional => 1,
default_key => 1,
enum => [
- qw(cirrus kyber qxl qxl2 qxl3 qxl4 none rdp serial0 serial1 serial2 serial3 std virtio virtio-gl vmware)
+ qw(cirrus kyber kyber-gl qxl qxl2 qxl3 qxl4 none rdp serial0 serial1 serial2 serial3 std virtio virtio-gl vmware)
],
},
memory => {
@@ -1491,6 +1492,7 @@ my $vga_map = {
# A display transport, not a card, so it picks one: virtio-vga rather than a GL
# variant, both of which currently break QEMU.
'kyber' => 'virtio-vga',
+ 'kyber-gl' => 'vhost-user-vga',
# Same reasoning for the RDP console: it reads the same D-Bus display.
'rdp' => 'virtio-vga',
};
@@ -1501,6 +1503,7 @@ my $vga_map_aarch64 = {
'virtio' => 'virtio-gpu',
'virtio-gl' => 'virtio-gpu-gl',
'kyber' => 'virtio-gpu',
+ 'kyber-gl' => 'vhost-user-gpu-pci',
'rdp' => 'virtio-gpu',
};
@@ -1548,6 +1551,12 @@ sub print_vga_device {
$memory = ",ram_size=67108864,vram_size=33554432";
}
+ my $chardev = "";
+ if ($vga->{type} eq 'kyber-gl') {
+ $memory = "";
+ $chardev = ",chardev=" . PVE::QemuServer::VhostUserGPU::chardev_id();
+ }
+
my $edidoff = "";
if ($type eq 'VGA' && windows_version($conf->{ostype})) {
$edidoff = ",edid=off" if (!defined($conf->{bios}) || $conf->{bios} ne 'ovmf');
@@ -1576,7 +1585,7 @@ sub print_vga_device {
if !PVE::Tools::dir_glob_regex('/dev/dri/', "renderD.*");
}
- return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}${edidoff}";
+ return "$type,id=${vgaid}${memory}${chardev}${max_outputs}${pciaddr}${edidoff}";
}
sub vm_is_volid_owner {
@@ -3429,9 +3438,15 @@ sub config_to_command {
push @$cmd, '-display', 'egl-headless,gl=core' if $vga->{type} eq 'virtio-gl'; # VIRGL
- if ($vga->{type} =~ /^(?:kyber|rdp)$/) {
+ if ($vga->{type} =~ /^(?:kyber|kyber-gl|rdp)$/) {
my $dbus = PVE::QemuServer::Helpers::dbus_socket($vmid);
my $display = "dbus,addr=unix:path=$dbus";
+ if ($vga->{type} eq 'kyber-gl') {
+ $display .= ",gl=on";
+ my $socket = PVE::QemuServer::VhostUserGPU::socket_file($vmid);
+ my $id = PVE::QemuServer::VhostUserGPU::chardev_id();
+ push @$cmd, '-chardev', "socket,id=$id,path=$socket";
+ }
# The display exports org.qemu.Display1.Audio only when told which audiodev to
# read, and nothing else can consume a dbus audiodev.
@@ -3474,6 +3489,7 @@ sub config_to_command {
}
my $virtiofs_enabled = PVE::QemuServer::Virtiofs::virtiofs_enabled($conf);
+ my $shared_memory = $virtiofs_enabled || ($vga->{type} // '') eq 'kyber-gl';
PVE::QemuServer::Memory::config(
$conf,
@@ -3481,7 +3497,7 @@ sub config_to_command {
$sockets,
$cores,
$hotplug_features->{memory},
- $virtiofs_enabled,
+ $shared_memory,
$cmd,
$machineFlags,
);
@@ -5849,7 +5865,9 @@ sub vm_start_nolock {
# QEMU connects to the D-Bus address, so the bus has to be listening first.
my $dbus_vga = parse_vga($conf->{vga} // '');
PVE::QemuServer::DBusDisplay::start($vmid)
- if ($dbus_vga->{type} // '') =~ /^(?:kyber|rdp)$/;
+ if ($dbus_vga->{type} // '') =~ /^(?:kyber|kyber-gl|rdp)$/;
+ PVE::QemuServer::VhostUserGPU::start($vmid)
+ if ($dbus_vga->{type} // '') eq 'kyber-gl';
my $tpmpid;
if ((my $tpm = $conf->{tpmstate0}) && !PVE::QemuConfig->is_template($conf)) {
@@ -6242,6 +6260,7 @@ sub vm_stop_cleanup {
PVE::QemuServer::Kyber::stop_controller($vmid);
PVE::QemuServer::RDP::stop_server($vmid);
PVE::QemuServer::DBusDisplay::stop($vmid);
+ PVE::QemuServer::VhostUserGPU::stop($vmid);
};
warn $@ if $@;
diff --git a/src/PVE/QemuServer/Kyber.pm b/src/PVE/QemuServer/Kyber.pm
index 3115f82..953413b 100644
--- a/src/PVE/QemuServer/Kyber.pm
+++ b/src/PVE/QemuServer/Kyber.pm
@@ -84,14 +84,16 @@ sub running_secret {
}
sub write_env {
- my ($vmid, $secret, $dataplane_port, $clipboard) = @_;
+ my ($vmid, $secret, $dataplane_port, $clipboard, $dmabuf) = @_;
my $clipboard_env = $clipboard ? 1 : 0;
+ my $dmabuf_env = $dmabuf ? 1 : 0;
my $env = <<"EOF";
KYBER_JWT_KEY=$secret
KYBER_DATAPLANE_PORT=$dataplane_port
KQS_CLIPBOARD=$clipboard_env
+KQS_DMABUF=$dmabuf_env
EOF
my $path = env_file($vmid);
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index 38e5aa6..1bd7a2f 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -32,6 +32,7 @@ SOURCES=Agent.pm \
RunState.pm \
StateFile.pm \
USB.pm \
+ VhostUserGPU.pm \
Virtiofs.pm \
VolumeChain.pm
diff --git a/src/PVE/QemuServer/VhostUserGPU.pm b/src/PVE/QemuServer/VhostUserGPU.pm
new file mode 100644
index 0000000..63d571b
--- /dev/null
+++ b/src/PVE/QemuServer/VhostUserGPU.pm
@@ -0,0 +1,103 @@
+package PVE::QemuServer::VhostUserGPU;
+
+# virgl in a helper process rather than inside QEMU, for the 'kyber-gl' display.
+# The helper renders and exports a dmabuf, so QEMU emits ScanoutDMABUF2 and the
+# console encoder imports it instead of reading back pixels.
+
+use strict;
+use warnings;
+
+use POSIX;
+use Time::HiRes qw(usleep);
+
+use PVE::ProcFSTools;
+use PVE::Tools qw(file_set_contents file_read_firstline);
+use PVE::QemuServer::Helpers;
+
+my $BINARY = '/usr/lib/kvm/vhost-user-gpu';
+
+sub socket_file {
+ my ($vmid) = @_;
+ return "$PVE::QemuServer::Helpers::var_run_tmpdir/$vmid.vhost-user-gpu.sock";
+}
+
+sub pidfile {
+ my ($vmid) = @_;
+ return "$PVE::QemuServer::Helpers::var_run_tmpdir/$vmid.vhost-user-gpu.pid";
+}
+
+sub chardev_id {
+ return 'vhost-user-gpu';
+}
+
+sub start {
+ my ($vmid) = @_;
+
+ die "vhost-user-gpu is not installed ($BINARY)\n" if !-x $BINARY;
+
+ stop($vmid);
+
+ my $socket = socket_file($vmid);
+ my $pidfile = pidfile($vmid);
+ my $log = "$PVE::QemuServer::Helpers::var_run_tmpdir/$vmid-vhost-user-gpu.log";
+
+ my $pid = fork();
+ die "could not fork to start vhost-user-gpu for VM $vmid\n" if !defined($pid);
+
+ if ($pid == 0) {
+ POSIX::setsid();
+ $0 = "task pve-vm$vmid-vhost-user-gpu";
+
+ my $pid2 = fork();
+ if (!defined($pid2)) {
+ POSIX::_exit(1);
+ } elsif ($pid2 == 0) {
+ open(STDIN, '<', '/dev/null');
+ open(STDOUT, '>>', $log);
+ open(STDERR, '>&', \*STDOUT);
+ exec($BINARY, '--virgl', '--socket-path', $socket);
+ POSIX::_exit(1);
+ }
+
+ eval { file_set_contents($pidfile, "$pid2\n") };
+ POSIX::_exit(0);
+ }
+
+ waitpid($pid, 0);
+
+ for (my $waited = 0; $waited < 5; $waited += 0.05) {
+ last if -S $socket;
+ usleep(50_000);
+ }
+ die "vhost-user-gpu for VM $vmid did not create $socket - see $log\n" if !-S $socket;
+
+ return;
+}
+
+sub stop {
+ my ($vmid) = @_;
+
+ my $pidfile = pidfile($vmid);
+ if (my $pid = eval { file_read_firstline($pidfile) }) {
+ if ($pid =~ m/^(\d+)$/) {
+ $pid = $1;
+ kill('TERM', $pid);
+
+ for (my $waited = 0; $waited < 5; $waited += 0.05) {
+ last if !PVE::ProcFSTools::check_process_running($pid);
+ usleep(50_000);
+ }
+ if (PVE::ProcFSTools::check_process_running($pid)) {
+ warn "vhost-user-gpu for VM $vmid did not exit, killing it\n";
+ kill('KILL', $pid);
+ }
+ }
+ }
+
+ unlink $pidfile;
+ unlink socket_file($vmid);
+
+ return;
+}
+
+1;
diff --git a/src/test/cfg2cmd/kyber-gl.conf b/src/test/cfg2cmd/kyber-gl.conf
new file mode 100644
index 0000000..e127bea
--- /dev/null
+++ b/src/test/cfg2cmd/kyber-gl.conf
@@ -0,0 +1,3 @@
+# TEST: Kyber console on the vhost-user GPU
+memory: 2048
+vga: kyber-gl
diff --git a/src/test/cfg2cmd/kyber-gl.conf.cmd b/src/test/cfg2cmd/kyber-gl.conf.cmd
new file mode 100644
index 0000000..d95b7df
--- /dev/null
+++ b/src/test/cfg2cmd/kyber-gl.conf.cmd
@@ -0,0 +1,29 @@
+/usr/bin/kvm
+-id 8006
+-name vm8006
+-no-shutdown
+-chardev 'socket,id=qmp,path=/var/run/qemu-server/8006.qmp,server=on,wait=off'
+-mon 'chardev=qmp,mode=control'
+-chardev 'socket,id=qmp-event,path=/var/run/qmeventd.sock,reconnect-ms=5000'
+-mon 'chardev=qmp-event,mode=control'
+-pidfile /var/run/qemu-server/8006.pid
+-daemonize
+-smp '1,sockets=1,cores=1,maxcpus=1'
+-nodefaults
+-boot 'menu=on,strict=on,reboot-timeout=1000,splash=/usr/share/qemu-server/bootsplash.jpg'
+-chardev 'socket,id=vhost-user-gpu,path=/var/run/qemu-server/8006.vhost-user-gpu.sock'
+-display 'dbus,addr=unix:path=/var/run/qemu-server/8006.dbusdisplay,gl=on'
+-vnc 'unix:/var/run/qemu-server/8006.vnc,password=on'
+-cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep
+-m 2048
+-object 'memory-backend-memfd,id=virtiofs-mem,size=2048M,share=on'
+-global 'PIIX4_PM.disable_s3=1'
+-global 'PIIX4_PM.disable_s4=1'
+-device 'pci-bridge,id=pci.1,chassis_nr=1,bus=pci.0,addr=0x1e'
+-device 'pci-bridge,id=pci.2,chassis_nr=2,bus=pci.0,addr=0x1f'
+-device 'piix3-usb-uhci,id=uhci,bus=pci.0,addr=0x1.0x2'
+-device 'usb-tablet,id=tablet,bus=uhci.0,port=1'
+-device 'vhost-user-vga,id=vga,chardev=vhost-user-gpu,bus=pci.0,addr=0x2'
+-device 'virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3,free-page-reporting=on'
+-iscsi 'initiator-name=iqn.1993-08.org.debian:01:aabbccddeeff'
+-machine 'memory-backend=virtiofs-mem,type=pc+pve0'
\ No newline at end of file
--
2.55.0
next prev parent reply other threads:[~2026-08-25 11:35 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-25 11:34 [RFC pve-http-server/qemu-server/pve-manager/pve-{qemu-kyber,kyberproxy, kyber-web,qemu-rdp,rdpproxy,rdp-web} 00/13] add rdp && kyber consoles for qemu over D-Bus display Alexandre Derumier
2026-08-25 11:34 ` [RFC pve-http-server 01/13] anyevent : proxy a path prefix to a local http proxy Alexandre Derumier
2026-08-25 11:34 ` [RFC qemu-server 02/13] add D-Bus display support Alexandre Derumier
2026-08-25 11:34 ` [RFC qemu-server 03/13] add kyber display Alexandre Derumier
2026-08-25 11:34 ` [RFC qemu-server 04/13] add rdp display Alexandre Derumier
2026-08-25 11:34 ` Alexandre Derumier [this message]
2026-08-25 11:34 ` [RFC pve-manager 06/13] ui: add kyber console Alexandre Derumier
2026-08-25 11:34 ` [RFC pve-manager 07/13] ui: add rdp console Alexandre Derumier
2026-08-25 11:34 ` [RFC pve-kyber-web 10/13] add pve-kyber-web: console's webassembly client Alexandre Derumier
2026-08-25 11:34 ` [RFC pve-qemu-rdp 11/13] Add pve-qemu-rdp: an RDP server for the console Alexandre Derumier
2026-08-25 11:34 ` [RFC pve-rdpproxy 12/13] Add pve-rdpproxy Alexandre Derumier
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260825113442.947620-6-alexandre.derumier@groupe-cyllene.com \
--to=alexandre.derumier@groupe-cyllene.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox