diff --git a/lib/meson.build b/lib/meson.build index 9e7e855..c3fc1a5 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -23,6 +23,7 @@ libmks_headers = [ ] libmks_private_sources = [ + 'mks-framebuffer.c', 'mks-paintable-listener.c', 'mks-read-only-list-model.c', diff --git a/lib/mks-framebuffer-private.h b/lib/mks-framebuffer-private.h new file mode 100644 index 0000000..c151c71 --- /dev/null +++ b/lib/mks-framebuffer-private.h @@ -0,0 +1,45 @@ +/* + * 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 new file mode 100644 index 0000000..0ac57a9 --- /dev/null +++ b/lib/mks-framebuffer.c @@ -0,0 +1,273 @@ +/* + * 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. + */ + gtk_snapshot_append_texture (snapshot, + GDK_TEXTURE (self->base_texture), + &GRAPHENE_RECT_INIT (0, 0, self->width, self->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); + + 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) +{ +}