1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
# Guest frame path and Looking Glass status (2026-10-21)
This page records the state of the "show the guest's GPU output on the host"
goal: what is proven, what is built, and exactly which pieces remain. It is
written from live evidence on the vGPU-IO stack (guest CUDA green, ivshmem
devices present, kvmfr module loaded on the host).
## 1. What is proven
* **Guest GPU rendering works through CUDA.** `tools/cuda-ab/frame_producer.cu`
renders an animated Julia set on the guest GPU (800x600 RGBA, ~10 fps),
copies the frame to host memory and publishes it into the ivshmem BAR
(`/sys/bus/pci/devices/0000:00:03.0/resource2`, the 128 MiB kvmfr device
QEMU backs with `/dev/shm/kvmfr-<uuid>`), with a small header
(`magic/version/width/height/stride/format/seq/timestamp`) at offset 1 MiB.
* **The host can read those frames.** `tools/lg/frame_reader.py` maps the same
shm file and writes each new sequence number out as a PPM, i.e. a real
GPU-rendered guest frame lands on the host. This is the full transport path
(guest GPU -> guest sysmem -> ivshmem -> host), independent of any display
protocol.
* **The ivshmem devices are visible in the guest** (`lspci`: two
"Inter-VM shared memory" functions; `resource2` of `0000:00:03.0` is the
128 MiB window, `0000:00:04.0` the 2 MiB cursor window), and the guest can
mmap them (root required for the sysfs resource files).
* **EGL enumerates the NVIDIA device in the guest.** With
`EGL_EXT_platform_device`/`EGL_EXT_device_enumeration` present,
`eglQueryDevicesEXT` returns 3 devices and `eglInitialize` on the NVIDIA
device succeeds (`vendor=NVIDIA version=1.5`).
## 2. What is blocked, and why
* **Guest GL contexts do not allocate.** `eglMakeCurrent` on an offscreen
pbuffer fails with `EGL_BAD_ALLOC` (0x3003) on every platform tried
(device, surfaceless, default). Xorg's own failure is the same class:
`(EE) NVIDIA(GPU-0): Failed to allocate VASpace object` then
`Failing initialization of X screen`. The driver cannot allocate GPU virtual
address space for a GL context/surface through the vGPU, so both the X
display path and the EGL offscreen path stop at the same wall. This is the
single remaining blocker for guest OpenGL (and therefore for GZDOOM /
doomsday rendering in the guest).
* **The Looking Glass client (B4) cannot consume our frames directly.**
`looking-glass-client` (installed at `/usr/local/bin/looking-glass-client`,
runs under Xvfb with the `libbfd` shim in `/tmp/opencode/lgcompat`) supports
two transports, LGMP and SPICE. Its `app:shmFile` option still expects an
**LGMP session** (the `KVMFR` session header plus the shared-memory message
queues), not a raw framebuffer. Upstream's frame *producer* (`host/`) is the
Windows DXGI/IDD path. So a Linux guest-side producer must either implement
the LGMP session protocol or we keep the frame reader as the host-side
substitute (which is what `frame_reader.py` is).
* **GZDOOM is not packaged for Ubuntu noble** (`apt-cache policy gzdoom` has
no candidate); `doomsday` + `freedoom` are installed as the fallback, but
doomsday cannot start a renderer without a working GL context (blocker
above).
## 3. Exact remaining pieces (in order of leverage)
1. **Fix the guest GL VASpace allocation.** This is an RM/vGPU-side gap: the
driver's `nv_vgpu` VASpace allocation for a GL context fails. Fixing it
unlocks Xorg, EGL offscreen GL, doomsday/GZDOOM, and the standard Looking
Glass guest path (the guest's GL capture driver would then work as
upstream expects).
2. **Implement the LGMP session producer** (or a small Linux capture client)
that writes frames into the ivshmem region with the KVMFR session header
and the frame queue protocol the client expects. The transport half is
already proven; this is protocol work in the guest (or a host-side
substitute that speaks LGMP).
3. **Packaging**: GZDOOM would need a source build or a flatpak/snap; the
doomsday fallback is installed and only blocked by (1).
## 4. Reproduce
```bash
# guest (root, because the sysfs resource is root-only):
sudo /tmp/frame_producer 60 800 600 10 # 60 frames, 800x600, 10 fps
# host:
python3 tools/lg/frame_reader.py --frames 5 --out /tmp/opencode/frames
# -> SAVED seq=N 800x600 fmt=0 -> /tmp/opencode/frames/frame-NNNN.ppm
# guest EGL probe (shows the enumeration success and the MakeCurrent wall):
gcc -O2 -o egl_frame tools/guest-diag/egl_frame.c -lEGL -lGLESv2
./egl_frame
```
## 5. Session-13 update (2026-10-21, r432→r441)
* The single-frame path is unchanged and still proven: guest `frame_producer`
(CUDA Julia) -> kvmfr ivshmem BAR -> host `frame_reader.py` ->
`/tmp/opencode/frames/frame-0001.png` (320x240, 1431 colours).
* `tools/lg/frame_sequence.sh` captures an **animated** sequence end to end
(reader first, then N frames from the guest). It is blocked by the
GR no-execute wedge: when the lane is wedged the render launches write zero
pages and the producer republishes stale identical frames (observed: 6/6
identical MD5s on r438; r439 published 5 identical plus one frame left in
the BAR by the previous boot). A multi-frame animation needs a healthy
lane; the frame pipeline itself is not the blocker.
* The GL wall is untouched: `Xorg :0` -> `(EE) NVIDIA(GPU-0): Failed to
allocate VASpace object`, guest dmesg `NVRM: vaspaceapiConstruct_IMPL:
Could not construct VA space. Status ffff`, i.e. `vmmCreateVaspace` inside
the guest's own RM. Next step: reproduce with the guest RM's
`ResmanDebugLevel` raised (a guest module reload, the service's m177 path)
to capture which inner call returns the generic error.
|