libmks/lib/mks-cairo-framebuffer.c
Christian Hergert 01dab99590 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.
2023-02-13 17:38:30 -08:00

499 lines
14 KiB
C

/* 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);
}