Files
komorebi/nix/dev/virglrenderer-video-memory-upload.patch
T
LGUG2Z 1915d6ac26 build(nix): hardware H.264 encode for the demo guest's RDP
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.
2026-10-28 18:43:06 -07:00

238 lines
9.3 KiB
Diff

virglrenderer: upload the picture to encode through memory
virglrenderer shares a video buffer with the host's VA-API driver as a dma-buf: it
exports the VA surface (vaExportSurfaceHandle) and imports it into EGL to copy the
guest's picture in. The d3d12 driver on WSL 2 has no dma-bufs, so the export fails
("export surface failed, err = 0x6"), nothing is uploaded and the encoder returns an
empty bitstream.
When the first export fails, read the planes of the guest's picture back from their
textures (glReadPixels, into the planes of a VAImage) and write them to the surface
through vaDeriveImage(), as FFmpeg's hwupload does; where the driver cannot derive an
image, a vaCreateImage() image is copied with vaPutImage(). On the d3d12 driver the
vaPutImage() copy came out with an all-zero luma plane (the chroma was right), where
the derived image encodes exactly: a luma ramp through the encoder comes back as
43 125 207 for 43 125 207. Where the export works nothing changes: it is tried first,
and given up on only after it has failed.
diff --git a/src/vrend/virgl_video.c b/src/vrend/virgl_video.c
index dc1e8cd..8b47837 100644
--- a/src/vrend/virgl_video.c
+++ b/src/vrend/virgl_video.c
@@ -438,12 +438,100 @@ static void destroy_video_dma_buf(struct virgl_video_dma_buf *dmabuf)
}
}
+/*
+ * Upload the picture to encode through memory, for a host that cannot export
+ * a video buffer as a dma-buf: the d3d12 driver on WSL 2 has no dma-bufs, and
+ * vaExportSurfaceHandle() fails on it. The callback reads the planes of the
+ * guest's picture into a VAImage, which vaPutImage() copies to the surface,
+ * as FFmpeg's hwupload does.
+ */
+static void encode_upload_picture_cpu(struct virgl_video_codec *codec,
+ struct virgl_video_buffer *buffer)
+{
+ VAStatus va_stat;
+ VAImageFormat fmt;
+ VAImage image;
+ void *data = NULL;
+ uint8_t *dst[3] = { NULL, NULL, NULL };
+ unsigned pitches[3] = { 0, 0, 0 };
+ unsigned i;
+ int ret;
+ bool derived = false;
+
+ if (!callbacks || !callbacks->encode_read_picture)
+ return;
+
+ memset(&fmt, 0, sizeof(fmt));
+ fmt.byte_order = VA_LSB_FIRST;
+ fmt.bits_per_pixel = 12;
+ switch (buffer->format) {
+ case PIPE_FORMAT_NV12:
+ fmt.fourcc = VA_FOURCC_NV12;
+ break;
+ case PIPE_FORMAT_NV21:
+ fmt.fourcc = VA_FOURCC_NV21;
+ break;
+ default:
+ virgl_error("upload through memory: pipe format %d not supported\n",
+ buffer->format);
+ return;
+ }
+
+ /*
+ * Write to the surface itself when the driver can show it as an image, as FFmpeg's
+ * hwupload does. On the d3d12 driver the picture of an image made with vaCreateImage() and
+ * copied by vaPutImage() came out with an all-zero luma plane (the chroma was right),
+ * where the same planes written through vaDeriveImage() encode exactly.
+ */
+ va_stat = vaDeriveImage(va_dpy, buffer->va_sfc, &image);
+ if (VA_STATUS_SUCCESS == va_stat) {
+ derived = true;
+ } else {
+ va_stat = vaCreateImage(va_dpy, &fmt, buffer->width, buffer->height, &image);
+ if (VA_STATUS_SUCCESS != va_stat) {
+ virgl_error("create image failed, err = 0x%x\n", va_stat);
+ return;
+ }
+ }
+
+ va_stat = vaMapBuffer(va_dpy, image.buf, &data);
+ if (VA_STATUS_SUCCESS != va_stat || !data) {
+ virgl_error("map image failed, err = 0x%x\n", va_stat);
+ goto destroy_image;
+ }
+
+ for (i = 0; i < image.num_planes && i < ARRAY_SIZE(dst); i++) {
+ dst[i] = (uint8_t *)data + image.offsets[i];
+ pitches[i] = image.pitches[i];
+ }
+
+ ret = callbacks->encode_read_picture(codec, buffer, image.num_planes,
+ dst, pitches);
+ vaUnmapBuffer(va_dpy, image.buf);
+ if (ret) {
+ virgl_error("read picture failed, err = %d\n", ret);
+ goto destroy_image;
+ }
+
+ if (!derived) {
+ va_stat = vaPutImage(va_dpy, buffer->va_sfc, image.image_id,
+ 0, 0, buffer->width, buffer->height,
+ 0, 0, buffer->width, buffer->height);
+ if (VA_STATUS_SUCCESS != va_stat)
+ virgl_error("put image failed, err = 0x%x\n", va_stat);
+ }
+
+destroy_image:
+ vaDestroyImage(va_dpy, image.image_id);
+}
+
static void encode_upload_picture(struct virgl_video_codec *codec,
struct virgl_video_buffer *buffer)
{
VAStatus va_stat;
+ static bool dmabuf_unsupported;
- if (!callbacks || !callbacks->encode_upload_picture)
+ if (!callbacks)
return;
va_stat = vaSyncSurface(va_dpy, buffer->va_sfc);
@@ -452,11 +540,20 @@ static void encode_upload_picture(struct virgl_video_codec *codec,
return;
}
- if (!buffer->dmabuf)
+ /* once a surface cannot be exported none can: go through memory */
+ if (!buffer->dmabuf && !dmabuf_unsupported && callbacks->encode_upload_picture) {
buffer->dmabuf = export_video_dma_buf(buffer, VIRGL_VIDEO_DMABUF_WRITE_ONLY);
+ if (!buffer->dmabuf) {
+ dmabuf_unsupported = true;
+ virgl_warn("cannot export a video buffer as a dma-buf, "
+ "uploading the pictures to encode through memory\n");
+ }
+ }
if (buffer->dmabuf)
callbacks->encode_upload_picture(codec, buffer->dmabuf);
+ else
+ encode_upload_picture_cpu(codec, buffer);
}
static void encode_completed(struct virgl_video_codec *codec,
diff --git a/src/vrend/virgl_video.h b/src/vrend/virgl_video.h
index e954559..c39d33d 100644
--- a/src/vrend/virgl_video.h
+++ b/src/vrend/virgl_video.h
@@ -115,6 +115,16 @@ struct virgl_video_callbacks {
void (*encode_upload_picture)(struct virgl_video_codec *codec,
const struct virgl_video_dma_buf *dmabuf);
+ /* Read the picture data to be encoded into memory, plane by plane, for
+ * a host whose video buffers cannot be exported as dma-bufs (the d3d12
+ * driver of WSL 2): plane i is written to dst[i], pitches[i] bytes a row.
+ * Returns 0 when the planes are filled. */
+ int (*encode_read_picture)(struct virgl_video_codec *codec,
+ struct virgl_video_buffer *buffer,
+ unsigned num_planes,
+ uint8_t * const *dst,
+ const unsigned *pitches);
+
/* Callback when encoding is complete, used to download the encoded data
* and reference picture */
void (*encode_completed)(struct virgl_video_codec *codec,
diff --git a/src/vrend/vrend_video.c b/src/vrend/vrend_video.c
index 57341d8..dc30c55 100644
--- a/src/vrend/vrend_video.c
+++ b/src/vrend/vrend_video.c
@@ -293,6 +293,58 @@ static void vrend_video_enocde_upload_picture(
sync_video_buffer_to_dmabuf(buf, dmabuf);
}
+/*
+ * The picture to encode is the planes of a video buffer the guest rendered to, which are
+ * textures here (R8 for the luma, RG8 for the chroma of NV12). Read them into the planes of a
+ * VAImage, for a host that cannot share its video buffer as a dma-buf.
+ */
+static int vrend_video_encode_read_picture(
+ struct virgl_video_codec *codec,
+ struct virgl_video_buffer *vbuf,
+ unsigned num_planes,
+ uint8_t * const *dst,
+ const unsigned *pitches)
+{
+ struct vrend_video_buffer *buf = vrend_video_buffer(vbuf);
+ int ret = 0;
+
+ (void)codec;
+
+ if (!buf)
+ return -1;
+
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ for (unsigned i = 0; i < num_planes && i < buf->num_planes; i++) {
+ struct vrend_video_plane *plane = &buf->planes[i];
+ struct vrend_resource *res;
+ /* NV12 and NV21 have a plane of one byte a pixel, and one of two */
+ const unsigned bytes_per_pixel = i ? 2 : 1;
+
+ res = vrend_renderer_ctx_res_lookup(buf->ctx->ctx, plane->res_handle);
+ if (!res) {
+ virgl_error("%s: res %d not found\n", __func__, plane->res_handle);
+ ret = -1;
+ continue;
+ }
+
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, plane->framebuffer);
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, res->gl_id, 0);
+
+ glPixelStorei(GL_PACK_ROW_LENGTH, pitches[i] / bytes_per_pixel);
+ glReadPixels(0, 0, res->base.width0, res->base.height0,
+ i ? GL_RG : GL_RED, GL_UNSIGNED_BYTE, dst[i]);
+ }
+
+ glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+ glPixelStorei(GL_PACK_ALIGNMENT, 4);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ return ret;
+}
+
static void vrend_video_encode_completed(
struct virgl_video_codec *codec,
const struct virgl_video_dma_buf *src_buf,
@@ -349,6 +401,7 @@ static void vrend_video_encode_completed(
static struct virgl_video_callbacks video_callbacks = {
.decode_completed = vrend_video_decode_completed,
.encode_upload_picture = vrend_video_enocde_upload_picture,
+ .encode_read_picture = vrend_video_encode_read_picture,
.encode_completed = vrend_video_encode_completed,
};