-
-
Notifications
You must be signed in to change notification settings - Fork 12.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Right now this is a module that encodes every frame delta from QEMU to an intermediate format so that we can use FFmpeg to encode it into a proper video. In the end, the goal is to have videos of the graphical machine output for the NixOS tests, which is especially useful in tests involving X where we're basically blind when we run into a race condition or a loaded Hydra node causes the test to fail. The reason why I picked the approach to use an intermediate format and encode it properly later is based on my benchmarks of some seemingly simple approaches I tried before. First of all I tried to search the web for solutions that already existed and found a few, but they weren't really suitable: * Use the screendump QMP command to collect frames from the VM, which works to some degree but it misses frames. * Enable SPICE[1] and capture video from the server, which I actually tried to implement before the next option. However, existing solutions for capturing video off a SPICE server are rare and when testing with my own PoC implementation, I got frame drops as well and I didn't manage to capture early boot. * Try a patch[2] from the QEMU development mailing list, which adds a HMP command to capture and encode it directly to a video. This was the slowest option of all and it even lead to test failures because we got a timeout during VM startup. * Similar results to SPICE I had when capturing video using VNC and VncProxy[3]. So I dug through the QEMU code base and found out that UI modules get frame deltas from Pixman, which is perfect for us, because we're not losing frames and it also allows direct access to pixel data. It also is fast and I couldn't even properly benchmark the overhead properly as tests usually tend to vary in speed for a few seconds. Before actually writing our own intermediate format, I tried to use an existing format that would be suitable for us. The requirements for this format would be to support different frame sizes and variable framerates, plus it needs to be very fast to encode. While asking in the #ffmpeg channel on freenode, the best format for these requirements would be using the NUT[4] format (thanks to "furq" for the suggestion). However while reading the format specification I came to the realisation that our requirements are so simple that even NUT is complicated in comparison, which is why I written our own format. The specification is as follows: The first byte (the opcode) is either an 'S' (0x53, for "switch") or an 'U' (0x55, for "update") and determines the format of the following data. A "switch" is a surface change, like eg. a resize of the display and the data following the opcode are the dimensions (width and height, both are unsigned 32 bit integers), format (unsigned 8 bit integer) and bytes per pixel (unsigned 8 bit integer, currently either 2 or 4) of the surface. An "update" is a portion of the region that has changed since the last update and it's followed by X, Y, width and height (all 32 bit unsigned integers) coordinates of the updated region, the absolute time (64 bit unsigned integer) and the raw frame data afterwards. Note that we don't provide a length here, because we can infer that from the bytes per pixel of the last "switch" packet and the coordinates. All of the data is in the native endian format of the host processor architecture, which is not a problem, because encoding of the final video will take place on the same processor architecture. All of the data is also gzip compressed, so that we don't accumulate gigabytes of frame data during test runs. I also moved the qemu_test expression out of the default.nix of the main qemu expression, so that when we improve this we don't accidentally break stuff for users of the normal QEMU. [1]: https://www.spice-space.org/ [2]: https://lists.gnu.org/archive/html/qemu-devel/2017-05/msg00865.html [3]: https://github.com/amitbet/vncproxy [4]: https://ffmpeg.org/~michael/nut.txt Signed-off-by: aszlig <aszlig@nix.build>
- Loading branch information
Showing
3 changed files
with
351 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
#include "qemu/osdep.h" | ||
#include "qemu-common.h" | ||
#include "qemu/timer.h" | ||
#include "ui/console.h" | ||
|
||
#include <zlib.h> | ||
|
||
static DisplayChangeListener *dcl; | ||
static gzFile output_video; | ||
|
||
/* These values are directly used in our intermediate format so that they can | ||
* later be mapped back in avutil. The constants here are just so that we have | ||
* the same naming conventions as avutil. | ||
* | ||
* Note also, that this is used in an uint8_t so be careful to not overflow and | ||
* also make sure that in case it is a packed pixel format it needs to be big | ||
* endian. The necessary conversion for little endian systems is done when we | ||
* encode it into a more common video format. | ||
*/ | ||
#define AV_PIX_FMT_RGB555BE 1 | ||
#define AV_PIX_FMT_RGB565BE 2 | ||
#define AV_PIX_FMT_0RGB 3 | ||
#define AV_PIX_FMT_RGB0 4 | ||
#define AV_PIX_FMT_BGR0 5 | ||
|
||
static bool write_packet(void *buf, size_t len) | ||
{ | ||
int i, written; | ||
|
||
for (i = 0; i < len; i += written) { | ||
written = gzwrite(output_video, buf + i, len - i); | ||
if (written <= 0) { | ||
fputs("Error writing compressed video packet.\n", stderr); | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
static void nixos_test_update(DisplayChangeListener *dcl, | ||
int x, int y, int w, int h) | ||
{ | ||
DisplaySurface *surf = qemu_console_surface(dcl->con); | ||
uint32_t x32 = x, y32 = y, w32 = w, h32 = h; | ||
uint64_t timestamp; | ||
size_t offset, datalen; | ||
void *buf, *bufp, *sdata; | ||
|
||
if (surf == NULL) | ||
return; | ||
|
||
timestamp = qemu_clock_get_ns(QEMU_CLOCK_REALTIME); | ||
offset = surface_bytes_per_pixel(surf) * x + surface_stride(surf) * y; | ||
datalen = surface_bytes_per_pixel(surf) * w * h; | ||
|
||
/* Bitstring: <<Op:8, X:32, Y:32, W:32, H:32, Time:64, Data/binary>> | ||
* Length: 1 + 4 + 4 + 4 + 4 + 8 + DataLen = 25 + DataLen | ||
*/ | ||
buf = g_malloc(25 + datalen); | ||
*(char*)buf = 'U'; | ||
memcpy(buf + 1, &x32, 4); | ||
memcpy(buf + 5, &y32, 4); | ||
memcpy(buf + 9, &w32, 4); | ||
memcpy(buf + 13, &h32, 4); | ||
memcpy(buf + 17, ×tamp, 8); | ||
|
||
bufp = buf + 25; | ||
sdata = surface_data(surf) + offset; | ||
|
||
/* Extract only the data of the rectangle but also taking stride into | ||
* account, so we don't need to handle padding while encoding this | ||
* intermediate format into a common video format. | ||
*/ | ||
while (h-- > 0) { | ||
memcpy(bufp, sdata, w * surface_bytes_per_pixel(surf)); | ||
sdata += surface_stride(surf); | ||
bufp += w * surface_bytes_per_pixel(surf); | ||
} | ||
|
||
if (!write_packet(buf, 25 + datalen)) { | ||
g_free(buf); | ||
exit(1); | ||
} | ||
g_free(buf); | ||
} | ||
|
||
static void nixos_test_switch(DisplayChangeListener *dcl, | ||
DisplaySurface *new_surface) | ||
{ | ||
void *buf; | ||
uint32_t width, height; | ||
uint8_t format, bpp; | ||
|
||
if (new_surface == NULL) | ||
return; | ||
|
||
width = surface_width(new_surface); | ||
height = surface_height(new_surface); | ||
bpp = surface_bytes_per_pixel(new_surface); | ||
|
||
switch (new_surface->format) { | ||
case PIXMAN_x1r5g5b5: format = AV_PIX_FMT_RGB555BE; break; | ||
case PIXMAN_r5g6b5: format = AV_PIX_FMT_RGB565BE; break; | ||
case PIXMAN_x8r8g8b8: format = AV_PIX_FMT_0RGB; break; | ||
case PIXMAN_r8g8b8x8: format = AV_PIX_FMT_RGB0; break; | ||
case PIXMAN_b8g8r8x8: format = AV_PIX_FMT_BGR0; break; | ||
default: return; | ||
} | ||
|
||
/* Bitstring: <<Op:8, Width:32, Height:32, Format:8, BPP:8>> | ||
* Length: 1 + 4 + 4 + 1 + 1 = 11 | ||
*/ | ||
buf = g_malloc(11); | ||
*(char*)buf = 'S'; | ||
memcpy(buf + 1, &width, 4); | ||
memcpy(buf + 5, &height, 4); | ||
memcpy(buf + 9, &format, 1); | ||
memcpy(buf + 10, &bpp, 1); | ||
|
||
if (!write_packet(buf, 11)) { | ||
g_free(buf); | ||
exit(1); | ||
} | ||
g_free(buf); | ||
|
||
/* We have a new surface (or a resize), so we need to send an update for | ||
* the whole new surface size to make sure we don't get artifacts from the | ||
* old surface. */ | ||
nixos_test_update(dcl, 0, 0, surface_width(new_surface), | ||
surface_height(new_surface)); | ||
} | ||
|
||
static void nixos_test_refresh(DisplayChangeListener *dcl) | ||
{ | ||
graphic_hw_update(dcl->con); | ||
} | ||
|
||
static bool nixos_test_check_format(DisplayChangeListener *dcl, | ||
pixman_format_code_t format) | ||
{ | ||
switch (format) { | ||
case PIXMAN_x1r5g5b5: | ||
case PIXMAN_r5g6b5: | ||
case PIXMAN_x8r8g8b8: | ||
case PIXMAN_r8g8b8x8: | ||
case PIXMAN_b8g8r8x8: | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
static const DisplayChangeListenerOps dcl_ops = { | ||
.dpy_name = "nixos-test", | ||
.dpy_gfx_update = nixos_test_update, | ||
.dpy_gfx_switch = nixos_test_switch, | ||
.dpy_gfx_check_format = nixos_test_check_format, | ||
.dpy_refresh = nixos_test_refresh, | ||
}; | ||
|
||
static void nixos_test_cleanup(void) | ||
{ | ||
gzclose_w(output_video); | ||
} | ||
|
||
#define HEADER \ | ||
"# This file contains raw frame data in an internal format used by\n" \ | ||
"# the 'nixos-test' QEMU UI module optimized for low overhead.\n" \ | ||
"#\n" \ | ||
"# In order to get this into a format that's actually watchable,\n" \ | ||
"# please use the 'nixos-test-encode-video' binary from the\n" \ | ||
"# 'qemu_test' package to encode it into another video format.\n" \ | ||
"#\n" | ||
|
||
static void nixos_test_display_init(DisplayState *ds, DisplayOptions *o) | ||
{ | ||
int outfd; | ||
QemuConsole *con; | ||
|
||
outfd = qemu_open(o->capture_file, | ||
O_WRONLY | O_CREAT | O_APPEND | O_BINARY, 0666); | ||
|
||
if (outfd < 0) { | ||
fprintf(stderr, "Failed to open file '%s' for video capture: %s\n", | ||
o->capture_file, strerror(errno)); | ||
exit(1); | ||
} | ||
|
||
/* When at the beginning of the file, let's write a short description about | ||
* the file in question so that people stumbling over it know what to do | ||
* with it. | ||
*/ | ||
if (lseek(outfd, 0, SEEK_END) == 0) { | ||
if (qemu_write_full(outfd, HEADER, sizeof(HEADER) -1) != sizeof HEADER) | ||
fprintf(stderr, "Unable to write video file header to '%s'.\n", | ||
o->capture_file); | ||
} | ||
|
||
/* We're using gzip here because we have a lot of repetition in frame data | ||
* and a test run without compressing the intermediate format can easily | ||
* grow to a few gigabytes, which will also cause slowdowns on slow disks. | ||
*/ | ||
if ((output_video = gzdopen(outfd, "ab1")) == NULL) { | ||
fprintf(stderr, "Unable to associate gzip stream with '%s'.\n", | ||
o->capture_file); | ||
qemu_close(outfd); | ||
exit(1); | ||
} | ||
|
||
con = qemu_console_lookup_by_index(0); | ||
if (!con) { | ||
fputs("Unable to look up console 0.\n", stderr); | ||
exit(1); | ||
} | ||
|
||
dcl = g_new0(DisplayChangeListener, 1); | ||
dcl->ops = &dcl_ops; | ||
dcl->con = con; | ||
register_displaychangelistener(dcl); | ||
|
||
fprintf(stderr, "Capturing intermediate video stream to '%s'.\n", | ||
o->capture_file); | ||
|
||
atexit(nixos_test_cleanup); | ||
} | ||
|
||
static QemuDisplay qemu_display_nixos_test = { | ||
.type = DISPLAY_TYPE_NIXOS_TEST, | ||
.init = nixos_test_display_init, | ||
}; | ||
|
||
static void register_nixos_test(void) | ||
{ | ||
qemu_display_register(&qemu_display_nixos_test); | ||
} | ||
|
||
type_init(register_nixos_test); |
105 changes: 105 additions & 0 deletions
105
pkgs/applications/virtualization/qemu/nixos-test-ui.patch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
diff --git a/qapi/ui.json b/qapi/ui.json | ||
index 9d6721037f..718db40fc7 100644 | ||
--- a/qapi/ui.json | ||
+++ b/qapi/ui.json | ||
@@ -1136,7 +1136,7 @@ | ||
# | ||
## | ||
{ 'enum' : 'DisplayType', | ||
- 'data' : [ 'default', 'none', 'gtk', 'sdl', | ||
+ 'data' : [ 'default', 'none', 'gtk', 'sdl', 'nixos-test', | ||
'egl-headless', 'curses', 'cocoa', | ||
'spice-app'] } | ||
|
||
@@ -1158,6 +1158,7 @@ | ||
{ 'union' : 'DisplayOptions', | ||
'base' : { 'type' : 'DisplayType', | ||
'*full-screen' : 'bool', | ||
+ '*capture-file' : 'str', | ||
'*window-close' : 'bool', | ||
'*show-cursor' : 'bool', | ||
'*gl' : 'DisplayGLMode' }, | ||
diff --git a/qemu-options.hx b/qemu-options.hx | ||
index 708583b4ce..1599e36902 100644 | ||
--- a/qemu-options.hx | ||
+++ b/qemu-options.hx | ||
@@ -1735,6 +1735,7 @@ DEF("display", HAS_ARG, QEMU_OPTION_display, | ||
#if defined(CONFIG_OPENGL) | ||
"-display egl-headless[,rendernode=<file>]\n" | ||
#endif | ||
+ "-display nixos-test=<filename>\n" | ||
"-display none\n" | ||
" select display backend type\n" | ||
" The default display is equivalent to\n " | ||
@@ -1796,6 +1797,10 @@ SRST | ||
Start QEMU as a Spice server and launch the default Spice client | ||
application. The Spice server will redirect the serial consoles | ||
and QEMU monitors. (Since 4.0) | ||
+ | ||
+ ``nixos-test`` | ||
+ Write raw video frames into the given filename instead of displaying | ||
+ anything. | ||
ERST | ||
|
||
DEF("nographic", 0, QEMU_OPTION_nographic, | ||
@@ -2223,6 +2228,15 @@ SRST | ||
valid audiodev. | ||
ERST | ||
|
||
+DEF("nixos-test", HAS_ARG, QEMU_OPTION_nixos_test, | ||
+ "-nixos-test <filename> shorthand for -display nixos-test=<filename>\n", | ||
+ QEMU_ARCH_ALL) | ||
+SRST | ||
+``-nixos-test filename`` | ||
+ Instead of displaying anything, capture all the output as raw video frames | ||
+ into the file name given by ``filename``. | ||
+ERST | ||
+ | ||
ARCHHEADING(, QEMU_ARCH_I386) | ||
|
||
ARCHHEADING(i386 target only:, QEMU_ARCH_I386) | ||
diff --git a/softmmu/vl.c b/softmmu/vl.c | ||
index 4eb9d1f7fd..5a3596a0d9 100644 | ||
--- a/softmmu/vl.c | ||
+++ b/softmmu/vl.c | ||
@@ -1966,6 +1966,15 @@ static void parse_display(const char *p) | ||
error_report("VNC requires a display argument vnc=<display>"); | ||
exit(1); | ||
} | ||
+ } else if (strstart(p, "nixos-test", &opts)) { | ||
+ dpy.type = DISPLAY_TYPE_NIXOS_TEST; | ||
+ if (*opts == '=') { | ||
+ dpy.capture_file = opts + 1; | ||
+ } else { | ||
+ error_report("the nixos-test option requires a filename argument" | ||
+ " nixos-test=<filename>"); | ||
+ exit(1); | ||
+ } | ||
} else { | ||
parse_display_qapi(p); | ||
} | ||
@@ -3430,6 +3439,10 @@ void qemu_init(int argc, char **argv, char **envp) | ||
error_report("SDL support is disabled"); | ||
exit(1); | ||
#endif | ||
+ case QEMU_OPTION_nixos_test: | ||
+ dpy.type = DISPLAY_TYPE_NIXOS_TEST; | ||
+ dpy.capture_file = optarg; | ||
+ break; | ||
case QEMU_OPTION_pidfile: | ||
pid_file = optarg; | ||
break; | ||
diff --git a/ui/Makefile.objs b/ui/Makefile.objs | ||
index 504b196479..d397253a11 100644 | ||
--- a/ui/Makefile.objs | ||
+++ b/ui/Makefile.objs | ||
@@ -48,6 +48,9 @@ x_keymap.o-cflags := $(X11_CFLAGS) | ||
x_keymap.o-libs := $(X11_LIBS) | ||
endif | ||
|
||
+common-obj-y += nixos-test.mo | ||
+nixos-test.mo-objs := nixos-test.o | ||
+ | ||
common-obj-$(CONFIG_CURSES) += curses.mo | ||
curses.mo-objs := curses.o | ||
curses.mo-cflags := $(CURSES_CFLAGS) $(ICONV_CFLAGS) |