[Spice-devel,v3,1/2] display-channel: Join drawables to improve rhel7 behaviour

Submitted by Frediano Ziglio on March 3, 2017, 11:46 a.m.

Details

Message ID 0b7b17a4b0cdb2c55aef84c3cfb074a150c0b5ec.1488541556.git-series.fziglio@redhat.com
State New
Headers show
Series "RHEL7 improvements" ( rev: 4 ) in Spice

Not browsing as part of any series.

Commit Message

Frediano Ziglio March 3, 2017, 11:46 a.m.
Due to the way RHEL7 works the images came out from guest using multiple
commands. This increase the commands to the client and cause the
video code to create and handle multiple streams creating some
visual glitches.
This patch attempt to detect and join the multiple commands to
avoid these issues.

Signed-off-by: Frediano Ziglio <fziglio@redhat.com>
---
 server/display-channel-private.h |   2 +-
 server/display-channel.c         | 220 +++++++++++++++++++++++++++++++-
 server/red-parse-qxl.h           |   1 +-
 server/red-worker.c              |  14 +-
 4 files changed, 230 insertions(+), 7 deletions(-)

Patch hide | download patch | download mbox

diff --git a/server/display-channel-private.h b/server/display-channel-private.h
index da807d1..62e03b6 100644
--- a/server/display-channel-private.h
+++ b/server/display-channel-private.h
@@ -69,6 +69,8 @@  struct DisplayChannelPrivate
 
     int gl_draw_async_count;
 
+    RedDrawable *joinable_drawable;
+
 /* TODO: some day unify this, make it more runtime.. */
     stat_info_t add_stat;
     stat_info_t exclude_stat;
diff --git a/server/display-channel.c b/server/display-channel.c
index 67a77ef..e95741f 100644
--- a/server/display-channel.c
+++ b/server/display-channel.c
@@ -78,6 +78,8 @@  display_channel_finalize(GObject *object)
 {
     DisplayChannel *self = DISPLAY_CHANNEL(object);
 
+    red_drawable_unref(self->priv->joinable_drawable);
+
     display_channel_destroy_surfaces(self);
     image_cache_reset(&self->priv->image_cache);
     monitors_config_unref(self->priv->monitors_config);
@@ -1176,8 +1178,190 @@  static void display_channel_add_drawable(DisplayChannel *display, Drawable *draw
 #endif
 }
 
-void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_drawable,
-                                  uint32_t process_commands_generation)
+/* Check that a given drawable is a simple copy that can
+ * possibly be joined to next one.
+ * This is used to work around some drivers which split larger image
+ * updates into smaller chunks. These chunks are generally horizontal
+ * strips. This can cause our stream-detection heuristics to generate
+ * multiple streams instead of a single stream, and results in more
+ * commands being sent to the client.
+ * One example is RHEL 7.
+ * Some of the check in this function are just to check that the
+ * operation is a opaque bitmap copy operations, some other just
+ * avoid to make the joining code more complicated just to support
+ * unseen commands.
+ */
+static bool red_drawable_joinable(const RedDrawable *drawable)
+{
+    // These 3 initial tests are arranged to minimize checks as
+    // they are arranged from more probable to less probable
+    if (drawable->clip.type != SPICE_CLIP_TYPE_NONE) {
+        return false;
+    }
+    if (drawable->type != QXL_DRAW_COPY) {
+        return false;
+    }
+    if (drawable->effect != QXL_EFFECT_OPAQUE) {
+        return false;
+    }
+    // Never seen, avoid to support during join
+    if (drawable->self_bitmap) {
+        return false;
+    }
+    // Consistency, a copy should not have dependencies
+    if (drawable->surface_deps[0] != -1 ||
+        drawable->surface_deps[1] != -1 ||
+        drawable->surface_deps[2] != -1) {
+        return false;
+    }
+
+    const SpiceCopy *copy = &drawable->u.copy;
+    // We need a bitmap
+    if (copy->src_bitmap == NULL) {
+        return false;
+    }
+    if (copy->rop_descriptor != SPICE_ROPD_OP_PUT) {
+        return false;
+    }
+    // Never seen, avoid to support during join
+    if (copy->mask.bitmap != NULL) {
+        return false;
+    }
+
+    // Limit image support to 32bit bitmap
+    const SpiceImage *image = copy->src_bitmap;
+    if (image->descriptor.type != SPICE_IMAGE_TYPE_BITMAP) {
+        return false;
+    }
+    const SpiceBitmap *bitmap = &image->u.bitmap;
+    if (bitmap->format != SPICE_BITMAP_FMT_RGBA && bitmap->format != SPICE_BITMAP_FMT_32BIT) {
+        return false;
+    }
+    // Never seen anything else, avoid to support during join.
+    // We could not easily join bitmap with different orientations
+    if (bitmap->flags != SPICE_BITMAP_FLAGS_TOP_DOWN) {
+        return false;
+    }
+    // Consistency, a 32bit image have no reason to have a palette
+    if (bitmap->palette != NULL) {
+        return false;
+    }
+    if (bitmap->data == NULL) {
+        return false;
+    }
+    // Never seen, avoid to support during join.
+    // Wouldn't be too hard to implement but looking at driver code this flag
+    // is not used so no worth implementing it.
+    if (bitmap->data->flags & SPICE_CHUNKS_FLAGS_UNSTABLE) {
+        return false;
+    }
+    // Consistency, the bitmap inside the image should fill the image
+    if (bitmap->x != image->descriptor.width ||
+        bitmap->y != image->descriptor.height) {
+        return false;
+    }
+    // Area should specify all image. If not we would have to
+    // adjust the memory chunks to take into account this or make other
+    // complicated cropping.
+    if (copy->src_area.left != 0 || copy->src_area.top != 0 ||
+        copy->src_area.right != bitmap->x || copy->src_area.bottom != bitmap->y) {
+        return false;
+    }
+    if (drawable->bbox.right - drawable->bbox.left != bitmap->x ||
+        drawable->bbox.bottom - drawable->bbox.top != bitmap->y) {
+        return false;
+    }
+
+    return true;
+}
+
+static bool red_drawable_can_join(const RedDrawable *first, const RedDrawable *second)
+{
+    if (!red_drawable_joinable(first) || !red_drawable_joinable(second)) {
+        return false;
+    }
+    if (first->surface_id != second->surface_id) {
+        return false;
+    }
+    // We can't join 2 different format
+    if (first->u.copy.src_bitmap->u.bitmap.format != second->u.copy.src_bitmap->u.bitmap.format) {
+        return false;
+    }
+    // They must have the same width
+    if (first->u.copy.src_bitmap->u.bitmap.x != second->u.copy.src_bitmap->u.bitmap.x ||
+        first->u.copy.src_bitmap->u.bitmap.stride != second->u.copy.src_bitmap->u.bitmap.stride) {
+        return false;
+    }
+    // Check that second is exactly under the first
+    if (first->bbox.left != second->bbox.left) {
+        return false;
+    }
+    if (first->bbox.bottom != second->bbox.top) {
+        return false;
+    }
+    // Check we can join the chunks
+    if (first->u.copy.src_bitmap->u.bitmap.data->flags !=
+        second->u.copy.src_bitmap->u.bitmap.data->flags) {
+        return false;
+    }
+    return true;
+}
+
+/* Create a new set of chunks containing the chunks of first+second set.
+ * Note that the resulting chunks will point to original data so
+ * you should pay attention in order to avoid double free or other
+ * corruptions.
+ * For instance you could set to zero the number of chunks of first and second
+ * just after calling this function or free the two sets paying attention
+ * they don't release the data.
+ */
+static SpiceChunks *chunks_join(const SpiceChunks *first, const SpiceChunks *second)
+{
+    // TODO use realloc to optimize
+    SpiceChunks *new_chunks = spice_chunks_new(first->num_chunks + second->num_chunks);
+    new_chunks->flags = first->flags;
+    new_chunks->data_size = first->data_size + second->data_size;
+    memcpy(new_chunks->chunk, first->chunk,
+           sizeof(first->chunk[0]) * first->num_chunks);
+    memcpy(new_chunks->chunk + first->num_chunks, second->chunk,
+           sizeof(second->chunk[0]) * second->num_chunks);
+    return new_chunks;
+}
+
+/* Join the first drawable with the second.
+ * The resulting drawable is returned.
+ * Once joined you should avoid to use first and second drawables.
+ */
+static RedDrawable *red_drawable_join(RedDrawable *first, RedDrawable *second)
+{
+    // Join drawing areas. The first is exactly on top of the second
+    uint32_t first_height = first->u.copy.src_bitmap->u.bitmap.y;
+    second->u.copy.src_bitmap->descriptor.flags &= ~SPICE_IMAGE_FLAGS_CACHE_ME;
+    second->bbox.top = first->bbox.top;
+    second->u.copy.src_bitmap->descriptor.height += first_height;
+    second->u.copy.src_bitmap->u.bitmap.y += first_height;
+    second->u.copy.src_area.bottom += first_height;
+
+    // Join chunks
+    SpiceChunks *new_chunks = chunks_join(first->u.copy.src_bitmap->u.bitmap.data,
+                                          second->u.copy.src_bitmap->u.bitmap.data);
+    // Prevent to free chunks copied in new structure
+    second->u.copy.src_bitmap->u.bitmap.data->num_chunks = 0;
+    first->u.copy.src_bitmap->u.bitmap.data->num_chunks = 0;
+    // Replace second set of chunks with the sum computed above
+    spice_chunks_destroy(second->u.copy.src_bitmap->u.bitmap.data);
+    second->u.copy.src_bitmap->u.bitmap.data = new_chunks;
+
+    // Create a chain of drawables. This is necessary to retain needed memory
+    // resources.
+    second->joined = first;
+
+    return second;
+}
+
+static void
+display_channel_process_draw_single(DisplayChannel *display, RedDrawable *red_drawable,
+                                    uint32_t process_commands_generation)
 {
     Drawable *drawable =
         display_channel_get_drawable(display, red_drawable->effect, red_drawable,
@@ -1192,6 +1376,38 @@  void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_draw
     drawable_unref(drawable);
 }
 
+void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_drawable,
+                                  uint32_t process_commands_generation)
+{
+    if (!red_drawable_joinable(red_drawable)) {
+        // Not joinable, process all
+        if (display->priv->joinable_drawable) {
+            display_channel_process_draw_single(display, display->priv->joinable_drawable,
+                                                process_commands_generation);
+            red_drawable_unref(display->priv->joinable_drawable);
+            display->priv->joinable_drawable = NULL;
+        }
+        display_channel_process_draw_single(display, red_drawable, process_commands_generation);
+        return;
+    }
+    // Drawable is joinable
+
+    if (display->priv->joinable_drawable != NULL) {
+        if (red_drawable_can_join(display->priv->joinable_drawable, red_drawable)) {
+            red_drawable = red_drawable_join(display->priv->joinable_drawable, red_drawable);
+        } else {
+            // They can't be joined
+            display_channel_process_draw_single(display, display->priv->joinable_drawable,
+                                                process_commands_generation);
+            red_drawable_unref(display->priv->joinable_drawable);
+        }
+    }
+
+    // Try to join with next one
+    red_drawable_ref(red_drawable);
+    display->priv->joinable_drawable = red_drawable;
+}
+
 int display_channel_wait_for_migrate_data(DisplayChannel *display)
 {
     uint64_t end_time = spice_get_monotonic_time_ns() + DISPLAY_CLIENT_MIGRATE_DATA_TIMEOUT;
diff --git a/server/red-parse-qxl.h b/server/red-parse-qxl.h
index 86a2d93..56cc906 100644
--- a/server/red-parse-qxl.h
+++ b/server/red-parse-qxl.h
@@ -57,6 +57,7 @@  typedef struct RedDrawable {
         SpiceWhiteness whiteness;
         SpiceComposite composite;
     } u;
+    struct RedDrawable *joined;
 } RedDrawable;
 
 static inline RedDrawable *red_drawable_ref(RedDrawable *drawable)
diff --git a/server/red-worker.c b/server/red-worker.c
index c88034b..897566e 100644
--- a/server/red-worker.c
+++ b/server/red-worker.c
@@ -94,12 +94,16 @@  static int display_is_connected(RedWorker *worker)
 
 void red_drawable_unref(RedDrawable *red_drawable)
 {
-    if (--red_drawable->refs) {
-        return;
+    while (red_drawable) {
+        if (--red_drawable->refs) {
+            return;
+        }
+        red_qxl_release_resource(red_drawable->qxl, red_drawable->release_info_ext);
+        red_put_drawable(red_drawable);
+        RedDrawable *next = red_drawable->joined;
+        free(red_drawable);
+        red_drawable = next;
     }
-    red_qxl_release_resource(red_drawable->qxl, red_drawable->release_info_ext);
-    red_put_drawable(red_drawable);
-    free(red_drawable);
 }
 
 static gboolean red_process_cursor_cmd(RedWorker *worker, const QXLCommandExt *ext)

Comments

> On 3 Mar 2017, at 12:46, Frediano Ziglio <fziglio@redhat.com> wrote:
> 
> Due to the way RHEL7 works the images came out from guest using multiple
> commands. This increase the commands to the client and cause the
> video code to create and handle multiple streams creating some
> visual glitches.
> This patch attempt to detect and join the multiple commands to
> avoid these issues.
> 
> Signed-off-by: Frediano Ziglio <fziglio@redhat.com>
> ---
> server/display-channel-private.h |   2 +-
> server/display-channel.c         | 220 +++++++++++++++++++++++++++++++-
> server/red-parse-qxl.h           |   1 +-
> server/red-worker.c              |  14 +-
> 4 files changed, 230 insertions(+), 7 deletions(-)
> 
> diff --git a/server/display-channel-private.h b/server/display-channel-private.h
> index da807d1..62e03b6 100644
> --- a/server/display-channel-private.h
> +++ b/server/display-channel-private.h
> @@ -69,6 +69,8 @@ struct DisplayChannelPrivate
> 
>    int gl_draw_async_count;
> 
> +    RedDrawable *joinable_drawable;
> +
> /* TODO: some day unify this, make it more runtime.. */
>    stat_info_t add_stat;
>    stat_info_t exclude_stat;
> diff --git a/server/display-channel.c b/server/display-channel.c
> index 67a77ef..e95741f 100644
> --- a/server/display-channel.c
> +++ b/server/display-channel.c
> @@ -78,6 +78,8 @@ display_channel_finalize(GObject *object)
> {
>    DisplayChannel *self = DISPLAY_CHANNEL(object);
> 
> +    red_drawable_unref(self->priv->joinable_drawable);
> +
>    display_channel_destroy_surfaces(self);
>    image_cache_reset(&self->priv->image_cache);
>    monitors_config_unref(self->priv->monitors_config);
> @@ -1176,8 +1178,190 @@ static void display_channel_add_drawable(DisplayChannel *display, Drawable *draw
> #endif
> }
> 
> -void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_drawable,
> -                                  uint32_t process_commands_generation)
> +/* Check that a given drawable is a simple copy that can
> + * possibly be joined to next one.
> + * This is used to work around some drivers which split larger image
> + * updates into smaller chunks. These chunks are generally horizontal
> + * strips. This can cause our stream-detection heuristics to generate
> + * multiple streams instead of a single stream, and results in more
> + * commands being sent to the client.
> + * One example is RHEL 7.
> + * Some of the check in this function are just to check that the
> + * operation is a opaque bitmap copy operations, some other just
> + * avoid to make the joining code more complicated just to support
> + * unseen commands.
> + */
> +static bool red_drawable_joinable(const RedDrawable *drawable)
> +{
> +    // These 3 initial tests are arranged to minimize checks as
> +    // they are arranged from more probable to less probable

Any reason not to group all the tests with || ?

> +    if (drawable->clip.type != SPICE_CLIP_TYPE_NONE) {
> +        return false;
> +    }
> +    if (drawable->type != QXL_DRAW_COPY) {
> +        return false;
> +    }
> +    if (drawable->effect != QXL_EFFECT_OPAQUE) {
> +        return false;
> +    }
> +    // Never seen, avoid to support during join
> +    if (drawable->self_bitmap) {
> +        return false;
> +    }
> +    // Consistency, a copy should not have dependencies
> +    if (drawable->surface_deps[0] != -1 ||
> +        drawable->surface_deps[1] != -1 ||
> +        drawable->surface_deps[2] != -1) {
> +        return false;
> +    }
> +
> +    const SpiceCopy *copy = &drawable->u.copy;
> +    // We need a bitmap
> +    if (copy->src_bitmap == NULL) {
> +        return false;
> +    }
> +    if (copy->rop_descriptor != SPICE_ROPD_OP_PUT) {
> +        return false;
> +    }
> +    // Never seen, avoid to support during join
> +    if (copy->mask.bitmap != NULL) {
> +        return false;
> +    }
> +
> +    // Limit image support to 32bit bitmap
> +    const SpiceImage *image = copy->src_bitmap;

For my education: I thought the style was to declare variables ahead of time (which I personally don’t like much). But I prefer the way you wrote it.

> +    if (image->descriptor.type != SPICE_IMAGE_TYPE_BITMAP) {
> +        return false;
> +    }
> +    const SpiceBitmap *bitmap = &image->u.bitmap;
> +    if (bitmap->format != SPICE_BITMAP_FMT_RGBA && bitmap->format != SPICE_BITMAP_FMT_32BIT) {
> +        return false;
> +    }

If you restrict to 32-bit bitmaps, it might be worth adding to the comment, and explaining why. Is it “never seen”, “can’t do” or “don’t want to do”?

> +    // Never seen anything else, avoid to support during join.
> +    // We could not easily join bitmap with different orientations
> +    if (bitmap->flags != SPICE_BITMAP_FLAGS_TOP_DOWN) {
> +        return false;
> +    }
> +    // Consistency, a 32bit image have no reason to have a palette
> +    if (bitmap->palette != NULL) {
> +        return false;
> +    }
> +    if (bitmap->data == NULL) {
> +        return false;
> +    }
> +    // Never seen, avoid to support during join.
> +    // Wouldn't be too hard to implement but looking at driver code this flag
> +    // is not used so no worth implementing it.
> +    if (bitmap->data->flags & SPICE_CHUNKS_FLAGS_UNSTABLE) {
> +        return false;
> +    }
> +    // Consistency, the bitmap inside the image should fill the image
> +    if (bitmap->x != image->descriptor.width ||
> +        bitmap->y != image->descriptor.height) {
> +        return false;
> +    }
> +    // Area should specify all image. If not we would have to
> +    // adjust the memory chunks to take into account this or make other
> +    // complicated cropping.
> +    if (copy->src_area.left != 0 || copy->src_area.top != 0 ||
> +        copy->src_area.right != bitmap->x || copy->src_area.bottom != bitmap->y) {
> +        return false;
> +    }
> +    if (drawable->bbox.right - drawable->bbox.left != bitmap->x ||
> +        drawable->bbox.bottom - drawable->bbox.top != bitmap->y) {
> +        return false;
> +    }
> +
> +    return true;
> +}
> +
> +static bool red_drawable_can_join(const RedDrawable *first, const RedDrawable *second)
> +{
> +    if (!red_drawable_joinable(first) || !red_drawable_joinable(second)) {
> +        return false;
> +    }
> +    if (first->surface_id != second->surface_id) {
> +        return false;
> +    }
> +    // We can't join 2 different format
> +    if (first->u.copy.src_bitmap->u.bitmap.format != second->u.copy.src_bitmap->u.bitmap.format) {
> +        return false;
> +    }
> +    // They must have the same width
> +    if (first->u.copy.src_bitmap->u.bitmap.x != second->u.copy.src_bitmap->u.bitmap.x ||
> +        first->u.copy.src_bitmap->u.bitmap.stride != second->u.copy.src_bitmap->u.bitmap.stride) {
> +        return false;
> +    }
> +    // Check that second is exactly under the first
> +    if (first->bbox.left != second->bbox.left) {
> +        return false;
> +    }
> +    if (first->bbox.bottom != second->bbox.top) {
> +        return false;
> +    }
> +    // Check we can join the chunks
> +    if (first->u.copy.src_bitmap->u.bitmap.data->flags !=
> +        second->u.copy.src_bitmap->u.bitmap.data->flags) {
> +        return false;
> +    }
> +    return true;
> +}
> +
> +/* Create a new set of chunks containing the chunks of first+second set.
> + * Note that the resulting chunks will point to original data so
> + * you should pay attention in order to avoid double free or other
> + * corruptions.
> + * For instance you could set to zero the number of chunks of first and second
> + * just after calling this function or free the two sets paying attention
> + * they don't release the data.
> + */
> +static SpiceChunks *chunks_join(const SpiceChunks *first, const SpiceChunks *second)
> +{
> +    // TODO use realloc to optimize
> +    SpiceChunks *new_chunks = spice_chunks_new(first->num_chunks + second->num_chunks);
> +    new_chunks->flags = first->flags;
> +    new_chunks->data_size = first->data_size + second->data_size;
> +    memcpy(new_chunks->chunk, first->chunk,
> +           sizeof(first->chunk[0]) * first->num_chunks);
> +    memcpy(new_chunks->chunk + first->num_chunks, second->chunk,
> +           sizeof(second->chunk[0]) * second->num_chunks);
> +    return new_chunks;
> +}
> +
> +/* Join the first drawable with the second.
> + * The resulting drawable is returned.
> + * Once joined you should avoid to use first and second drawables.
> + */
> +static RedDrawable *red_drawable_join(RedDrawable *first, RedDrawable *second)
> +{
> +    // Join drawing areas. The first is exactly on top of the second
> +    uint32_t first_height = first->u.copy.src_bitmap->u.bitmap.y;
> +    second->u.copy.src_bitmap->descriptor.flags &= ~SPICE_IMAGE_FLAGS_CACHE_ME;
> +    second->bbox.top = first->bbox.top;
> +    second->u.copy.src_bitmap->descriptor.height += first_height;
> +    second->u.copy.src_bitmap->u.bitmap.y += first_height;
> +    second->u.copy.src_area.bottom += first_height;
> +
> +    // Join chunks
> +    SpiceChunks *new_chunks = chunks_join(first->u.copy.src_bitmap->u.bitmap.data,
> +                                          second->u.copy.src_bitmap->u.bitmap.data);
> +    // Prevent to free chunks copied in new structure
> +    second->u.copy.src_bitmap->u.bitmap.data->num_chunks = 0;
> +    first->u.copy.src_bitmap->u.bitmap.data->num_chunks = 0;
> +    // Replace second set of chunks with the sum computed above
> +    spice_chunks_destroy(second->u.copy.src_bitmap->u.bitmap.data);
> +    second->u.copy.src_bitmap->u.bitmap.data = new_chunks;
> +
> +    // Create a chain of drawables. This is necessary to retain needed memory
> +    // resources.
> +    second->joined = first;
> +
> +    return second;
> +}
> +
> +static void
> +display_channel_process_draw_single(DisplayChannel *display, RedDrawable *red_drawable,
> +                                    uint32_t process_commands_generation)
> {
>    Drawable *drawable =
>        display_channel_get_drawable(display, red_drawable->effect, red_drawable,
> @@ -1192,6 +1376,38 @@ void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_draw
>    drawable_unref(drawable);
> }
> 
> +void display_channel_process_draw(DisplayChannel *display, RedDrawable *red_drawable,
> +                                  uint32_t process_commands_generation)
> +{
> +    if (!red_drawable_joinable(red_drawable)) {
> +        // Not joinable, process all
> +        if (display->priv->joinable_drawable) {
> +            display_channel_process_draw_single(display, display->priv->joinable_drawable,
> +                                                process_commands_generation);
> +            red_drawable_unref(display->priv->joinable_drawable);
> +            display->priv->joinable_drawable = NULL;
> +        }
> +        display_channel_process_draw_single(display, red_drawable, process_commands_generation);
> +        return;
> +    }
> +    // Drawable is joinable
> +
> +    if (display->priv->joinable_drawable != NULL) {
> +        if (red_drawable_can_join(display->priv->joinable_drawable, red_drawable)) {
> +            red_drawable = red_drawable_join(display->priv->joinable_drawable, red_drawable);
> +        } else {
> +            // They can't be joined
> +            display_channel_process_draw_single(display, display->priv->joinable_drawable,
> +                                                process_commands_generation);
> +            red_drawable_unref(display->priv->joinable_drawable);
> +        }
> +    }
> +
> +    // Try to join with next one
> +    red_drawable_ref(red_drawable);
> +    display->priv->joinable_drawable = red_drawable;
> +}
> +
> int display_channel_wait_for_migrate_data(DisplayChannel *display)
> {
>    uint64_t end_time = spice_get_monotonic_time_ns() + DISPLAY_CLIENT_MIGRATE_DATA_TIMEOUT;
> diff --git a/server/red-parse-qxl.h b/server/red-parse-qxl.h
> index 86a2d93..56cc906 100644
> --- a/server/red-parse-qxl.h
> +++ b/server/red-parse-qxl.h
> @@ -57,6 +57,7 @@ typedef struct RedDrawable {
>        SpiceWhiteness whiteness;
>        SpiceComposite composite;
>    } u;
> +    struct RedDrawable *joined;
> } RedDrawable;
> 
> static inline RedDrawable *red_drawable_ref(RedDrawable *drawable)
> diff --git a/server/red-worker.c b/server/red-worker.c
> index c88034b..897566e 100644
> --- a/server/red-worker.c
> +++ b/server/red-worker.c
> @@ -94,12 +94,16 @@ static int display_is_connected(RedWorker *worker)
> 
> void red_drawable_unref(RedDrawable *red_drawable)
> {
> -    if (--red_drawable->refs) {
> -        return;
> +    while (red_drawable) {
> +        if (--red_drawable->refs) {
> +            return;
> +        }
> +        red_qxl_release_resource(red_drawable->qxl, red_drawable->release_info_ext);
> +        red_put_drawable(red_drawable);
> +        RedDrawable *next = red_drawable->joined;
> +        free(red_drawable);
> +        red_drawable = next;
>    }
> -    red_qxl_release_resource(red_drawable->qxl, red_drawable->release_info_ext);
> -    red_put_drawable(red_drawable);
> -    free(red_drawable);
> }
> 
> static gboolean red_process_cursor_cmd(RedWorker *worker, const QXLCommandExt *ext)
> -- 
> git-series 0.9.1
> _______________________________________________
> Spice-devel mailing list
> Spice-devel@lists.freedesktop.org
> https://lists.freedesktop.org/mailman/listinfo/spice-devel