diff --git a/lib/meson.build b/lib/meson.build index 6bbbdc7..253b3bb 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -22,7 +22,7 @@ libmks_headers = [ ] libmks_private_sources = [ - 'mks-framebuffer.c', + 'mks-cairo-framebuffer.c', 'mks-read-only-list-model.c', gnome.gdbus_codegen('mks-qemu', diff --git a/lib/mks-cairo-framebuffer-private.h b/lib/mks-cairo-framebuffer-private.h new file mode 100644 index 0000000..81dec9c --- /dev/null +++ b/lib/mks-cairo-framebuffer-private.h @@ -0,0 +1,47 @@ +/* mks-cairo-framebuffer.h + * + * Copyright 2023 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define MKS_TYPE_CAIRO_FRAMEBUFFER (mks_cairo_framebuffer_get_type()) + +G_DECLARE_FINAL_TYPE (MksCairoFramebuffer, mks_cairo_framebuffer, MKS, CAIRO_FRAMEBUFFER, GObject) + +MksCairoFramebuffer *mks_cairo_framebuffer_new (cairo_format_t format, + guint width, + guint height); +cairo_format_t mks_cairo_framebuffer_get_format (MksCairoFramebuffer *self); +guint mks_cairo_framebuffer_get_width (MksCairoFramebuffer *self); +guint mks_cairo_framebuffer_get_height (MksCairoFramebuffer *self); +cairo_t *mks_cairo_framebuffer_update (MksCairoFramebuffer *self, + guint x, + guint y, + guint width, + guint height); +void mks_cairo_framebuffer_copy_to (MksCairoFramebuffer *self, + MksCairoFramebuffer *dest); +void mks_cairo_framebuffer_clear (MksCairoFramebuffer *self); + +G_END_DECLS diff --git a/lib/mks-cairo-framebuffer.c b/lib/mks-cairo-framebuffer.c new file mode 100644 index 0000000..4c2a056 --- /dev/null +++ b/lib/mks-cairo-framebuffer.c @@ -0,0 +1,498 @@ +/* mks-cairo-framebuffer.c + * + * Copyright 2023 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include "mks-cairo-framebuffer-private.h" + +#define TILE_WIDTH 256 +#define TILE_HEIGHT 256 + +struct _MksCairoFramebuffer +{ + GObject parent_instance; + + /* The underlying surface we'll draw to */ + cairo_surface_t *surface; + + /* A GBytes that we can use to reference slices which ultimately + * references the cairo surface. This goes against the internal design + * of GdkTexture (which are supposed to be immutable) so that we can + * avoid additional copies beyond the one to the GPU. + * + * We somewhat abuse the GdkSnapshot diffing here by giving a new memory + * texture for tiles that changed even though they will point to the + * same memory. That way the renderer will upload the new contents for + * that area instead of using the previously cached texture. + */ + GBytes *content; + + /* The GdkMemoryTexture tiles we'll export using indices + * in the format [row0:col0,col1,.. to rowN:colN] + */ + GPtrArray *tiles; + + /* The format our framebuffer uses and corresponding format + * the uploaded textures will use. + */ + cairo_format_t format; + GdkMemoryFormat memory_format; + + /* The stride for the framebuffer so that the memory texture + * can skip past the rest of the framebuffer data. + */ + guint stride; + + /* Number of bytes per-pixel */ + guint bpp; + + /* The height and width of our cairo surface */ + guint height; + guint width; + + /* The number of tiles horizontally and vertically */ + guint n_columns; + guint n_rows; +}; + +enum { + PROP_0, + PROP_FORMAT, + PROP_HEIGHT, + PROP_WIDTH, + N_PROPS +}; + +static cairo_user_data_key_t invalidate_key; + +static int +mks_cairo_framebuffer_get_intrinsic_width (GdkPaintable *paintable) +{ + return mks_cairo_framebuffer_get_width (MKS_CAIRO_FRAMEBUFFER (paintable)); +} + +static int +mks_cairo_framebuffer_get_intrinsic_height (GdkPaintable *paintable) +{ + return mks_cairo_framebuffer_get_height (MKS_CAIRO_FRAMEBUFFER (paintable)); +} + +static double +mks_cairo_framebuffer_get_intrinsic_aspect_ratio (GdkPaintable *paintable) +{ + double width = gdk_paintable_get_intrinsic_width (paintable); + double height = gdk_paintable_get_intrinsic_width (paintable); + + return width / height; +} + +static void +mks_cairo_framebuffer_snapshot (GdkPaintable *paintable, + GdkSnapshot *snapshot, + double width, + double height) +{ + MksCairoFramebuffer *self = MKS_CAIRO_FRAMEBUFFER (paintable); + static GdkRGBA black = {0,0,0,1}; + double width_ratio = width / self->width; + double height_ratio = height / self->height; + + if (width_ratio > height_ratio) + width_ratio = height_ratio; + else + height_ratio = width_ratio; + + gtk_snapshot_save (snapshot); + gtk_snapshot_scale (snapshot, width_ratio, height_ratio); + + gtk_snapshot_append_color (snapshot, + &black, + &GRAPHENE_RECT_INIT (0, 0, self->width, self->height)); + + for (guint row = 0; row < self->n_rows; row++) + { + guint row_pos = row * self->n_columns; + + for (guint col = 0; col < self->n_columns; col++) + { + guint col_pos = row_pos + col; + + if G_UNLIKELY (self->tiles->pdata[col_pos] == NULL) + { + guint tile_y = row * TILE_HEIGHT; + guint tile_x = col * TILE_WIDTH; + + gsize byte_offset = (tile_y * self->stride) + (tile_x * self->bpp); + gsize n_bytes = (TILE_HEIGHT-1) * self->stride + (TILE_WIDTH * self->bpp); + + g_autoptr(GBytes) bytes = g_bytes_new_from_bytes (self->content, byte_offset, n_bytes); + + self->tiles->pdata[col_pos] = + gdk_memory_texture_new (TILE_WIDTH, + TILE_HEIGHT, + self->memory_format, + bytes, + self->stride); + } + + gtk_snapshot_append_texture (snapshot, + self->tiles->pdata[col_pos], + &GRAPHENE_RECT_INIT (col * TILE_WIDTH, + row * TILE_HEIGHT, + TILE_WIDTH, + TILE_HEIGHT)); + } + } + + gtk_snapshot_restore (snapshot); +} + +static void +paintable_iface_init (GdkPaintableInterface *iface) +{ + iface->get_intrinsic_width = mks_cairo_framebuffer_get_intrinsic_width; + iface->get_intrinsic_height = mks_cairo_framebuffer_get_intrinsic_height; + iface->get_intrinsic_aspect_ratio = mks_cairo_framebuffer_get_intrinsic_aspect_ratio; + iface->snapshot = mks_cairo_framebuffer_snapshot; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (MksCairoFramebuffer, mks_cairo_framebuffer, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, paintable_iface_init)) + +static GParamSpec *properties [N_PROPS]; + +static inline void +realign (guint *value, + guint alignment) +{ + guint rem = (*value) % alignment; + + if (rem != 0) + *value += (alignment - rem); +} + +static void +texture_clear (gpointer data) +{ + GdkTexture *texture = data; + + if (texture != NULL) + g_object_unref (texture); +} + +static void +mks_cairo_framebuffer_constructed (GObject *object) +{ + MksCairoFramebuffer *self = (MksCairoFramebuffer *)object; + guint real_width; + guint real_height; + + G_OBJECT_CLASS (mks_cairo_framebuffer_parent_class)->constructed (object); + + switch (self->format) + { + case CAIRO_FORMAT_ARGB32: + case CAIRO_FORMAT_RGB24: +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + self->memory_format = GDK_MEMORY_B8G8R8A8_PREMULTIPLIED; +#else + self->memory_format = GDK_MEMORY_A8R8G8B8_PREMULTIPLIED; +#endif + break; + + case CAIRO_FORMAT_A8: + case CAIRO_FORMAT_A1: + case CAIRO_FORMAT_RGB16_565: + case CAIRO_FORMAT_RGB30: + case CAIRO_FORMAT_RGB96F: + case CAIRO_FORMAT_RGBA128F: + case CAIRO_FORMAT_INVALID: + default: + g_warning ("Unsupported memory format from cairo format: 0x%x", + self->format); + return; + } + + real_width = self->width; + real_height = self->height; + + realign (&real_width, TILE_WIDTH); + realign (&real_height, TILE_HEIGHT); + + g_assert (real_width % TILE_WIDTH == 0); + g_assert (real_height % TILE_HEIGHT == 0); + + self->surface = cairo_image_surface_create (self->format, real_width, real_height); + + if (self->surface == NULL) + { + g_warning ("Cairo surface creation failed: format=0x%x width=%u height=%u", + self->format, real_width, real_height); + return; + } + + self->stride = cairo_format_stride_for_width (self->format, real_width); + self->bpp = self->stride / real_width; + + /* Currently only 4bbp are supported */ + g_assert (self->bpp == 4); + + self->content = g_bytes_new_with_free_func (cairo_image_surface_get_data (self->surface), + self->stride * real_height, + (GDestroyNotify) cairo_surface_destroy, + cairo_surface_reference (self->surface)); + + self->n_columns = real_width / TILE_WIDTH; + self->n_rows = real_height / TILE_HEIGHT; + + self->tiles = g_ptr_array_new_full (self->n_columns * self->n_rows, texture_clear); + g_ptr_array_set_size (self->tiles, self->n_columns * self->n_rows); +} + +static void +mks_cairo_framebuffer_dispose (GObject *object) +{ + MksCairoFramebuffer *self = (MksCairoFramebuffer *)object; + + g_clear_pointer (&self->content, g_bytes_unref); + g_clear_pointer (&self->surface, cairo_surface_destroy); + g_clear_pointer (&self->tiles, g_ptr_array_unref); + + G_OBJECT_CLASS (mks_cairo_framebuffer_parent_class)->dispose (object); +} + +static void +mks_cairo_framebuffer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MksCairoFramebuffer *self = MKS_CAIRO_FRAMEBUFFER (object); + + switch (prop_id) + { + case PROP_FORMAT: + g_value_set_enum (value, mks_cairo_framebuffer_get_format (self)); + break; + + case PROP_HEIGHT: + g_value_set_uint (value, mks_cairo_framebuffer_get_height (self)); + break; + + case PROP_WIDTH: + g_value_set_uint (value, mks_cairo_framebuffer_get_width (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mks_cairo_framebuffer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MksCairoFramebuffer *self = MKS_CAIRO_FRAMEBUFFER (object); + + switch (prop_id) + { + case PROP_FORMAT: + self->format = g_value_get_enum (value); + break; + + case PROP_HEIGHT: + self->height = g_value_get_uint (value); + break; + + case PROP_WIDTH: + self->width = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mks_cairo_framebuffer_class_init (MksCairoFramebufferClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = mks_cairo_framebuffer_constructed; + object_class->dispose = mks_cairo_framebuffer_dispose; + object_class->get_property = mks_cairo_framebuffer_get_property; + object_class->set_property = mks_cairo_framebuffer_set_property; + + properties[PROP_FORMAT] = + g_param_spec_enum ("format", NULL, NULL, + CAIRO_GOBJECT_TYPE_FORMAT, + CAIRO_FORMAT_RGB24, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties[PROP_HEIGHT] = + g_param_spec_uint ("height", NULL, NULL, + 0, G_MAXUINT, 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + properties[PROP_WIDTH] = + g_param_spec_uint ("width", NULL, NULL, + 0, G_MAXUINT, 0, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +mks_cairo_framebuffer_init (MksCairoFramebuffer *self) +{ + self->format = CAIRO_FORMAT_RGB24; +} + +MksCairoFramebuffer * +mks_cairo_framebuffer_new (cairo_format_t format, + guint width, + guint height) +{ + g_return_val_if_fail (width > 0, NULL); + g_return_val_if_fail (height > 0, NULL); + + return g_object_new (MKS_TYPE_CAIRO_FRAMEBUFFER, + "format", format, + "height", height, + "width", width, + NULL); +} + +static void +invalidate_on_destroy (gpointer data) +{ + g_autoptr(MksCairoFramebuffer) self = data; + + gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); +} + +cairo_t * +mks_cairo_framebuffer_update (MksCairoFramebuffer *self, + guint x, + guint y, + guint width, + guint height) +{ + cairo_t *cr; + guint col1, col2; + guint row1, row2; + + g_return_val_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self), NULL); + g_return_val_if_fail (self->surface != NULL, NULL); + + col1 = MIN (x / TILE_WIDTH, self->n_columns-1); + col2 = MIN ((x + width) / TILE_WIDTH, self->n_columns-1); + + row1 = MIN (y / TILE_HEIGHT, self->n_rows-1); + row2 = MIN ((y + height) / TILE_HEIGHT, self->n_rows-1); + + for (guint row = row1; row <= row2; row++) + { + guint row_pos = row * self->n_rows; + + for (guint col = col1; col <= col2; col++) + { + guint col_pos = row_pos + col; + + g_assert (col_pos < self->tiles->len); + + g_clear_object (&self->tiles->pdata[col_pos]); + } + } + + cr = cairo_create (self->surface); + cairo_rectangle (cr, x, y, width, height); + cairo_clip (cr); + + cairo_set_user_data (cr, + &invalidate_key, + g_object_ref (self), + invalidate_on_destroy); + + return cr; +} + +void +mks_cairo_framebuffer_clear (MksCairoFramebuffer *self) +{ + cairo_t *cr; + + g_return_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self)); + + cr = cairo_create (self->surface); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_rectangle (cr, 0, 0, + self->n_columns * TILE_WIDTH, + self->n_rows * TILE_HEIGHT); + cairo_set_source_rgba (cr, 0, 0, 0, 1); + cairo_fill (cr); + cairo_destroy (cr); +} + +cairo_format_t +mks_cairo_framebuffer_get_format (MksCairoFramebuffer *self) +{ + g_return_val_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self), 0); + + return self->format; +} + +guint +mks_cairo_framebuffer_get_height (MksCairoFramebuffer *self) +{ + g_return_val_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self), 0); + + return self->height; +} + +guint +mks_cairo_framebuffer_get_width (MksCairoFramebuffer *self) +{ + g_return_val_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self), 0); + + return self->width; +} + +void +mks_cairo_framebuffer_copy_to (MksCairoFramebuffer *self, + MksCairoFramebuffer *dest) +{ + cairo_t *cr; + + g_return_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (self)); + g_return_if_fail (MKS_IS_CAIRO_FRAMEBUFFER (dest)); + + cr = cairo_create (dest->surface); + cairo_set_source_surface (cr, self->surface, 0, 0); + cairo_rectangle (cr, 0, 0, self->width, self->height); + //cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_fill (cr); + cairo_destroy (cr); +} diff --git a/lib/mks-framebuffer-private.h b/lib/mks-framebuffer-private.h deleted file mode 100644 index c151c71..0000000 --- a/lib/mks-framebuffer-private.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * mks-framebuffer-private.h - * - * Copyright 2023 Christian Hergert - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include - -G_BEGIN_DECLS - -#define MKS_TYPE_FRAMEBUFFER (mks_framebuffer_get_type()) - -G_DECLARE_FINAL_TYPE (MksFramebuffer, mks_framebuffer, MKS, FRAMEBUFFER, GObject) - -MksFramebuffer *mks_framebuffer_new (guint width, - guint height, - cairo_format_t format); -void mks_framebuffer_update (MksFramebuffer *self, - guint x, - guint y, - guint width, - guint height, - guint stride, - cairo_format_t format, - const guint8 *data, - gsize data_len); - -G_END_DECLS diff --git a/lib/mks-framebuffer.c b/lib/mks-framebuffer.c deleted file mode 100644 index 8e7b4d7..0000000 --- a/lib/mks-framebuffer.c +++ /dev/null @@ -1,280 +0,0 @@ -/* - * mks-framebuffer.c - * - * Copyright 2023 Christian Hergert - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#include "config.h" - -#include -#include - -#include "mks-framebuffer-private.h" - -/* The surface we're drawing to. The framebuffer isn't our fast path, - * (that is DMA-BUF) but we should still try to make it reasonably - * fast for the situations we may need to support. - * - * We use a single surface and update it as new content comes in, which - * would race against the upload to the GPU except that we're on the - * same thread and therefore the GPU upload has already happened for the - * last frame if we're here already. - * - * The @damage region is updated as content changes so that we can - * calculate how many pixels were damaged since the last snapshot. If it's - * beyond our threshold ratio, then we snapshot with one big texture to - * update (the whole surface) rather than the whole surface + damage - * rectangles. - * - * The reason for this is that the GL renderer will likely already have - * our "full framebuffer" (minus recent damages) in VRAM so we can reuse - * it and then draw small damage rectangles after that (which get uploaded - * on every frame). - * - * But again, what we really are hoping for us the DMA-BUF paintable to - * get used instead. - */ - -/* The percentage of the framebuffer that must be damaged before a new - * scanout is performed instead of using damage rectangles. - */ -#define THRESHOLD_RATIO (.5) - -struct _MksFramebuffer -{ - GObject parent_instance; - cairo_surface_t *surface; - cairo_region_t *damage; - GdkPaintable *base_texture; - guint width; - guint height; - guint threshold; - cairo_format_t format; -}; - -static int -mks_framebuffer_get_intrinsic_width (GdkPaintable *paintable) -{ - return MKS_FRAMEBUFFER (paintable)->width; -} - -static int -mks_framebuffer_get_intrinsic_height (GdkPaintable *paintable) -{ - return MKS_FRAMEBUFFER (paintable)->height; -} - -static double -mks_framebuffer_get_intrinsic_aspect_ratio (GdkPaintable *paintable) -{ - double width = MKS_FRAMEBUFFER (paintable)->width; - double height = MKS_FRAMEBUFFER (paintable)->height; - - return width / height; -} - -static inline gboolean -mks_framebuffer_damage_over_threshold (MksFramebuffer *self) -{ - guint area = 0; - guint n_rects; - - g_assert (MKS_IS_FRAMEBUFFER (self)); - - n_rects = cairo_region_num_rectangles (self->damage); - - for (guint i = 0; i < n_rects; i++) - { - cairo_rectangle_int_t rect; - cairo_region_get_rectangle (self->damage, i, &rect); - area += rect.width * rect.height; - } - - return area > self->threshold; -} - -static void -mks_framebuffer_snapshot (GdkPaintable *paintable, - GdkSnapshot *snapshot, - double width, - double height) -{ - MksFramebuffer *self = MKS_FRAMEBUFFER (paintable); - guint n_rects; - - g_assert (GDK_IS_PAINTABLE (paintable)); - g_assert (GDK_IS_SNAPSHOT (snapshot)); - - if G_UNLIKELY (mks_framebuffer_damage_over_threshold (self)) - { - cairo_region_destroy (self->damage); - self->damage = cairo_region_create (); - g_clear_object (&self->base_texture); - } - - if G_UNLIKELY (self->base_texture == NULL) - { - GtkSnapshot *texture_snapshot = gtk_snapshot_new (); - cairo_t *cr; - - cr = gtk_snapshot_append_cairo (texture_snapshot, - &GRAPHENE_RECT_INIT (0, 0, self->width, self->height)); - cairo_set_source_surface (cr, self->surface, 0, 0); - cairo_rectangle (cr, 0, 0, self->width, self->height); - cairo_fill (cr); - cairo_destroy (cr); - - self->base_texture = gtk_snapshot_free_to_paintable (texture_snapshot, - &GRAPHENE_SIZE_INIT (self->width, self->height)); - } - - /* Always draw our "base texture" even though it's going to be - * composited over on the GPU. It saves us a large GPU upload since - * the GL renderer will cache the texture in VRAM in many cases. - */ - gdk_paintable_snapshot (GDK_PAINTABLE (self->base_texture), snapshot, width, height); - - /* Now draw our damage rectangles which are going to require an upload - * since we can't reuse them between frames without a lot of tracking. - * You could do that though, if you reset the damage each snapshot and - * then go and and hash/index them for re-use. - */ - n_rects = cairo_region_num_rectangles (self->damage); - for (guint i = 0; i < n_rects; i++) - { - cairo_rectangle_int_t rect; - cairo_t *cr; - - cairo_region_get_rectangle (self->damage, i, &rect); - cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT (rect.x, rect.y, rect.width, rect.height)); - cairo_set_source_surface (cr, self->surface, -rect.x, -rect.y); - cairo_rectangle (cr, 0, 0, rect.width, rect.height); - cairo_destroy (cr); - } -} - -static void -paintable_iface_init (GdkPaintableInterface *iface) -{ - iface->get_intrinsic_width = mks_framebuffer_get_intrinsic_width; - iface->get_intrinsic_height = mks_framebuffer_get_intrinsic_height; - iface->get_intrinsic_aspect_ratio = mks_framebuffer_get_intrinsic_aspect_ratio; - iface->snapshot = mks_framebuffer_snapshot; -} - -G_DEFINE_FINAL_TYPE_WITH_CODE (MksFramebuffer, mks_framebuffer, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, paintable_iface_init)) - -MksFramebuffer * -mks_framebuffer_new (guint width, - guint height, - cairo_format_t format) -{ - g_autoptr(MksFramebuffer) self = NULL; - cairo_t *cr; - - g_return_val_if_fail (width > 0, NULL); - g_return_val_if_fail (height > 0, NULL); - g_return_val_if_fail (format != 0, NULL); - - self = g_object_new (MKS_TYPE_FRAMEBUFFER, NULL); - self->width = width; - self->height = height; - self->format = format; - self->damage = cairo_region_create (); - self->threshold = (width * height) * THRESHOLD_RATIO; - - if (!(self->surface = cairo_image_surface_create (format, width, height))) - return NULL; - - cr = cairo_create (self->surface); - cairo_rectangle (cr, 0, 0, width, height); - cairo_set_source_rgb (cr, 0, 0, 0); - cairo_fill (cr); - cairo_destroy (cr); - - return g_steal_pointer (&self); -} - -void -mks_framebuffer_update (MksFramebuffer *self, - guint x, - guint y, - guint width, - guint height, - guint stride, - cairo_format_t format, - const guint8 *data, - gsize data_len) -{ - cairo_surface_t *surface; - cairo_t *cr; - - g_return_if_fail (MKS_IS_FRAMEBUFFER (self)); - g_return_if_fail (data != NULL || data_len == 0); - - if G_UNLIKELY (data == NULL || data_len == 0) - return; - - if (stride < width || - stride < cairo_format_stride_for_width (format, width) || - ((guint64)stride * (guint64)height) > data_len) - return; - - surface = cairo_image_surface_create_for_data ((guint8 *)data, format, width, height, stride); - cr = cairo_create (self->surface); - cairo_set_source_surface (cr, surface, x, y); - cairo_rectangle (cr, x, y, width, height); - cairo_surface_destroy (surface); - cairo_destroy (cr); - - if (x == 0 && y == 0 && width == self->width && height == self->height) - { - g_clear_pointer (&self->damage, cairo_region_destroy); - self->damage = cairo_region_create (); - } - else - { - cairo_region_union_rectangle (self->damage, - &(cairo_rectangle_int_t) { x, y, width, height }); - } -} - -static void -mks_framebuffer_finalize (GObject *object) -{ - MksFramebuffer *self = (MksFramebuffer *)object; - - g_clear_pointer (&self->surface, cairo_surface_destroy); - g_clear_pointer (&self->damage, cairo_region_destroy); - - G_OBJECT_CLASS (mks_framebuffer_parent_class)->finalize (object); -} - -static void -mks_framebuffer_class_init (MksFramebufferClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->finalize = mks_framebuffer_finalize; -} - -static void -mks_framebuffer_init (MksFramebuffer *self) -{ -} diff --git a/lib/mks-paintable.c b/lib/mks-paintable.c index db608d9..8053d5d 100644 --- a/lib/mks-paintable.c +++ b/lib/mks-paintable.c @@ -27,22 +27,19 @@ #include #include -#include "mks-framebuffer-private.h" +#include "mks-cairo-framebuffer-private.h" #include "mks-paintable-private.h" #include "mks-qemu.h" struct _MksPaintable { - GObject parent_instance; + GObject parent_instance; - MksQemuListener *listener; - GDBusConnection *connection; - MksFramebuffer *framebuffer; + MksQemuListener *listener; + GDBusConnection *connection; + MksCairoFramebuffer *framebuffer; - guint width; - guint height; - - guint mode : 2; + guint mode : 2; }; enum { @@ -80,13 +77,23 @@ _pixman_format_to_cairo_format (guint pixman_format) static int mks_paintable_get_intrinsic_height (GdkPaintable *paintable) { - return MKS_PAINTABLE (paintable)->height; + MksPaintable *self = MKS_PAINTABLE (paintable); + + if (self->mode == MODE_FRAMEBUFFER) + return gdk_paintable_get_intrinsic_height (GDK_PAINTABLE (self->framebuffer)); + + return 0; } static int mks_paintable_get_intrinsic_width (GdkPaintable *paintable) { - return MKS_PAINTABLE (paintable)->width; + MksPaintable *self = MKS_PAINTABLE (paintable); + + if (self->mode == MODE_FRAMEBUFFER) + return gdk_paintable_get_intrinsic_width (GDK_PAINTABLE (self->framebuffer)); + + return 0; } static double @@ -94,10 +101,10 @@ mks_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable) { MksPaintable *self = MKS_PAINTABLE (paintable); - if (self->width == 0 || self->height == 0) - return 1.; + if (self->mode == MODE_FRAMEBUFFER) + return gdk_paintable_get_intrinsic_aspect_ratio (GDK_PAINTABLE (self->framebuffer)); - return (double)self->width / (double)self->height; + return .0; } static void @@ -112,8 +119,7 @@ mks_paintable_snapshot (GdkPaintable *paintable, g_assert (GDK_IS_SNAPSHOT (snapshot)); if (self->mode == MODE_FRAMEBUFFER) - gdk_paintable_snapshot (GDK_PAINTABLE (self->framebuffer), - snapshot, width, height); + gdk_paintable_snapshot (GDK_PAINTABLE (self->framebuffer), snapshot, width, height); } static void @@ -135,6 +141,7 @@ mks_paintable_dispose (GObject *object) g_clear_object (&self->connection); g_clear_object (&self->listener); + g_clear_object (&self->framebuffer); G_OBJECT_CLASS (mks_paintable_parent_class)->dispose (object); } @@ -152,6 +159,60 @@ mks_paintable_init (MksPaintable *self) { } +static void +mks_paintable_set_framebuffer (MksPaintable *self, + MksCairoFramebuffer *framebuffer) +{ + gboolean size_changed; + + g_assert (MKS_IS_PAINTABLE (self)); + g_assert (!framebuffer || MKS_IS_CAIRO_FRAMEBUFFER (framebuffer)); + + if (self->framebuffer == framebuffer) + return; + + size_changed = self->framebuffer == NULL || + framebuffer == NULL || + mks_cairo_framebuffer_get_width (self->framebuffer) != mks_cairo_framebuffer_get_width (framebuffer) || + mks_cairo_framebuffer_get_height (self->framebuffer) != mks_cairo_framebuffer_get_height (framebuffer); + + if (self->framebuffer != NULL) + { + g_signal_handlers_disconnect_by_func (self->framebuffer, + G_CALLBACK (gdk_paintable_invalidate_size), + self); + g_signal_handlers_disconnect_by_func (self->framebuffer, + G_CALLBACK (gdk_paintable_invalidate_contents), + self); + g_clear_object (&self->framebuffer); + self->mode = MODE_INITIAL; + } + + if (framebuffer != NULL) + { + g_print ("Framebuffer set to %ux%u\n", + mks_cairo_framebuffer_get_width (framebuffer), + mks_cairo_framebuffer_get_height (framebuffer)); + self->framebuffer = g_object_ref (framebuffer); + g_signal_connect_object (self->framebuffer, + "invalidate-size", + G_CALLBACK (gdk_paintable_invalidate_size), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->framebuffer, + "invalidate-contents", + G_CALLBACK (gdk_paintable_invalidate_contents), + self, + G_CONNECT_SWAPPED); + self->mode = MODE_FRAMEBUFFER; + } + + gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); + + if (size_changed) + gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); +} + static gboolean mks_paintable_listener_update_dmabuf (MksPaintable *self, GDBusMethodInvocation *invocation, @@ -165,8 +226,6 @@ mks_paintable_listener_update_dmabuf (MksPaintable *self, g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - g_print ("Update dmabuf\n"); - mks_qemu_listener_complete_update_dmabuf (listener, invocation); return TRUE; @@ -185,21 +244,10 @@ mks_paintable_listener_scanout_dmabuf (MksPaintable *self, gboolean y0_top, MksQemuListener *listener) { - gboolean size_changed; - g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - g_print ("Scanout dmabuf\n"); - - size_changed = width != self->width || height != self->height; - - if (size_changed) - gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); - else - gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); - mks_qemu_listener_complete_scanout_dmabuf (listener, invocation, NULL); return TRUE; @@ -217,34 +265,70 @@ mks_paintable_listener_update (MksPaintable *self, GVariant *bytes, MksQemuListener *listener) { + cairo_surface_t *source; + cairo_t *cr; + guint8 *data; cairo_format_t format; + gsize data_len; g_assert (MKS_IS_PAINTABLE (self)); g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - if (!(format = _pixman_format_to_cairo_format (pixman_format))) + if (self->mode != MODE_FRAMEBUFFER || + !(format = _pixman_format_to_cairo_format (pixman_format))) { g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "Pixman format not supported"); + "Invalid operation"); return TRUE; } - if (self->mode == MODE_FRAMEBUFFER) + data = (guint8 *)g_variant_get_bytestring (bytes); + data_len = g_variant_n_children (bytes); + + if (data_len < cairo_format_stride_for_width (format, width) * height) { - const guint8 *data; - gsize data_len; - - data_len = g_variant_n_children (bytes); - data = (const guint8 *)g_variant_get_bytestring (bytes); - - mks_framebuffer_update (self->framebuffer, x, y, width, height, stride, format, data, data_len); - - gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); + g_dbus_method_invocation_return_error_literal (invocation, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Stride invalid for size"); + return TRUE; } + /* We can get in a protocol race condition here in that we will get updates + * for framebuffer content _BEFORE_ we'll get notified of property changes + * about the MksQemuConsole's size. + * + * To overcome that, if we detect something larger than our current + * framebuffer, we'll resize it and draw over the old contents in a + * new framebuffer. + * + * When shrinking, we can do this as well and then handle it when the + * console size notification arrives. + * + * Generally this is seen at startup during EFI/BIOS. + */ + if (x + width > mks_cairo_framebuffer_get_width (self->framebuffer) || + y + height > mks_cairo_framebuffer_get_height (self->framebuffer)) + { + guint max_width = MAX (mks_cairo_framebuffer_get_width (self->framebuffer), x + width); + guint max_height = MAX (mks_cairo_framebuffer_get_height (self->framebuffer), y + height); + g_autoptr(MksCairoFramebuffer) framebuffer = mks_cairo_framebuffer_new (format, max_width, max_height); + + mks_cairo_framebuffer_copy_to (self->framebuffer, framebuffer); + mks_paintable_set_framebuffer (self, framebuffer); + } + + source = cairo_image_surface_create_for_data (data, format, width, height, stride); + cr = mks_cairo_framebuffer_update (self->framebuffer, x, y, width, height); + cairo_set_source_surface (cr, source, 0, 0); + cairo_rectangle (cr, x, y, width, height); + cairo_paint (cr); + cairo_destroy (cr); + cairo_surface_destroy (source); + mks_qemu_listener_complete_update (listener, invocation); return TRUE; @@ -260,9 +344,10 @@ mks_paintable_listener_scanout (MksPaintable *self, GVariant *bytes, MksQemuListener *listener) { + cairo_surface_t *source; + cairo_t *cr; + guint8 *data; cairo_format_t format; - gboolean size_changed; - const guint8 *data; gsize data_len; g_assert (MKS_IS_PAINTABLE (self)); @@ -279,27 +364,34 @@ mks_paintable_listener_scanout (MksPaintable *self, return TRUE; } - size_changed = width != self->width || height != self->height; + data = (guint8 *)g_variant_get_bytestring (bytes); + data_len = g_variant_n_children (bytes); - if (size_changed || self->framebuffer == NULL) + if (data_len < cairo_format_stride_for_width (format, width) * height) { - g_clear_object (&self->framebuffer); - self->framebuffer = mks_framebuffer_new (width, height, format); + g_dbus_method_invocation_return_error_literal (invocation, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Stride invalid for size"); + return TRUE; } - data_len = g_variant_n_children (bytes); - data = (const guint8 *)g_variant_get_bytestring (bytes); - mks_framebuffer_update (self->framebuffer, - 0, 0, width, height, - stride, format, - data, data_len); + if (self->framebuffer == NULL || + width != mks_cairo_framebuffer_get_width (self->framebuffer) || + height != mks_cairo_framebuffer_get_height (self->framebuffer)) + { + g_autoptr(MksCairoFramebuffer) framebuffer = mks_cairo_framebuffer_new (format, width, height); - self->mode = MODE_FRAMEBUFFER; + mks_paintable_set_framebuffer (self, framebuffer); + } - if (size_changed) - gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); - else - gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); + source = cairo_image_surface_create_for_data (data, format, width, height, stride); + cr = mks_cairo_framebuffer_update (self->framebuffer, 0, 0, width, height); + cairo_set_source_surface (cr, source, 0, 0); + cairo_rectangle (cr, 0, 0, width, height); + cairo_paint (cr); + cairo_destroy (cr); + cairo_surface_destroy (source); mks_qemu_listener_complete_scanout (listener, invocation); @@ -320,8 +412,6 @@ mks_paintable_listener_cursor_define (MksPaintable *self, g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - g_print ("Cursor Define\n"); - mks_qemu_listener_complete_cursor_define (listener, invocation); return TRUE; @@ -339,8 +429,6 @@ mks_paintable_listener_mouse_set (MksPaintable *self, g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - g_print ("Mouse Set\n"); - mks_qemu_listener_complete_mouse_set (listener, invocation); return TRUE; @@ -355,9 +443,13 @@ mks_paintable_listener_disable (MksPaintable *self, g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); g_assert (MKS_QEMU_IS_LISTENER (listener)); - gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); + if (self->mode == MODE_FRAMEBUFFER) + { + if (self->framebuffer != NULL) + mks_cairo_framebuffer_clear (self->framebuffer); + } - g_print ("Disable\n"); + gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); mks_qemu_listener_complete_disable (listener, invocation); diff --git a/tools/mks.c b/tools/mks.c index f830e1e..5c8c123 100644 --- a/tools/mks.c +++ b/tools/mks.c @@ -66,6 +66,8 @@ main (int argc, "title", "Mouse, Keyboard, Screen", NULL); picture = g_object_new (GTK_TYPE_PICTURE, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, NULL); gtk_window_set_child (window, GTK_WIDGET (picture)); g_signal_connect_swapped (window,