The Spice server administrator can specify the encoder and codec preferences to optimize for CPU or bandwidth usage. Preferences are described in a semi-colon separated list of encoder:codec pairs. The server has a default preference list which can explicitly be selected by specifying 'auto'. Signed-off-by: Francois Gouget <fgouget@xxxxxxxxxxxxxxx> --- server/dcc-send.c | 2 +- server/display-channel.c | 10 +++ server/display-channel.h | 4 ++ server/gstreamer-encoder.c | 6 +- server/mjpeg-encoder.c | 6 +- server/red-qxl.c | 9 +++ server/red-qxl.h | 6 ++ server/red-worker.c | 18 +++++- server/reds.c | 156 ++++++++++++++++++++++++++++++++++++++++----- server/reds.h | 1 + server/spice-server.h | 8 +++ server/spice-server.syms | 5 ++ server/stream.c | 29 +++++++-- server/video-encoder.h | 20 +++++- 14 files changed, 251 insertions(+), 29 deletions(-) diff --git a/server/dcc-send.c b/server/dcc-send.c index a0443f0..497f879 100644 --- a/server/dcc-send.c +++ b/server/dcc-send.c @@ -2146,7 +2146,7 @@ static void marshall_stream_start(RedChannelClient *rcc, stream_create.surface_id = 0; stream_create.id = get_stream_id(DCC_TO_DC(dcc), stream); stream_create.flags = stream->top_down ? SPICE_STREAM_FLAGS_TOP_DOWN : 0; - stream_create.codec_type = SPICE_VIDEO_CODEC_TYPE_MJPEG; + stream_create.codec_type = agent->video_encoder->codec_type; stream_create.src_width = stream->width; stream_create.src_height = stream->height; diff --git a/server/display-channel.c b/server/display-channel.c index 0ba4064..00c0bfd 100644 --- a/server/display-channel.c +++ b/server/display-channel.c @@ -199,6 +199,14 @@ void display_channel_set_stream_video(DisplayChannel *display, int stream_video) display->stream_video = stream_video; } +void display_channel_set_video_codecs(DisplayChannel *display, GArray *video_codecs) +{ + spice_return_if_fail(display); + + g_array_unref(display->video_codecs); + display->video_codecs = g_array_ref(video_codecs); +} + static void stop_streams(DisplayChannel *display) { Ring *ring = &display->streams; @@ -1949,6 +1957,7 @@ static SpiceCanvas *image_surfaces_get(SpiceImageSurfaces *surfaces, uint32_t su DisplayChannel* display_channel_new(SpiceServer *reds, RedWorker *worker, int migrate, int stream_video, + GArray *video_codecs, uint32_t n_surfaces) { DisplayChannel *display; @@ -2000,6 +2009,7 @@ DisplayChannel* display_channel_new(SpiceServer *reds, RedWorker *worker, drawables_init(display); image_cache_init(&display->image_cache); display->stream_video = stream_video; + display->video_codecs = g_array_ref(video_codecs); display_channel_init_streams(display); return display; diff --git a/server/display-channel.h b/server/display-channel.h index 5891e94..7a4067c 100644 --- a/server/display-channel.h +++ b/server/display-channel.h @@ -185,6 +185,7 @@ struct DisplayChannel { uint32_t glz_drawable_count; int stream_video; + GArray *video_codecs; uint32_t stream_count; Stream streams_buf[NUM_STREAMS]; Stream *free_streams; @@ -246,6 +247,7 @@ DisplayChannel* display_channel_new (SpiceServe RedWorker *worker, int migrate, int stream_video, + GArray *video_codecs, uint32_t n_surfaces); void display_channel_create_surface (DisplayChannel *display, uint32_t surface_id, uint32_t width, uint32_t height, @@ -267,6 +269,8 @@ void display_channel_update (DisplayCha void display_channel_free_some (DisplayChannel *display); void display_channel_set_stream_video (DisplayChannel *display, int stream_video); +void display_channel_set_video_codecs (DisplayChannel *display, + GArray *video_codecs); int display_channel_get_streams_timeout (DisplayChannel *display); void display_channel_compress_stats_print (const DisplayChannel *display); void display_channel_compress_stats_reset (DisplayChannel *display); diff --git a/server/gstreamer-encoder.c b/server/gstreamer-encoder.c index c3db28f..67a757f 100644 --- a/server/gstreamer-encoder.c +++ b/server/gstreamer-encoder.c @@ -525,9 +525,12 @@ static void spice_gst_encoder_get_stats(VideoEncoder *video_encoder, } } -VideoEncoder *gstreamer_encoder_new(uint64_t starting_bit_rate, +VideoEncoder *gstreamer_encoder_new(SpiceVideoCodecType codec_type, + uint64_t starting_bit_rate, VideoEncoderRateControlCbs *cbs) { + spice_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL); + GError *err = NULL; if (!gst_init_check(NULL, NULL, &err)) { spice_warning("GStreamer error: %s", err->message); @@ -542,6 +545,7 @@ VideoEncoder *gstreamer_encoder_new(uint64_t starting_bit_rate, encoder->base.notify_server_frame_drop = spice_gst_encoder_notify_server_frame_drop; encoder->base.get_bit_rate = spice_gst_encoder_get_bit_rate; encoder->base.get_stats = spice_gst_encoder_get_stats; + encoder->base.codec_type = codec_type; if (cbs) { encoder->cbs = *cbs; diff --git a/server/mjpeg-encoder.c b/server/mjpeg-encoder.c index 57708cd..5c143a6 100644 --- a/server/mjpeg-encoder.c +++ b/server/mjpeg-encoder.c @@ -1341,17 +1341,21 @@ static void mjpeg_encoder_get_stats(VideoEncoder *video_encoder, stats->avg_quality = (double)encoder->avg_quality / encoder->num_frames; } -VideoEncoder *mjpeg_encoder_new(uint64_t starting_bit_rate, +VideoEncoder *mjpeg_encoder_new(SpiceVideoCodecType codec_type, + uint64_t starting_bit_rate, VideoEncoderRateControlCbs *cbs) { MJpegEncoder *encoder = spice_new0(MJpegEncoder, 1); + spice_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL); + encoder->base.destroy = mjpeg_encoder_destroy; encoder->base.encode_frame = mjpeg_encoder_encode_frame; encoder->base.client_stream_report = mjpeg_encoder_client_stream_report; encoder->base.notify_server_frame_drop = mjpeg_encoder_notify_server_frame_drop; encoder->base.get_bit_rate = mjpeg_encoder_get_bit_rate; encoder->base.get_stats = mjpeg_encoder_get_stats; + encoder->base.codec_type = codec_type; encoder->first_frame = TRUE; encoder->rate_control.byte_rate = starting_bit_rate / 8; encoder->starting_bit_rate = starting_bit_rate; diff --git a/server/red-qxl.c b/server/red-qxl.c index 47fafab..b84ae69 100644 --- a/server/red-qxl.c +++ b/server/red-qxl.c @@ -1045,6 +1045,15 @@ void red_qxl_on_sv_change(QXLInstance *qxl, int sv) &payload); } +void red_qxl_on_vc_change(QXLInstance *qxl, GArray *video_codecs) +{ + RedWorkerMessageSetVideoCodecs payload; + payload.video_codecs = g_array_ref(video_codecs); + dispatcher_send_message(qxl->st->dispatcher, + RED_WORKER_MESSAGE_SET_VIDEO_CODECS, + &payload); +} + void red_qxl_set_mouse_mode(QXLInstance *qxl, uint32_t mode) { RedWorkerMessageSetMouseMode payload; diff --git a/server/red-qxl.h b/server/red-qxl.h index c9b6b36..00c5486 100644 --- a/server/red-qxl.h +++ b/server/red-qxl.h @@ -27,6 +27,7 @@ void red_qxl_init(SpiceServer *reds, QXLInstance *qxl); void red_qxl_on_ic_change(QXLInstance *qxl, SpiceImageCompression ic); void red_qxl_on_sv_change(QXLInstance *qxl, int sv); +void red_qxl_on_vc_change(QXLInstance *qxl, GArray* video_codecs); void red_qxl_set_mouse_mode(QXLInstance *qxl, uint32_t mode); void red_qxl_attach_worker(QXLInstance *qxl); void red_qxl_set_compression_level(QXLInstance *qxl, int level); @@ -113,6 +114,7 @@ enum { RED_WORKER_MESSAGE_DRIVER_UNLOAD, RED_WORKER_MESSAGE_GL_SCANOUT, RED_WORKER_MESSAGE_GL_DRAW_ASYNC, + RED_WORKER_MESSAGE_SET_VIDEO_CODECS, RED_WORKER_MESSAGE_COUNT // LAST }; @@ -250,6 +252,10 @@ typedef struct RedWorkerMessageSetStreamingVideo { uint32_t streaming_video; } RedWorkerMessageSetStreamingVideo; +typedef struct RedWorkerMessageSetVideoCodecs { + GArray* video_codecs; +} RedWorkerMessageSetVideoCodecs; + typedef struct RedWorkerMessageSetMouseMode { uint32_t mode; } RedWorkerMessageSetMouseMode; diff --git a/server/red-worker.c b/server/red-worker.c index e754bd2..55dd171 100644 --- a/server/red-worker.c +++ b/server/red-worker.c @@ -1052,6 +1052,15 @@ static void handle_dev_set_streaming_video(void *opaque, void *payload) display_channel_set_stream_video(worker->display_channel, msg->streaming_video); } +void handle_dev_set_video_codecs(void *opaque, void *payload) +{ + RedWorkerMessageSetVideoCodecs *msg = payload; + RedWorker *worker = opaque; + + display_channel_set_video_codecs(worker->display_channel, msg->video_codecs); + g_array_unref(msg->video_codecs); +} + static void handle_dev_set_mouse_mode(void *opaque, void *payload) { RedWorkerMessageSetMouseMode *msg = payload; @@ -1325,6 +1334,11 @@ static void register_callbacks(Dispatcher *dispatcher) sizeof(RedWorkerMessageSetStreamingVideo), DISPATCHER_NONE); dispatcher_register_handler(dispatcher, + RED_WORKER_MESSAGE_SET_VIDEO_CODECS, + handle_dev_set_video_codecs, + sizeof(RedWorkerMessageSetVideoCodecs), + DISPATCHER_NONE); + dispatcher_register_handler(dispatcher, RED_WORKER_MESSAGE_SET_MOUSE_MODE, handle_dev_set_mouse_mode, sizeof(RedWorkerMessageSetMouseMode), @@ -1509,7 +1523,9 @@ RedWorker* red_worker_new(QXLInstance *qxl, reds_register_channel(reds, channel); // TODO: handle seemless migration. Temp, setting migrate to FALSE - worker->display_channel = display_channel_new(reds, worker, FALSE, reds_get_streaming_video(reds), + worker->display_channel = display_channel_new(reds, worker, FALSE, + reds_get_streaming_video(reds), + reds_get_video_codecs(reds), init_info.n_surfaces); channel = RED_CHANNEL(worker->display_channel); diff --git a/server/reds.c b/server/reds.c index 4fd1d35..43fd5c3 100644 --- a/server/reds.c +++ b/server/reds.c @@ -71,6 +71,7 @@ #include "utils.h" #include "reds-private.h" +#include "video-encoder.h" static void reds_client_monitors_config(RedsState *reds, VDAgentMonitorsConfig *monitors_config); static gboolean reds_use_client_monitors_config(RedsState *reds); @@ -178,6 +179,7 @@ struct RedServerConfig { gboolean ticketing_enabled; uint32_t streaming_video; + GArray* video_codecs; SpiceImageCompression image_compression; uint32_t playback_compression; spice_wan_compression_t jpeg_state; @@ -298,6 +300,7 @@ static void reds_add_char_device(RedsState *reds, RedCharDevice *dev); static void reds_send_mm_time(RedsState *reds); static void reds_on_ic_change(RedsState *reds); static void reds_on_sv_change(RedsState *reds); +static void reds_on_vc_change(RedsState *reds); static void reds_on_vm_stop(RedsState *reds); static void reds_on_vm_start(RedsState *reds); static void reds_set_mouse_mode(RedsState *reds, uint32_t mode); @@ -3496,6 +3499,7 @@ err: } static const char default_renderer[] = "sw"; +static const char default_video_codecs[] = "spice:mjpeg;gstreamer:mjpeg"; /* new interface */ SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void) @@ -3517,6 +3521,7 @@ SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void) memset(reds->config->spice_uuid, 0, sizeof(reds->config->spice_uuid)); reds->config->ticketing_enabled = TRUE; /* ticketing enabled by default */ reds->config->streaming_video = SPICE_STREAM_VIDEO_FILTER; + reds->config->video_codecs = g_array_new(FALSE, FALSE, sizeof(RedVideoCodec)); reds->config->image_compression = SPICE_IMAGE_COMPRESSION_AUTO_GLZ; reds->config->playback_compression = TRUE; reds->config->jpeg_state = SPICE_WAN_COMPRESSION_AUTO; @@ -3528,37 +3533,129 @@ SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void) return reds; } -typedef struct RendererInfo { - int id; +typedef struct { + uint32_t id; const char *name; -} RendererInfo; +} EnumNames; -static const RendererInfo renderers_info[] = { +static gboolean get_name_index(const EnumNames names[], const char *name, uint32_t *index) +{ + if (name) { + int i; + for (i = 0; names[i].name; i++) { + if (strcmp(name, names[i].name) == 0) { + *index = i; + return TRUE; + } + } + } + return FALSE; +} + +static const EnumNames renderer_names[] = { {RED_RENDERER_SW, "sw"}, {RED_RENDERER_INVALID, NULL}, }; -static const RendererInfo *find_renderer(const char *name) +static gboolean reds_add_renderer(RedsState *reds, const char *name) +{ + uint32_t index; + + if (reds->config->renderers->len == RED_RENDERER_LAST || + !get_name_index(renderer_names, name, &index)) { + return FALSE; + } + g_array_append_val(reds->config->renderers, renderer_names[index].id); + return TRUE; +} + +static const EnumNames video_encoder_names[] = { + {0, "spice"}, + {1, "gstreamer"}, + {0, NULL}, +}; + +static new_video_encoder_t video_encoder_procs[] = { + &mjpeg_encoder_new, +#ifdef HAVE_GSTREAMER_1_0 + &gstreamer_encoder_new, +#else + NULL, +#endif +}; + +static const EnumNames video_codec_names[] = { + {SPICE_VIDEO_CODEC_TYPE_MJPEG, "mjpeg"}, + {0, NULL}, +}; + +static int video_codec_caps[] = { + SPICE_DISPLAY_CAP_CODEC_MJPEG, +}; + + +/* Expected string: encoder:codec;encoder:codec */ +static const char* parse_video_codecs(const char *codecs, char **encoder, + char **codec) { - const RendererInfo *inf = renderers_info; - while (inf->name) { - if (strcmp(name, inf->name) == 0) { - return inf; + if (!codecs) { + return NULL; + } + while (*codecs == ';') { + codecs++; + } + if (!*codecs) { + return NULL; + } + int n; + *encoder = *codec = NULL; + if (sscanf(codecs, "%m[0-9a-zA-Z_]:%m[0-9a-zA-Z_]%n", encoder, codec, &n) != 2) { + while (*codecs != '\0' && *codecs != ';') { + codecs++; } - inf++; + return codecs; } - return NULL; + return codecs + n; } -static int reds_add_renderer(RedsState *reds, const char *name) +static void reds_set_video_codecs(RedsState *reds, const char *codecs) { - const RendererInfo *inf; + char *encoder_name, *codec_name; - if (reds->config->renderers->len == RED_RENDERER_LAST || !(inf = find_renderer(name))) { - return FALSE; + if (strcmp(codecs, "auto") == 0) { + codecs = default_video_codecs; + } + + /* The video_codecs array is immutable */ + g_array_unref(reds->config->video_codecs); + reds->config->video_codecs = g_array_new(FALSE, FALSE, sizeof(RedVideoCodec)); + const char *c = codecs; + while ( (c = parse_video_codecs(c, &encoder_name, &codec_name)) ) { + uint32_t encoder_index, codec_index; + if (!encoder_name || !codec_name) { + spice_warning("spice: invalid encoder:codec value at %s", codecs); + + } else if (!get_name_index(video_encoder_names, encoder_name, &encoder_index)){ + spice_warning("spice: unknown video encoder %s", encoder_name); + + } else if (!get_name_index(video_codec_names, codec_name, &codec_index)) { + spice_warning("spice: unknown video codec %s", codec_name); + + } else if (!video_encoder_procs[encoder_index]) { + spice_warning("spice: unsupported video encoder %s", encoder_name); + + } else { + RedVideoCodec new_codec; + new_codec.create = video_encoder_procs[encoder_index]; + new_codec.type = video_codec_names[codec_index].id; + new_codec.cap = video_codec_caps[codec_index]; + g_array_append_val(reds->config->video_codecs, new_codec); + } + + free(encoder_name); + free(codec_name); + codecs = c; } - g_array_append_val(reds->config->renderers, inf->id); - return TRUE; } SPICE_GNUC_VISIBLE int spice_server_init(SpiceServer *reds, SpiceCoreInterface *core) @@ -3569,12 +3666,16 @@ SPICE_GNUC_VISIBLE int spice_server_init(SpiceServer *reds, SpiceCoreInterface * if (reds->config->renderers->len == 0) { reds_add_renderer(reds, default_renderer); } + if (reds->config->video_codecs->len == 0) { + reds_set_video_codecs(reds, default_video_codecs); + } return ret; } SPICE_GNUC_VISIBLE void spice_server_destroy(SpiceServer *reds) { g_array_unref(reds->config->renderers); + g_array_unref(reds->config->video_codecs); free(reds->config); if (reds->main_channel) { main_channel_close(reds->main_channel); @@ -3876,6 +3977,18 @@ uint32_t reds_get_streaming_video(const RedsState *reds) return reds->config->streaming_video; } +SPICE_GNUC_VISIBLE int spice_server_set_video_codecs(SpiceServer *reds, const char *video_codecs) +{ + reds_set_video_codecs(reds, video_codecs); + reds_on_vc_change(reds); + return 0; +} + +GArray* reds_get_video_codecs(const RedsState *reds) +{ + return reds->config->video_codecs; +} + SPICE_GNUC_VISIBLE int spice_server_set_playback_compression(SpiceServer *reds, int enable) { reds->config->playback_compression = !!enable; @@ -4268,6 +4381,15 @@ void reds_on_sv_change(RedsState *reds) } } +void reds_on_vc_change(RedsState *reds) +{ + GList *l; + + for (l = reds->qxl_instances; l != NULL; l = l->next) { + red_qxl_on_vc_change(l->data, reds_get_video_codecs(reds)); + } +} + void reds_on_vm_stop(RedsState *reds) { GList *l; diff --git a/server/reds.h b/server/reds.h index 1f05081..cd62fc1 100644 --- a/server/reds.h +++ b/server/reds.h @@ -93,6 +93,7 @@ void reds_on_main_channel_migrate(RedsState *reds, MainChannelClient *mcc); void reds_set_client_mm_time_latency(RedsState *reds, RedClient *client, uint32_t latency); uint32_t reds_get_streaming_video(const RedsState *reds); +GArray* reds_get_video_codecs(const RedsState *reds); spice_wan_compression_t reds_get_jpeg_state(const RedsState *reds); spice_wan_compression_t reds_get_zlib_glz_state(const RedsState *reds); SpiceCoreInterfaceInternal* reds_get_core_interface(RedsState *reds); diff --git a/server/spice-server.h b/server/spice-server.h index 87c5c59..6eb1b1d 100644 --- a/server/spice-server.h +++ b/server/spice-server.h @@ -114,6 +114,14 @@ enum { }; int spice_server_set_streaming_video(SpiceServer *s, int value); + +enum { + SPICE_STREAMING_INVALID, + SPICE_STREAMING_SPICE, + SPICE_STREAMING_GSTREAMER +}; + +int spice_server_set_video_codecs(SpiceServer *s, const char* video_codecs); int spice_server_set_playback_compression(SpiceServer *s, int enable); int spice_server_set_agent_mouse(SpiceServer *s, int enable); int spice_server_set_agent_copypaste(SpiceServer *s, int enable); diff --git a/server/spice-server.syms b/server/spice-server.syms index 5c3e53c..edf04a4 100644 --- a/server/spice-server.syms +++ b/server/spice-server.syms @@ -168,3 +168,8 @@ global: spice_qxl_gl_scanout; spice_qxl_gl_draw_async; } SPICE_SERVER_0.12.6; + +SPICE_SERVER_0.13.2 { +global: + spice_server_set_video_codecs; +} SPICE_SERVER_0.13.1; diff --git a/server/stream.c b/server/stream.c index f16beb8..9f74a6b 100644 --- a/server/stream.c +++ b/server/stream.c @@ -711,17 +711,34 @@ static VideoEncoder* dcc_create_video_encoder(DisplayChannelClient *dcc, uint64_t starting_bit_rate, VideoEncoderRateControlCbs *cbs) { + DisplayChannel *display = DCC_TO_DC(dcc); RedChannelClient *rcc = RED_CHANNEL_CLIENT(dcc); int client_has_multi_codec = red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_MULTI_CODEC); - if (!client_has_multi_codec || red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_CODEC_MJPEG)) { -#ifdef HAVE_GSTREAMER_1_0 - VideoEncoder* video_encoder = gstreamer_encoder_new(starting_bit_rate, cbs); + int i; + + for (i = 0; i < display->video_codecs->len; i++) { + RedVideoCodec* video_codec = &g_array_index (display->video_codecs, RedVideoCodec, i); + + if (!client_has_multi_codec && + video_codec->type != SPICE_VIDEO_CODEC_TYPE_MJPEG) { + /* Old clients only support MJPEG */ + continue; + } + if (client_has_multi_codec && + !red_channel_client_test_remote_cap(rcc, video_codec->cap)) { + /* The client is recent but does not support this codec */ + continue; + } + + VideoEncoder* video_encoder = video_codec->create(video_codec->type, starting_bit_rate, cbs); if (video_encoder) { return video_encoder; } -#endif - /* Use the builtin MJPEG video encoder as a fallback */ - return mjpeg_encoder_new(starting_bit_rate, cbs); + } + + /* Try to use the builtin MJPEG video encoder as a fallback */ + if (!client_has_multi_codec || red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_CODEC_MJPEG)) { + return mjpeg_encoder_new(SPICE_VIDEO_CODEC_TYPE_MJPEG, starting_bit_rate, cbs); } return NULL; diff --git a/server/video-encoder.h b/server/video-encoder.h index 708432b..7a04bc6 100644 --- a/server/video-encoder.h +++ b/server/video-encoder.h @@ -120,6 +120,9 @@ struct VideoEncoder { * statistics. */ void (*get_stats)(VideoEncoder *encoder, VideoEncoderStats *stats); + + /* The codec being used by the video encoder */ + SpiceVideoCodecType codec_type; }; @@ -152,17 +155,30 @@ typedef struct VideoEncoderRateControlCbs { /* Instantiates the video encoder. * + * @codec_type: The codec to use. * @starting_bit_rate: An initial estimate of the available stream bit rate * or zero if the client does not support rate control. * @cbs: A set of callback methods to be used for rate control. * @return: A pointer to a structure implementing the VideoEncoder * methods. */ -VideoEncoder* mjpeg_encoder_new(uint64_t starting_bit_rate, +typedef VideoEncoder* (*new_video_encoder_t)(SpiceVideoCodecType codec_type, + uint64_t starting_bit_rate, + VideoEncoderRateControlCbs *cbs); + +VideoEncoder* mjpeg_encoder_new(SpiceVideoCodecType codec_type, + uint64_t starting_bit_rate, VideoEncoderRateControlCbs *cbs); #ifdef HAVE_GSTREAMER_1_0 -VideoEncoder* gstreamer_encoder_new(uint64_t starting_bit_rate, +VideoEncoder* gstreamer_encoder_new(SpiceVideoCodecType codec_type, + uint64_t starting_bit_rate, VideoEncoderRateControlCbs *cbs); #endif +typedef struct RedVideoCodec { + new_video_encoder_t create; + SpiceVideoCodecType type; + uint32_t cap; +} RedVideoCodec; + #endif -- 2.8.1 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel