/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* Copyright (C) 2010 Red Hat, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see . */ #include "config.h" #ifdef HAVE_SYS_TYPES_H #include #endif #include "glib-compat.h" #include "spice-client.h" #include "spice-common.h" #include "spice-marshal.h" #include "spice-channel-priv.h" #include "spice-session-priv.h" #include "channel-display-priv.h" #include "decode.h" /** * SECTION:channel-display * @short_description: remote display area * @title: Display Channel * @section_id: * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay * @stability: Stable * @include: spice-client.h * * A class that handles the rendering of the remote display and inform * of its updates. * * The creation of the main graphic buffer is signaled with * #SpiceDisplayChannel::display-primary-create. * * The update of regions is notified by * #SpiceDisplayChannel::display-invalidate signals. */ #define SPICE_DISPLAY_CHANNEL_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_DISPLAY_CHANNEL, SpiceDisplayChannelPrivate)) #define MONITORS_MAX 256 struct _SpiceDisplayChannelPrivate { GHashTable *surfaces; display_surface *primary; display_cache *images; display_cache *palettes; SpiceImageCache image_cache; SpicePaletteCache palette_cache; SpiceImageSurfaces image_surfaces; SpiceGlzDecoderWindow *glz_window; display_stream **streams; int nstreams; gboolean mark; guint mark_false_event_id; GArray *monitors; guint monitors_max; gboolean enable_adaptive_streaming; #ifdef G_OS_WIN32 HDC dc; #endif SpiceGlScanout scanout; }; G_DEFINE_TYPE(SpiceDisplayChannel, spice_display_channel, SPICE_TYPE_CHANNEL) /* Properties */ enum { PROP_0, PROP_WIDTH, PROP_HEIGHT, PROP_MONITORS, PROP_MONITORS_MAX, PROP_GL_SCANOUT, }; enum { SPICE_DISPLAY_PRIMARY_CREATE, SPICE_DISPLAY_PRIMARY_DESTROY, SPICE_DISPLAY_INVALIDATE, SPICE_DISPLAY_MARK, SPICE_DISPLAY_GL_DRAW, SPICE_DISPLAY_LAST_SIGNAL, }; static guint signals[SPICE_DISPLAY_LAST_SIGNAL]; static void spice_display_channel_up(SpiceChannel *channel); static void channel_set_handlers(SpiceChannelClass *klass); static void clear_surfaces(SpiceChannel *channel, gboolean keep_primary); static void clear_streams(SpiceChannel *channel); static display_surface *find_surface(SpiceDisplayChannelPrivate *c, guint32 surface_id); static gboolean display_stream_render(display_stream *st); static void spice_display_channel_reset(SpiceChannel *channel, gboolean migrating); static void spice_display_channel_reset_capabilities(SpiceChannel *channel); static void destroy_canvas(display_surface *surface); static void _msg_in_unref_func(gpointer data, gpointer user_data); static void display_session_mm_time_reset_cb(SpiceSession *session, gpointer data); static SpiceGlScanout* spice_gl_scanout_copy(const SpiceGlScanout *scanout); G_DEFINE_BOXED_TYPE(SpiceGlScanout, spice_gl_scanout, (GBoxedCopyFunc)spice_gl_scanout_copy, (GBoxedFreeFunc)spice_gl_scanout_free) /* ------------------------------------------------------------------ */ static SpiceGlScanout* spice_gl_scanout_copy(const SpiceGlScanout *scanout) { SpiceGlScanout *so = g_new(SpiceGlScanout, 1); *so = *scanout; so->fd = dup(so->fd); return so; } void spice_gl_scanout_free(SpiceGlScanout *scanout) { close(scanout->fd); g_free(scanout); } static void spice_display_channel_dispose(GObject *object) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv; if (c->mark_false_event_id != 0) { g_source_remove(c->mark_false_event_id); c->mark_false_event_id = 0; } if (c->scanout.fd >= 0) { close(c->scanout.fd); c->scanout.fd = -1; } if (G_OBJECT_CLASS(spice_display_channel_parent_class)->dispose) G_OBJECT_CLASS(spice_display_channel_parent_class)->dispose(object); } static void spice_display_channel_finalize(GObject *object) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv; g_clear_pointer(&c->monitors, g_array_unref); clear_surfaces(SPICE_CHANNEL(object), FALSE); g_hash_table_unref(c->surfaces); clear_streams(SPICE_CHANNEL(object)); g_clear_pointer(&c->palettes, cache_free); if (G_OBJECT_CLASS(spice_display_channel_parent_class)->finalize) G_OBJECT_CLASS(spice_display_channel_parent_class)->finalize(object); } static void spice_display_channel_constructed(GObject *object) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv; SpiceSession *s = spice_channel_get_session(SPICE_CHANNEL(object)); g_return_if_fail(s != NULL); spice_session_get_caches(s, &c->images, &c->glz_window); c->palettes = cache_new(g_free); g_return_if_fail(c->glz_window != NULL); g_return_if_fail(c->images != NULL); g_return_if_fail(c->palettes != NULL); c->monitors = g_array_new(FALSE, TRUE, sizeof(SpiceDisplayMonitorConfig)); spice_g_signal_connect_object(s, "mm-time-reset", G_CALLBACK(display_session_mm_time_reset_cb), SPICE_CHANNEL(object), 0); if (G_OBJECT_CLASS(spice_display_channel_parent_class)->constructed) G_OBJECT_CLASS(spice_display_channel_parent_class)->constructed(object); } static void spice_display_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { SpiceDisplayChannel *channel = SPICE_DISPLAY_CHANNEL(object); SpiceDisplayChannelPrivate *c = channel->priv; switch (prop_id) { case PROP_WIDTH: { g_value_set_uint(value, c->primary ? c->primary->width : 0); break; } case PROP_HEIGHT: { g_value_set_uint(value, c->primary ? c->primary->height : 0); break; } case PROP_MONITORS: { g_value_set_boxed(value, c->monitors); break; } case PROP_MONITORS_MAX: { g_value_set_uint(value, c->monitors_max); break; } case PROP_GL_SCANOUT: { g_value_set_static_boxed(value, spice_display_get_gl_scanout(channel)); break; } default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void spice_display_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { switch (prop_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } /* main or coroutine context */ static void spice_display_channel_reset(SpiceChannel *channel, gboolean migrating) { /* palettes, images, and glz_window are cleared in the session */ clear_streams(channel); clear_surfaces(channel, TRUE); SPICE_CHANNEL_CLASS(spice_display_channel_parent_class)->channel_reset(channel, migrating); } static void spice_display_channel_class_init(SpiceDisplayChannelClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass); gobject_class->finalize = spice_display_channel_finalize; gobject_class->dispose = spice_display_channel_dispose; gobject_class->get_property = spice_display_get_property; gobject_class->set_property = spice_display_set_property; gobject_class->constructed = spice_display_channel_constructed; channel_class->channel_up = spice_display_channel_up; channel_class->channel_reset = spice_display_channel_reset; channel_class->channel_reset_capabilities = spice_display_channel_reset_capabilities; g_object_class_install_property (gobject_class, PROP_HEIGHT, g_param_spec_uint("height", "Display height", "The primary surface height", 0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_WIDTH, g_param_spec_uint("width", "Display width", "The primary surface width", 0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceDisplayChannel:monitors: * * Current monitors configuration. * * Since: 0.13 */ g_object_class_install_property (gobject_class, PROP_MONITORS, g_param_spec_boxed("monitors", "Display monitors", "The monitors configuration", G_TYPE_ARRAY, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceDisplayChannel:monitors-max: * * The maximum number of monitors the server or guest supports. * May change during client lifetime, for instance guest may * reboot or dynamically adjust this. * * Since: 0.13 */ g_object_class_install_property (gobject_class, PROP_MONITORS_MAX, g_param_spec_uint("monitors-max", "Max display monitors", "The current maximum number of monitors", 1, MONITORS_MAX, 1, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceDisplayChannel:gl-scanout: * @display: the #SpiceDisplayChannel that emitted the signal * * The last #SpiceGlScanout received. * * Since: 0.31 */ g_object_class_install_property (gobject_class, PROP_GL_SCANOUT, g_param_spec_boxed("gl-scanout", "GL scanout", "GL scanout", SPICE_TYPE_GL_SCANOUT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceDisplayChannel::display-primary-create: * @display: the #SpiceDisplayChannel that emitted the signal * @format: %SPICE_SURFACE_FMT_32_xRGB or %SPICE_SURFACE_FMT_16_555; * @width: width resolution * @height: height resolution * @stride: the buffer stride ("width" padding) * @shmid: identifier of the shared memory segment associated with * the @imgdata, or -1 if not shm * @imgdata: pointer to surface buffer * * The #SpiceDisplayChannel::display-primary-create signal * provides main display buffer data. **/ signals[SPICE_DISPLAY_PRIMARY_CREATE] = g_signal_new("display-primary-create", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceDisplayChannelClass, display_primary_create), NULL, NULL, g_cclosure_user_marshal_VOID__INT_INT_INT_INT_INT_POINTER, G_TYPE_NONE, 6, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_POINTER); /** * SpiceDisplayChannel::display-primary-destroy: * @display: the #SpiceDisplayChannel that emitted the signal * * The #SpiceDisplayChannel::display-primary-destroy signal is * emitted when the primary surface is freed and should not be * accessed anymore. **/ signals[SPICE_DISPLAY_PRIMARY_DESTROY] = g_signal_new("display-primary-destroy", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceDisplayChannelClass, display_primary_destroy), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * SpiceDisplayChannel::display-invalidate: * @display: the #SpiceDisplayChannel that emitted the signal * @x: x position * @y: y position * @width: width * @height: height * * The #SpiceDisplayChannel::display-invalidate signal is emitted * when the rectangular region x/y/w/h of the primary buffer is * updated. **/ signals[SPICE_DISPLAY_INVALIDATE] = g_signal_new("display-invalidate", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceDisplayChannelClass, display_invalidate), NULL, NULL, g_cclosure_user_marshal_VOID__INT_INT_INT_INT, G_TYPE_NONE, 4, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT); /** * SpiceDisplayChannel::display-mark: * @display: the #SpiceDisplayChannel that emitted the signal * @mark: %TRUE when the display mark has been received * * The #SpiceDisplayChannel::display-mark signal is emitted when * the %RED_DISPLAY_MARK command is received, and the display * should be exposed. **/ signals[SPICE_DISPLAY_MARK] = g_signal_new("display-mark", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceDisplayChannelClass, display_mark), NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); /** * SpiceDisplayChannel::gl-draw: * @display: the #SpiceDisplayChannel that emitted the signal * @x: x position * @y: y position * @width: width * @height: height * * The #SpiceDisplayChannel::draw signal is emitted when the * rectangular region x/y/w/h of the GL scanout is updated and * must be drawn. When the draw is finished, you must call * spice_display_gl_draw_done() in order to release the GL * resources. * * Since: 0.31 **/ signals[SPICE_DISPLAY_GL_DRAW] = g_signal_new("gl-draw", G_OBJECT_CLASS_TYPE(gobject_class), 0, 0, NULL, NULL, g_cclosure_user_marshal_VOID__UINT_UINT_UINT_UINT, G_TYPE_NONE, 4, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT); g_type_class_add_private(klass, sizeof(SpiceDisplayChannelPrivate)); sw_canvas_init(); quic_init(); rop3_init(); channel_set_handlers(SPICE_CHANNEL_CLASS(klass)); } /** * spice_display_get_primary: * @channel: a #SpiceDisplayChannel * @surface_id: a surface id * @primary: a #SpiceDisplayPrimary * * Retrieve primary display surface @surface_id. * * Returns: %TRUE if the primary surface was found and its details * collected in @primary. */ gboolean spice_display_get_primary(SpiceChannel *channel, guint32 surface_id, SpiceDisplayPrimary *primary) { g_return_val_if_fail(SPICE_IS_DISPLAY_CHANNEL(channel), FALSE); g_return_val_if_fail(primary != NULL, FALSE); SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; display_surface *surface = find_surface(c, surface_id); if (surface == NULL) return FALSE; g_return_val_if_fail(surface->primary, FALSE); primary->format = surface->format; primary->width = surface->width; primary->height = surface->height; primary->stride = surface->stride; primary->shmid = -1; primary->data = surface->data; primary->marked = c->mark; CHANNEL_DEBUG(channel, "get primary %p", primary->data); return TRUE; } /** * spice_display_change_preferred_compression: * @channel: a #SpiceDisplayChannel * @compression: a #SpiceImageCompression * * Tells the spice server to change the preferred image compression * for the @channel. * * Since: 0.31 */ void spice_display_change_preferred_compression(SpiceChannel *channel, gint compression) { SpiceMsgOut *out; SpiceMsgcDisplayPreferredCompression pref_comp_msg; g_return_if_fail(SPICE_IS_DISPLAY_CHANNEL(channel)); g_return_if_fail(compression > SPICE_IMAGE_COMPRESSION_INVALID && compression < SPICE_IMAGE_COMPRESSION_ENUM_END); if (!spice_channel_test_capability(channel, SPICE_DISPLAY_CAP_PREF_COMPRESSION)) { CHANNEL_DEBUG(channel, "does not have capability to change the preferred compression"); return; } CHANNEL_DEBUG(channel, "changing preferred compression to %d", compression); pref_comp_msg.image_compression = compression; out = spice_msg_out_new(channel, SPICE_MSGC_DISPLAY_PREFERRED_COMPRESSION); out->marshallers->msgc_display_preferred_compression(out->marshaller, &pref_comp_msg); spice_msg_out_send_internal(out); } /** * spice_display_get_gl_scanout: * @channel: a #SpiceDisplayChannel * * Returns: the current GL scanout * * Since: 0.31 **/ const SpiceGlScanout * spice_display_get_gl_scanout(SpiceDisplayChannel *channel) { g_return_val_if_fail(SPICE_IS_DISPLAY_CHANNEL(channel), NULL); return &channel->priv->scanout; } /* ------------------------------------------------------------------ */ static void image_put(SpiceImageCache *cache, uint64_t id, pixman_image_t *image) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, image_cache); cache_add(c->images, id, pixman_image_ref(image)); } typedef struct _WaitImageData { gboolean lossy; SpiceImageCache *cache; uint64_t id; pixman_image_t *image; } WaitImageData; static gboolean wait_image(gpointer data) { gboolean lossy; WaitImageData *wait = data; SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(wait->cache, SpiceDisplayChannelPrivate, image_cache); pixman_image_t *image = cache_find_lossy(c->images, wait->id, &lossy); if (!image || (lossy && !wait->lossy)) return FALSE; wait->image = pixman_image_ref(image); return TRUE; } static pixman_image_t *image_get(SpiceImageCache *cache, uint64_t id) { WaitImageData wait = { .lossy = TRUE, .cache = cache, .id = id, .image = NULL }; if (!g_coroutine_condition_wait(g_coroutine_self(), wait_image, &wait)) SPICE_DEBUG("wait image got cancelled"); return wait.image; } static void palette_put(SpicePaletteCache *cache, SpicePalette *palette) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache); cache_add(c->palettes, palette->unique, g_memdup(palette, sizeof(SpicePalette) + palette->num_ents * sizeof(palette->ents[0]))); } static SpicePalette *palette_get(SpicePaletteCache *cache, uint64_t id) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache); /* here the returned pointer is weak, no ref given to caller. it * seems spice canvas usage is exclusively temporary, so it's ok. * palette_release is a noop. */ return cache_find(c->palettes, id); } static void palette_remove(SpicePaletteCache *cache, uint64_t id) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache); cache_remove(c->palettes, id); } static void palette_release(SpicePaletteCache *cache, SpicePalette *palette) { /* there is no refcount of palette, see palette_get() */ } static void image_put_lossy(SpiceImageCache *cache, uint64_t id, pixman_image_t *surface) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, image_cache); #ifndef NDEBUG g_warn_if_fail(cache_find(c->images, id) == NULL); #endif cache_add_lossy(c->images, id, pixman_image_ref(surface), TRUE); } static void image_replace_lossy(SpiceImageCache *cache, uint64_t id, pixman_image_t *surface) { image_put(cache, id, surface); } static pixman_image_t* image_get_lossless(SpiceImageCache *cache, uint64_t id) { WaitImageData wait = { .lossy = FALSE, .cache = cache, .id = id, .image = NULL }; if (!g_coroutine_condition_wait(g_coroutine_self(), wait_image, &wait)) SPICE_DEBUG("wait lossless got cancelled"); return wait.image; } static SpiceCanvas *surfaces_get(SpiceImageSurfaces *surfaces, uint32_t surface_id) { SpiceDisplayChannelPrivate *c = SPICE_CONTAINEROF(surfaces, SpiceDisplayChannelPrivate, image_surfaces); display_surface *s = find_surface(c, surface_id); return s ? s->canvas : NULL; } static SpiceImageCacheOps image_cache_ops = { .put = image_put, .get = image_get, .put_lossy = image_put_lossy, .replace_lossy = image_replace_lossy, .get_lossless = image_get_lossless, }; static SpicePaletteCacheOps palette_cache_ops = { .put = palette_put, .get = palette_get, .release = palette_release, }; static SpiceImageSurfacesOps image_surfaces_ops = { .get = surfaces_get }; #if defined(G_OS_WIN32) static HDC create_compatible_dc(void) { HDC dc = CreateCompatibleDC(NULL); if (!dc) { g_warning("create compatible DC failed"); } return dc; } #endif static void spice_display_channel_reset_capabilities(SpiceChannel *channel) { spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_SIZED_STREAM); spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_MONITORS_CONFIG); spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_COMPOSITE); spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_A8_SURFACE); #ifdef USE_LZ4 spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_LZ4_COMPRESSION); #endif if (SPICE_DISPLAY_CHANNEL(channel)->priv->enable_adaptive_streaming) { spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_STREAM_REPORT); } #ifdef G_OS_UNIX spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_GL_SCANOUT); #endif } static void destroy_surface(gpointer data) { display_surface *surface = data; destroy_canvas(surface); g_free(surface); } static void spice_display_channel_init(SpiceDisplayChannel *channel) { SpiceDisplayChannelPrivate *c; c = channel->priv = SPICE_DISPLAY_CHANNEL_GET_PRIVATE(channel); c->surfaces = g_hash_table_new_full(NULL, NULL, NULL, destroy_surface); c->image_cache.ops = &image_cache_ops; c->palette_cache.ops = &palette_cache_ops; c->image_surfaces.ops = &image_surfaces_ops; #if defined(G_OS_WIN32) c->dc = create_compatible_dc(); #endif c->monitors_max = 1; c->scanout.fd = -1; if (g_getenv("SPICE_DISABLE_ADAPTIVE_STREAMING")) { SPICE_DEBUG("adaptive video disabled"); c->enable_adaptive_streaming = FALSE; } else { c->enable_adaptive_streaming = TRUE; } spice_display_channel_reset_capabilities(SPICE_CHANNEL(channel)); } /* ------------------------------------------------------------------ */ static int create_canvas(SpiceChannel *channel, display_surface *surface) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; if (surface->primary) { if (c->primary) { if (c->primary->width == surface->width && c->primary->height == surface->height) { CHANNEL_DEBUG(channel, "Reusing existing primary surface"); return 0; } g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0); g_hash_table_remove(c->surfaces, GINT_TO_POINTER(c->primary->surface_id)); } CHANNEL_DEBUG(channel, "Create primary canvas"); } surface->data = g_malloc0(surface->size); g_return_val_if_fail(c->glz_window, 0); g_warn_if_fail(surface->canvas == NULL); g_warn_if_fail(surface->glz_decoder == NULL); g_warn_if_fail(surface->zlib_decoder == NULL); g_warn_if_fail(surface->jpeg_decoder == NULL); surface->glz_decoder = glz_decoder_new(c->glz_window); surface->zlib_decoder = zlib_decoder_new(); surface->jpeg_decoder = jpeg_decoder_new(); surface->canvas = canvas_create_for_data(surface->width, surface->height, surface->format, surface->data, surface->stride, &c->image_cache, &c->palette_cache, &c->image_surfaces, surface->glz_decoder, surface->jpeg_decoder, surface->zlib_decoder); g_return_val_if_fail(surface->canvas != NULL, 0); g_hash_table_insert(c->surfaces, GINT_TO_POINTER(surface->surface_id), surface); if (surface->primary) { g_warn_if_fail(c->primary == NULL); c->primary = surface; g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_CREATE], 0, surface->format, surface->width, surface->height, surface->stride, -1, surface->data); if (!spice_channel_test_capability(channel, SPICE_DISPLAY_CAP_MONITORS_CONFIG)) { g_array_set_size(c->monitors, 1); SpiceDisplayMonitorConfig *config = &g_array_index(c->monitors, SpiceDisplayMonitorConfig, 0); config->x = config->y = 0; config->width = surface->width; config->height = surface->height; g_coroutine_object_notify(G_OBJECT(channel), "monitors"); } } return 0; } static void destroy_canvas(display_surface *surface) { if (surface == NULL) return; glz_decoder_destroy(surface->glz_decoder); zlib_decoder_destroy(surface->zlib_decoder); jpeg_decoder_destroy(surface->jpeg_decoder); g_free(surface->data); surface->data = NULL; surface->canvas->ops->destroy(surface->canvas); surface->canvas = NULL; } static display_surface *find_surface(SpiceDisplayChannelPrivate *c, guint32 surface_id) { if (c->primary && c->primary->surface_id == surface_id) return c->primary; return g_hash_table_lookup(c->surfaces, GINT_TO_POINTER(surface_id)); } /* main or coroutine context */ static void clear_surfaces(SpiceChannel *channel, gboolean keep_primary) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; GHashTableIter iter; display_surface *surface; if (!keep_primary) { c->primary = NULL; g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0); } g_hash_table_iter_init(&iter, c->surfaces); while (g_hash_table_iter_next(&iter, NULL, (gpointer*)&surface)) { if (keep_primary && surface->primary) { CHANNEL_DEBUG(channel, "keeping existing primary surface, migration or reset"); continue; } g_hash_table_iter_remove(&iter); } } /* coroutine context */ static void emit_invalidate(SpiceChannel *channel, SpiceRect *bbox) { g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_INVALIDATE], 0, bbox->left, bbox->top, bbox->right - bbox->left, bbox->bottom - bbox->top); } /* ------------------------------------------------------------------ */ /* coroutine context */ static void spice_display_channel_up(SpiceChannel *channel) { SpiceMsgOut *out; SpiceSession *s = spice_channel_get_session(channel); SpiceMsgcDisplayInit init; int cache_size; int glz_window_size; SpiceImageCompression preferred_compression = SPICE_IMAGE_COMPRESSION_INVALID; g_object_get(s, "cache-size", &cache_size, "glz-window-size", &glz_window_size, "preferred-compression", &preferred_compression, NULL); CHANNEL_DEBUG(channel, "%s: cache_size %d, glz_window_size %d (bytes)", __FUNCTION__, cache_size, glz_window_size); init.pixmap_cache_id = 1; init.glz_dictionary_id = 1; init.pixmap_cache_size = cache_size / 4; /* pixels */ init.glz_dictionary_window_size = glz_window_size / 4; /* pixels */ out = spice_msg_out_new(channel, SPICE_MSGC_DISPLAY_INIT); out->marshallers->msgc_display_init(out->marshaller, &init); spice_msg_out_send_internal(out); /* notify of existence of this monitor */ g_coroutine_object_notify(G_OBJECT(channel), "monitors"); if (preferred_compression != SPICE_IMAGE_COMPRESSION_INVALID) { spice_display_change_preferred_compression(channel, preferred_compression); } } #define DRAW(type) { \ display_surface *surface = \ find_surface(SPICE_DISPLAY_CHANNEL(channel)->priv, \ op->base.surface_id); \ g_return_if_fail(surface != NULL); \ surface->canvas->ops->draw_##type(surface->canvas, &op->base.box, \ &op->base.clip, &op->data); \ if (surface->primary) { \ emit_invalidate(channel, &op->base.box); \ } \ } /* coroutine context */ static void display_handle_mode(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgDisplayMode *mode = spice_msg_in_parsed(in); display_surface *surface; g_warn_if_fail(c->mark == FALSE); surface = g_new0(display_surface, 1); surface->format = mode->bits == 32 ? SPICE_SURFACE_FMT_32_xRGB : SPICE_SURFACE_FMT_16_555; surface->width = mode->x_res; surface->height = mode->y_res; surface->stride = surface->width * 4; surface->size = surface->height * surface->stride; surface->primary = true; create_canvas(channel, surface); } /* coroutine context */ static void display_handle_mark(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; CHANNEL_DEBUG(channel, "%s", __FUNCTION__); g_return_if_fail(c->primary != NULL); #ifdef EXTRA_CHECKS g_warn_if_fail(c->mark == FALSE); #endif c->mark = TRUE; g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, TRUE); } /* coroutine context */ static void display_handle_reset(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; display_surface *surface = c->primary; CHANNEL_DEBUG(channel, "%s: TODO detach_from_screen", __FUNCTION__); if (surface != NULL) surface->canvas->ops->clear(surface->canvas); cache_clear(c->palettes); c->mark = FALSE; g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, FALSE); } /* coroutine context */ static void display_handle_copy_bits(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayCopyBits *op = spice_msg_in_parsed(in); SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; display_surface *surface = find_surface(c, op->base.surface_id); g_return_if_fail(surface != NULL); surface->canvas->ops->copy_bits(surface->canvas, &op->base.box, &op->base.clip, &op->src_pos); if (surface->primary) { emit_invalidate(channel, &op->base.box); } } /* coroutine context */ static void display_handle_inv_list(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceResourceList *list = spice_msg_in_parsed(in); int i; for (i = 0; i < list->count; i++) { guint64 id = list->resources[i].id; switch (list->resources[i].type) { case SPICE_RES_TYPE_PIXMAP: if (!cache_remove(c->images, id)) SPICE_DEBUG("fail to remove image %" G_GUINT64_FORMAT, id); break; default: g_return_if_reached(); break; } } } /* coroutine context */ static void display_handle_inv_pixmap_all(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; spice_channel_handle_wait_for_channels(channel, in); cache_clear(c->images); } /* coroutine context */ static void display_handle_inv_palette(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgDisplayInvalOne* op = spice_msg_in_parsed(in); palette_remove(&c->palette_cache, op->id); } /* coroutine context */ static void display_handle_inv_palette_all(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; cache_clear(c->palettes); } /* ------------------------------------------------------------------ */ static void display_update_stream_region(display_stream *st) { int i; switch (st->clip->type) { case SPICE_CLIP_TYPE_RECTS: region_clear(&st->region); for (i = 0; i < st->clip->rects->num_rects; i++) { region_add(&st->region, &st->clip->rects->rects[i]); } st->have_region = true; break; case SPICE_CLIP_TYPE_NONE: default: st->have_region = false; break; } } /* coroutine context */ static void display_handle_stream_create(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgDisplayStreamCreate *op = spice_msg_in_parsed(in); display_stream *st; CHANNEL_DEBUG(channel, "%s: id %d", __FUNCTION__, op->id); if (op->id >= c->nstreams) { int n = c->nstreams; if (!c->nstreams) { c->nstreams = 1; } while (op->id >= c->nstreams) { c->nstreams *= 2; } c->streams = realloc(c->streams, c->nstreams * sizeof(c->streams[0])); memset(c->streams + n, 0, (c->nstreams - n) * sizeof(c->streams[0])); } g_return_if_fail(c->streams[op->id] == NULL); c->streams[op->id] = g_new0(display_stream, 1); st = c->streams[op->id]; st->msg_create = in; spice_msg_in_ref(in); st->clip = &op->clip; st->codec = op->codec_type; st->surface = find_surface(c, op->surface_id); st->msgq = g_queue_new(); st->channel = channel; st->drops_seqs_stats_arr = g_array_new(FALSE, FALSE, sizeof(drops_sequence_stats)); region_init(&st->region); display_update_stream_region(st); switch (st->codec) { case SPICE_VIDEO_CODEC_TYPE_MJPEG: stream_mjpeg_init(st); break; } } /* coroutine or main context */ static gboolean display_stream_schedule(display_stream *st) { SpiceSession *session = spice_channel_get_session(st->channel); guint32 time, d; SpiceStreamDataHeader *op; SpiceMsgIn *in; SPICE_DEBUG("%s", __FUNCTION__); if (st->timeout || !session) return TRUE; time = spice_session_get_mm_time(session); in = g_queue_peek_head(st->msgq); if (in == NULL) { return TRUE; } op = spice_msg_in_parsed(in); if (time < op->multi_media_time) { d = op->multi_media_time - time; SPICE_DEBUG("scheduling next stream render in %u ms", d); st->timeout = g_timeout_add(d, (GSourceFunc)display_stream_render, st); return TRUE; } else { SPICE_DEBUG("%s: rendering too late by %u ms (ts: %u, mmtime: %u), dropping ", __FUNCTION__, time - op->multi_media_time, op->multi_media_time, time); in = g_queue_pop_head(st->msgq); spice_msg_in_unref(in); st->num_drops_on_playback++; if (g_queue_get_length(st->msgq) == 0) return TRUE; } return FALSE; } static SpiceRect *stream_get_dest(display_stream *st) { if (st->msg_data == NULL || spice_msg_in_type(st->msg_data) != SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) { SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create); return &info->dest; } else { SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data); return &op->dest; } } static uint32_t stream_get_flags(display_stream *st) { SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create); return info->flags; } G_GNUC_INTERNAL uint32_t stream_get_current_frame(display_stream *st, uint8_t **data) { if (st->msg_data == NULL) { *data = NULL; return 0; } switch (spice_msg_in_type(st->msg_data)) { case SPICE_MSG_DISPLAY_STREAM_DATA: { SpiceMsgDisplayStreamData *op = spice_msg_in_parsed(st->msg_data); *data = op->data; return op->data_size; } case SPICE_MSG_DISPLAY_STREAM_DATA_SIZED: { SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data); *data = op->data; return op->data_size; } default: *data = NULL; g_return_val_if_reached(0); } } G_GNUC_INTERNAL void stream_get_dimensions(display_stream *st, int *width, int *height) { g_return_if_fail(width != NULL); g_return_if_fail(height != NULL); if (st->msg_data == NULL || spice_msg_in_type(st->msg_data) != SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) { SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create); *width = info->stream_width; *height = info->stream_height; } else { SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data); *width = op->width; *height = op->height; } } /* main context */ static gboolean display_stream_render(display_stream *st) { SpiceMsgIn *in; st->timeout = 0; do { in = g_queue_pop_head(st->msgq); g_return_val_if_fail(in != NULL, FALSE); st->msg_data = in; switch (st->codec) { case SPICE_VIDEO_CODEC_TYPE_MJPEG: stream_mjpeg_data(st); break; } if (st->out_frame) { int width; int height; SpiceRect *dest; uint8_t *data; int stride; stream_get_dimensions(st, &width, &height); dest = stream_get_dest(st); data = st->out_frame; stride = width * sizeof(uint32_t); if (!(stream_get_flags(st) & SPICE_STREAM_FLAGS_TOP_DOWN)) { data += stride * (height - 1); stride = -stride; } st->surface->canvas->ops->put_image( st->surface->canvas, #ifdef G_OS_WIN32 SPICE_DISPLAY_CHANNEL(st->channel)->priv->dc, #endif dest, data, width, height, stride, st->have_region ? &st->region : NULL); if (st->surface->primary) g_signal_emit(st->channel, signals[SPICE_DISPLAY_INVALIDATE], 0, dest->left, dest->top, dest->right - dest->left, dest->bottom - dest->top); } st->msg_data = NULL; spice_msg_in_unref(in); in = g_queue_peek_head(st->msgq); if (in == NULL) break; if (display_stream_schedule(st)) return FALSE; } while (1); return FALSE; } /* after a sequence of 3 drops, push a report to the server, even * if the report window is bigger */ #define STREAM_REPORT_DROP_SEQ_LEN_LIMIT 3 static void display_update_stream_report(SpiceDisplayChannel *channel, uint32_t stream_id, uint32_t frame_time, int32_t latency) { display_stream *st = channel->priv->streams[stream_id]; guint64 now; if (!st->report_is_active) { return; } now = g_get_monotonic_time(); if (st->report_num_frames == 0) { st->report_start_frame_time = frame_time; st->report_start_time = now; } st->report_num_frames++; if (latency < 0) { // drop st->report_num_drops++; st->report_drops_seq_len++; } else { st->report_drops_seq_len = 0; } if (st->report_num_frames >= st->report_max_window || now - st->report_start_time >= st->report_timeout || st->report_drops_seq_len >= STREAM_REPORT_DROP_SEQ_LEN_LIMIT) { SpiceMsgcDisplayStreamReport report; SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(channel)); SpiceMsgOut *msg; report.stream_id = stream_id; report.unique_id = st->report_id; report.start_frame_mm_time = st->report_start_frame_time; report.end_frame_mm_time = frame_time; report.num_frames = st->report_num_frames; report.num_drops = st-> report_num_drops; report.last_frame_delay = latency; if (spice_session_is_playback_active(session)) { report.audio_delay = spice_session_get_playback_latency(session); } else { report.audio_delay = UINT_MAX; } msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_DISPLAY_STREAM_REPORT); msg->marshallers->msgc_display_stream_report(msg->marshaller, &report); spice_msg_out_send(msg); st->report_start_time = 0; st->report_start_frame_time = 0; st->report_num_frames = 0; st->report_num_drops = 0; st->report_drops_seq_len = 0; } } static void display_stream_reset_rendering_timer(display_stream *st) { SPICE_DEBUG("%s", __FUNCTION__); if (st->timeout != 0) { g_source_remove(st->timeout); st->timeout = 0; } while (!display_stream_schedule(st)) { } } /* * Migration can occur between 2 spice-servers with different mm-times. * Then, the following cases can happen after migration completes: * (We refer to src/dst-time as the mm-times on the src/dst servers): * * (case 1) Frames with time ~= dst-time arrive to the client before the * playback-channel updates the session's mm-time (i.e., the mm_time * of the session is still based on the src-time). * (a) If src-time < dst-time: * display_stream_schedule schedules the next rendering to * ~(dst-time - src-time) milliseconds from now. * Since we assume monotonic mm_time, display_stream_schedule, * returns immediately when a rendering timeout * has already been set, and doesn't update the timeout, * even after the mm_time is updated. * When src-time << dst-time, a significant video frames loss will occur. * (b) If src-time > dst-time * Frames will be dropped till the mm-time will be updated. * (case 2) mm-time is synced with dst-time, but frames that were in the command * ring during migration still arrive (such frames hold src-time). * (a) If src-time < dst-time * The frames that hold src-time will be dropped, since their * mm_time < session-mm_time. But all the new frames that are generated in * the driver after migration, will be rendered appropriately. * (b) If src-time > dst-time * Similar consequences as in 1 (a) * case 2 is less likely, since at takes at least 20 frames till the dst-server re-identifies * the video stream and starts sending stream data * * display_session_mm_time_reset_cb handles case 1.a, and * display_stream_test_frames_mm_time_reset handles case 2.b */ /* main context */ static void display_session_mm_time_reset_cb(SpiceSession *session, gpointer data) { SpiceChannel *channel = data; SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; guint i; CHANNEL_DEBUG(channel, "%s", __FUNCTION__); for (i = 0; i < c->nstreams; i++) { display_stream *st; if (c->streams[i] == NULL) { continue; } SPICE_DEBUG("%s: stream-id %d", __FUNCTION__, i); st = c->streams[i]; display_stream_reset_rendering_timer(st); } } /* coroutine context */ static void display_stream_test_frames_mm_time_reset(display_stream *st, SpiceMsgIn *new_frame_msg, guint32 mm_time) { SpiceStreamDataHeader *tail_op, *new_op; SpiceMsgIn *tail_msg; SPICE_DEBUG("%s", __FUNCTION__); g_return_if_fail(new_frame_msg != NULL); tail_msg = g_queue_peek_tail(st->msgq); if (!tail_msg) { return; } tail_op = spice_msg_in_parsed(tail_msg); new_op = spice_msg_in_parsed(new_frame_msg); if (new_op->multi_media_time < tail_op->multi_media_time) { SPICE_DEBUG("new-frame-time < tail-frame-time (%u < %u):" " reseting stream, id %d", new_op->multi_media_time, tail_op->multi_media_time, new_op->id); g_queue_foreach(st->msgq, _msg_in_unref_func, NULL); g_queue_clear(st->msgq); display_stream_reset_rendering_timer(st); } } #define STREAM_PLAYBACK_SYNC_DROP_SEQ_LEN_LIMIT 5 /* coroutine context */ static void display_handle_stream_data(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceStreamDataHeader *op = spice_msg_in_parsed(in); display_stream *st; guint32 mmtime; int32_t latency; g_return_if_fail(c != NULL); g_return_if_fail(c->streams != NULL); g_return_if_fail(c->nstreams > op->id); st = c->streams[op->id]; mmtime = spice_session_get_mm_time(spice_channel_get_session(channel)); if (spice_msg_in_type(in) == SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) { CHANNEL_DEBUG(channel, "stream %d contains sized data", op->id); } if (op->multi_media_time == 0) { g_critical("Received frame with invalid 0 timestamp! perhaps wrong graphic driver?"); op->multi_media_time = mmtime + 100; /* workaround... */ } if (!st->num_input_frames) { st->first_frame_mm_time = op->multi_media_time; } st->num_input_frames++; latency = op->multi_media_time - mmtime; if (latency < 0) { CHANNEL_DEBUG(channel, "stream data too late by %u ms (ts: %u, mmtime: %u), dropping", mmtime - op->multi_media_time, op->multi_media_time, mmtime); st->arrive_late_time += mmtime - op->multi_media_time; st->num_drops_on_receive++; if (!st->cur_drops_seq_stats.len) { st->cur_drops_seq_stats.start_mm_time = op->multi_media_time; } st->cur_drops_seq_stats.len++; st->playback_sync_drops_seq_len++; } else { CHANNEL_DEBUG(channel, "video latency: %d", latency); spice_msg_in_ref(in); display_stream_test_frames_mm_time_reset(st, in, mmtime); g_queue_push_tail(st->msgq, in); while (!display_stream_schedule(st)) { } if (st->cur_drops_seq_stats.len) { st->cur_drops_seq_stats.duration = op->multi_media_time - st->cur_drops_seq_stats.start_mm_time; g_array_append_val(st->drops_seqs_stats_arr, st->cur_drops_seq_stats); memset(&st->cur_drops_seq_stats, 0, sizeof(st->cur_drops_seq_stats)); st->num_drops_seqs++; } st->playback_sync_drops_seq_len = 0; } if (c->enable_adaptive_streaming) { display_update_stream_report(SPICE_DISPLAY_CHANNEL(channel), op->id, op->multi_media_time, latency); if (st->playback_sync_drops_seq_len >= STREAM_PLAYBACK_SYNC_DROP_SEQ_LEN_LIMIT) { spice_session_sync_playback_latency(spice_channel_get_session(channel)); st->playback_sync_drops_seq_len = 0; } } } /* coroutine context */ static void display_handle_stream_clip(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgDisplayStreamClip *op = spice_msg_in_parsed(in); display_stream *st; g_return_if_fail(c != NULL); g_return_if_fail(c->streams != NULL); g_return_if_fail(c->nstreams > op->id); st = c->streams[op->id]; if (st->msg_clip) { spice_msg_in_unref(st->msg_clip); } spice_msg_in_ref(in); st->msg_clip = in; st->clip = &op->clip; display_update_stream_region(st); } static void _msg_in_unref_func(gpointer data, gpointer user_data) { spice_msg_in_unref(data); } static void destroy_stream(SpiceChannel *channel, int id) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; display_stream *st; guint64 drops_duration_total = 0; guint32 num_out_frames; int i; g_return_if_fail(c != NULL); g_return_if_fail(c->streams != NULL); g_return_if_fail(c->nstreams > id); st = c->streams[id]; if (!st) return; num_out_frames = st->num_input_frames - st->num_drops_on_receive - st->num_drops_on_playback; CHANNEL_DEBUG(channel, "%s: id=%d #in-frames=%d out/in=%.2f " "#drops-on-receive=%d avg-late-time(ms)=%.2f " "#drops-on-playback=%d", __FUNCTION__, id, st->num_input_frames, num_out_frames / (double)st->num_input_frames, st->num_drops_on_receive, st->num_drops_on_receive ? st->arrive_late_time / ((double)st->num_drops_on_receive): 0, st->num_drops_on_playback); if (st->num_drops_seqs) { CHANNEL_DEBUG(channel, "%s: #drops-sequences=%u ==>", __FUNCTION__, st->num_drops_seqs); } for (i = 0; i < st->num_drops_seqs; i++) { drops_sequence_stats *stats = &g_array_index(st->drops_seqs_stats_arr, drops_sequence_stats, i); drops_duration_total += stats->duration; CHANNEL_DEBUG(channel, "%s: \t len=%u start-ms=%u duration-ms=%u", __FUNCTION__, stats->len, stats->start_mm_time - st->first_frame_mm_time, stats->duration); } if (st->num_drops_seqs) { CHANNEL_DEBUG(channel, "%s: drops-total-duration=%"G_GUINT64_FORMAT" ==>", __FUNCTION__, drops_duration_total); } g_array_free(st->drops_seqs_stats_arr, TRUE); switch (st->codec) { case SPICE_VIDEO_CODEC_TYPE_MJPEG: stream_mjpeg_cleanup(st); break; } if (st->msg_clip) spice_msg_in_unref(st->msg_clip); spice_msg_in_unref(st->msg_create); g_queue_foreach(st->msgq, _msg_in_unref_func, NULL); g_queue_free(st->msgq); if (st->timeout != 0) g_source_remove(st->timeout); g_free(st); c->streams[id] = NULL; } static void clear_streams(SpiceChannel *channel) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; int i; for (i = 0; i < c->nstreams; i++) { destroy_stream(channel, i); } g_free(c->streams); c->streams = NULL; c->nstreams = 0; } /* coroutine context */ static void display_handle_stream_destroy(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayStreamDestroy *op = spice_msg_in_parsed(in); g_return_if_fail(op != NULL); CHANNEL_DEBUG(channel, "%s: id %d", __FUNCTION__, op->id); destroy_stream(channel, op->id); } /* coroutine context */ static void display_handle_stream_destroy_all(SpiceChannel *channel, SpiceMsgIn *in) { clear_streams(channel); } /* coroutine context */ static void display_handle_stream_activate_report(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgDisplayStreamActivateReport *op = spice_msg_in_parsed(in); display_stream *st; g_return_if_fail(c != NULL); g_return_if_fail(c->streams != NULL); g_return_if_fail(c->nstreams > op->stream_id); st = c->streams[op->stream_id]; g_return_if_fail(st != NULL); st->report_is_active = TRUE; st->report_id = op->unique_id; st->report_max_window = op->max_window_size; st->report_timeout = op->timeout_ms * 1000; st->report_start_time = 0; st->report_start_frame_time = 0; st->report_num_frames = 0; st->report_num_drops = 0; st->report_drops_seq_len = 0; } /* ------------------------------------------------------------------ */ /* coroutine context */ static void display_handle_draw_fill(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawFill *op = spice_msg_in_parsed(in); DRAW(fill); } /* coroutine context */ static void display_handle_draw_opaque(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawOpaque *op = spice_msg_in_parsed(in); DRAW(opaque); } /* coroutine context */ static void display_handle_draw_copy(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawCopy *op = spice_msg_in_parsed(in); DRAW(copy); } /* coroutine context */ static void display_handle_draw_blend(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawBlend *op = spice_msg_in_parsed(in); DRAW(blend); } /* coroutine context */ static void display_handle_draw_blackness(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawBlackness *op = spice_msg_in_parsed(in); DRAW(blackness); } static void display_handle_draw_whiteness(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawWhiteness *op = spice_msg_in_parsed(in); DRAW(whiteness); } /* coroutine context */ static void display_handle_draw_invers(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawInvers *op = spice_msg_in_parsed(in); DRAW(invers); } /* coroutine context */ static void display_handle_draw_rop3(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawRop3 *op = spice_msg_in_parsed(in); DRAW(rop3); } /* coroutine context */ static void display_handle_draw_stroke(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawStroke *op = spice_msg_in_parsed(in); DRAW(stroke); } /* coroutine context */ static void display_handle_draw_text(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawText *op = spice_msg_in_parsed(in); DRAW(text); } /* coroutine context */ static void display_handle_draw_transparent(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawTransparent *op = spice_msg_in_parsed(in); DRAW(transparent); } /* coroutine context */ static void display_handle_draw_alpha_blend(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawAlphaBlend *op = spice_msg_in_parsed(in); DRAW(alpha_blend); } /* coroutine context */ static void display_handle_draw_composite(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayDrawComposite *op = spice_msg_in_parsed(in); DRAW(composite); } /* coroutine context */ static void display_handle_surface_create(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; SpiceMsgSurfaceCreate *create = spice_msg_in_parsed(in); display_surface *surface = g_new0(display_surface, 1); surface->surface_id = create->surface_id; surface->format = create->format; surface->width = create->width; surface->height = create->height; surface->stride = create->width * 4; surface->size = surface->height * surface->stride; if (create->flags & SPICE_SURFACE_FLAGS_PRIMARY) { SPICE_DEBUG("primary flags: %d", create->flags); surface->primary = true; create_canvas(channel, surface); if (c->mark_false_event_id != 0) { g_source_remove(c->mark_false_event_id); c->mark_false_event_id = FALSE; } } else { surface->primary = false; create_canvas(channel, surface); } } static gboolean display_mark_false(gpointer data) { SpiceChannel *channel = data; SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; c->mark = FALSE; g_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, FALSE); c->mark_false_event_id = 0; return FALSE; } /* coroutine context */ static void display_handle_surface_destroy(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgSurfaceDestroy *destroy = spice_msg_in_parsed(in); SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; display_surface *surface; g_return_if_fail(destroy != NULL); surface = find_surface(c, destroy->surface_id); if (surface == NULL) { /* this is not a problem in spicec, it happens as well and returns.. */ /* g_warn_if_reached(); */ return; } if (surface->primary) { int id = spice_channel_get_channel_id(channel); CHANNEL_DEBUG(channel, "%d: FIXME primary destroy, but is display really disabled?", id); /* this is done with a timeout in spicec as well, it's *ugly* */ if (id != 0 && c->mark_false_event_id == 0) { c->mark_false_event_id = g_timeout_add_seconds(1, display_mark_false, channel); } c->primary = NULL; g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0); } g_hash_table_remove(c->surfaces, GINT_TO_POINTER(surface->surface_id)); } #define CLAMP_CHECK(x, low, high) (((x) > (high)) ? TRUE : (((x) < (low)) ? TRUE : FALSE)) /* coroutine context */ static void display_handle_monitors_config(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayMonitorsConfig *config = spice_msg_in_parsed(in); SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv; guint i; g_return_if_fail(config != NULL); if (config->count == 0) { CHANNEL_DEBUG(channel, "received empty monitor config"); return; } CHANNEL_DEBUG(channel, "received new monitors config from guest: n: %d/%d", config->count, config->max_allowed); c->monitors_max = config->max_allowed; if (CLAMP_CHECK(c->monitors_max, 1, MONITORS_MAX)) { g_warning("MonitorConfig max_allowed is not within permitted range, clamping"); c->monitors_max = CLAMP(c->monitors_max, 1, MONITORS_MAX); } if (CLAMP_CHECK(config->count, 1, c->monitors_max)) { g_warning("MonitorConfig count is not within permitted range, clamping"); config->count = CLAMP(config->count, 1, c->monitors_max); } c->monitors = g_array_set_size(c->monitors, config->count); for (i = 0; i < config->count; i++) { SpiceDisplayMonitorConfig *mc = &g_array_index(c->monitors, SpiceDisplayMonitorConfig, i); SpiceHead *head = &config->heads[i]; CHANNEL_DEBUG(channel, "monitor id: %u, surface id: %u, +%u+%u-%ux%u", head->id, head->surface_id, head->x, head->y, head->width, head->height); mc->id = head->id; mc->surface_id = head->surface_id; mc->x = head->x; mc->y = head->y; mc->width = head->width; mc->height = head->height; } g_coroutine_object_notify(G_OBJECT(channel), "monitors"); } #ifdef G_OS_UNIX /* coroutine context */ static void display_handle_gl_scanout_unix(SpiceChannel *channel, SpiceMsgIn *in) { SpiceDisplayChannel *display = SPICE_DISPLAY_CHANNEL(channel); SpiceDisplayChannelPrivate *c = display->priv; SpiceMsgDisplayGlScanoutUnix *scanout = spice_msg_in_parsed(in); scanout->drm_dma_buf_fd = -1; if (scanout->drm_fourcc_format != 0) { scanout->drm_dma_buf_fd = spice_channel_unix_read_fd(channel); CHANNEL_DEBUG(channel, "gl scanout fd: %d", scanout->drm_dma_buf_fd); } c->scanout.y0top = scanout->flags & SPICE_GL_SCANOUT_FLAGS_Y0TOP; if (c->scanout.fd >= 0) close(c->scanout.fd); c->scanout.fd = scanout->drm_dma_buf_fd; c->scanout.width = scanout->width; c->scanout.height = scanout->height; c->scanout.stride = scanout->stride; c->scanout.format = scanout->drm_fourcc_format; g_coroutine_object_notify(G_OBJECT(channel), "gl-scanout"); } #endif /* coroutine context */ static void display_handle_gl_draw(SpiceChannel *channel, SpiceMsgIn *in) { SpiceMsgDisplayGlDraw *draw = spice_msg_in_parsed(in); CHANNEL_DEBUG(channel, "gl draw %dx%d+%d+%d", draw->w, draw->h, draw->x, draw->y); g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_GL_DRAW], 0, draw->x, draw->y, draw->w, draw->h); } /** * spice_display_gl_draw_done: * @channel: a #SpiceDisplayChannel * * After a SpiceDisplayChannel::gl-draw is emitted, the client should * draw the current display with the current GL scanout, and must * release the GL resource with a call to spice_display_gl_draw_done() * (failing to do so for each gl-draw may result in a frozen display). * * Since: 0.31 **/ void spice_display_gl_draw_done(SpiceDisplayChannel *display) { SpiceChannel *channel; SpiceMsgOut *out; g_return_if_fail(SPICE_IS_DISPLAY_CHANNEL(display)); channel = SPICE_CHANNEL(display); out = spice_msg_out_new(channel, SPICE_MSGC_DISPLAY_GL_DRAW_DONE); out->marshallers->msgc_display_gl_draw_done(out->marshaller, NULL); spice_msg_out_send_internal(out); } static void channel_set_handlers(SpiceChannelClass *klass) { static const spice_msg_handler handlers[] = { [ SPICE_MSG_DISPLAY_MODE ] = display_handle_mode, [ SPICE_MSG_DISPLAY_MARK ] = display_handle_mark, [ SPICE_MSG_DISPLAY_RESET ] = display_handle_reset, [ SPICE_MSG_DISPLAY_COPY_BITS ] = display_handle_copy_bits, [ SPICE_MSG_DISPLAY_INVAL_LIST ] = display_handle_inv_list, [ SPICE_MSG_DISPLAY_INVAL_ALL_PIXMAPS ] = display_handle_inv_pixmap_all, [ SPICE_MSG_DISPLAY_INVAL_PALETTE ] = display_handle_inv_palette, [ SPICE_MSG_DISPLAY_INVAL_ALL_PALETTES ] = display_handle_inv_palette_all, [ SPICE_MSG_DISPLAY_STREAM_CREATE ] = display_handle_stream_create, [ SPICE_MSG_DISPLAY_STREAM_DATA ] = display_handle_stream_data, [ SPICE_MSG_DISPLAY_STREAM_CLIP ] = display_handle_stream_clip, [ SPICE_MSG_DISPLAY_STREAM_DESTROY ] = display_handle_stream_destroy, [ SPICE_MSG_DISPLAY_STREAM_DESTROY_ALL ] = display_handle_stream_destroy_all, [ SPICE_MSG_DISPLAY_STREAM_DATA_SIZED ] = display_handle_stream_data, [ SPICE_MSG_DISPLAY_STREAM_ACTIVATE_REPORT ] = display_handle_stream_activate_report, [ SPICE_MSG_DISPLAY_DRAW_FILL ] = display_handle_draw_fill, [ SPICE_MSG_DISPLAY_DRAW_OPAQUE ] = display_handle_draw_opaque, [ SPICE_MSG_DISPLAY_DRAW_COPY ] = display_handle_draw_copy, [ SPICE_MSG_DISPLAY_DRAW_BLEND ] = display_handle_draw_blend, [ SPICE_MSG_DISPLAY_DRAW_BLACKNESS ] = display_handle_draw_blackness, [ SPICE_MSG_DISPLAY_DRAW_WHITENESS ] = display_handle_draw_whiteness, [ SPICE_MSG_DISPLAY_DRAW_INVERS ] = display_handle_draw_invers, [ SPICE_MSG_DISPLAY_DRAW_ROP3 ] = display_handle_draw_rop3, [ SPICE_MSG_DISPLAY_DRAW_STROKE ] = display_handle_draw_stroke, [ SPICE_MSG_DISPLAY_DRAW_TEXT ] = display_handle_draw_text, [ SPICE_MSG_DISPLAY_DRAW_TRANSPARENT ] = display_handle_draw_transparent, [ SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND ] = display_handle_draw_alpha_blend, [ SPICE_MSG_DISPLAY_DRAW_COMPOSITE ] = display_handle_draw_composite, [ SPICE_MSG_DISPLAY_SURFACE_CREATE ] = display_handle_surface_create, [ SPICE_MSG_DISPLAY_SURFACE_DESTROY ] = display_handle_surface_destroy, [ SPICE_MSG_DISPLAY_MONITORS_CONFIG ] = display_handle_monitors_config, #ifdef G_OS_UNIX [ SPICE_MSG_DISPLAY_GL_SCANOUT_UNIX ] = display_handle_gl_scanout_unix, #endif [ SPICE_MSG_DISPLAY_GL_DRAW ] = display_handle_gl_draw, }; spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers)); }