lib: add MksCairoFramebuffer

This adds MksCairoFramebuffer which does tessellation to get damage regions
for the framebuffer. Still some bugs involved, but good enough to get some
bits on the screen which is what I'm looking for right now.
This commit is contained in:
Christian Hergert 2023-02-13 17:38:30 -08:00
parent 84d1d827c1
commit 01dab99590
7 changed files with 704 additions and 390 deletions

View File

@ -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',

View File

@ -0,0 +1,47 @@
/* mks-cairo-framebuffer.h
*
* Copyright 2023 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cairo.h>
#include <gdk/gdk.h>
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

498
lib/mks-cairo-framebuffer.c Normal file
View File

@ -0,0 +1,498 @@
/* mks-cairo-framebuffer.c
*
* Copyright 2023 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <cairo-gobject.h>
#include <gtk/gtk.h>
#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);
}

View File

@ -1,45 +0,0 @@
/*
* mks-framebuffer-private.h
*
* Copyright 2023 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <gdk/gdk.h>
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

View File

@ -1,280 +0,0 @@
/*
* mks-framebuffer.c
*
* Copyright 2023 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <cairo-gobject.h>
#include <gtk/gtk.h>
#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)
{
}

View File

@ -27,22 +27,19 @@
#include <glib/gstdio.h>
#include <pixman.h>
#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);

View File

@ -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,