From 543c14a8b12825585dd1115f4500432eb62cc27e Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Mon, 23 Mar 2026 20:18:45 +0000 Subject: [PATCH 1/2] Add GBM surface backend for zero-copy GPU buffer sharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new surface backend that allocates pixel buffers through GBM (Generic Buffer Manager), allowing Cairo-rendered content to be imported directly by GPU compositors via DMA-BUF file descriptors without an intermediate memory copy. The backend renders on the CPU via pixman (like the image backend) but allocates its buffer through GBM with a linear modifier, making it suitable for zero-copy handoff to GPU-based compositors such as Mutter/Clutter (GNOME) or GSK (GTK 4). New public API (cairo-gbm.h): - cairo_gbm_surface_create() — allocate a new GBM-backed surface - cairo_gbm_surface_create_for_bo() — wrap an existing gbm_bo - cairo_gbm_surface_get_bo() — retrieve the underlying gbm_bo - cairo_gbm_surface_get_dma_buf_fd() — export a DMA-BUF fd for the buffer Build integration: - New 'gbm' meson option (auto-detected via libgbm + libdrm) - CAIRO_HAS_GBM_SURFACE feature flag Test coverage: - Dedicated API + error-path tests (test/gbm-surface.c) - Surface-as-source pattern test (test/gbm-surface-source.c) - GBM boilerplate target for the full test suite - Entries in api-special-cases.c and error-setters.c --- boilerplate/cairo-boilerplate-gbm.c | 172 +++++++ boilerplate/meson.build | 1 + meson.build | 13 + meson.options | 1 + src/cairo-gbm-surface.c | 717 ++++++++++++++++++++++++++++ src/cairo-gbm.h | 93 ++++ src/cairo-pattern.c | 1 + src/cairo.h | 5 +- src/meson.build | 4 + test/api-special-cases.c | 25 + test/error-setters.c | 8 + test/gbm-surface-source.c | 74 +++ test/gbm-surface.c | 438 +++++++++++++++++ test/meson.build | 9 + 14 files changed, 1560 insertions(+), 1 deletion(-) create mode 100644 boilerplate/cairo-boilerplate-gbm.c create mode 100644 src/cairo-gbm-surface.c create mode 100644 src/cairo-gbm.h create mode 100644 test/gbm-surface-source.c create mode 100644 test/gbm-surface.c diff --git a/boilerplate/cairo-boilerplate-gbm.c b/boilerplate/cairo-boilerplate-gbm.c new file mode 100644 index 000000000..0d0162ef0 --- /dev/null +++ b/boilerplate/cairo-boilerplate-gbm.c @@ -0,0 +1,172 @@ +/* + * Copyright © 2026 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of + * Red Hat, Inc. not be used in advertising or publicity pertaining to + * distribution of the software without specific, written prior + * permission. Red Hat, Inc. makes no representations about the + * suitability of this software for any purpose. It is provided "as + * is" without express or implied warranty. + * + * RED HAT, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL RED HAT, INC. BE LIABLE FOR ANY SPECIAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Alberto Ruiz + */ + +#include "cairo-boilerplate-private.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +typedef struct _gbm_target_closure { + struct gbm_device *gbm_device; + int drm_fd; +} gbm_target_closure_t; + +static struct gbm_device * +_open_gbm_device (int *fd_out) +{ + drmDevice *devices[16]; + int n, i; + + n = drmGetDevices2 (0, devices, 16); + for (i = 0; i < n; i++) { + int fd; + struct gbm_device *dev; + + if (!(devices[i]->available_nodes & (1 << DRM_NODE_RENDER))) + continue; + + fd = open (devices[i]->nodes[DRM_NODE_RENDER], O_RDWR); + if (fd < 0) + continue; + + dev = gbm_create_device (fd); + if (dev) { + *fd_out = fd; + drmFreeDevices (devices, n); + return dev; + } + close (fd); + } + + if (n > 0) + drmFreeDevices (devices, n); + return NULL; +} + +static cairo_surface_t * +_cairo_boilerplate_gbm_create_surface (const char *name, + cairo_content_t content, + double width, + double height, + double max_width, + double max_height, + cairo_boilerplate_mode_t mode, + void **closure) +{ + gbm_target_closure_t *gtc; + cairo_format_t format; + cairo_surface_t *surface; + + *closure = gtc = xcalloc (1, sizeof (gbm_target_closure_t)); + + gtc->gbm_device = _open_gbm_device (>c->drm_fd); + if (gtc->gbm_device == NULL) { + free (gtc); + CAIRO_BOILERPLATE_DEBUG (("Failed to open GBM device\n")); + return NULL; + } + + width = ceil (width); + if (width < 1) + width = 1; + + height = ceil (height); + if (height < 1) + height = 1; + + if (content == CAIRO_CONTENT_COLOR) + format = CAIRO_FORMAT_RGB24; + else if (content == CAIRO_CONTENT_ALPHA) + format = CAIRO_FORMAT_A8; + else + format = CAIRO_FORMAT_ARGB32; + + surface = cairo_gbm_surface_create (gtc->gbm_device, + format, + (int) width, + (int) height); + if (cairo_surface_status (surface)) { + cairo_surface_destroy (surface); + gbm_device_destroy (gtc->gbm_device); + close (gtc->drm_fd); + free (gtc); + CAIRO_BOILERPLATE_DEBUG (("Failed to create GBM surface\n")); + return NULL; + } + + return surface; +} + +static void +_cairo_boilerplate_gbm_cleanup (void *closure) +{ + gbm_target_closure_t *gtc = closure; + + if (gtc->gbm_device) + gbm_device_destroy (gtc->gbm_device); + if (gtc->drm_fd >= 0) + close (gtc->drm_fd); + + free (gtc); +} + +static const cairo_boilerplate_target_t targets[] = { + { + "gbm", "image", NULL, NULL, + CAIRO_SURFACE_TYPE_GBM, CAIRO_CONTENT_COLOR_ALPHA, 0, + "cairo_gbm_surface_create", + _cairo_boilerplate_gbm_create_surface, + cairo_surface_create_similar, + NULL, NULL, + _cairo_boilerplate_get_image_surface, + cairo_surface_write_to_png, + _cairo_boilerplate_gbm_cleanup, + NULL, + NULL, + TRUE, FALSE, FALSE + }, + { + "gbm", "image", NULL, NULL, + CAIRO_SURFACE_TYPE_GBM, CAIRO_CONTENT_COLOR, 0, + "cairo_gbm_surface_create", + _cairo_boilerplate_gbm_create_surface, + cairo_surface_create_similar, + NULL, NULL, + _cairo_boilerplate_get_image_surface, + cairo_surface_write_to_png, + _cairo_boilerplate_gbm_cleanup, + NULL, + NULL, + FALSE, FALSE, FALSE + }, +}; +CAIRO_BOILERPLATE (gbm, targets) diff --git a/boilerplate/meson.build b/boilerplate/meson.build index 544bfcc09..1a5eaf7fc 100644 --- a/boilerplate/meson.build +++ b/boilerplate/meson.build @@ -13,6 +13,7 @@ cairo_boilerplate_feature_sources = { 'cairo-ps': ['cairo-boilerplate-ps.c'], 'cairo-svg': ['cairo-boilerplate-svg.c'], 'cairo-script': ['cairo-boilerplate-script.c'], + 'cairo-gbm': ['cairo-boilerplate-gbm.c'], } foreach feature: built_features diff --git a/meson.build b/meson.build index 11525ea84..393ecdf53 100644 --- a/meson.build +++ b/meson.build @@ -692,6 +692,18 @@ if not get_option('tee').disabled() }] endif +gbm_dep = dependency('gbm', required: get_option('gbm')) +libdrm_dep = dependency('libdrm', required: get_option('gbm')) +if gbm_dep.found() and libdrm_dep.found() + feature_conf.set('CAIRO_HAS_GBM_SURFACE', 1) + built_features += [{ + 'name': 'cairo-gbm', + 'description': 'GBM surface backend', + 'deps': [gbm_dep, libdrm_dep], + }] + deps += [gbm_dep, libdrm_dep] +endif + incbase = include_directories('.') foreach check : check_sizeofs @@ -859,6 +871,7 @@ summary({ 'Recording': true, 'Observer': true, 'Mime': true, + 'GBM': feature_conf.get('CAIRO_HAS_GBM_SURFACE', 0) == 1, 'Tee': feature_conf.get('CAIRO_HAS_TEE_SURFACE', 0) == 1, 'Xlib': feature_conf.get('CAIRO_HAS_XLIB_SURFACE', 0) == 1, 'Xlib Xrender': feature_conf.get('CAIRO_HAS_XLIB_XRENDER_SURFACE', 0) == 1, diff --git a/meson.options b/meson.options index 8aa6814bb..6274c3c03 100644 --- a/meson.options +++ b/meson.options @@ -4,6 +4,7 @@ option('fontconfig', type : 'feature', value : 'auto') option('freetype', type : 'feature', value : 'auto') # Cairo surface backends +option('gbm', type : 'feature', value : 'auto') option('png', type : 'feature', value : 'auto') # png and svg surfaces option('quartz', type : 'feature', value : 'auto') option('tee', type : 'feature', value : 'auto') diff --git a/src/cairo-gbm-surface.c b/src/cairo-gbm-surface.c new file mode 100644 index 000000000..97f988c07 --- /dev/null +++ b/src/cairo-gbm-surface.c @@ -0,0 +1,717 @@ +/* cairo - a vector graphics library with display and print output + * + * Copyright © 2026 Red Hat, Inc. + * + * Author: Alberto Ruiz + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + * The Original Code is the cairo graphics library. + */ + +/** + * SECTION:cairo-gbm + * @Title: GBM Surfaces + * @Short_Description: Rendering to GBM buffers for zero-copy GPU handoff + * @See_Also: #cairo_surface_t + * + * A GBM surface allocates its pixel buffer through GBM (Generic Buffer + * Manager), producing memory that is directly importable by a GPU + * compositor via DMA-BUF. Rendering is still done on the CPU via + * pixman, but the result can be handed to a GPU-based compositor + * (e.g. GSK in GTK4, or Mutter) without an intermediate copy. + **/ + +#include "cairoint.h" + +#include "cairo-default-context-private.h" +#include "cairo-error-private.h" +#include "cairo-image-surface-private.h" +#include "cairo-surface-private.h" + +#include "cairo-gbm.h" + +#include +#include + +typedef struct _cairo_gbm_surface { + cairo_surface_t base; + + struct gbm_device *gbm_device; + struct gbm_bo *bo; + cairo_bool_t owns_bo; + + uint32_t drm_format; + int width; + int height; + int stride; + + void *map_data; + unsigned char *mapped_data; + cairo_image_surface_t *image; +} cairo_gbm_surface_t; + +static uint32_t +_cairo_format_to_drm_format (cairo_format_t format) +{ + switch (format) { + case CAIRO_FORMAT_ARGB32: + return DRM_FORMAT_ARGB8888; + case CAIRO_FORMAT_RGB24: + return DRM_FORMAT_XRGB8888; + case CAIRO_FORMAT_RGB30: + return DRM_FORMAT_XRGB2101010; + case CAIRO_FORMAT_RGB16_565: + return DRM_FORMAT_RGB565; + case CAIRO_FORMAT_A8: + return DRM_FORMAT_R8; + case CAIRO_FORMAT_INVALID: + case CAIRO_FORMAT_A1: + case CAIRO_FORMAT_RGB96F: + case CAIRO_FORMAT_RGBA128F: + return 0; + } + return 0; +} + +static cairo_format_t +_drm_format_to_cairo_format (uint32_t drm_format) +{ + switch (drm_format) { + case DRM_FORMAT_ARGB8888: + return CAIRO_FORMAT_ARGB32; + case DRM_FORMAT_XRGB8888: + return CAIRO_FORMAT_RGB24; + case DRM_FORMAT_XRGB2101010: + return CAIRO_FORMAT_RGB30; + case DRM_FORMAT_RGB565: + return CAIRO_FORMAT_RGB16_565; + case DRM_FORMAT_R8: + return CAIRO_FORMAT_A8; + default: + return CAIRO_FORMAT_INVALID; + } +} + +static cairo_status_t +_cairo_gbm_surface_map (cairo_gbm_surface_t *surface) +{ + uint32_t map_stride; + + if (surface->mapped_data) + return CAIRO_STATUS_SUCCESS; + + surface->mapped_data = gbm_bo_map (surface->bo, + 0, 0, + surface->width, + surface->height, + GBM_BO_TRANSFER_READ_WRITE, + &map_stride, + &surface->map_data); + if (surface->mapped_data == NULL) + return _cairo_error (CAIRO_STATUS_NO_MEMORY); + + surface->stride = (int) map_stride; + return CAIRO_STATUS_SUCCESS; +} + +static void +_cairo_gbm_surface_unmap (cairo_gbm_surface_t *surface) +{ + if (surface->mapped_data == NULL) + return; + + gbm_bo_unmap (surface->bo, surface->map_data); + surface->map_data = NULL; + surface->mapped_data = NULL; +} + +static cairo_image_surface_t * +_cairo_gbm_surface_ensure_image (cairo_gbm_surface_t *surface) +{ + cairo_status_t status; + cairo_format_t format; + pixman_format_code_t pixman_format; + + if (surface->image) + return surface->image; + + status = _cairo_gbm_surface_map (surface); + if (unlikely (status)) + return (cairo_image_surface_t *) + _cairo_surface_create_in_error (status); + + format = _drm_format_to_cairo_format (surface->drm_format); + pixman_format = _cairo_format_to_pixman_format_code (format); + + surface->image = (cairo_image_surface_t *) + _cairo_image_surface_create_with_pixman_format (surface->mapped_data, + pixman_format, + surface->width, + surface->height, + surface->stride); + if (unlikely (surface->image->base.status)) + return surface->image; + + surface->image->base.is_clear = surface->base.is_clear; + + return surface->image; +} + +static void +_cairo_gbm_surface_discard_image (cairo_gbm_surface_t *surface) +{ + if (surface->image == NULL) + return; + + cairo_surface_finish (&surface->image->base); + cairo_surface_destroy (&surface->image->base); + surface->image = NULL; +} + +static cairo_status_t +_cairo_gbm_surface_finish (void *abstract_surface) +{ + cairo_gbm_surface_t *surface = abstract_surface; + + _cairo_gbm_surface_discard_image (surface); + _cairo_gbm_surface_unmap (surface); + + if (surface->bo && surface->owns_bo) { + gbm_bo_destroy (surface->bo); + surface->bo = NULL; + } + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_surface_t * +_cairo_gbm_surface_create_similar (void *abstract_surface, + cairo_content_t content, + int width, + int height) +{ + cairo_gbm_surface_t *other = abstract_surface; + cairo_format_t format; + + format = _cairo_format_from_content (content); + + return cairo_gbm_surface_create (other->gbm_device, format, width, height); +} + +static cairo_image_surface_t * +_cairo_gbm_surface_map_to_image (void *abstract_surface, + const cairo_rectangle_int_t *extents) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + cairo_surface_t *sub; + uint8_t *data; + pixman_format_code_t pixman_format; + cairo_format_t format; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image; + + if (extents->x == 0 && + extents->y == 0 && + extents->width == surface->width && + extents->height == surface->height) + { + return (cairo_image_surface_t *) + cairo_surface_reference (&image->base); + } + + format = _drm_format_to_cairo_format (surface->drm_format); + pixman_format = _cairo_format_to_pixman_format_code (format); + + data = surface->mapped_data; + data += extents->y * surface->stride; + data += extents->x * (PIXMAN_FORMAT_BPP (pixman_format) / 8); + + sub = _cairo_image_surface_create_with_pixman_format (data, + pixman_format, + extents->width, + extents->height, + surface->stride); + + cairo_surface_set_device_offset (sub, -extents->x, -extents->y); + return (cairo_image_surface_t *) sub; +} + +static cairo_int_status_t +_cairo_gbm_surface_unmap_image (void *abstract_surface, + cairo_image_surface_t *image) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *own_image; + + own_image = _cairo_gbm_surface_ensure_image (surface); + if (&image->base != &own_image->base) { + cairo_surface_finish (&image->base); + cairo_surface_destroy (&image->base); + } + + return CAIRO_INT_STATUS_SUCCESS; +} + +static cairo_surface_t * +_cairo_gbm_surface_source (void *abstract_surface, + cairo_rectangle_int_t *extents) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return &image->base; + + if (extents) { + extents->x = extents->y = 0; + extents->width = surface->width; + extents->height = surface->height; + } + + return &image->base; +} + +static cairo_status_t +_cairo_gbm_surface_acquire_source_image (void *abstract_surface, + cairo_image_surface_t **image_out, + void **image_extra) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + *image_out = image; + *image_extra = NULL; + return CAIRO_STATUS_SUCCESS; +} + +static void +_cairo_gbm_surface_release_source_image (void *abstract_surface, + cairo_image_surface_t *image, + void *image_extra) +{ +} + +static cairo_bool_t +_cairo_gbm_surface_get_extents (void *abstract_surface, + cairo_rectangle_int_t *rectangle) +{ + cairo_gbm_surface_t *surface = abstract_surface; + + rectangle->x = 0; + rectangle->y = 0; + rectangle->width = surface->width; + rectangle->height = surface->height; + + return TRUE; +} + +static void +_cairo_gbm_surface_get_font_options (void *abstract_surface, + cairo_font_options_t *options) +{ + _cairo_font_options_init_default (options); + cairo_font_options_set_hint_metrics (options, CAIRO_HINT_METRICS_ON); + _cairo_font_options_set_round_glyph_positions (options, CAIRO_ROUND_GLYPH_POS_ON); +} + +static cairo_status_t +_cairo_gbm_surface_flush (void *abstract_surface, unsigned flags) +{ + cairo_gbm_surface_t *surface = abstract_surface; + + if (flags) + return CAIRO_STATUS_SUCCESS; + + _cairo_gbm_surface_discard_image (surface); + _cairo_gbm_surface_unmap (surface); + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_status_t +_cairo_gbm_surface_mark_dirty (void *abstract_surface, + int x, int y, + int width, int height) +{ + return CAIRO_STATUS_SUCCESS; +} + +static cairo_int_status_t +_cairo_gbm_surface_paint (void *abstract_surface, + cairo_operator_t op, + const cairo_pattern_t *source, + const cairo_clip_t *clip) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + return _cairo_image_surface_paint (&image->base, op, source, clip); +} + +static cairo_int_status_t +_cairo_gbm_surface_mask (void *abstract_surface, + cairo_operator_t op, + const cairo_pattern_t *source, + const cairo_pattern_t *mask, + const cairo_clip_t *clip) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + return _cairo_image_surface_mask (&image->base, op, source, mask, clip); +} + +static cairo_int_status_t +_cairo_gbm_surface_stroke (void *abstract_surface, + cairo_operator_t op, + const cairo_pattern_t *source, + const cairo_path_fixed_t *path, + const cairo_stroke_style_t *style, + const cairo_matrix_t *ctm, + const cairo_matrix_t *ctm_inverse, + double tolerance, + cairo_antialias_t antialias, + const cairo_clip_t *clip) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + return _cairo_image_surface_stroke (&image->base, op, source, + path, style, + ctm, ctm_inverse, + tolerance, antialias, clip); +} + +static cairo_int_status_t +_cairo_gbm_surface_fill (void *abstract_surface, + cairo_operator_t op, + const cairo_pattern_t *source, + const cairo_path_fixed_t *path, + cairo_fill_rule_t fill_rule, + double tolerance, + cairo_antialias_t antialias, + const cairo_clip_t *clip) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + return _cairo_image_surface_fill (&image->base, op, source, + path, fill_rule, + tolerance, antialias, clip); +} + +static cairo_int_status_t +_cairo_gbm_surface_glyphs (void *abstract_surface, + cairo_operator_t op, + const cairo_pattern_t *source, + cairo_glyph_t *glyphs, + int num_glyphs, + cairo_scaled_font_t *scaled_font, + const cairo_clip_t *clip) +{ + cairo_gbm_surface_t *surface = abstract_surface; + cairo_image_surface_t *image; + + image = _cairo_gbm_surface_ensure_image (surface); + if (unlikely (image->base.status)) + return image->base.status; + + return _cairo_image_surface_glyphs (&image->base, op, source, + glyphs, num_glyphs, + scaled_font, clip); +} + +static const cairo_surface_backend_t cairo_gbm_surface_backend = { + CAIRO_SURFACE_TYPE_GBM, + _cairo_gbm_surface_finish, + + _cairo_default_context_create, + + _cairo_gbm_surface_create_similar, + NULL, /* create_similar_image */ + _cairo_gbm_surface_map_to_image, + _cairo_gbm_surface_unmap_image, + + _cairo_gbm_surface_source, + _cairo_gbm_surface_acquire_source_image, + _cairo_gbm_surface_release_source_image, + NULL, /* snapshot */ + + NULL, /* copy_page */ + NULL, /* show_page */ + + _cairo_gbm_surface_get_extents, + _cairo_gbm_surface_get_font_options, + + _cairo_gbm_surface_flush, + _cairo_gbm_surface_mark_dirty, + + _cairo_gbm_surface_paint, + _cairo_gbm_surface_mask, + _cairo_gbm_surface_stroke, + _cairo_gbm_surface_fill, + NULL, /* fill_stroke */ + _cairo_gbm_surface_glyphs, +}; + +/** + * cairo_gbm_surface_create: + * @device: a GBM device obtained from gbm_create_device() + * @format: the format of pixels in the surface + * @width: width of the surface, in pixels + * @height: height of the surface, in pixels + * + * Creates a GBM surface of the specified format and dimensions. The + * buffer is allocated via GBM with a linear modifier, making it + * suitable for CPU rendering while remaining directly importable by a + * GPU compositor through its DMA-BUF file descriptor. + * + * Not all #cairo_format_t values have a DRM equivalent. The supported + * formats are: %CAIRO_FORMAT_ARGB32, %CAIRO_FORMAT_RGB24, + * %CAIRO_FORMAT_RGB30, %CAIRO_FORMAT_RGB16_565, and %CAIRO_FORMAT_A8. + * + * Return value: a pointer to the newly created surface. The caller + * owns the surface and should call cairo_surface_destroy() when done + * with it. + * + * This function always returns a valid pointer, but it will return a + * pointer to a "nil" surface if an error occurs. You can use + * cairo_surface_status() to check for this. + * + * Since: 1.18 + **/ +cairo_surface_t * +cairo_gbm_surface_create (struct gbm_device *device, + cairo_format_t format, + int width, + int height) +{ + cairo_gbm_surface_t *surface; + uint32_t drm_format; + struct gbm_bo *bo; + cairo_content_t content; + + if (device == NULL) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_NULL_POINTER)); + + if (width <= 0 || height <= 0) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_INVALID_SIZE)); + + if (! CAIRO_FORMAT_VALID (format)) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_INVALID_FORMAT)); + + drm_format = _cairo_format_to_drm_format (format); + if (drm_format == 0) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_INVALID_FORMAT)); + + bo = gbm_bo_create_with_modifiers2 (device, + width, height, + drm_format, + (const uint64_t[]){ DRM_FORMAT_MOD_LINEAR }, + 1, + GBM_BO_USE_LINEAR | GBM_BO_USE_RENDERING); + if (bo == NULL) { + bo = gbm_bo_create (device, + width, height, + drm_format, + GBM_BO_USE_LINEAR | GBM_BO_USE_RENDERING); + } + if (bo == NULL) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_NO_MEMORY)); + + surface = _cairo_calloc (sizeof (cairo_gbm_surface_t)); + if (unlikely (surface == NULL)) { + gbm_bo_destroy (bo); + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_NO_MEMORY)); + } + + content = _cairo_content_from_format (format); + + _cairo_surface_init (&surface->base, + &cairo_gbm_surface_backend, + NULL, /* device */ + content, + FALSE); /* is_vector */ + + surface->gbm_device = device; + surface->bo = bo; + surface->owns_bo = TRUE; + surface->drm_format = drm_format; + surface->width = width; + surface->height = height; + surface->stride = 0; + surface->map_data = NULL; + surface->mapped_data = NULL; + surface->image = NULL; + + return &surface->base; +} + +/** + * cairo_gbm_surface_create_for_bo: + * @bo: an existing GBM buffer object + * @format: the #cairo_format_t matching the bo's pixel layout + * + * Wraps an existing GBM buffer object as a cairo surface. The caller + * retains ownership of @bo and must ensure it outlives the returned + * surface. The surface will not destroy the buffer object when finished. + * + * Return value: a pointer to the newly created surface. The caller + * owns the surface and should call cairo_surface_destroy() when done + * with it. + * + * Since: 1.18 + **/ +cairo_surface_t * +cairo_gbm_surface_create_for_bo (struct gbm_bo *bo, + cairo_format_t format) +{ + cairo_gbm_surface_t *surface; + uint32_t drm_format; + cairo_content_t content; + + if (bo == NULL) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_NULL_POINTER)); + + if (! CAIRO_FORMAT_VALID (format)) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_INVALID_FORMAT)); + + drm_format = _cairo_format_to_drm_format (format); + if (drm_format == 0) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_INVALID_FORMAT)); + + surface = _cairo_calloc (sizeof (cairo_gbm_surface_t)); + if (unlikely (surface == NULL)) + return _cairo_surface_create_in_error (_cairo_error (CAIRO_STATUS_NO_MEMORY)); + + content = _cairo_content_from_format (format); + + _cairo_surface_init (&surface->base, + &cairo_gbm_surface_backend, + NULL, /* device */ + content, + FALSE); /* is_vector */ + + surface->gbm_device = gbm_bo_get_device (bo); + surface->bo = bo; + surface->owns_bo = FALSE; + surface->drm_format = drm_format; + surface->width = gbm_bo_get_width (bo); + surface->height = gbm_bo_get_height (bo); + surface->stride = 0; + surface->map_data = NULL; + surface->mapped_data = NULL; + surface->image = NULL; + + return &surface->base; +} + +/** + * cairo_gbm_surface_get_bo: + * @surface: a GBM surface + * + * Returns the underlying #gbm_bo associated with this surface. + * + * Return value: the GBM buffer object, or %NULL if the surface is not + * a GBM surface or is in an error state. + * + * Since: 1.18 + **/ +struct gbm_bo * +cairo_gbm_surface_get_bo (cairo_surface_t *surface) +{ + cairo_gbm_surface_t *gbm_surface = (cairo_gbm_surface_t *) surface; + + if (surface->backend != &cairo_gbm_surface_backend) + return NULL; + + if (unlikely (surface->status)) + return NULL; + + if (unlikely (surface->finished)) + return NULL; + + return gbm_surface->bo; +} + +/** + * cairo_gbm_surface_get_dma_buf_fd: + * @surface: a GBM surface + * + * Obtains a DMA-BUF file descriptor for the surface's backing buffer. + * This fd can be imported by an EGL/Vulkan compositor for zero-copy + * texture access. + * + * The caller takes ownership of the returned fd and must close() it + * when no longer needed. + * + * Return value: a DMA-BUF file descriptor, or -1 on error. The caller + * must close() the fd when done. + * + * Since: 1.18 + **/ +int +cairo_gbm_surface_get_dma_buf_fd (cairo_surface_t *surface) +{ + cairo_gbm_surface_t *gbm_surface = (cairo_gbm_surface_t *) surface; + + if (surface->backend != &cairo_gbm_surface_backend) + return -1; + + if (unlikely (surface->status)) + return -1; + + if (unlikely (surface->finished)) + return -1; + + if (unlikely (gbm_surface->bo == NULL)) + return -1; + + return gbm_bo_get_fd (gbm_surface->bo); +} diff --git a/src/cairo-gbm.h b/src/cairo-gbm.h new file mode 100644 index 000000000..3c0d148af --- /dev/null +++ b/src/cairo-gbm.h @@ -0,0 +1,93 @@ +/* cairo - a vector graphics library with display and print output + * + * Copyright © 2026 Red Hat, Inc. + * + * Author: Alberto Ruiz + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + * The Original Code is the cairo graphics library. + */ + +#ifndef CAIRO_GBM_H +#define CAIRO_GBM_H + +#include "cairo.h" + +#if CAIRO_HAS_GBM_SURFACE + +#include + +CAIRO_BEGIN_DECLS + +/** + * SECTION:cairo-gbm + * @Title: GBM Surfaces + * @Short_Description: Rendering to GBM (Generic Buffer Manager) buffers + * @See_Also: #cairo_surface_t + * + * The GBM surface provides the ability to render to memory buffers + * allocated via GBM (Generic Buffer Manager). These buffers can be + * shared with the GPU via DMA-BUF file descriptors, enabling zero-copy + * handoff to GPU-based compositors such as those used by GTK4 and + * Mutter. + * + * Rendering is performed by pixman on the CPU side, but the backing + * memory is GPU-importable, avoiding the texture upload that would + * otherwise be required when the compositor needs to display the + * content. + */ + +/** + * CAIRO_HAS_GBM_SURFACE: + * + * Defined if the GBM surface backend is available. + * This macro can be used to conditionally compile backend-specific code. + * + * Since: 1.18 + */ + +cairo_public cairo_surface_t * +cairo_gbm_surface_create (struct gbm_device *device, + cairo_format_t format, + int width, + int height); + +cairo_public cairo_surface_t * +cairo_gbm_surface_create_for_bo (struct gbm_bo *bo, + cairo_format_t format); + +cairo_public struct gbm_bo * +cairo_gbm_surface_get_bo (cairo_surface_t *surface); + +cairo_public int +cairo_gbm_surface_get_dma_buf_fd (cairo_surface_t *surface); + +CAIRO_END_DECLS + +#else /* CAIRO_HAS_GBM_SURFACE */ +# error Cairo was not compiled with support for the GBM backend +#endif /* CAIRO_HAS_GBM_SURFACE */ + +#endif /* CAIRO_GBM_H */ diff --git a/src/cairo-pattern.c b/src/cairo-pattern.c index 4d713e6c6..5743ec300 100644 --- a/src/cairo-pattern.c +++ b/src/cairo-pattern.c @@ -4763,6 +4763,7 @@ _cairo_debug_print_surface_pattern (FILE *file, case CAIRO_SURFACE_TYPE_SKIA: s = "skia"; break; /* Deprecated */ case CAIRO_SURFACE_TYPE_SUBSURFACE: s = "subsurface"; break; case CAIRO_SURFACE_TYPE_COGL: s = "cogl"; break; + case CAIRO_SURFACE_TYPE_GBM: s = "gbm"; break; default: s = "invalid"; ASSERT_NOT_REACHED; break; } fprintf (file, " surface type: %s\n", s); diff --git a/src/cairo.h b/src/cairo.h index d53cc3d09..20400236b 100644 --- a/src/cairo.h +++ b/src/cairo.h @@ -2535,6 +2535,8 @@ cairo_surface_status (cairo_surface_t *surface); * cairo_surface_create_for_rectangle(), since 1.10 * @CAIRO_SURFACE_TYPE_COGL: This surface is of type Cogl, since 1.12, deprecated 1.18 * (Cogl support have been removed, this surface type will never be set by cairo) + * @CAIRO_SURFACE_TYPE_GBM: The surface is of type GBM (Generic Buffer Manager), + * since 1.18 * * #cairo_surface_type_t is used to describe the type of a given * surface. The surface types are also known as "backends" or "surface @@ -2585,7 +2587,8 @@ typedef enum _cairo_surface_type { CAIRO_SURFACE_TYPE_XML, CAIRO_SURFACE_TYPE_SKIA, CAIRO_SURFACE_TYPE_SUBSURFACE, - CAIRO_SURFACE_TYPE_COGL + CAIRO_SURFACE_TYPE_COGL, + CAIRO_SURFACE_TYPE_GBM } cairo_surface_type_t; cairo_public cairo_surface_type_t diff --git a/src/meson.build b/src/meson.build index ac06ac61a..5af266822 100644 --- a/src/meson.build +++ b/src/meson.build @@ -198,6 +198,9 @@ cairo_feature_sources = { 'cairo-tee': [ 'cairo-tee-surface.c', ], + 'cairo-gbm': [ + 'cairo-gbm-surface.c', + ], } cairo_feature_headers = { @@ -215,6 +218,7 @@ cairo_feature_headers = { 'cairo-gl': ['cairo-gl.h'], 'cairo-script': ['cairo-script.h'], 'cairo-tee': ['cairo-tee.h'], + 'cairo-gbm': ['cairo-gbm.h'], 'cairo-vg': ['cairo-vg.h'], } diff --git a/test/api-special-cases.c b/test/api-special-cases.c index d926aa652..197411e5c 100644 --- a/test/api-special-cases.c +++ b/test/api-special-cases.c @@ -80,6 +80,9 @@ #if CAIRO_HAS_TEE_SURFACE #include #endif +#if CAIRO_HAS_GBM_SURFACE +#include +#endif #if CAIRO_HAS_XCB_SURFACE #include #endif @@ -1402,6 +1405,24 @@ test_cairo_svg_surface_restrict_to_version (cairo_surface_t *surface) #endif /* CAIRO_HAS_SVG_SURFACE */ +#if CAIRO_HAS_GBM_SURFACE + +static cairo_test_status_t +test_cairo_gbm_surface_get_bo (cairo_surface_t *surface) +{ + cairo_gbm_surface_get_bo (surface); + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_cairo_gbm_surface_get_dma_buf_fd (cairo_surface_t *surface) +{ + cairo_gbm_surface_get_dma_buf_fd (surface); + return CAIRO_TEST_SUCCESS; +} + +#endif /* CAIRO_HAS_GBM_SURFACE */ + #if CAIRO_HAS_XCB_SURFACE static cairo_test_status_t @@ -1659,6 +1680,10 @@ struct { #if CAIRO_HAS_SVG_SURFACE TEST (cairo_svg_surface_restrict_to_version, CAIRO_SURFACE_TYPE_SVG, TRUE), #endif +#if CAIRO_HAS_GBM_SURFACE + TEST (cairo_gbm_surface_get_bo, CAIRO_SURFACE_TYPE_GBM, FALSE), + TEST (cairo_gbm_surface_get_dma_buf_fd, CAIRO_SURFACE_TYPE_GBM, FALSE), +#endif #if CAIRO_HAS_XCB_SURFACE TEST (cairo_xcb_surface_set_size, CAIRO_SURFACE_TYPE_XCB, TRUE), TEST (cairo_xcb_surface_set_drawable, CAIRO_SURFACE_TYPE_XCB, TRUE), diff --git a/test/error-setters.c b/test/error-setters.c index ecbb78fde..85af8f50a 100644 --- a/test/error-setters.c +++ b/test/error-setters.c @@ -35,6 +35,9 @@ #if CAIRO_HAS_PS_SURFACE #include #endif +#if CAIRO_HAS_GBM_SURFACE +#include +#endif #if CAIRO_HAS_XCB_SURFACE #include #endif @@ -64,6 +67,11 @@ preamble (cairo_test_context_t *ctx) cairo_ps_surface_dsc_begin_page_setup (surface); #endif +#if CAIRO_HAS_GBM_SURFACE + cairo_gbm_surface_get_bo (surface); + cairo_gbm_surface_get_dma_buf_fd (surface); +#endif + #if CAIRO_HAS_XCB_SURFACE cairo_xcb_surface_set_size (surface, 0, 0); #endif diff --git a/test/gbm-surface-source.c b/test/gbm-surface-source.c new file mode 100644 index 000000000..4ab1902b2 --- /dev/null +++ b/test/gbm-surface-source.c @@ -0,0 +1,74 @@ +/* + * Copyright © 2026 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of + * Red Hat, Inc. not be used in advertising or publicity pertaining to + * distribution of the software without specific, written prior + * permission. Red Hat, Inc. makes no representations about the + * suitability of this software for any purpose. It is provided "as + * is" without express or implied warranty. + * + * RED HAT, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL RED HAT, INC. BE LIABLE FOR ANY SPECIAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Alberto Ruiz + */ + +#include "cairo-test.h" + +#include + +#include +#include +#include +#include + +#include "surface-source.c" + +static cairo_surface_t * +create_source_surface (int size) +{ +#if CAIRO_HAS_GBM_SURFACE + int fd; + struct gbm_device *dev; + cairo_surface_t *surface; + + fd = open ("/dev/dri/renderD128", O_RDWR); + if (fd < 0) + return NULL; + + dev = gbm_create_device (fd); + if (dev == NULL) { + close (fd); + return NULL; + } + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, size, size); + if (cairo_surface_status (surface)) { + cairo_surface_destroy (surface); + gbm_device_destroy (dev); + close (fd); + return NULL; + } + + return surface; +#else + return NULL; +#endif +} + +CAIRO_TEST (gbm_surface_source, + "Test using a GBM surface as the source", + "source, gbm", /* keywords */ + NULL, /* requirements */ + SIZE, SIZE, + preamble, draw) diff --git a/test/gbm-surface.c b/test/gbm-surface.c new file mode 100644 index 000000000..e5f9d656d --- /dev/null +++ b/test/gbm-surface.c @@ -0,0 +1,438 @@ +/* + * Copyright © 2026 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software + * and its documentation for any purpose is hereby granted without + * fee, provided that the above copyright notice appear in all copies + * and that both that copyright notice and this permission notice + * appear in supporting documentation, and that the name of + * Red Hat, Inc. not be used in advertising or publicity pertaining to + * distribution of the software without specific, written prior + * permission. Red Hat, Inc. makes no representations about the + * suitability of this software for any purpose. It is provided "as + * is" without express or implied warranty. + * + * RED HAT, INC. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL RED HAT, INC. BE LIABLE FOR ANY SPECIAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Author: Alberto Ruiz + */ + +#include "cairo-test.h" + +#include + +#include +#include +#include + +static struct gbm_device * +open_gbm_device (int *fd_out) +{ + int fd; + struct gbm_device *dev; + + fd = open ("/dev/dri/renderD128", O_RDWR); + if (fd < 0) + return NULL; + + dev = gbm_create_device (fd); + if (dev == NULL) { + close (fd); + return NULL; + } + + *fd_out = fd; + return dev; +} + +static cairo_test_status_t +test_create_valid (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Failed to create ARGB32 GBM surface\n"); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + if (cairo_surface_get_type (surface) != CAIRO_SURFACE_TYPE_GBM) { + cairo_test_log (ctx, "Surface type is not CAIRO_SURFACE_TYPE_GBM\n"); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_RGB24, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Failed to create RGB24 GBM surface\n"); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_create_error_paths (cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + + /* _cairo_surface_create_in_error() has no dedicated nil surface for + * CAIRO_STATUS_NULL_POINTER, so it falls through to NO_MEMORY. */ + surface = cairo_gbm_surface_create (NULL, CAIRO_FORMAT_ARGB32, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_NO_MEMORY) { + cairo_test_log (ctx, "NULL device: expected NO_MEMORY, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (NULL, CAIRO_FORMAT_INVALID, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_NO_MEMORY) { + cairo_test_log (ctx, "NULL device + invalid format: expected NO_MEMORY, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_create_invalid_format (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_INVALID, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_FORMAT) { + cairo_test_log (ctx, "INVALID format: expected INVALID_FORMAT, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_A1, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_FORMAT) { + cairo_test_log (ctx, "A1 format (no DRM equivalent): expected INVALID_FORMAT, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_RGB96F, 100, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_FORMAT) { + cairo_test_log (ctx, "RGB96F format (no DRM equivalent): expected INVALID_FORMAT, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_create_invalid_size (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, 0, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_SIZE) { + cairo_test_log (ctx, "Zero width: expected INVALID_SIZE, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, 100, 0); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_SIZE) { + cairo_test_log (ctx, "Zero height: expected INVALID_SIZE, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, -1, 100); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_SIZE) { + cairo_test_log (ctx, "Negative width: expected INVALID_SIZE, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_create_for_bo (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + struct gbm_bo *bo; + + bo = gbm_bo_create (dev, 64, 64, GBM_FORMAT_ARGB8888, + GBM_BO_USE_LINEAR | GBM_BO_USE_RENDERING); + if (bo == NULL) { + cairo_test_log (ctx, "Failed to create gbm_bo for test\n"); + return CAIRO_TEST_FAILURE; + } + + surface = cairo_gbm_surface_create_for_bo (bo, CAIRO_FORMAT_ARGB32); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Failed to create surface for bo\n"); + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + return CAIRO_TEST_FAILURE; + } + if (cairo_surface_get_type (surface) != CAIRO_SURFACE_TYPE_GBM) { + cairo_test_log (ctx, "create_for_bo: type is not GBM\n"); + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + return CAIRO_TEST_FAILURE; + } + + /* Verify get_bo returns the same bo */ + if (cairo_gbm_surface_get_bo (surface) != bo) { + cairo_test_log (ctx, "create_for_bo: get_bo returned wrong pointer\n"); + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + return CAIRO_TEST_FAILURE; + } + + /* Destroy surface — bo should survive (caller ownership) */ + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_create_for_bo_error_paths (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + struct gbm_bo *bo; + + surface = cairo_gbm_surface_create_for_bo (NULL, CAIRO_FORMAT_ARGB32); + if (cairo_surface_status (surface) != CAIRO_STATUS_NO_MEMORY) { + cairo_test_log (ctx, "NULL bo: expected NO_MEMORY, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + + bo = gbm_bo_create (dev, 64, 64, GBM_FORMAT_ARGB8888, + GBM_BO_USE_LINEAR | GBM_BO_USE_RENDERING); + if (bo == NULL) { + cairo_test_log (ctx, "Failed to create gbm_bo for error test\n"); + return CAIRO_TEST_FAILURE; + } + + surface = cairo_gbm_surface_create_for_bo (bo, CAIRO_FORMAT_INVALID); + if (cairo_surface_status (surface) != CAIRO_STATUS_INVALID_FORMAT) { + cairo_test_log (ctx, "Invalid format with valid bo: expected INVALID_FORMAT, got %d\n", + cairo_surface_status (surface)); + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + return CAIRO_TEST_FAILURE; + } + cairo_surface_destroy (surface); + gbm_bo_destroy (bo); + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_getters_wrong_type (cairo_test_context_t *ctx) +{ + cairo_surface_t *image; + + image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 10, 10); + + if (cairo_gbm_surface_get_bo (image) != NULL) { + cairo_test_log (ctx, "get_bo on image surface should return NULL\n"); + cairo_surface_destroy (image); + return CAIRO_TEST_FAILURE; + } + if (cairo_gbm_surface_get_dma_buf_fd (image) != -1) { + cairo_test_log (ctx, "get_dma_buf_fd on image surface should return -1\n"); + cairo_surface_destroy (image); + return CAIRO_TEST_FAILURE; + } + + cairo_surface_destroy (image); + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_dma_buf_fd (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + int fd; + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, 64, 64); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Failed to create surface for fd test\n"); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + + fd = cairo_gbm_surface_get_dma_buf_fd (surface); + if (fd < 0) { + cairo_test_log (ctx, "get_dma_buf_fd returned %d\n", fd); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + close (fd); + + cairo_surface_destroy (surface); + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_draw_and_flush (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + cairo_surface_t *surface; + cairo_t *cr; + int fd; + + surface = cairo_gbm_surface_create (dev, CAIRO_FORMAT_ARGB32, 64, 64); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Failed to create surface for draw test\n"); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + + cr = cairo_create (surface); + cairo_set_source_rgb (cr, 1, 0, 0); + cairo_rectangle (cr, 0, 0, 32, 32); + cairo_fill (cr); + if (cairo_status (cr) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, "Drawing failed: %d\n", cairo_status (cr)); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + cairo_destroy (cr); + + cairo_surface_flush (surface); + + fd = cairo_gbm_surface_get_dma_buf_fd (surface); + if (fd < 0) { + cairo_test_log (ctx, "get_dma_buf_fd after flush returned %d\n", fd); + cairo_surface_destroy (surface); + return CAIRO_TEST_FAILURE; + } + close (fd); + + cairo_surface_destroy (surface); + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +test_all_supported_formats (struct gbm_device *dev, cairo_test_context_t *ctx) +{ + static const cairo_format_t formats[] = { + CAIRO_FORMAT_ARGB32, + CAIRO_FORMAT_RGB24, + CAIRO_FORMAT_RGB30, + CAIRO_FORMAT_RGB16_565, + CAIRO_FORMAT_A8, + }; + unsigned int i; + + for (i = 0; i < sizeof (formats) / sizeof (formats[0]); i++) { + cairo_surface_t *surface; + + surface = cairo_gbm_surface_create (dev, formats[i], 64, 64); + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) { + cairo_test_log (ctx, + "Format %d: create failed with status %d " + "(GPU may not support this DRM format, skipping)\n", + formats[i], cairo_surface_status (surface)); + cairo_surface_destroy (surface); + continue; + } + cairo_surface_destroy (surface); + } + + return CAIRO_TEST_SUCCESS; +} + +static cairo_test_status_t +preamble (cairo_test_context_t *ctx) +{ + struct gbm_device *dev; + int drm_fd; + cairo_test_status_t status; + + status = test_create_error_paths (ctx); + if (status != CAIRO_TEST_SUCCESS) + return status; + + status = test_getters_wrong_type (ctx); + if (status != CAIRO_TEST_SUCCESS) + return status; + + dev = open_gbm_device (&drm_fd); + if (dev == NULL) { + cairo_test_log (ctx, "No DRM render node available, skipping\n"); + return CAIRO_TEST_UNTESTED; + } + + status = test_create_valid (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_create_invalid_format (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_create_invalid_size (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_create_for_bo (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_create_for_bo_error_paths (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_dma_buf_fd (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_draw_and_flush (dev, ctx); + if (status != CAIRO_TEST_SUCCESS) + goto CLEANUP; + + status = test_all_supported_formats (dev, ctx); + +CLEANUP: + gbm_device_destroy (dev); + close (drm_fd); + return status; +} + +CAIRO_TEST (gbm_surface, + "Test GBM surface API: creation, error handling, getters, draw, flush, DMA-BUF fd", + "gbm, api", /* keywords */ + NULL, /* requirements */ + 0, 0, + preamble, NULL) diff --git a/test/meson.build b/test/meson.build index 3b460afc5..639cd39b4 100644 --- a/test/meson.build +++ b/test/meson.build @@ -487,6 +487,11 @@ test_xlib_xrender_sources = [ 'get-xrender-format.c', ] +test_gbm_sources = [ + 'gbm-surface.c', + 'gbm-surface-source.c', +] + test_multi_page_sources = [ 'multi-page.c', 'mime-unique-id.c', @@ -586,6 +591,10 @@ if feature_conf.get('CAIRO_HAS_SVG_SURFACE', 0) == 1 add_fallback_resolution = true endif +if feature_conf.get('CAIRO_HAS_GBM_SURFACE', 0) == 1 + test_sources += test_gbm_sources +endif + if feature_conf.get('CAIRO_HAS_XCB_SURFACE', 0) == 1 test_sources += test_xcb_sources endif From a1cef62f43046df11f3eb665c1d72e70c8dda83c Mon Sep 17 00:00:00 2001 From: Alberto Ruiz Date: Mon, 23 Mar 2026 20:22:51 +0000 Subject: [PATCH 2/2] Add GBM vs image-surface EGL benchmark example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add examples/gbm-egl-benchmark.c, a headless benchmark that compares the display-path cost of uploading Cairo-rendered content to a GL texture via two methods: Image surface: glTexSubImage2D (CPU → GPU memcpy) GBM surface: EGL DMA-BUF import (zero-copy) The benchmark renders an animated Cairo scene at the requested resolution, then measures only the upload/import step per frame. It runs entirely on a DRM render node with no window system required. Typical results on AMD Radeon RX 580: 1080p (7.9 MB/frame): ~24x faster with GBM (1.42 ms → 0.06 ms) 4K (31.6 MB/frame): ~80x faster with GBM (5.18 ms → 0.07 ms) Built automatically when gbm, egl, and glesv2 are all available. --- examples/gbm-egl-benchmark.c | 915 +++++++++++++++++++++++++++++++++++ examples/meson.build | 46 ++ meson.build | 2 + 3 files changed, 963 insertions(+) create mode 100644 examples/gbm-egl-benchmark.c create mode 100644 examples/meson.build diff --git a/examples/gbm-egl-benchmark.c b/examples/gbm-egl-benchmark.c new file mode 100644 index 000000000..47c369f8f --- /dev/null +++ b/examples/gbm-egl-benchmark.c @@ -0,0 +1,915 @@ +/* gbm-egl-benchmark.c — Compare image-surface vs GBM-surface display paths + * + * Renders an animated scene with Cairo onto both an image surface and a + * GBM surface, then uploads each to a GL texture: + * + * Image path: glTexSubImage2D (CPU memcpy to GPU) + * GBM path: EGL DMA-BUF import (zero-copy) + * + * Headless mode (default): + * Prints per-frame timing for the upload/import step. + * No window system required — runs entirely on a DRM render node. + * + * Windowed mode (--window): + * Opens a Wayland window and renders the scene live using the GBM + * zero-copy path, with a timing overlay comparing both methods. + * + * Build: meson builds this automatically when the dependencies are met. + * Run: ./gbm-egl-benchmark [--window] [--frames N] [--width W] [--height H] + * + * Copyright © 2026 Red Hat, Inc. + * Author: Alberto Ruiz + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAS_WAYLAND +#include +#include +#include "xdg-shell-client-protocol.h" +#endif + +/* ------------------------------------------------------------------ */ +/* Timing helpers */ +/* ------------------------------------------------------------------ */ + +static double +now_ms (void) +{ + struct timespec ts; + clock_gettime (CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; +} + +/* ------------------------------------------------------------------ */ +/* EGL helpers */ +/* ------------------------------------------------------------------ */ + +static PFNEGLCREATEIMAGEKHRPROC create_image; +static PFNEGLDESTROYIMAGEKHRPROC destroy_image; +static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC image_target_texture; + +static void +load_egl_procs (void) +{ + create_image = (PFNEGLCREATEIMAGEKHRPROC) + eglGetProcAddress ("eglCreateImageKHR"); + destroy_image = (PFNEGLDESTROYIMAGEKHRPROC) + eglGetProcAddress ("eglDestroyImageKHR"); + image_target_texture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) + eglGetProcAddress ("glEGLImageTargetTexture2DOES"); +} + +/* ------------------------------------------------------------------ */ +/* EGL bootstrap — headless (GBM platform, surfaceless) */ +/* ------------------------------------------------------------------ */ + +static int +init_egl_headless (struct gbm_device *gbm_dev, + EGLDisplay *out_dpy, EGLContext *out_ctx) +{ + static const EGLint config_attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + static const EGLint ctx_attribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE + }; + PFNEGLGETPLATFORMDISPLAYEXTPROC get_platform_display; + EGLDisplay dpy; + EGLConfig cfg; + EGLContext ctx; + EGLint n; + + get_platform_display = (PFNEGLGETPLATFORMDISPLAYEXTPROC) + eglGetProcAddress ("eglGetPlatformDisplayEXT"); + if (!get_platform_display) + return -1; + + dpy = get_platform_display (EGL_PLATFORM_GBM_KHR, gbm_dev, NULL); + if (dpy == EGL_NO_DISPLAY) + return -1; + if (!eglInitialize (dpy, NULL, NULL)) + return -1; + if (!eglBindAPI (EGL_OPENGL_ES_API)) + return -1; + if (!eglChooseConfig (dpy, config_attribs, &cfg, 1, &n) || n < 1) + return -1; + + ctx = eglCreateContext (dpy, cfg, EGL_NO_CONTEXT, ctx_attribs); + if (ctx == EGL_NO_CONTEXT) + return -1; + if (!eglMakeCurrent (dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx)) + return -1; + + load_egl_procs (); + if (!create_image || !destroy_image || !image_target_texture) + return -1; + + *out_dpy = dpy; + *out_ctx = ctx; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* GL shader helpers */ +/* ------------------------------------------------------------------ */ + +static GLuint +compile_shader (GLenum type, const char *src) +{ + GLuint s = glCreateShader (type); + GLint ok; + glShaderSource (s, 1, &src, NULL); + glCompileShader (s); + glGetShaderiv (s, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[512]; + glGetShaderInfoLog (s, sizeof log, NULL, log); + fprintf (stderr, "shader compile: %s\n", log); + } + return s; +} + +static GLuint +build_program (void) +{ + static const char *vert_src = + "attribute vec2 pos;\n" + "attribute vec2 uv_in;\n" + "varying vec2 uv;\n" + "void main() {\n" + " gl_Position = vec4(pos, 0.0, 1.0);\n" + " uv = uv_in;\n" + "}\n"; + static const char *frag_src = + "precision mediump float;\n" + "varying vec2 uv;\n" + "uniform sampler2D tex;\n" + "void main() {\n" + " vec4 c = texture2D(tex, uv);\n" + " gl_FragColor = vec4(c.b, c.g, c.r, c.a);\n" /* BGRA→RGBA swizzle */ + "}\n"; + + GLuint vs = compile_shader (GL_VERTEX_SHADER, vert_src); + GLuint fs = compile_shader (GL_FRAGMENT_SHADER, frag_src); + GLuint prog = glCreateProgram (); + GLint ok; + + glAttachShader (prog, vs); + glAttachShader (prog, fs); + glBindAttribLocation (prog, 0, "pos"); + glBindAttribLocation (prog, 1, "uv_in"); + glLinkProgram (prog); + glGetProgramiv (prog, GL_LINK_STATUS, &ok); + if (!ok) { + char log[512]; + glGetProgramInfoLog (prog, sizeof log, NULL, log); + fprintf (stderr, "program link: %s\n", log); + } + glDeleteShader (vs); + glDeleteShader (fs); + return prog; +} + +static void +draw_quad (GLuint prog, GLuint tex, + float x0, float y0, float x1, float y1) +{ + float verts[] = { + x0, y0, 0, 1, + x1, y0, 1, 1, + x0, y1, 0, 0, + x1, y1, 1, 0, + }; + + glUseProgram (prog); + glActiveTexture (GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, tex); + glUniform1i (glGetUniformLocation (prog, "tex"), 0); + + glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 16, verts); + glEnableVertexAttribArray (0); + glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 16, verts + 2); + glEnableVertexAttribArray (1); + glDrawArrays (GL_TRIANGLE_STRIP, 0, 4); +} + +/* ------------------------------------------------------------------ */ +/* Animated Cairo scene */ +/* ------------------------------------------------------------------ */ + +static void +draw_frame (cairo_surface_t *surface, int width, int height, int frame) +{ + cairo_t *cr = cairo_create (surface); + double cx = width / 2.0; + double cy = height / 2.0; + double r = (width < height ? width : height) * 0.4; + double angle = frame * 0.05; + int i; + + cairo_pattern_t *bg = cairo_pattern_create_linear (0, 0, width, height); + cairo_pattern_add_color_stop_rgb (bg, 0.0, 0.05, 0.05, 0.15); + cairo_pattern_add_color_stop_rgb (bg, 1.0, 0.15, 0.05, 0.05); + cairo_set_source (cr, bg); + cairo_paint (cr); + cairo_pattern_destroy (bg); + + for (i = 0; i < 12; i++) { + double a = angle + i * M_PI / 6.0; + double hue = fmod (i / 12.0 + frame * 0.002, 1.0); + double red = fabs (hue * 6.0 - 3.0) - 1.0; + double green = 2.0 - fabs (hue * 6.0 - 2.0); + double blue = 2.0 - fabs (hue * 6.0 - 4.0); + if (red < 0) red = 0; + if (red > 1) red = 1; + if (green < 0) green = 0; + if (green > 1) green = 1; + if (blue < 0) blue = 0; + if (blue > 1) blue = 1; + + cairo_save (cr); + cairo_translate (cr, cx, cy); + cairo_rotate (cr, a); + cairo_set_source_rgba (cr, red, green, blue, 0.6); + cairo_set_line_width (cr, r * 0.08); + cairo_arc (cr, 0, 0, r * (0.5 + 0.3 * sin (angle * 2 + i)), + -0.3, 0.3); + cairo_stroke (cr); + cairo_restore (cr); + } + + double pulse = 0.15 + 0.08 * sin (frame * 0.1); + cairo_pattern_t *radial = cairo_pattern_create_radial ( + cx, cy, 0, cx, cy, r * pulse * 3); + cairo_pattern_add_color_stop_rgba (radial, 0.0, 1, 1, 1, 0.9); + cairo_pattern_add_color_stop_rgba (radial, 1.0, 1, 1, 1, 0.0); + cairo_set_source (cr, radial); + cairo_arc (cr, cx, cy, r * pulse * 3, 0, 2 * M_PI); + cairo_fill (cr); + cairo_pattern_destroy (radial); + + char buf[64]; + snprintf (buf, sizeof buf, "frame %d", frame); + cairo_set_source_rgb (cr, 1, 1, 1); + cairo_select_font_face (cr, "sans-serif", + CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size (cr, height * 0.04); + cairo_move_to (cr, 10, height - 10); + cairo_show_text (cr, buf); + + cairo_destroy (cr); +} + +/* ------------------------------------------------------------------ */ +/* Image-surface path: glTexSubImage2D upload */ +/* ------------------------------------------------------------------ */ + +static double +upload_image_surface (cairo_surface_t *surface, GLuint tex, + int width, int height) +{ + double t0, t1; + + cairo_surface_flush (surface); + glBindTexture (GL_TEXTURE_2D, tex); + + t0 = now_ms (); + glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, width, height, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, + cairo_image_surface_get_data (surface)); + glFinish (); + t1 = now_ms (); + + return t1 - t0; +} + +/* ------------------------------------------------------------------ */ +/* GBM-surface path: EGL DMA-BUF import */ +/* ------------------------------------------------------------------ */ + +static double +import_gbm_surface (EGLDisplay dpy, cairo_surface_t *surface, GLuint tex, + int width, int height) +{ + double t0, t1; + EGLImageKHR image; + struct gbm_bo *bo; + int stride, fd; + + cairo_surface_flush (surface); + + bo = cairo_gbm_surface_get_bo (surface); + stride = gbm_bo_get_stride (bo); + fd = cairo_gbm_surface_get_dma_buf_fd (surface); + if (fd < 0) + return -1; + + EGLint attribs[] = { + EGL_WIDTH, width, + EGL_HEIGHT, height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888, + EGL_DMA_BUF_PLANE0_FD_EXT, fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, stride, + EGL_NONE + }; + + glBindTexture (GL_TEXTURE_2D, tex); + + t0 = now_ms (); + image = create_image (dpy, EGL_NO_CONTEXT, + EGL_LINUX_DMA_BUF_EXT, NULL, attribs); + if (image == EGL_NO_IMAGE_KHR) { + close (fd); + return -1; + } + image_target_texture (GL_TEXTURE_2D, image); + glFinish (); + t1 = now_ms (); + + destroy_image (dpy, image); + close (fd); + + return t1 - t0; +} + +/* ------------------------------------------------------------------ */ +/* Headless benchmark */ +/* ------------------------------------------------------------------ */ + +static int +run_headless (struct gbm_device *gbm_dev, int width, int height, int frames) +{ + EGLDisplay egl_dpy; + EGLContext egl_ctx; + cairo_surface_t *img_surface, *gbm_surface; + GLuint tex_img, tex_gbm; + double sum_img = 0, sum_gbm = 0; + double max_img = 0, max_gbm = 0; + int i; + + if (init_egl_headless (gbm_dev, &egl_dpy, &egl_ctx) < 0) { + fprintf (stderr, "Failed to init headless EGL\n"); + return 1; + } + + printf ("EGL vendor: %s\n", eglQueryString (egl_dpy, EGL_VENDOR)); + printf ("GL renderer: %s\n\n", glGetString (GL_RENDERER)); + + img_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + width, height); + gbm_surface = cairo_gbm_surface_create (gbm_dev, CAIRO_FORMAT_ARGB32, + width, height); + + glGenTextures (1, &tex_img); + glBindTexture (GL_TEXTURE_2D, tex_img); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, NULL); + + glGenTextures (1, &tex_gbm); + glBindTexture (GL_TEXTURE_2D, tex_gbm); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + /* Warm up */ + draw_frame (img_surface, width, height, 0); + upload_image_surface (img_surface, tex_img, width, height); + draw_frame (gbm_surface, width, height, 0); + import_gbm_surface (egl_dpy, gbm_surface, tex_gbm, width, height); + + printf ("%5s %12s %12s %s\n", + "frame", "image (ms)", "gbm (ms)", "speedup"); + printf ("%5s %12s %12s %s\n", + "-----", "----------", "--------", "-------"); + + for (i = 0; i < frames; i++) { + double t_img, t_gbm; + + draw_frame (img_surface, width, height, i); + t_img = upload_image_surface (img_surface, tex_img, width, height); + + draw_frame (gbm_surface, width, height, i); + t_gbm = import_gbm_surface (egl_dpy, gbm_surface, tex_gbm, + width, height); + if (t_gbm < 0) + break; + + sum_img += t_img; + sum_gbm += t_gbm; + if (t_img > max_img) max_img = t_img; + if (t_gbm > max_gbm) max_gbm = t_gbm; + + if (i < 10 || (i + 1) % 10 == 0) + printf ("%5d %10.3f %10.3f %.1fx\n", + i, t_img, t_gbm, + t_gbm > 0 ? t_img / t_gbm : 0); + } + + printf ("\n=== Summary (%d frames at %dx%d) ===\n", frames, width, height); + printf (" image gbm\n"); + printf (" avg upload: %8.3f ms %8.3f ms\n", + sum_img / frames, sum_gbm / frames); + printf (" max upload: %8.3f ms %8.3f ms\n", max_img, max_gbm); + printf (" total: %8.1f ms %8.1f ms\n", sum_img, sum_gbm); + printf (" avg speedup: %.1fx\n", + sum_gbm > 0 ? sum_img / sum_gbm : 0); + + cairo_surface_destroy (img_surface); + cairo_surface_destroy (gbm_surface); + glDeleteTextures (1, &tex_img); + glDeleteTextures (1, &tex_gbm); + eglDestroyContext (egl_dpy, egl_ctx); + eglTerminate (egl_dpy); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Wayland windowed mode */ +/* ------------------------------------------------------------------ */ + +#ifdef HAS_WAYLAND + +static struct wl_display *wl_dpy; +static struct wl_compositor *wl_comp; +static struct wl_surface *wl_surf; +static struct xdg_wm_base *xdg_base; +static struct xdg_surface *xdg_surf; +static struct xdg_toplevel *xdg_top; +static int configured; +static int running = 1; + +static void xdg_wm_base_ping (void *data, struct xdg_wm_base *base, uint32_t serial) +{ + (void) data; + xdg_wm_base_pong (base, serial); +} + +static const struct xdg_wm_base_listener xdg_base_listener = { + .ping = xdg_wm_base_ping, +}; + +static void xdg_surface_configure (void *data, struct xdg_surface *surf, uint32_t serial) +{ + (void) data; + xdg_surface_ack_configure (surf, serial); + configured = 1; +} + +static const struct xdg_surface_listener xdg_surf_listener = { + .configure = xdg_surface_configure, +}; + +static void xdg_toplevel_configure (void *data, struct xdg_toplevel *top, + int32_t w, int32_t h, + struct wl_array *states) +{ + (void) data; (void) top; (void) w; (void) h; (void) states; +} + +static void xdg_toplevel_close (void *data, struct xdg_toplevel *top) +{ + (void) data; (void) top; + running = 0; +} + +static void xdg_toplevel_configure_bounds (void *data, struct xdg_toplevel *top, + int32_t w, int32_t h) +{ + (void) data; (void) top; (void) w; (void) h; +} + +static void xdg_toplevel_wm_capabilities (void *data, struct xdg_toplevel *top, + struct wl_array *caps) +{ + (void) data; (void) top; (void) caps; +} + +static const struct xdg_toplevel_listener xdg_top_listener = { + .configure = xdg_toplevel_configure, + .close = xdg_toplevel_close, + .configure_bounds = xdg_toplevel_configure_bounds, + .wm_capabilities = xdg_toplevel_wm_capabilities, +}; + +static void registry_global (void *data, struct wl_registry *reg, + uint32_t name, const char *iface, uint32_t ver) +{ + (void) data; + if (!strcmp (iface, wl_compositor_interface.name)) + wl_comp = wl_registry_bind (reg, name, &wl_compositor_interface, 1); + else if (!strcmp (iface, xdg_wm_base_interface.name)) + xdg_base = wl_registry_bind (reg, name, &xdg_wm_base_interface, 1); +} + +static void registry_global_remove (void *data, struct wl_registry *reg, uint32_t name) +{ + (void) data; (void) reg; (void) name; +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_global, + .global_remove = registry_global_remove, +}; + +static int +init_egl_wayland (struct gbm_device *gbm_dev, + int width, int height, + EGLDisplay *out_dpy, EGLContext *out_ctx, + EGLSurface *out_surf, + struct wl_egl_window **out_egl_win) +{ + static const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + static const EGLint ctx_attribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE + }; + EGLDisplay dpy; + EGLConfig cfg; + EGLContext ctx; + EGLSurface surf; + EGLint n; + struct wl_egl_window *egl_win; + + dpy = eglGetDisplay ((EGLNativeDisplayType) wl_dpy); + if (dpy == EGL_NO_DISPLAY) + return -1; + if (!eglInitialize (dpy, NULL, NULL)) + return -1; + if (!eglBindAPI (EGL_OPENGL_ES_API)) + return -1; + if (!eglChooseConfig (dpy, config_attribs, &cfg, 1, &n) || n < 1) + return -1; + + ctx = eglCreateContext (dpy, cfg, EGL_NO_CONTEXT, ctx_attribs); + if (ctx == EGL_NO_CONTEXT) + return -1; + + egl_win = wl_egl_window_create (wl_surf, width, height); + if (!egl_win) + return -1; + + surf = eglCreateWindowSurface (dpy, cfg, + (EGLNativeWindowType) egl_win, NULL); + if (surf == EGL_NO_SURFACE) { + wl_egl_window_destroy (egl_win); + return -1; + } + + if (!eglMakeCurrent (dpy, surf, surf, ctx)) { + eglDestroySurface (dpy, surf); + wl_egl_window_destroy (egl_win); + return -1; + } + + load_egl_procs (); + if (!create_image || !destroy_image || !image_target_texture) { + fprintf (stderr, "EGL image extensions not available\n"); + return -1; + } + + *out_dpy = dpy; + *out_ctx = ctx; + *out_surf = surf; + *out_egl_win = egl_win; + return 0; +} + +static void +draw_overlay (cairo_surface_t *surface, int width, int height, + int frame, double t_img, double t_gbm, double fps) +{ + cairo_t *cr = cairo_create (surface); + char buf[128]; + + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + + /* Semi-transparent background strip */ + cairo_set_source_rgba (cr, 0, 0, 0, 0.7); + cairo_rectangle (cr, 0, 0, width, 80); + cairo_fill (cr); + + cairo_select_font_face (cr, "sans-serif", + CAIRO_FONT_SLANT_NORMAL, + CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size (cr, 18); + + /* Title line */ + cairo_set_source_rgb (cr, 1, 1, 1); + snprintf (buf, sizeof buf, + "Cairo GBM Backend Demo | %.0f FPS | frame %d", + fps, frame); + cairo_move_to (cr, 12, 28); + cairo_show_text (cr, buf); + + /* Timing comparison */ + cairo_set_font_size (cr, 15); + cairo_set_source_rgb (cr, 0.6, 0.8, 1.0); + snprintf (buf, sizeof buf, + "image upload: %.2f ms", t_img); + cairo_move_to (cr, 12, 52); + cairo_show_text (cr, buf); + + cairo_set_source_rgb (cr, 0.6, 1.0, 0.6); + snprintf (buf, sizeof buf, + "GBM zero-copy: %.2f ms (%.0fx faster)", + t_gbm, t_gbm > 0 ? t_img / t_gbm : 0); + cairo_move_to (cr, 12, 72); + cairo_show_text (cr, buf); + + cairo_destroy (cr); +} + +static int +run_windowed (struct gbm_device *gbm_dev, int width, int height, int frames) +{ + EGLDisplay egl_dpy; + EGLContext egl_ctx; + EGLSurface egl_surf; + struct wl_egl_window *egl_win; + struct wl_registry *reg; + cairo_surface_t *img_surface, *gbm_surface, *overlay_surface; + GLuint tex_gbm, tex_overlay, prog; + int frame = 0; + double fps = 0; + double last_fps_time; + int fps_count = 0; + double t_img = 0, t_gbm = 0; + + wl_dpy = wl_display_connect (NULL); + if (!wl_dpy) { + fprintf (stderr, "Cannot connect to Wayland display.\n" + "Run without --window for headless benchmark.\n"); + return 1; + } + + reg = wl_display_get_registry (wl_dpy); + wl_registry_add_listener (reg, ®istry_listener, NULL); + wl_display_roundtrip (wl_dpy); + + if (!wl_comp || !xdg_base) { + fprintf (stderr, "Missing wl_compositor or xdg_wm_base\n"); + return 1; + } + + xdg_wm_base_add_listener (xdg_base, &xdg_base_listener, NULL); + + wl_surf = wl_compositor_create_surface (wl_comp); + xdg_surf = xdg_wm_base_get_xdg_surface (xdg_base, wl_surf); + xdg_surface_add_listener (xdg_surf, &xdg_surf_listener, NULL); + xdg_top = xdg_surface_get_toplevel (xdg_surf); + xdg_toplevel_add_listener (xdg_top, &xdg_top_listener, NULL); + xdg_toplevel_set_title (xdg_top, "Cairo GBM Benchmark"); + xdg_toplevel_set_app_id (xdg_top, "cairo-gbm-benchmark"); + wl_surface_commit (wl_surf); + + /* Wait for the first configure */ + while (!configured) + wl_display_roundtrip (wl_dpy); + + if (init_egl_wayland (gbm_dev, width, height, + &egl_dpy, &egl_ctx, &egl_surf, &egl_win) < 0) { + fprintf (stderr, "Failed to init Wayland EGL\n"); + return 1; + } + + printf ("EGL vendor: %s\n", eglQueryString (egl_dpy, EGL_VENDOR)); + printf ("GL renderer: %s\n\n", glGetString (GL_RENDERER)); + + prog = build_program (); + + /* Cairo surfaces: GBM for display, image for timing comparison */ + gbm_surface = cairo_gbm_surface_create (gbm_dev, CAIRO_FORMAT_ARGB32, + width, height); + img_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + width, height); + overlay_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + width, 80); + + /* GL textures */ + glGenTextures (1, &tex_gbm); + glBindTexture (GL_TEXTURE_2D, tex_gbm); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + GLuint tex_img; + glGenTextures (1, &tex_img); + glBindTexture (GL_TEXTURE_2D, tex_img); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, NULL); + + glGenTextures (1, &tex_overlay); + glBindTexture (GL_TEXTURE_2D, tex_overlay); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, width, 80, 0, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, NULL); + + glEnable (GL_BLEND); + glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + last_fps_time = now_ms (); + + while (running && (frames == 0 || frame < frames)) { + wl_display_dispatch_pending (wl_dpy); + + /* Render the scene to both surfaces and time the uploads */ + draw_frame (gbm_surface, width, height, frame); + t_gbm = import_gbm_surface (egl_dpy, gbm_surface, tex_gbm, + width, height); + + draw_frame (img_surface, width, height, frame); + t_img = upload_image_surface (img_surface, tex_img, width, height); + + /* FPS counter */ + fps_count++; + double now = now_ms (); + if (now - last_fps_time >= 1000.0) { + fps = fps_count * 1000.0 / (now - last_fps_time); + fps_count = 0; + last_fps_time = now; + } + + /* Update overlay */ + draw_overlay (overlay_surface, width, 80, + frame, t_img, t_gbm, fps); + cairo_surface_flush (overlay_surface); + glBindTexture (GL_TEXTURE_2D, tex_overlay); + glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, width, 80, + GL_BGRA_EXT, GL_UNSIGNED_BYTE, + cairo_image_surface_get_data (overlay_surface)); + + /* Draw scene (GBM zero-copy texture) and overlay */ + glViewport (0, 0, width, height); + glClearColor (0, 0, 0, 1); + glClear (GL_COLOR_BUFFER_BIT); + + glDisable (GL_BLEND); + draw_quad (prog, tex_gbm, -1, -1, 1, 1); + + glEnable (GL_BLEND); + float overlay_top = 1.0; + float overlay_bot = 1.0 - (80.0 / height) * 2.0; + draw_quad (prog, tex_overlay, -1, overlay_bot, 1, overlay_top); + + eglSwapBuffers (egl_dpy, egl_surf); + frame++; + } + + printf ("\nFinal: %d frames, image upload avg %.2f ms, " + "GBM import avg %.2f ms\n", + frame, t_img, t_gbm); + + cairo_surface_destroy (gbm_surface); + cairo_surface_destroy (img_surface); + cairo_surface_destroy (overlay_surface); + glDeleteTextures (1, &tex_gbm); + glDeleteTextures (1, &tex_img); + glDeleteTextures (1, &tex_overlay); + glDeleteProgram (prog); + eglDestroySurface (egl_dpy, egl_surf); + wl_egl_window_destroy (egl_win); + eglDestroyContext (egl_dpy, egl_ctx); + eglTerminate (egl_dpy); + xdg_toplevel_destroy (xdg_top); + xdg_surface_destroy (xdg_surf); + wl_surface_destroy (wl_surf); + xdg_wm_base_destroy (xdg_base); + wl_compositor_destroy (wl_comp); + wl_registry_destroy (reg); + wl_display_disconnect (wl_dpy); + return 0; +} + +#endif /* HAS_WAYLAND */ + +/* ------------------------------------------------------------------ */ +/* Main */ +/* ------------------------------------------------------------------ */ + +static void +usage (const char *prog) +{ + fprintf (stderr, + "Usage: %s [--window] [--frames N] [--width W] [--height H]\n" + "\n" + " --window Open a Wayland window showing the animated scene\n" + " rendered via GBM zero-copy, with a timing overlay.\n" + " Without this flag, runs a headless benchmark.\n" + " --frames Number of frames (default 100, 0 = unlimited in window mode)\n" + " --width Surface width (default 1920)\n" + " --height Surface height (default 1080)\n", + prog); +} + +int +main (int argc, char **argv) +{ + int width = 1920; + int height = 1080; + int frames = -1; /* -1 means "not set by user" */ + int windowed = 0; + int drm_fd; + struct gbm_device *gbm_dev; + int i, ret; + + for (i = 1; i < argc; i++) { + if (!strcmp (argv[i], "--window")) + windowed = 1; + else if (!strcmp (argv[i], "--frames") && i + 1 < argc) + frames = atoi (argv[++i]); + else if (!strcmp (argv[i], "--width") && i + 1 < argc) + width = atoi (argv[++i]); + else if (!strcmp (argv[i], "--height") && i + 1 < argc) + height = atoi (argv[++i]); + else { + usage (argv[0]); + return 1; + } + } + + if (frames < 0) + frames = windowed ? 0 : 100; + + printf ("GBM/EGL benchmark: %dx%d, %s, %d frames%s\n", + width, height, + windowed ? "windowed" : "headless", + frames, + frames == 0 ? " (unlimited)" : ""); + printf ("Buffer size: %.1f MB per frame (ARGB32)\n\n", + width * height * 4.0 / (1024 * 1024)); + + { + drmDevice *devices[16]; + int n = drmGetDevices2 (0, devices, 16); + + for (i = 0; i < n; i++) { + if (!(devices[i]->available_nodes & (1 << DRM_NODE_RENDER))) + continue; + drm_fd = open (devices[i]->nodes[DRM_NODE_RENDER], O_RDWR); + if (drm_fd < 0) + continue; + gbm_dev = gbm_create_device (drm_fd); + if (gbm_dev) + break; + close (drm_fd); + drm_fd = -1; + } + if (n > 0) + drmFreeDevices (devices, n); + } + if (!gbm_dev) { + fprintf (stderr, "No usable DRM render node found\n"); + return 1; + } + + if (windowed) { +#ifdef HAS_WAYLAND + ret = run_windowed (gbm_dev, width, height, frames); +#else + fprintf (stderr, "--window requires Wayland support " + "(not available in this build)\n"); + ret = 1; +#endif + } else { + ret = run_headless (gbm_dev, width, height, frames); + } + + gbm_device_destroy (gbm_dev); + close (drm_fd); + return ret; +} diff --git a/examples/meson.build b/examples/meson.build new file mode 100644 index 000000000..805cca0b7 --- /dev/null +++ b/examples/meson.build @@ -0,0 +1,46 @@ +egl_dep = dependency('egl', required: false) +glesv2_dep = dependency('glesv2', required: false) + +if not (feature_conf.get('CAIRO_HAS_GBM_SURFACE', 0) == 1 and egl_dep.found() and glesv2_dep.found()) + subdir_done() +endif + +benchmark_deps = [libcairo_dep, gbm_dep, libdrm_dep, egl_dep, glesv2_dep] +benchmark_sources = ['gbm-egl-benchmark.c'] +benchmark_c_args = [] + +# Wayland support for --window mode +wl_client_dep = dependency('wayland-client', required: false) +wl_egl_dep = dependency('wayland-egl', required: false) +wl_protocols_dep = dependency('wayland-protocols', required: false) +wl_scanner = find_program('wayland-scanner', required: false) + +if wl_client_dep.found() and wl_egl_dep.found() and wl_protocols_dep.found() and wl_scanner.found() + wl_protocols_dir = wl_protocols_dep.get_variable('pkgdatadir') + + xdg_shell_xml = wl_protocols_dir / 'stable' / 'xdg-shell' / 'xdg-shell.xml' + + xdg_shell_client_header = custom_target('xdg-shell-client-header', + input: xdg_shell_xml, + output: 'xdg-shell-client-protocol.h', + command: [wl_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) + + xdg_shell_code = custom_target('xdg-shell-code', + input: xdg_shell_xml, + output: 'xdg-shell-protocol.c', + command: [wl_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + + benchmark_sources += [xdg_shell_client_header, xdg_shell_code] + benchmark_deps += [wl_client_dep, wl_egl_dep] + benchmark_c_args += ['-DHAS_WAYLAND'] +endif + +executable('gbm-egl-benchmark', + benchmark_sources, + dependencies: benchmark_deps, + c_args: benchmark_c_args, + link_args: ['-lm'], + install: false, +) diff --git a/meson.build b/meson.build index 393ecdf53..4a88f745d 100644 --- a/meson.build +++ b/meson.build @@ -843,6 +843,8 @@ if not get_option('tests').disabled() and feature_conf.get('CAIRO_HAS_PNG_FUNCTI subdir('perf') endif +subdir('examples') + if get_option('gtk_doc') doc_srcdir = include_directories('src') subdir('doc/public')