GNU bug report logs - #80272
[PATCH 0/6] Add Skia as alternative graphics backend for PGTK (resend with attachments)

Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.

Package: emacs; Reported by: Arthur Heymans <arthur@HIDDEN>; Keywords: patch; dated Tue, 27 Jan 2026 07:51:02 UTC; Maintainer for emacs is bug-gnu-emacs@HIDDEN.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 27 Jan 2026 07:50:06 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Jan 27 02:50:05 2026
Received: from localhost ([127.0.0.1]:57884 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1vkdpt-00023p-PJ
	for submit <at> debbugs.gnu.org; Tue, 27 Jan 2026 02:50:05 -0500
Received: from lists.gnu.org ([2001:470:142::17]:50434)
 by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.84_2) (envelope-from <arthur@HIDDEN>)
 id 1vkdps-00023A-5v
 for submit <at> debbugs.gnu.org; Tue, 27 Jan 2026 02:50:04 -0500
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <arthur@HIDDEN>)
 id 1vkdpm-0002S4-M1
 for bug-gnu-emacs@HIDDEN; Tue, 27 Jan 2026 02:49:58 -0500
Received: from mail-ej1-x629.google.com ([2a00:1450:4864:20::629])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128)
 (Exim 4.90_1) (envelope-from <arthur@HIDDEN>)
 id 1vkdpb-00025g-IT
 for bug-gnu-emacs@HIDDEN; Tue, 27 Jan 2026 02:49:58 -0500
Received: by mail-ej1-x629.google.com with SMTP id
 a640c23a62f3a-b7cf4a975d2so772647066b.2
 for <bug-gnu-emacs@HIDDEN>; Mon, 26 Jan 2026 23:49:47 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=aheymans-xyz.20230601.gappssmtp.com; s=20230601; t=1769500185; x=1770104985;
 darn=gnu.org; 
 h=mime-version:message-id:date:user-agent:subject:to:from:from:to:cc
 :subject:date:message-id:reply-to;
 bh=XFxUO+bNtihqESTU0nEe4/a6vL6Ac6BfuzKAk4A5Kyw=;
 b=R6+12e+fwfCnrAepcKrlPKH4ZOXgVKreko/bjuB3yvZZ/swfdQ5UcDG+lscVoXAuRD
 YwF6aqRHu4cEH2mEpYnnQqN2ZzqmEL+Bp2NsdJm4/WdKjx8s7cGbGUxQRaoVh5GXm8Z7
 Qy5xd270XsEq1HIx2dDpr65QLxabC8iCidVmMij+TcIJTo2mGYNWDJy1Ez7M5WU1YJ92
 yBvQIYa5x3Rf5auGi3It6v6A4PyKhpAC/4lklL3xEAu8S/J+Z9PaVbYv4PTsvxSHN7J1
 UhRlQ+o4kJS3XmnK96LMfcJ/r+m88x82WvWaY8aT4xQ47MppSARY4WxDkCic0jEIHTu7
 H5DA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20230601; t=1769500185; x=1770104985;
 h=mime-version:message-id:date:user-agent:subject:to:from:x-gm-gg
 :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to;
 bh=XFxUO+bNtihqESTU0nEe4/a6vL6Ac6BfuzKAk4A5Kyw=;
 b=IUBAdlTyxBM64V7yO4krnv+rREuXnXh4C22F1d+YBl7SO+om5j8vt8AIrnHwfmXdES
 8vSwjchZA5T/MLDDdZZfnTxwH2r6KKAhX3KDhxBPbYSXLG4wIaSdT3YMQteDmhS+7U1G
 6hQqhRFXDFGo5Nx7QF1CJXtcUf8ABm/tQl3OpvgM34WeCi63i/5BKyFGCpz6JjQwUHNf
 JesgI6CHBVsVq1+V4K9/Nlc9v3rKpheIdgqdR6KEv/GzxKdTtkQuFCKwrfWeS1CgKMYC
 TGg7db5KuLVe4MqjChBEOvaXXaxkkGdTbt8MjU39+36fvWC13cz2oTkZCH9Uen6PK55z
 zC+g==
X-Gm-Message-State: AOJu0YwbpBoUg1md+65m5p7yxEFfPySu/5ksU/Wg8om9t/t7IxFLyRuH
 Py3VvW9rLw+wP8hFVEybTmckEj967RYLaQwWR8wYfA1If8xPC+cLUQ20AVIKGdxWRVUJuGpXx2K
 ZxiN6U7I=
X-Gm-Gg: AZuq6aJxbKxbpD0rPmmQ/Z1pfvYY32Gd3a8cksrgF2pZcW6N0xrPrSCtMiMOScVKyAk
 VcDFGdxjV0euXRqM/N3EoEJNdNUfSjE4iIjDyP+1/iHMdX2QFlu5eodz8YMsaaFBV8AiiecmjuX
 Sm+j3GaEC3cyNPOhjsUK0zFLuWqZNpRpQEFS5+frcNKzKg6UK3x3aMK+nI/Z6lry69/2hYwwzV1
 vQa3xrF6e6oYT8tv5fUFOR2cgg+p9gni1JsVm7Ei8OkjXGCMz74pXlFZPz8DEFAZ+hHaFCNFw2a
 YJKxBPtbzlDxX2b2qX0VXUQRtxwysI8i2lp2wtRajw0jWj5hEEIiYRaMjcobKML2JiVPBzsWMLq
 sj3Mf+j4KfGwMLjZXY6eNDLccGKI7Uo7Wf3PncNfB7Cjn+L+6Yx/aLu6c2pGpt57SoYgsALViX8
 1Xe0gyLVJAnkgr/0Z4ZwrQizka5mQUF7aPmVGqwJDytM8qZLIUYMlSMCgjojfzkEw60UqA6GF4F
 xhhl3rMY9e0FxfwMnJtPXQ=
X-Received: by 2002:a17:907:3e8c:b0:b87:8172:257 with SMTP id
 a640c23a62f3a-b8dab4a09f3mr69786466b.64.1769500185030; 
 Mon, 26 Jan 2026 23:49:45 -0800 (PST)
Received: from gmktec-k11 (228.243-245-81.adsl-dyn.isp.belgacom.be.
 [81.245.243.228]) by smtp.gmail.com with ESMTPSA id
 a640c23a62f3a-b885b445747sm742450166b.30.2026.01.26.23.49.44
 for <bug-gnu-emacs@HIDDEN>
 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
 Mon, 26 Jan 2026 23:49:44 -0800 (PST)
From: Arthur Heymans <arthur@HIDDEN>
To: bug-gnu-emacs@HIDDEN
Subject: [PATCH 0/6] Add Skia as alternative graphics backend for PGTK
 (resend with attachments)
User-Agent: mu4e 1.12.13; emacs 30.2
Date: Tue, 27 Jan 2026 08:49:43 +0100
Message-ID: <874io7xzoo.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
Received-SPF: pass client-ip=2a00:1450:4864:20::629;
 envelope-from=arthur@HIDDEN; helo=mail-ej1-x629.google.com
X-Spam_score_int: 14
X-Spam_score: 1.4
X-Spam_bar: +
X-Spam_report: (1.4 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1,
 FROM_SUSPICIOUS_NTLD=0.499, FROM_SUSPICIOUS_NTLD_FP=0.843,
 PDS_OTHER_BAD_TLD=1.999, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001,
 SPF_PASS=-0.001 autolearn=no autolearn_force=no
X-Spam_action: no action
X-Debbugs-Envelope-To: submit
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>

--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0001-Add-Skia-C-wrapper-library.patch

From a91e741c90d748029ad29b2b3cb126196bc37ddc Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:03:17 +0100
Subject: [PATCH 1/6] Add Skia C wrapper library

This adds a C wrapper around Google's Skia 2D graphics library.  The
wrapper provides a C API for Emacs to use Skia's drawing operations
without requiring C++ in the main Emacs codebase.

* src/skia/emacs_skia.h: New file.  C API wrapper for Skia library
providing surface, canvas, paint, font, image, and GL context types
with associated functions for 2D drawing operations.

* src/skia/emacs_skia.cpp: New file.  Implementation of the Skia C
wrapper using Skia's C++ API.  Supports both CPU and GPU (OpenGL)
rendering backends.
---
 src/skia/emacs_skia.cpp | 2271 +++++++++++++++++++++++++++++++++++++++
 src/skia/emacs_skia.h   |  650 +++++++++++
 2 files changed, 2921 insertions(+)
 create mode 100644 src/skia/emacs_skia.cpp
 create mode 100644 src/skia/emacs_skia.h

diff --git a/src/skia/emacs_skia.cpp b/src/skia/emacs_skia.cpp
new file mode 100644
index 00000000000..5fd0594d083
--- /dev/null
+++ b/src/skia/emacs_skia.cpp
@@ -0,0 +1,2271 @@
+/* Minimal Skia C API implementation for Emacs
+   Copyright (C) 2024-2026 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs 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.
+
+GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. */
+
+#include <config.h>
+#include "emacs_skia.h"
+
+#include <cstdio>
+
+/* Debug logging for Skia failures.
+   Define SKIA_DEBUG to enable verbose error messages to stderr.
+   This is useful for diagnosing font loading, image decoding,
+   or GL context creation failures.  */
+#ifdef SKIA_DEBUG
+#define SKIA_LOG_ERROR(fmt, ...) \
+  fprintf (stderr, "Skia error: " fmt "\n", ##__VA_ARGS__)
+#else
+#define SKIA_LOG_ERROR(fmt, ...) ((void)0)
+#endif
+
+/* Skia C++ headers - paths work with both:
+   - Nix skia package: -I.../include/skia -> codec/SkCodec.h
+   - Source build: -I.../skia -> include/codec/SkCodec.h  */
+#include "codec/SkCodec.h"
+#include "core/SkCanvas.h"
+#include "core/SkColorSpace.h"
+#include "core/SkData.h"
+#include "core/SkFont.h"
+#include "core/SkFontMetrics.h"
+#include "core/SkFontMgr.h"
+#include "core/SkImage.h"
+#include "core/SkPaint.h"
+#include "core/SkPath.h"
+#include "core/SkPathBuilder.h"
+#include "core/SkPathEffect.h"
+#include "core/SkShader.h"
+#include "core/SkStream.h"
+#include "core/SkSurface.h"
+#include "core/SkTileMode.h"
+#include "core/SkTypeface.h"
+#include "effects/SkDashPathEffect.h"
+#include "encode/SkPngEncoder.h"
+
+/* Platform-specific font manager for Linux (fontconfig) */
+#ifdef HAVE_FONTCONFIG
+# include <fontconfig/fontconfig.h>
+# include "ports/SkFontMgr_fontconfig.h"
+# include "ports/SkFontScanner_FreeType.h"
+#endif
+
+#include <cmath>
+#include <cstring>
+#include <map>
+#include <memory>
+
+/* PDF and SVG support */
+#ifdef SK_PDF
+# include "docs/SkPDFDocument.h"
+#endif
+
+#ifdef SK_SVG
+# include "svg/SkSVGCanvas.h"
+#endif
+
+#ifdef SK_GL
+# include "gpu/ganesh/GrBackendSurface.h"
+# include "gpu/ganesh/GrDirectContext.h"
+# include "gpu/ganesh/SkSurfaceGanesh.h"
+# include "gpu/ganesh/gl/GrGLAssembleInterface.h"
+# include "gpu/ganesh/gl/GrGLBackendSurface.h"
+# include "gpu/ganesh/gl/GrGLDirectContext.h"
+# include "gpu/ganesh/gl/GrGLInterface.h"
+/* GL types for fence sync - use epoxy for portable GL loading.  */
+# include <epoxy/gl.h>
+/* For dlsym fallback when getting GL proc addresses.  */
+# include <dlfcn.h>
+#endif
+
+/* ============================================================
+   Internal type wrappers
+   ============================================================ */
+
+/* Canvas wrapper must be defined before surface so it can be embedded.  */
+struct emacs_skia_canvas
+{
+  SkCanvas *canvas; /* Borrowed pointer, owned by surface/document */
+};
+
+struct emacs_skia_surface
+{
+  sk_sp<SkSurface> surface;
+  void *pixels; /* For raster surfaces with external pixels */
+#ifdef SK_GL
+  GrDirectContext *context; /* For GPU surfaces, to flush */
+#endif
+  /* Per-surface canvas wrapper to avoid thread-safety issues with
+     static variables.  The wrapper holds a borrowed pointer to the
+     SkCanvas owned by the surface.  */
+  emacs_skia_canvas canvas_wrapper;
+};
+
+struct emacs_skia_paint
+{
+  SkPaint paint;
+};
+
+struct emacs_skia_font
+{
+  SkFont font;
+  sk_sp<SkTypeface> typeface;
+};
+
+struct emacs_skia_typeface
+{
+  sk_sp<SkTypeface> typeface;
+};
+
+struct emacs_skia_image
+{
+  sk_sp<SkImage> image;
+};
+
+struct emacs_skia_path
+{
+  SkPathBuilder builder;
+};
+
+#ifdef SK_GL
+struct emacs_skia_gl_context
+{
+  sk_sp<GrDirectContext> context;
+};
+#else
+struct emacs_skia_gl_context
+{
+  int dummy;
+};
+#endif
+
+/* ============================================================
+   Constants
+   ============================================================ */
+
+/* GL surface configuration.  */
+#ifdef SK_GL
+/* Number of MSAA samples for GL surfaces.  0 = no multisampling.  */
+constexpr int GL_SURFACE_MSAA_SAMPLES = 0;
+/* Number of stencil buffer bits.  Skia needs stencil for clip mask
+   operations.  8 bits is standard and widely supported.  */
+constexpr int GL_SURFACE_STENCIL_BITS = 8;
+#endif
+
+/* ============================================================
+   Helper functions
+   ============================================================ */
+
+static inline SkRect
+to_sk_rect (const emacs_skia_rect_t *r)
+{
+  return SkRect::MakeLTRB (r->left, r->top, r->right, r->bottom);
+}
+
+static inline SkIRect
+to_sk_irect (const emacs_skia_irect_t *r)
+{
+  return SkIRect::MakeLTRB (r->left, r->top, r->right, r->bottom);
+}
+
+static inline SkPoint
+to_sk_point (emacs_skia_point_t p)
+{
+  return SkPoint::Make (p.x, p.y);
+}
+
+static inline SkColor
+to_sk_color (emacs_skia_color_t c)
+{
+  return static_cast<SkColor> (c);
+}
+
+static inline SkBlendMode
+to_sk_blend_mode (emacs_skia_blend_mode_t mode)
+{
+  switch (mode)
+    {
+    case EMACS_SKIA_BLEND_SRC:
+      return SkBlendMode::kSrc;
+    case EMACS_SKIA_BLEND_SRC_OVER:
+      return SkBlendMode::kSrcOver;
+    case EMACS_SKIA_BLEND_DST_OVER:
+      return SkBlendMode::kDstOver;
+    case EMACS_SKIA_BLEND_CLEAR:
+      return SkBlendMode::kClear;
+    case EMACS_SKIA_BLEND_XOR:
+      return SkBlendMode::kXor;
+    case EMACS_SKIA_BLEND_DIFFERENCE:
+      return SkBlendMode::kDifference;
+    case EMACS_SKIA_BLEND_EXCLUSION:
+      return SkBlendMode::kExclusion;
+    default:
+      return SkBlendMode::kSrcOver;
+    }
+}
+
+static inline SkFontHinting
+to_sk_hinting (emacs_skia_hinting_t h)
+{
+  switch (h)
+    {
+    case EMACS_SKIA_HINTING_NONE:
+      return SkFontHinting::kNone;
+    case EMACS_SKIA_HINTING_SLIGHT:
+      return SkFontHinting::kSlight;
+    case EMACS_SKIA_HINTING_NORMAL:
+      return SkFontHinting::kNormal;
+    case EMACS_SKIA_HINTING_FULL:
+      return SkFontHinting::kFull;
+    default:
+      return SkFontHinting::kNormal;
+    }
+}
+
+static inline SkFont::Edging
+to_sk_edging (emacs_skia_antialias_t aa)
+{
+  switch (aa)
+    {
+    case EMACS_SKIA_ANTIALIAS_NONE:
+      return SkFont::Edging::kAlias;
+    case EMACS_SKIA_ANTIALIAS_NORMAL:
+      return SkFont::Edging::kAntiAlias;
+    case EMACS_SKIA_ANTIALIAS_SUBPIXEL:
+      return SkFont::Edging::kSubpixelAntiAlias;
+    default:
+      return SkFont::Edging::kAntiAlias;
+    }
+}
+
+/* ============================================================
+   Global font manager
+   ============================================================ */
+
+/* Thread safety: Initialized lazily on first use from the main thread.
+   Emacs display code runs single-threaded, so no synchronization is
+   needed.  The Skia SkFontMgr is itself thread-safe once created.  */
+static sk_sp<SkFontMgr> global_font_mgr;
+
+static sk_sp<SkFontMgr>
+get_font_mgr (void)
+{
+  if (!global_font_mgr)
+    {
+#ifdef HAVE_FONTCONFIG
+      /* Use fontconfig-based font manager on Linux */
+      global_font_mgr
+	= SkFontMgr_New_FontConfig (nullptr,
+				    SkFontScanner_Make_FreeType ());
+#else
+      /* Fallback to empty font manager (typeface creation will fail)
+       */
+      global_font_mgr = SkFontMgr::RefEmpty ();
+#endif
+    }
+  return global_font_mgr;
+}
+
+/* ============================================================
+   Initialization / Cleanup
+   ============================================================ */
+
+void
+emacs_skia_init (void)
+{
+  /* Initialize global font manager */
+  (void) get_font_mgr ();
+}
+
+/* Forward declaration for SVG cleanup.  */
+#ifdef SK_SVG
+static void cleanup_svg_canvas_map (void);
+#endif
+
+void
+emacs_skia_cleanup (void)
+{
+#ifdef SK_SVG
+  /* Clean up any leaked SVG canvases.  */
+  cleanup_svg_canvas_map ();
+#endif
+
+  /* Release global font manager */
+  global_font_mgr.reset ();
+}
+
+/* ============================================================
+   Capability Queries
+   ============================================================ */
+
+bool
+emacs_skia_has_gl_support (void)
+{
+#ifdef SK_GL
+  return true;
+#else
+  return false;
+#endif
+}
+
+bool
+emacs_skia_has_pdf_support (void)
+{
+#ifdef SK_PDF
+  return true;
+#else
+  return false;
+#endif
+}
+
+bool
+emacs_skia_has_svg_support (void)
+{
+#ifdef SK_SVG
+  return true;
+#else
+  return false;
+#endif
+}
+
+/* ============================================================
+   GL Context
+   ============================================================ */
+
+/* Create GL context using the native GL interface.  This uses the
+   currently active GL context (e.g., set by GDK).  */
+
+#ifdef SK_GL
+/* GL proc loader using dlsym.  This works for both GLX and EGL contexts
+   when the context is already current.  libepoxy is used for GL types
+   and function declarations, but we use dlsym for proc address lookup
+   for maximum compatibility.
+
+   Thread safety: Initialized lazily on first GL context creation from
+   the main thread.  Once loaded, the handle is never modified or closed.
+   Emacs display code runs single-threaded, so no synchronization needed.  */
+static void *gl_lib_handle = nullptr;
+
+static GrGLFuncPtr
+get_gl_proc (void *ctx, const char *name)
+{
+  (void) ctx;
+
+  /* Lazy-load GL library.  Try libGL first (desktop), then GLES (mobile/Wayland).  */
+  if (!gl_lib_handle)
+    {
+      gl_lib_handle = dlopen ("libGL.so.1", RTLD_LAZY | RTLD_GLOBAL);
+      if (!gl_lib_handle)
+	gl_lib_handle = dlopen ("libGL.so", RTLD_LAZY | RTLD_GLOBAL);
+      if (!gl_lib_handle)
+	gl_lib_handle = dlopen ("libGLESv2.so.2", RTLD_LAZY | RTLD_GLOBAL);
+      if (!gl_lib_handle)
+	gl_lib_handle = dlopen ("libGLESv2.so", RTLD_LAZY | RTLD_GLOBAL);
+    }
+
+  if (!gl_lib_handle)
+    return nullptr;
+
+  return (GrGLFuncPtr) dlsym (gl_lib_handle, name);
+}
+#endif
+
+emacs_skia_gl_context_t *
+emacs_skia_gl_context_create_native (void)
+{
+#ifdef SK_GL
+  /* Use GrGLMakeAssembledInterface with libepoxy's function loader.  */
+  auto interface = GrGLMakeAssembledInterface (nullptr, get_gl_proc);
+  if (!interface)
+    {
+      fprintf (stderr, "Skia: Failed to create GL interface "
+	       "(is GL context current?)\n");
+      return nullptr;
+    }
+
+  auto grContext = GrDirectContexts::MakeGL (interface);
+  if (!grContext)
+    {
+      fprintf (stderr, "Skia: Failed to create GrDirectContext "
+	       "(GL version may be too old)\n");
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_gl_context_t;
+  result->context = grContext;
+  return result;
+#else
+  return nullptr;
+#endif
+}
+
+#ifdef SK_GL
+emacs_skia_gl_context_t *
+emacs_skia_gl_context_create (emacs_skia_gl_get_proc_fn get_proc,
+			      void *ctx)
+{
+  auto interface = GrGLMakeAssembledInterface (ctx, (GrGLGetProc)
+						      get_proc);
+  if (!interface)
+    {
+      fprintf (stderr, "Skia: Failed to assemble GL interface\n");
+      return nullptr;
+    }
+
+  auto grContext = GrDirectContexts::MakeGL (interface);
+  if (!grContext)
+    {
+      fprintf (stderr, "Skia: Failed to create GrDirectContext\n");
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_gl_context_t;
+  result->context = grContext;
+  return result;
+}
+
+void
+emacs_skia_gl_context_destroy (emacs_skia_gl_context_t *ctx)
+{
+  if (ctx)
+    {
+      ctx->context->abandonContext ();
+      delete ctx;
+    }
+}
+
+void
+emacs_skia_gl_context_flush (emacs_skia_gl_context_t *ctx)
+{
+  if (ctx && ctx->context)
+    ctx->context->flushAndSubmit ();
+}
+
+void
+emacs_skia_gl_context_reset (emacs_skia_gl_context_t *ctx)
+{
+  if (ctx && ctx->context)
+    {
+      /* Reset Skia's internal GL state tracking.  This is needed
+	 after destroying a surface that was wrapped around a backend
+	 render target, as Skia may have cached state related to that
+	 target. resetContext() tells Skia to re-query GL state on
+	 next use.  */
+      ctx->context->resetContext ();
+    }
+}
+#else
+emacs_skia_gl_context_t *
+emacs_skia_gl_context_create (emacs_skia_gl_get_proc_fn get_proc,
+			      void *ctx)
+{
+  (void) get_proc;
+  (void) ctx;
+  return nullptr;
+}
+
+void
+emacs_skia_gl_context_destroy (emacs_skia_gl_context_t *ctx)
+{
+  (void) ctx;
+}
+
+void
+emacs_skia_gl_context_flush (emacs_skia_gl_context_t *ctx)
+{
+  (void) ctx;
+}
+
+void
+emacs_skia_gl_context_reset (emacs_skia_gl_context_t *ctx)
+{
+  (void) ctx;
+}
+#endif
+
+/* ============================================================
+   GL Fence Sync
+   ============================================================ */
+
+#ifdef SK_GL
+struct emacs_skia_fence
+{
+  GLsync sync;
+};
+
+emacs_skia_fence_t *
+emacs_skia_fence_create (void)
+{
+  auto *fence = new emacs_skia_fence_t;
+  fence->sync = glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+  return fence;
+}
+
+bool
+emacs_skia_fence_wait (emacs_skia_fence_t *fence, uint64_t timeout_ns)
+{
+  if (!fence || !fence->sync)
+    return true;
+
+  GLenum result = glClientWaitSync (fence->sync,
+				    GL_SYNC_FLUSH_COMMANDS_BIT,
+				    timeout_ns);
+  return result != GL_TIMEOUT_EXPIRED;
+}
+
+bool
+emacs_skia_fence_is_signaled (emacs_skia_fence_t *fence)
+{
+  if (!fence || !fence->sync)
+    return true;
+
+  GLint status = GL_UNSIGNALED;
+  GLsizei length;
+  glGetSynciv (fence->sync, GL_SYNC_STATUS, sizeof (status),
+	       &length, &status);
+  return status == GL_SIGNALED;
+}
+
+void
+emacs_skia_fence_destroy (emacs_skia_fence_t *fence)
+{
+  if (fence)
+    {
+      if (fence->sync)
+	glDeleteSync (fence->sync);
+      delete fence;
+    }
+}
+
+#else /* !SK_GL */
+
+emacs_skia_fence_t *
+emacs_skia_fence_create (void)
+{
+  return nullptr;
+}
+
+bool
+emacs_skia_fence_wait (emacs_skia_fence_t *fence, uint64_t timeout_ns)
+{
+  (void) fence;
+  (void) timeout_ns;
+  return true;
+}
+
+bool
+emacs_skia_fence_is_signaled (emacs_skia_fence_t *fence)
+{
+  (void) fence;
+  return true;
+}
+
+void
+emacs_skia_fence_destroy (emacs_skia_fence_t *fence)
+{
+  (void) fence;
+}
+
+#endif /* SK_GL */
+
+/* ============================================================
+   Surface
+   ============================================================ */
+
+emacs_skia_surface_t *
+emacs_skia_surface_create_raster (int width, int height)
+{
+  SkImageInfo info = SkImageInfo::MakeN32Premul (width, height);
+  auto surface = SkSurfaces::Raster (info);
+  if (!surface)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_surface_t;
+  result->surface = surface;
+  result->pixels = nullptr;
+#ifdef SK_GL
+  result->context = nullptr;
+#endif
+  return result;
+}
+
+#ifdef SK_GL
+emacs_skia_surface_t *
+emacs_skia_surface_create_gl (emacs_skia_gl_context_t *ctx, int width,
+			      int height, unsigned int framebuffer_id,
+			      unsigned int format)
+{
+  if (!ctx || !ctx->context)
+    return nullptr;
+
+  GrGLFramebufferInfo fbInfo;
+  fbInfo.fFBOID = framebuffer_id;
+  fbInfo.fFormat = format;
+
+  /* Create backend render target with stencil buffer for clip mask
+     operations.  */
+  auto backendRT
+    = GrBackendRenderTargets::MakeGL (width, height,
+				      GL_SURFACE_MSAA_SAMPLES,
+				      GL_SURFACE_STENCIL_BITS, fbInfo);
+
+  auto surface = SkSurfaces::
+    WrapBackendRenderTarget (ctx->context.get (), backendRT,
+			     kBottomLeft_GrSurfaceOrigin,
+			     kRGBA_8888_SkColorType, nullptr,
+			     nullptr);
+
+  if (!surface)
+    return nullptr;
+
+  auto result = new emacs_skia_surface_t;
+  result->surface = surface;
+  result->pixels = nullptr;
+  result->context = ctx->context.get ();
+  return result;
+}
+#else
+emacs_skia_surface_t *
+emacs_skia_surface_create_gl (emacs_skia_gl_context_t *ctx, int width,
+			      int height, unsigned int framebuffer_id,
+			      unsigned int format)
+{
+  (void) ctx;
+  (void) width;
+  (void) height;
+  (void) framebuffer_id;
+  (void) format;
+  return nullptr;
+}
+#endif
+
+emacs_skia_surface_t *
+emacs_skia_surface_create_from_pixels (int width, int height,
+				       void *pixels, size_t row_bytes)
+{
+  SkImageInfo info = SkImageInfo::MakeN32Premul (width, height);
+  auto surface = SkSurfaces::WrapPixels (info, pixels, row_bytes);
+  if (!surface)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_surface_t;
+  result->surface = surface;
+  result->pixels = pixels;
+#ifdef SK_GL
+  result->context = nullptr;
+#endif
+  return result;
+}
+
+void
+emacs_skia_surface_destroy (emacs_skia_surface_t *surface)
+{
+  if (surface)
+    {
+      delete surface;
+    }
+}
+
+emacs_skia_canvas_t *
+emacs_skia_surface_get_canvas (emacs_skia_surface_t *surface)
+{
+  if (!surface || !surface->surface)
+    return nullptr;
+
+  /* Use the per-surface canvas wrapper to avoid thread-safety issues.
+     The canvas pointer is borrowed from the SkSurface.  */
+  surface->canvas_wrapper.canvas = surface->surface->getCanvas ();
+  return &surface->canvas_wrapper;
+}
+
+void *
+emacs_skia_surface_get_pixels (emacs_skia_surface_t *surface)
+{
+  if (!surface || !surface->surface)
+    {
+      return nullptr;
+    }
+  SkPixmap pixmap;
+  if (surface->surface->peekPixels (&pixmap))
+    {
+      return const_cast<void *> (pixmap.addr ());
+    }
+  return surface->pixels;
+}
+
+void
+emacs_skia_surface_flush (emacs_skia_surface_t *surface)
+{
+  if (surface && surface->surface)
+    {
+      /* For raster surfaces (which we currently use), pixels are
+	 directly accessible and no flush is needed.  For GPU
+	 surfaces, we would need to call
+	 GrDirectContext::flushAndSubmit() instead.  */
+#ifdef SK_GL
+      if (surface->context)
+	surface->context->flushAndSubmit ();
+#endif
+    }
+}
+
+int
+emacs_skia_surface_get_width (emacs_skia_surface_t *surface)
+{
+  return surface ? surface->surface->width () : 0;
+}
+
+int
+emacs_skia_surface_get_height (emacs_skia_surface_t *surface)
+{
+  return surface ? surface->surface->height () : 0;
+}
+
+emacs_skia_image_t *
+emacs_skia_surface_make_image_snapshot (emacs_skia_surface_t *surface)
+{
+  if (!surface || !surface->surface)
+    return nullptr;
+
+  sk_sp<SkImage> image = surface->surface->makeImageSnapshot ();
+  if (!image)
+    return nullptr;
+
+  auto *result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+emacs_skia_image_t *
+emacs_skia_surface_make_image_snapshot_rect (
+  emacs_skia_surface_t *surface, const emacs_skia_irect_t *rect)
+{
+  if (!surface || !surface->surface || !rect)
+    return nullptr;
+
+  SkIRect sk_rect = SkIRect::MakeLTRB (rect->left, rect->top,
+				       rect->right, rect->bottom);
+  sk_sp<SkImage> image
+    = surface->surface->makeImageSnapshot (sk_rect);
+  if (!image)
+    return nullptr;
+
+  auto *result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+/* ============================================================
+   Canvas
+   ============================================================ */
+
+void
+emacs_skia_canvas_save (emacs_skia_canvas_t *canvas)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->save ();
+    }
+}
+
+void
+emacs_skia_canvas_save_layer (emacs_skia_canvas_t *canvas,
+			      const emacs_skia_rect_t *bounds)
+{
+  if (canvas && canvas->canvas)
+    {
+      if (bounds)
+	canvas->canvas->saveLayer (to_sk_rect (bounds), nullptr);
+      else
+	canvas->canvas->saveLayer (nullptr, nullptr);
+    }
+}
+
+void
+emacs_skia_canvas_restore (emacs_skia_canvas_t *canvas)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->restore ();
+    }
+}
+
+int
+emacs_skia_canvas_get_save_count (emacs_skia_canvas_t *canvas)
+{
+  return canvas && canvas->canvas ? canvas->canvas->getSaveCount ()
+				  : 0;
+}
+
+void
+emacs_skia_canvas_restore_to_count (emacs_skia_canvas_t *canvas,
+				    int count)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->restoreToCount (count);
+    }
+}
+
+void
+emacs_skia_canvas_clear (emacs_skia_canvas_t *canvas,
+			 emacs_skia_color_t color)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->clear (to_sk_color (color));
+    }
+}
+
+void
+emacs_skia_canvas_clip_rect (emacs_skia_canvas_t *canvas,
+			     const emacs_skia_rect_t *rect)
+{
+  if (canvas && canvas->canvas && rect)
+    {
+      canvas->canvas->clipRect (to_sk_rect (rect));
+    }
+}
+
+void
+emacs_skia_canvas_clip_irect (emacs_skia_canvas_t *canvas,
+			      const emacs_skia_irect_t *rect)
+{
+  if (canvas && canvas->canvas && rect)
+    {
+      canvas->canvas->clipIRect (to_sk_irect (rect));
+    }
+}
+
+void
+emacs_skia_canvas_get_clip_bounds (emacs_skia_canvas_t *canvas,
+				   emacs_skia_rect_t *bounds)
+{
+  if (canvas && canvas->canvas && bounds)
+    {
+      SkRect sk_bounds = canvas->canvas->getLocalClipBounds ();
+      bounds->left = sk_bounds.left ();
+      bounds->top = sk_bounds.top ();
+      bounds->right = sk_bounds.right ();
+      bounds->bottom = sk_bounds.bottom ();
+    }
+}
+
+void
+emacs_skia_canvas_translate (emacs_skia_canvas_t *canvas, float dx,
+			     float dy)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->translate (dx, dy);
+    }
+}
+
+void
+emacs_skia_canvas_scale (emacs_skia_canvas_t *canvas, float sx,
+			 float sy)
+{
+  if (canvas && canvas->canvas)
+    {
+      canvas->canvas->scale (sx, sy);
+    }
+}
+
+void
+emacs_skia_canvas_draw_rect (emacs_skia_canvas_t *canvas,
+			     const emacs_skia_rect_t *rect,
+			     emacs_skia_paint_t *paint)
+{
+  if (canvas && canvas->canvas && rect && paint)
+    {
+      canvas->canvas->drawRect (to_sk_rect (rect), paint->paint);
+    }
+}
+
+void
+emacs_skia_canvas_draw_irect (emacs_skia_canvas_t *canvas,
+			      const emacs_skia_irect_t *rect,
+			      emacs_skia_paint_t *paint)
+{
+  if (canvas && canvas->canvas && rect && paint)
+    {
+      canvas->canvas->drawIRect (to_sk_irect (rect), paint->paint);
+    }
+}
+
+void
+emacs_skia_canvas_draw_line (emacs_skia_canvas_t *canvas, float x0,
+			     float y0, float x1, float y1,
+			     emacs_skia_paint_t *paint)
+{
+  if (canvas && canvas->canvas && paint)
+    {
+      canvas->canvas->drawLine (x0, y0, x1, y1, paint->paint);
+    }
+}
+
+/* Stack buffer size for glyph positions - covers most common cases.  */
+constexpr int GLYPH_STACK_BUFFER_SIZE = 64;
+
+void
+emacs_skia_canvas_draw_glyphs (emacs_skia_canvas_t *canvas, int count,
+			       const emacs_skia_glyph_t *glyphs,
+			       const emacs_skia_point_t *positions,
+			       emacs_skia_point_t origin,
+			       emacs_skia_font_t *font,
+			       emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !glyphs || !positions || !font
+      || !paint || count <= 0)
+    return;
+
+  /* Use stack buffer for small glyph counts to avoid heap allocation.
+     Most text rendering fits within 64 glyphs per call.  */
+  SkPoint stack_buffer[GLYPH_STACK_BUFFER_SIZE];
+  SkPoint *sk_positions;
+  bool heap_allocated = false;
+
+  if (count <= GLYPH_STACK_BUFFER_SIZE)
+    {
+      sk_positions = stack_buffer;
+    }
+  else
+    {
+      sk_positions = new SkPoint[count];
+      heap_allocated = true;
+    }
+
+  /* Convert positions to SkPoint array.  */
+  for (int i = 0; i < count; i++)
+    {
+      sk_positions[i] = to_sk_point (positions[i]);
+    }
+
+  /* New Skia API uses SkSpan instead of raw pointers.  */
+  SkSpan<const SkGlyphID> glyph_span (glyphs, count);
+  SkSpan<const SkPoint> pos_span (sk_positions, count);
+  canvas->canvas->drawGlyphs (glyph_span, pos_span,
+			      to_sk_point (origin), font->font,
+			      paint->paint);
+
+  if (heap_allocated)
+    delete[] sk_positions;
+}
+
+void
+emacs_skia_canvas_draw_image (emacs_skia_canvas_t *canvas,
+			      emacs_skia_image_t *image, float x,
+			      float y, emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !image || !image->image)
+    {
+      return;
+    }
+
+  SkSamplingOptions sampling (SkFilterMode::kLinear);
+  canvas->canvas->drawImage (image->image.get (), x, y, sampling,
+			     paint ? &paint->paint : nullptr);
+}
+
+void
+emacs_skia_canvas_draw_image_rect (emacs_skia_canvas_t *canvas,
+				   emacs_skia_image_t *image,
+				   const emacs_skia_rect_t *src,
+				   const emacs_skia_rect_t *dst,
+				   emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !image || !image->image || !dst)
+    {
+      return;
+    }
+
+  SkSamplingOptions sampling (SkFilterMode::kLinear);
+  SkRect sk_src = src ? to_sk_rect (src)
+		      : SkRect::MakeWH (image->image->width (),
+					image->image->height ());
+
+  canvas->canvas->drawImageRect (image->image.get (), sk_src,
+				 to_sk_rect (dst), sampling,
+				 paint ? &paint->paint : nullptr,
+				 SkCanvas::kStrict_SrcRectConstraint);
+}
+
+void
+emacs_skia_canvas_draw_path (emacs_skia_canvas_t *canvas,
+			     emacs_skia_path_t *path,
+			     emacs_skia_paint_t *paint)
+{
+  if (canvas && canvas->canvas && path && paint)
+    {
+      /* Convert builder to path for drawing.  */
+      canvas->canvas->drawPath (path->builder.snapshot (),
+				paint->paint);
+    }
+}
+
+void
+emacs_skia_canvas_draw_arc (emacs_skia_canvas_t *canvas,
+			    const emacs_skia_rect_t *oval,
+			    float start_angle, float sweep_angle,
+			    bool use_center,
+			    emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !oval || !paint)
+    return;
+
+  canvas->canvas->drawArc (to_sk_rect (oval), start_angle,
+			   sweep_angle, use_center, paint->paint);
+}
+
+void
+emacs_skia_canvas_draw_image_with_mask (emacs_skia_canvas_t *canvas,
+					emacs_skia_image_t *image,
+					emacs_skia_image_t *mask,
+					float x, float y,
+					emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !image || !image->image)
+    return;
+
+  /* If no mask, just draw the image normally */
+  if (!mask || !mask->image)
+    {
+      emacs_skia_canvas_draw_image (canvas, image, x, y, paint);
+      return;
+    }
+
+  /* Use a layer with mask as alpha:
+     1. Save the canvas state and create a layer
+     2. Draw the mask as the alpha channel
+     3. Draw the image with SrcIn blend mode (uses mask alpha)
+     4. Restore the layer */
+  canvas->canvas->save ();
+
+  SkRect bounds = SkRect::MakeXYWH (x, y, image->image->width (),
+				    image->image->height ());
+  canvas->canvas->saveLayer (bounds, nullptr);
+
+  /* Draw mask as grayscale (will become alpha) */
+  SkSamplingOptions sampling (SkFilterMode::kNearest);
+  canvas->canvas->drawImage (mask->image.get (), x, y, sampling,
+			     nullptr);
+
+  /* Draw image with SrcIn to use mask as alpha */
+  SkPaint srcInPaint;
+  srcInPaint.setBlendMode (SkBlendMode::kSrcIn);
+  if (paint)
+    srcInPaint.setColor (paint->paint.getColor ());
+  canvas->canvas->drawImage (image->image.get (), x, y, sampling,
+			     &srcInPaint);
+
+  canvas->canvas->restore (); /* Restore layer */
+  canvas->canvas->restore (); /* Restore original state */
+}
+
+void
+emacs_skia_canvas_clip_path (emacs_skia_canvas_t *canvas,
+			     emacs_skia_path_t *path)
+{
+  if (canvas && canvas->canvas && path)
+    {
+      /* Convert builder to path for clipping.  */
+      canvas->canvas->clipPath (path->builder.snapshot ());
+    }
+}
+
+/* ============================================================
+   Paint
+   ============================================================ */
+
+emacs_skia_paint_t *
+emacs_skia_paint_create (void)
+{
+  auto paint = new emacs_skia_paint_t;
+  paint->paint.setAntiAlias (true);
+  return paint;
+}
+
+void
+emacs_skia_paint_destroy (emacs_skia_paint_t *paint)
+{
+  delete paint;
+}
+
+void
+emacs_skia_paint_set_color (emacs_skia_paint_t *paint,
+			    emacs_skia_color_t color)
+{
+  if (paint)
+    {
+      paint->paint.setColor (to_sk_color (color));
+    }
+}
+
+emacs_skia_color_t
+emacs_skia_paint_get_color (emacs_skia_paint_t *paint)
+{
+  return paint ? paint->paint.getColor () : 0;
+}
+
+void
+emacs_skia_paint_set_alpha (emacs_skia_paint_t *paint, uint8_t alpha)
+{
+  if (paint)
+    {
+      paint->paint.setAlpha (alpha);
+    }
+}
+
+void
+emacs_skia_paint_set_antialias (emacs_skia_paint_t *paint,
+				bool antialias)
+{
+  if (paint)
+    {
+      paint->paint.setAntiAlias (antialias);
+    }
+}
+
+void
+emacs_skia_paint_set_blend_mode (emacs_skia_paint_t *paint,
+				 emacs_skia_blend_mode_t mode)
+{
+  if (paint)
+    {
+      paint->paint.setBlendMode (to_sk_blend_mode (mode));
+    }
+}
+
+void
+emacs_skia_paint_set_stroke (emacs_skia_paint_t *paint, bool stroke)
+{
+  if (paint)
+    {
+      paint->paint.setStyle (stroke ? SkPaint::kStroke_Style
+				    : SkPaint::kFill_Style);
+    }
+}
+
+void
+emacs_skia_paint_set_stroke_width (emacs_skia_paint_t *paint,
+				   float width)
+{
+  if (paint)
+    {
+      paint->paint.setStrokeWidth (width);
+    }
+}
+
+void
+emacs_skia_paint_set_dash (emacs_skia_paint_t *paint,
+			   const float *intervals, int count,
+			   float phase)
+{
+  if (!paint || !intervals || count < 2)
+    return;
+
+  /* Skia requires SkScalar array, which is float.
+     Newer Skia versions use SkSpan instead of pointer+count.  */
+  auto effect
+    = SkDashPathEffect::Make (SkSpan<const float> (intervals, count),
+			      phase);
+  paint->paint.setPathEffect (effect);
+}
+
+void
+emacs_skia_paint_clear_dash (emacs_skia_paint_t *paint)
+{
+  if (paint)
+    {
+      paint->paint.setPathEffect (nullptr);
+    }
+}
+
+void
+emacs_skia_paint_set_image_shader (emacs_skia_paint_t *paint,
+				   emacs_skia_image_t *image)
+{
+  if (!paint || !image || !image->image)
+    return;
+
+  /* Create a tiled shader from the image */
+  SkSamplingOptions sampling (SkFilterMode::kNearest);
+  auto shader
+    = image->image->makeShader (SkTileMode::kRepeat,
+				SkTileMode::kRepeat, sampling);
+  paint->paint.setShader (shader);
+}
+
+void
+emacs_skia_paint_clear_shader (emacs_skia_paint_t *paint)
+{
+  if (paint)
+    {
+      paint->paint.setShader (nullptr);
+    }
+}
+
+/* ============================================================
+   Font and Typeface
+   ============================================================ */
+
+emacs_skia_typeface_t *
+emacs_skia_typeface_create_from_file (const char *path)
+{
+  sk_sp<SkFontMgr> fontMgr = get_font_mgr ();
+  if (!fontMgr)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_file: no font manager available");
+      return nullptr;
+    }
+
+  auto typeface = fontMgr->makeFromFile (path);
+  if (!typeface)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_file: failed to load '%s'", path);
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_typeface_t;
+  result->typeface = typeface;
+  return result;
+}
+
+emacs_skia_typeface_t *
+emacs_skia_typeface_create_from_data (const void *data, size_t size)
+{
+  sk_sp<SkFontMgr> fontMgr = get_font_mgr ();
+  if (!fontMgr)
+    {
+      return nullptr;
+    }
+
+  auto skdata = SkData::MakeWithCopy (data, size);
+  auto typeface = fontMgr->makeFromData (skdata);
+  if (!typeface)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_typeface_t;
+  result->typeface = typeface;
+  return result;
+}
+
+emacs_skia_typeface_t *
+emacs_skia_typeface_create_from_name (const char *family_name,
+				      int weight, int width,
+				      int slant)
+{
+  sk_sp<SkFontMgr> fontMgr = get_font_mgr ();
+  if (!fontMgr)
+    {
+      return nullptr;
+    }
+
+  SkFontStyle style (weight, width,
+		     static_cast<SkFontStyle::Slant> (slant));
+  auto typeface = fontMgr->legacyMakeTypeface (family_name, style);
+  if (!typeface)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_typeface_t;
+  result->typeface = typeface;
+  return result;
+}
+
+void
+emacs_skia_typeface_destroy (emacs_skia_typeface_t *typeface)
+{
+  delete typeface;
+}
+
+#ifdef HAVE_FONTCONFIG
+emacs_skia_typeface_t *
+emacs_skia_typeface_create_from_fc_pattern (FcPattern *pattern)
+{
+  if (!pattern)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_fc_pattern: null pattern");
+      return nullptr;
+    }
+
+  /* Extract the font file path and index from the pattern */
+  FcChar8 *file = nullptr;
+  int index = 0;
+
+  if (FcPatternGetString (pattern, FC_FILE, 0, &file) != FcResultMatch
+      || !file)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_fc_pattern: no file in pattern");
+      return nullptr;
+    }
+
+  FcPatternGetInteger (pattern, FC_INDEX, 0, &index);
+
+  sk_sp<SkFontMgr> fontMgr = get_font_mgr ();
+  if (!fontMgr)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_fc_pattern: no font manager");
+      return nullptr;
+    }
+
+  /* Load the typeface from file with the specified face index */
+  auto typeface
+    = fontMgr->makeFromFile (reinterpret_cast<const char *> (file),
+			     index);
+  if (!typeface)
+    {
+      SKIA_LOG_ERROR ("typeface_create_from_fc_pattern: failed to load '%s' index %d",
+		      file, index);
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_typeface_t;
+  result->typeface = typeface;
+  return result;
+}
+#endif
+
+const char *
+emacs_skia_typeface_get_path (emacs_skia_typeface_t *typeface)
+{
+  /* Skia doesn't expose the font file path directly.
+     This would need platform-specific code or tracking during
+     creation. For now, return nullptr - callers should use the path
+     they originally provided.  */
+  (void) typeface;
+  return nullptr;
+}
+
+int
+emacs_skia_typeface_get_index (emacs_skia_typeface_t *typeface)
+{
+  /* Skia doesn't expose the face index directly.
+     Return 0 as the default.  */
+  (void) typeface;
+  return 0;
+}
+
+emacs_skia_font_t *
+emacs_skia_font_create (emacs_skia_typeface_t *typeface, float size)
+{
+  auto font = new emacs_skia_font_t;
+  if (typeface && typeface->typeface)
+    {
+      font->font = SkFont (typeface->typeface, size);
+      font->typeface = typeface->typeface;
+    }
+  else
+    {
+      font->font = SkFont (nullptr, size);
+    }
+  font->font.setSubpixel (true);
+  return font;
+}
+
+void
+emacs_skia_font_destroy (emacs_skia_font_t *font)
+{
+  delete font;
+}
+
+void
+emacs_skia_font_set_size (emacs_skia_font_t *font, float size)
+{
+  if (font)
+    {
+      font->font.setSize (size);
+    }
+}
+
+float
+emacs_skia_font_get_size (emacs_skia_font_t *font)
+{
+  return font ? font->font.getSize () : 0;
+}
+
+void
+emacs_skia_font_set_hinting (emacs_skia_font_t *font,
+			     emacs_skia_hinting_t hinting)
+{
+  if (font)
+    {
+      font->font.setHinting (to_sk_hinting (hinting));
+    }
+}
+
+void
+emacs_skia_font_set_edging (emacs_skia_font_t *font,
+			    emacs_skia_antialias_t edging)
+{
+  if (font)
+    {
+      font->font.setEdging (to_sk_edging (edging));
+    }
+}
+
+void
+emacs_skia_font_set_subpixel (emacs_skia_font_t *font, bool subpixel)
+{
+  if (font)
+    {
+      font->font.setSubpixel (subpixel);
+    }
+}
+
+void
+emacs_skia_font_get_metrics (emacs_skia_font_t *font,
+			     emacs_skia_font_metrics_t *metrics)
+{
+  if (!font || !metrics)
+    {
+      return;
+    }
+
+  SkFontMetrics sk_metrics;
+  font->font.getMetrics (&sk_metrics);
+
+  metrics->ascent = sk_metrics.fAscent;
+  metrics->descent = sk_metrics.fDescent;
+  metrics->leading = sk_metrics.fLeading;
+  metrics->avg_char_width = sk_metrics.fAvgCharWidth;
+  metrics->max_char_width = sk_metrics.fMaxCharWidth;
+  metrics->x_height = sk_metrics.fXHeight;
+  metrics->cap_height = sk_metrics.fCapHeight;
+}
+
+void
+emacs_skia_font_get_extents (emacs_skia_font_t *font,
+			     emacs_skia_font_extents_t *extents)
+{
+  if (!font || !extents)
+    return;
+
+  SkFontMetrics sk_metrics;
+  font->font.getMetrics (&sk_metrics);
+
+  /* Cairo convention: ascent and descent are positive values.
+     Skia convention: ascent is negative (distance up from baseline).
+   */
+  extents->ascent = -sk_metrics.fAscent;
+  extents->descent = sk_metrics.fDescent;
+  extents->height
+    = -sk_metrics.fAscent + sk_metrics.fDescent + sk_metrics.fLeading;
+  extents->max_x_advance = sk_metrics.fMaxCharWidth;
+  extents->max_y_advance = 0; /* Horizontal fonts */
+}
+
+void
+emacs_skia_font_get_glyph_extents (
+  emacs_skia_font_t *font, const emacs_skia_glyph_t *glyphs,
+  int count, emacs_skia_glyph_extents_t *extents)
+{
+  if (!font || !glyphs || !extents || count <= 0)
+    return;
+
+  /* Get bounds and widths for all glyphs */
+  std::vector<SkRect> bounds (count);
+  std::vector<SkScalar> widths (count);
+
+  SkSpan<const SkGlyphID> glyph_span (glyphs, count);
+  SkSpan<SkScalar> width_span (widths.data (), count);
+  SkSpan<SkRect> bounds_span (bounds.data (), count);
+
+  font->font.getWidthsBounds (glyph_span, width_span, bounds_span,
+			      nullptr);
+
+  /* Convert to emacs_skia_glyph_extents_t format
+     (compatible with Cairo's cairo_text_extents_t) */
+  for (int i = 0; i < count; i++)
+    {
+      extents[i].x_bearing = bounds[i].left ();
+      extents[i].y_bearing = bounds[i].top ();
+      extents[i].width = bounds[i].width ();
+      extents[i].height = bounds[i].height ();
+      extents[i].x_advance = widths[i];
+      extents[i].y_advance = 0; /* Horizontal fonts */
+    }
+}
+
+void
+emacs_skia_font_get_glyph_bounds (emacs_skia_font_t *font,
+				  const emacs_skia_glyph_t *glyphs,
+				  int count,
+				  emacs_skia_rect_t *bounds)
+{
+  if (!font || !glyphs || !bounds || count <= 0)
+    return;
+
+  std::vector<SkRect> sk_bounds (count);
+  SkSpan<const SkGlyphID> glyph_span (glyphs, count);
+  SkSpan<SkRect> bounds_span (sk_bounds.data (), count);
+
+  font->font.getBounds (glyph_span, bounds_span, nullptr);
+
+  for (int i = 0; i < count; i++)
+    {
+      bounds[i].left = sk_bounds[i].left ();
+      bounds[i].top = sk_bounds[i].top ();
+      bounds[i].right = sk_bounds[i].right ();
+      bounds[i].bottom = sk_bounds[i].bottom ();
+    }
+}
+
+int
+emacs_skia_font_text_to_glyphs (emacs_skia_font_t *font,
+				const char *text, size_t byte_length,
+				emacs_skia_glyph_t *glyphs,
+				int max_glyphs)
+{
+  if (!font || !text)
+    {
+      return 0;
+    }
+
+  /* New Skia API uses SkSpan for output buffer */
+  SkSpan<SkGlyphID> glyph_span (glyphs, max_glyphs);
+  size_t count
+    = font->font.textToGlyphs (text, byte_length,
+			       SkTextEncoding::kUTF8, glyph_span);
+  return static_cast<int> (count);
+}
+
+emacs_skia_glyph_t
+emacs_skia_font_char_to_glyph (emacs_skia_font_t *font,
+			       int32_t codepoint)
+{
+  if (!font)
+    {
+      return 0;
+    }
+  return font->font.unicharToGlyph (codepoint);
+}
+
+void
+emacs_skia_font_get_widths (emacs_skia_font_t *font,
+			    const emacs_skia_glyph_t *glyphs,
+			    int count, float *widths)
+{
+  if (!font || !glyphs || !widths)
+    {
+      return;
+    }
+  /* New Skia API uses SkSpan instead of raw pointers */
+  SkSpan<const SkGlyphID> glyph_span (glyphs, count);
+  SkSpan<SkScalar> width_span (widths, count);
+  font->font.getWidths (glyph_span, width_span);
+}
+
+float
+emacs_skia_font_measure_text (emacs_skia_font_t *font,
+			      const char *text, size_t byte_length)
+{
+  if (!font || !text)
+    {
+      return 0;
+    }
+  return font->font.measureText (text, byte_length,
+				 SkTextEncoding::kUTF8);
+}
+
+/* ============================================================
+   Image
+   ============================================================ */
+
+emacs_skia_image_t *
+emacs_skia_image_create_from_pixels (int width, int height,
+				     const void *pixels,
+				     size_t row_bytes, bool has_alpha)
+{
+  SkColorType colorType
+    = has_alpha ? kRGBA_8888_SkColorType : kRGB_888x_SkColorType;
+  SkAlphaType alphaType
+    = has_alpha ? kPremul_SkAlphaType : kOpaque_SkAlphaType;
+  SkImageInfo info
+    = SkImageInfo::Make (width, height, colorType, alphaType);
+
+  auto image = SkImages::RasterFromPixmapCopy (
+    SkPixmap (info, pixels, row_bytes));
+
+  if (!image)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+emacs_skia_image_t *
+emacs_skia_image_create_from_bgra_pixels (int width, int height,
+					  const void *pixels,
+					  size_t row_bytes,
+					  bool has_alpha)
+{
+  /* BGRA is Cairo's native format on little-endian machines.
+     Skia supports BGRA natively with kBGRA_8888_SkColorType. */
+  SkColorType colorType = kBGRA_8888_SkColorType;
+  SkAlphaType alphaType
+    = has_alpha ? kPremul_SkAlphaType : kOpaque_SkAlphaType;
+  SkImageInfo info
+    = SkImageInfo::Make (width, height, colorType, alphaType);
+
+  auto image = SkImages::RasterFromPixmapCopy (
+    SkPixmap (info, pixels, row_bytes));
+
+  if (!image)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+emacs_skia_image_t *
+emacs_skia_image_create_from_encoded (const void *data, size_t size)
+{
+  auto skdata = SkData::MakeWithCopy (data, size);
+  auto image = SkImages::DeferredFromEncodedData (skdata);
+
+  if (!image)
+    {
+      return nullptr;
+    }
+
+  auto result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+void
+emacs_skia_image_destroy (emacs_skia_image_t *image)
+{
+  delete image;
+}
+
+int
+emacs_skia_image_get_width (emacs_skia_image_t *image)
+{
+  return image && image->image ? image->image->width () : 0;
+}
+
+int
+emacs_skia_image_get_height (emacs_skia_image_t *image)
+{
+  return image && image->image ? image->image->height () : 0;
+}
+
+emacs_skia_image_t *
+emacs_skia_image_create_from_bitmap (const unsigned char *data,
+				     int width, int height,
+				     int stride)
+{
+  if (!data || width <= 0 || height <= 0)
+    return nullptr;
+
+  /* Calculate stride if not provided */
+  if (stride <= 0)
+    stride = (width + 7) / 8;
+
+  /* Convert 1-bit packed data to 8-bit alpha.
+     Cairo A1 format: MSB first, rows padded to 32-bit boundary.
+     We convert to Skia A8 format for compatibility.  */
+  int a8_stride = width;
+  std::vector<uint8_t> a8_data (width * height);
+
+  for (int y = 0; y < height; y++)
+    {
+      const unsigned char *src_row = data + y * stride;
+      uint8_t *dst_row = a8_data.data () + y * a8_stride;
+
+      for (int x = 0; x < width; x++)
+	{
+	  int byte_idx = x / 8;
+	  int bit_idx = 7 - (x % 8); /* MSB first */
+	  bool bit_set = (src_row[byte_idx] >> bit_idx) & 1;
+	  dst_row[x] = bit_set ? 255 : 0;
+	}
+    }
+
+  /* Create A8 (alpha-only) image */
+  SkImageInfo info
+    = SkImageInfo::Make (width, height, kAlpha_8_SkColorType,
+			 kPremul_SkAlphaType);
+  auto image = SkImages::RasterFromPixmapCopy (
+    SkPixmap (info, a8_data.data (), a8_stride));
+
+  if (!image)
+    return nullptr;
+
+  auto result = new emacs_skia_image_t;
+  result->image = image;
+  return result;
+}
+
+/* ============================================================
+   Path
+   ============================================================ */
+
+emacs_skia_path_t *
+emacs_skia_path_create (void)
+{
+  return new emacs_skia_path_t;
+}
+
+void
+emacs_skia_path_destroy (emacs_skia_path_t *path)
+{
+  delete path;
+}
+
+void
+emacs_skia_path_reset (emacs_skia_path_t *path)
+{
+  if (path)
+    path->builder.reset ();
+}
+
+void
+emacs_skia_path_move_to (emacs_skia_path_t *path, float x, float y)
+{
+  if (path)
+    path->builder.moveTo (x, y);
+}
+
+void
+emacs_skia_path_line_to (emacs_skia_path_t *path, float x, float y)
+{
+  if (path)
+    path->builder.lineTo (x, y);
+}
+
+void
+emacs_skia_path_rel_line_to (emacs_skia_path_t *path, float dx,
+			     float dy)
+{
+  if (path)
+    path->builder.rLineTo (dx, dy);
+}
+
+void
+emacs_skia_path_arc_to (emacs_skia_path_t *path,
+			const emacs_skia_rect_t *oval,
+			float start_angle, float sweep_angle,
+			bool force_move_to)
+{
+  if (!path || !oval)
+    return;
+
+  /* SkPathBuilder::arcTo uses forceMoveTo parameter.  */
+  path->builder.arcTo (to_sk_rect (oval), start_angle, sweep_angle,
+		       force_move_to);
+}
+
+void
+emacs_skia_path_close (emacs_skia_path_t *path)
+{
+  if (path)
+    path->builder.close ();
+}
+
+void
+emacs_skia_path_add_rect (emacs_skia_path_t *path,
+			  const emacs_skia_rect_t *rect)
+{
+  if (path && rect)
+    path->builder.addRect (to_sk_rect (rect));
+}
+
+/* ============================================================
+   Document Export (PDF/SVG)
+   ============================================================ */
+
+/* Custom SkWStream that writes to a callback function.  */
+class CallbackWStream : public SkWStream
+{
+public:
+  CallbackWStream (emacs_skia_write_fn fn, void *ctx)
+      : write_fn_ (fn), ctx_ (ctx), bytes_written_ (0)
+  {
+  }
+
+  bool write (const void *buffer, size_t size) override
+  {
+    if (!write_fn_)
+      return false;
+    size_t written = write_fn_ (ctx_, buffer, size);
+    bytes_written_ += written;
+    return written == size;
+  }
+
+  void flush () override
+  {
+    /* Callback stream doesn't buffer, so nothing to flush.  */
+  }
+
+  size_t bytesWritten () const override { return bytes_written_; }
+
+private:
+  emacs_skia_write_fn write_fn_;
+  void *ctx_;
+  size_t bytes_written_;
+};
+
+struct emacs_skia_document
+{
+#ifdef SK_PDF
+  sk_sp<SkDocument> document;
+  std::unique_ptr<CallbackWStream> stream;
+  SkCanvas *current_page; /* Borrowed from document */
+  /* Per-document canvas wrapper for thread safety.  */
+  emacs_skia_canvas canvas_wrapper;
+#else
+  int dummy;
+#endif
+};
+
+/* Canvas wrapper for SVG that owns its stream.  */
+struct emacs_skia_svg_canvas_data
+{
+  std::unique_ptr<CallbackWStream> stream;
+  std::unique_ptr<SkCanvas> canvas;
+  /* Per-SVG-canvas wrapper for thread safety.  */
+  emacs_skia_canvas canvas_wrapper;
+};
+
+#ifdef SK_PDF
+emacs_skia_document_t *
+emacs_skia_document_create_pdf (emacs_skia_write_fn write_fn,
+				void *write_ctx, float width,
+				float height)
+{
+  auto stream
+    = std::make_unique<CallbackWStream> (write_fn, write_ctx);
+
+  SkPDF::Metadata metadata;
+  /* Could add metadata like title, creator, etc. here.  */
+
+  auto document = SkPDF::MakeDocument (stream.get (), metadata);
+  if (!document)
+    return nullptr;
+
+  auto result = new emacs_skia_document_t;
+  result->document = document;
+  result->stream = std::move (stream);
+  result->current_page = nullptr;
+
+  return result;
+}
+
+emacs_skia_canvas_t *
+emacs_skia_document_begin_page (emacs_skia_document_t *doc,
+				float width, float height)
+{
+  if (!doc || !doc->document)
+    return nullptr;
+
+  /* End any existing page first.  */
+  if (doc->current_page)
+    {
+      doc->document->endPage ();
+      doc->current_page = nullptr;
+    }
+
+  doc->current_page
+    = doc->document->beginPage (width, height, nullptr);
+  if (!doc->current_page)
+    return nullptr;
+
+  /* Use the per-document canvas wrapper for thread safety.  */
+  doc->canvas_wrapper.canvas = doc->current_page;
+  return &doc->canvas_wrapper;
+}
+
+void
+emacs_skia_document_end_page (emacs_skia_document_t *doc)
+{
+  if (doc && doc->document && doc->current_page)
+    {
+      doc->document->endPage ();
+      doc->current_page = nullptr;
+    }
+}
+
+void
+emacs_skia_document_close (emacs_skia_document_t *doc)
+{
+  if (doc)
+    {
+      if (doc->document)
+	{
+	  /* End any open page.  */
+	  if (doc->current_page)
+	    doc->document->endPage ();
+	  doc->document->close ();
+	}
+      delete doc;
+    }
+}
+#else
+/* Stub implementations when PDF support is not compiled in.  */
+emacs_skia_document_t *
+emacs_skia_document_create_pdf (emacs_skia_write_fn write_fn,
+				void *write_ctx, float width,
+				float height)
+{
+  (void) write_fn;
+  (void) write_ctx;
+  (void) width;
+  (void) height;
+  return nullptr;
+}
+
+emacs_skia_canvas_t *
+emacs_skia_document_begin_page (emacs_skia_document_t *doc,
+				float width, float height)
+{
+  (void) doc;
+  (void) width;
+  (void) height;
+  return nullptr;
+}
+
+void
+emacs_skia_document_end_page (emacs_skia_document_t *doc)
+{
+  (void) doc;
+}
+
+void
+emacs_skia_document_close (emacs_skia_document_t *doc)
+{
+  (void) doc;
+}
+#endif
+
+#ifdef SK_SVG
+/* Global map to track SVG canvas data.
+   This is needed because we return a generic emacs_skia_canvas_t
+   pointer but need to track the underlying stream ownership.
+
+   Thread safety: Accessed only from the main thread during SVG export
+   operations.  Emacs display code runs single-threaded, so no mutex
+   is needed.  The map is cleaned up in emacs_skia_cleanup() to prevent
+   leaks if canvases are not properly finished.  */
+static std::map<SkCanvas *, emacs_skia_svg_canvas_data *>
+  svg_canvas_map;
+
+/* Clean up any remaining SVG canvases that were not properly finished.
+   Called from emacs_skia_cleanup() to prevent memory leaks.  */
+static void
+cleanup_svg_canvas_map (void)
+{
+  for (auto &entry : svg_canvas_map)
+    delete entry.second;
+  svg_canvas_map.clear ();
+}
+
+emacs_skia_canvas_t *
+emacs_skia_svg_canvas_create (emacs_skia_write_fn write_fn,
+			      void *write_ctx, float width,
+			      float height)
+{
+  auto stream
+    = std::make_unique<CallbackWStream> (write_fn, write_ctx);
+
+  SkRect bounds = SkRect::MakeWH (width, height);
+  auto canvas = SkSVGCanvas::Make (bounds, stream.get ());
+  if (!canvas)
+    return nullptr;
+
+  /* Store the canvas data so we can clean up later.  */
+  auto data = new emacs_skia_svg_canvas_data;
+  data->stream = std::move (stream);
+  data->canvas = std::move (canvas);
+
+  svg_canvas_map[data->canvas.get ()] = data;
+
+  /* Use the per-SVG-canvas wrapper for thread safety.  */
+  data->canvas_wrapper.canvas = data->canvas.get ();
+  return &data->canvas_wrapper;
+}
+
+void
+emacs_skia_svg_canvas_finish (emacs_skia_canvas_t *canvas)
+{
+  if (!canvas || !canvas->canvas)
+    return;
+
+  auto it = svg_canvas_map.find (canvas->canvas);
+  if (it != svg_canvas_map.end ())
+    {
+      emacs_skia_svg_canvas_data *data = it->second;
+      /* Deleting the canvas flushes and finishes the SVG output.  */
+      svg_canvas_map.erase (it);
+      delete data;
+    }
+}
+#else
+/* Stub implementations when SVG support is not compiled in.  */
+emacs_skia_canvas_t *
+emacs_skia_svg_canvas_create (emacs_skia_write_fn write_fn,
+			      void *write_ctx, float width,
+			      float height)
+{
+  (void) write_fn;
+  (void) write_ctx;
+  (void) width;
+  (void) height;
+  return nullptr;
+}
+
+void
+emacs_skia_svg_canvas_finish (emacs_skia_canvas_t *canvas)
+{
+  (void) canvas;
+}
+#endif
+
+/* ============================================================
+   PNG Export
+   ============================================================ */
+
+/* Helper to encode pixmap to PNG and write via callback.
+   Uses SkDynamicMemoryWStream to avoid RTTI issues with custom streams.  */
+static bool
+encode_png_to_callback (const SkPixmap &pixmap,
+			emacs_skia_write_fn write_fn, void *write_ctx)
+{
+  SkDynamicMemoryWStream stream;
+  SkPngEncoder::Options options;
+
+  if (!SkPngEncoder::Encode (&stream, pixmap, options))
+    return false;
+
+  /* Write the encoded data to the callback.  */
+  sk_sp<SkData> data = stream.detachAsData ();
+  if (!data)
+    return false;
+
+  size_t written = write_fn (write_ctx, data->data (), data->size ());
+  return written == data->size ();
+}
+
+/* Write surface to PNG using a callback function.
+   Returns true on success, false on failure.  */
+bool
+emacs_skia_surface_write_to_png (emacs_skia_surface_t *surface,
+				 emacs_skia_write_fn write_fn,
+				 void *write_ctx)
+{
+  if (!surface || !surface->surface || !write_fn)
+    return false;
+
+  /* Create an image snapshot of the surface.  */
+  sk_sp<SkImage> image = surface->surface->makeImageSnapshot ();
+  if (!image)
+    return false;
+
+  /* Read pixels into a raster format suitable for encoding.  */
+  SkPixmap pixmap;
+  if (!image->peekPixels (&pixmap))
+    {
+      /* For GPU surfaces, we need to read back the pixels.  */
+      SkImageInfo info
+	= SkImageInfo::Make (image->width (), image->height (),
+			     kRGBA_8888_SkColorType,
+			     kUnpremul_SkAlphaType);
+      std::vector<uint8_t> pixels (info.computeMinByteSize ());
+      if (!image->readPixels (info, pixels.data (), info.minRowBytes (),
+			      0, 0))
+	return false;
+
+      SkPixmap temp_pixmap (info, pixels.data (), info.minRowBytes ());
+      return encode_png_to_callback (temp_pixmap, write_fn, write_ctx);
+    }
+
+  return encode_png_to_callback (pixmap, write_fn, write_ctx);
+}
+
+/* Write image to PNG using a callback function.
+   Returns true on success, false on failure.  */
+bool
+emacs_skia_image_write_to_png (emacs_skia_image_t *image,
+			       emacs_skia_write_fn write_fn,
+			       void *write_ctx)
+{
+  if (!image || !image->image || !write_fn)
+    return false;
+
+  SkPixmap pixmap;
+  if (!image->image->peekPixels (&pixmap))
+    {
+      /* Need to read pixels back.  */
+      SkImageInfo info
+	= SkImageInfo::Make (image->image->width (),
+			     image->image->height (),
+			     kRGBA_8888_SkColorType,
+			     kUnpremul_SkAlphaType);
+      std::vector<uint8_t> pixels (info.computeMinByteSize ());
+      if (!image->image->readPixels (info, pixels.data (),
+				     info.minRowBytes (), 0, 0))
+	return false;
+
+      SkPixmap temp_pixmap (info, pixels.data (), info.minRowBytes ());
+      return encode_png_to_callback (temp_pixmap, write_fn, write_ctx);
+    }
+
+  return encode_png_to_callback (pixmap, write_fn, write_ctx);
+}
+
+/* ============================================================
+   Image Transformation
+   ============================================================ */
+
+/* Image transformation data for Skia.
+   This is used to store transformation matrices for images,
+   replacing the use of cairo_pattern_t for this purpose.  */
+struct emacs_skia_image_transform
+{
+  /* 3x3 transformation matrix in row-major order.
+     [a c e]   [0 2 4]
+     [b d f] = [1 3 5]
+     [0 0 1]   (implicit) */
+  float matrix[6];
+  /* Filter mode: true = bilinear, false = nearest neighbor.  */
+  bool smoothing;
+};
+
+emacs_skia_image_transform_t *
+emacs_skia_image_transform_create (void)
+{
+  auto result = new emacs_skia_image_transform_t;
+  /* Initialize to identity matrix.  */
+  result->matrix[0] = 1.0f; /* a = scale x */
+  result->matrix[1] = 0.0f; /* b = skew y */
+  result->matrix[2] = 0.0f; /* c = skew x */
+  result->matrix[3] = 1.0f; /* d = scale y */
+  result->matrix[4] = 0.0f; /* e = translate x */
+  result->matrix[5] = 0.0f; /* f = translate y */
+  result->smoothing = true;
+  return result;
+}
+
+void
+emacs_skia_image_transform_destroy (emacs_skia_image_transform_t *transform)
+{
+  delete transform;
+}
+
+void
+emacs_skia_image_transform_set_matrix (emacs_skia_image_transform_t *transform,
+				       const float matrix[6])
+{
+  if (transform && matrix)
+    {
+      for (int i = 0; i < 6; i++)
+	transform->matrix[i] = matrix[i];
+    }
+}
+
+void
+emacs_skia_image_transform_get_matrix (emacs_skia_image_transform_t *transform,
+				       float matrix[6])
+{
+  if (transform && matrix)
+    {
+      for (int i = 0; i < 6; i++)
+	matrix[i] = transform->matrix[i];
+    }
+}
+
+void
+emacs_skia_image_transform_set_smoothing (emacs_skia_image_transform_t *transform,
+					  bool smoothing)
+{
+  if (transform)
+    transform->smoothing = smoothing;
+}
+
+bool
+emacs_skia_image_transform_get_smoothing (emacs_skia_image_transform_t *transform)
+{
+  return transform ? transform->smoothing : true;
+}
+
+/* Draw an image with transformation applied.  */
+void
+emacs_skia_canvas_draw_image_transformed (emacs_skia_canvas_t *canvas,
+					  emacs_skia_image_t *image,
+					  emacs_skia_image_transform_t *transform,
+					  float x, float y,
+					  emacs_skia_paint_t *paint)
+{
+  if (!canvas || !canvas->canvas || !image || !image->image)
+    return;
+
+  canvas->canvas->save ();
+
+  if (transform)
+    {
+      /* Apply the transformation matrix.
+	 Skia uses column-major SkMatrix, but our matrix is in
+	 [a c e; b d f] format which maps to:
+	 SkMatrix: [scaleX, skewX, transX, skewY, scaleY, transY, ...] */
+      SkMatrix sk_matrix;
+      sk_matrix.setAll (transform->matrix[0], /* scaleX = a */
+			transform->matrix[2], /* skewX = c */
+			transform->matrix[4], /* transX = e */
+			transform->matrix[1], /* skewY = b */
+			transform->matrix[3], /* scaleY = d */
+			transform->matrix[5], /* transY = f */
+			0, 0, 1);
+      canvas->canvas->concat (sk_matrix);
+    }
+
+  SkSamplingOptions sampling (
+    transform && transform->smoothing ? SkFilterMode::kLinear
+				      : SkFilterMode::kNearest);
+
+  if (paint)
+    canvas->canvas->drawImage (image->image.get (), x, y, sampling,
+			       &paint->paint);
+  else
+    canvas->canvas->drawImage (image->image.get (), x, y, sampling);
+
+  canvas->canvas->restore ();
+}
+
+/* Calculate stride for pixel buffers (replacement for
+   cairo_format_stride_for_width). Skia uses 4-byte aligned rows for RGBA.  */
+int
+emacs_skia_format_stride_for_width (int format, int width)
+{
+  /* format: 0 = A8 (1 byte per pixel), 1 = RGB24/ARGB32 (4 bytes per pixel) */
+  int bytes_per_pixel = (format == 0) ? 1 : 4;
+  int stride = width * bytes_per_pixel;
+  /* Align to 4 bytes (Skia's default alignment).  */
+  return (stride + 3) & ~3;
+}
diff --git a/src/skia/emacs_skia.h b/src/skia/emacs_skia.h
new file mode 100644
index 00000000000..230ca35c169
--- /dev/null
+++ b/src/skia/emacs_skia.h
@@ -0,0 +1,650 @@
+/* Minimal Skia C API for Emacs
+   Copyright (C) 2024-2026 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs 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.
+
+GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. */
+
+#ifndef EMACS_SKIA_H
+#define EMACS_SKIA_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /* Opaque types */
+  typedef struct emacs_skia_surface emacs_skia_surface_t;
+  typedef struct emacs_skia_canvas emacs_skia_canvas_t;
+  typedef struct emacs_skia_paint emacs_skia_paint_t;
+  typedef struct emacs_skia_font emacs_skia_font_t;
+  typedef struct emacs_skia_typeface emacs_skia_typeface_t;
+  typedef struct emacs_skia_image emacs_skia_image_t;
+  typedef struct emacs_skia_gl_context emacs_skia_gl_context_t;
+
+  /* Color representation: ARGB in native byte order */
+  typedef uint32_t emacs_skia_color_t;
+
+#define EMACS_SKIA_COLOR(a, r, g, b)               \
+  (((uint32_t) (a) << 24) | ((uint32_t) (r) << 16) \
+   | ((uint32_t) (g) << 8) | (uint32_t) (b))
+
+#define EMACS_SKIA_COLOR_RGB(r, g, b) EMACS_SKIA_COLOR (255, r, g, b)
+
+  /* Rectangle */
+  typedef struct
+  {
+    float left, top, right, bottom;
+  } emacs_skia_rect_t;
+
+  /* Integer rectangle */
+  typedef struct
+  {
+    int32_t left, top, right, bottom;
+  } emacs_skia_irect_t;
+
+  /* Point */
+  typedef struct
+  {
+    float x, y;
+  } emacs_skia_point_t;
+
+  /* Glyph ID */
+  typedef uint16_t emacs_skia_glyph_t;
+
+  /* Font metrics */
+  typedef struct
+  {
+    float ascent;
+    float descent;
+    float leading;
+    float avg_char_width;
+    float max_char_width;
+    float x_height;
+    float cap_height;
+  } emacs_skia_font_metrics_t;
+
+  /* Glyph extents (replacement for cairo_text_extents_t) */
+  typedef struct
+  {
+    float x_bearing; /* Left side bearing */
+    float
+      y_bearing;  /* Top side bearing (negative = above baseline) */
+    float width;  /* Glyph width */
+    float height; /* Glyph height */
+    float x_advance; /* Horizontal advance */
+    float y_advance; /* Vertical advance (usually 0) */
+  } emacs_skia_glyph_extents_t;
+
+  /* Font extents (replacement for cairo_font_extents_t) */
+  typedef struct
+  {
+    float ascent;	 /* Distance from baseline to top */
+    float descent;	 /* Distance from baseline to bottom */
+    float height;	 /* Recommended line height */
+    float max_x_advance; /* Maximum horizontal advance */
+    float max_y_advance; /* Maximum vertical advance */
+  } emacs_skia_font_extents_t;
+
+  /* Blend modes */
+  typedef enum
+  {
+    EMACS_SKIA_BLEND_SRC,
+    EMACS_SKIA_BLEND_SRC_OVER,
+    EMACS_SKIA_BLEND_DST_OVER,
+    EMACS_SKIA_BLEND_CLEAR,
+    EMACS_SKIA_BLEND_XOR,
+    EMACS_SKIA_BLEND_DIFFERENCE,
+    EMACS_SKIA_BLEND_EXCLUSION,
+  } emacs_skia_blend_mode_t;
+
+  /* Opaque path type for complex shapes */
+  typedef struct emacs_skia_path emacs_skia_path_t;
+
+  /* Anti-alias mode */
+  typedef enum
+  {
+    EMACS_SKIA_ANTIALIAS_NONE,
+    EMACS_SKIA_ANTIALIAS_NORMAL,
+    EMACS_SKIA_ANTIALIAS_SUBPIXEL,
+  } emacs_skia_antialias_t;
+
+  /* Font hinting */
+  typedef enum
+  {
+    EMACS_SKIA_HINTING_NONE,
+    EMACS_SKIA_HINTING_SLIGHT,
+    EMACS_SKIA_HINTING_NORMAL,
+    EMACS_SKIA_HINTING_FULL,
+  } emacs_skia_hinting_t;
+
+  /* ============================================================
+     Initialization / Cleanup
+     ============================================================ */
+
+  /* Initialize the Skia subsystem. Call once at startup. */
+  void emacs_skia_init (void);
+
+  /* Cleanup the Skia subsystem. Call once at shutdown. */
+  void emacs_skia_cleanup (void);
+
+  /* ============================================================
+     Capability Queries
+     ============================================================ */
+
+  /* Query whether specific Skia features are available.
+     These functions return true if the feature was compiled in.  */
+  bool emacs_skia_has_gl_support (void);
+  bool emacs_skia_has_pdf_support (void);
+  bool emacs_skia_has_svg_support (void);
+
+  /* ============================================================
+     GL Context (for GPU-accelerated rendering)
+     ============================================================ */
+
+  /* Create a GL context for GPU rendering.
+     gl_get_proc: function to get GL proc addresses
+     Returns NULL on failure. */
+  typedef void (*emacs_skia_gl_proc_t) (void);
+  typedef emacs_skia_gl_proc_t (*emacs_skia_gl_get_proc_fn) (
+    void *ctx, const char *name);
+
+  emacs_skia_gl_context_t *
+  emacs_skia_gl_context_create (emacs_skia_gl_get_proc_fn get_proc,
+				void *ctx);
+
+  /* Create a GL context using the native GL interface.  This uses
+     the currently active GL context (e.g., set by GDK).  */
+  emacs_skia_gl_context_t *emacs_skia_gl_context_create_native (void);
+
+  void emacs_skia_gl_context_destroy (emacs_skia_gl_context_t *ctx);
+
+  /* Flush pending GPU operations */
+  void emacs_skia_gl_context_flush (emacs_skia_gl_context_t *ctx);
+
+  /* Reset the GL context state tracking.  Call this after destroying
+     a GL surface to clear Skia's internal caches.  */
+  void emacs_skia_gl_context_reset (emacs_skia_gl_context_t *ctx);
+
+  /* ============================================================
+     GL Fence Sync (for async GPU synchronization)
+     ============================================================ */
+
+  /* Opaque fence object for non-blocking GPU synchronization.  */
+  typedef struct emacs_skia_fence emacs_skia_fence_t;
+
+  /* Create a fence that will be signaled when all preceding GL
+     commands have completed on the GPU.  */
+  emacs_skia_fence_t *emacs_skia_fence_create (void);
+
+  /* Wait for fence to be signaled.  Returns true if signaled within
+     timeout_ns nanoseconds, false if timed out.  Pass 0 for no wait
+     (poll), or UINT64_MAX for infinite wait.  */
+  bool emacs_skia_fence_wait (emacs_skia_fence_t *fence, uint64_t timeout_ns);
+
+  /* Check if fence is signaled without waiting.  */
+  bool emacs_skia_fence_is_signaled (emacs_skia_fence_t *fence);
+
+  /* Destroy fence object.  */
+  void emacs_skia_fence_destroy (emacs_skia_fence_t *fence);
+
+  /* ============================================================
+     Surface
+     ============================================================ */
+
+  /* Create a raster (CPU) surface */
+  emacs_skia_surface_t *emacs_skia_surface_create_raster (int width,
+							  int height);
+
+  /* Create a GPU-backed surface using GL framebuffer */
+  emacs_skia_surface_t *emacs_skia_surface_create_gl (
+    emacs_skia_gl_context_t *ctx, int width, int height,
+    unsigned int framebuffer_id, unsigned int format);
+
+  /* Create a surface wrapping existing pixel data */
+  emacs_skia_surface_t *emacs_skia_surface_create_from_pixels (
+    int width, int height, void *pixels, size_t row_bytes);
+
+  void emacs_skia_surface_destroy (emacs_skia_surface_t *surface);
+
+  /* Get canvas from surface */
+  emacs_skia_canvas_t *
+  emacs_skia_surface_get_canvas (emacs_skia_surface_t *surface);
+
+  /* Get pixel data (for raster surfaces) */
+  void *emacs_skia_surface_get_pixels (emacs_skia_surface_t *surface);
+
+  /* Flush surface drawing */
+  void emacs_skia_surface_flush (emacs_skia_surface_t *surface);
+
+  int emacs_skia_surface_get_width (emacs_skia_surface_t *surface);
+  int emacs_skia_surface_get_height (emacs_skia_surface_t *surface);
+
+  /* Make an image snapshot from the surface (for scrolling/copying)
+   */
+  emacs_skia_image_t *emacs_skia_surface_make_image_snapshot (
+    emacs_skia_surface_t *surface);
+  emacs_skia_image_t *emacs_skia_surface_make_image_snapshot_rect (
+    emacs_skia_surface_t *surface, const emacs_skia_irect_t *rect);
+
+  /* Write callback for PNG/document output.
+     Returns number of bytes written, or 0 on error.  */
+  typedef size_t (*emacs_skia_write_fn) (void *ctx, const void *data,
+					 size_t size);
+
+  /* Write surface to PNG using a callback function.
+     Returns true on success, false on failure.  */
+  bool emacs_skia_surface_write_to_png (emacs_skia_surface_t *surface,
+					emacs_skia_write_fn write_fn,
+					void *write_ctx);
+
+  /* Write image to PNG using a callback function.
+     Returns true on success, false on failure.  */
+  bool emacs_skia_image_write_to_png (emacs_skia_image_t *image,
+				      emacs_skia_write_fn write_fn,
+				      void *write_ctx);
+
+  /* Calculate stride for pixel buffers.
+     format: 0 = A8 (1 byte/pixel), 1 = RGB24/ARGB32 (4 bytes/pixel).
+     Returns stride in bytes (4-byte aligned).  */
+  int emacs_skia_format_stride_for_width (int format, int width);
+
+  /* ============================================================
+     Image Transformation
+     ============================================================ */
+
+  /* Opaque image transformation type.  */
+  typedef struct emacs_skia_image_transform emacs_skia_image_transform_t;
+
+  /* Create/destroy image transformation.  */
+  emacs_skia_image_transform_t *emacs_skia_image_transform_create (void);
+  void emacs_skia_image_transform_destroy (
+    emacs_skia_image_transform_t *transform);
+
+  /* Set transformation matrix.
+     matrix is [a, b, c, d, e, f] representing:
+     [a c e]
+     [b d f]
+     [0 0 1]  */
+  void emacs_skia_image_transform_set_matrix (
+    emacs_skia_image_transform_t *transform, const float matrix[6]);
+  void emacs_skia_image_transform_get_matrix (
+    emacs_skia_image_transform_t *transform, float matrix[6]);
+
+  /* Set/get smoothing (filter mode).  */
+  void emacs_skia_image_transform_set_smoothing (
+    emacs_skia_image_transform_t *transform, bool smoothing);
+  bool emacs_skia_image_transform_get_smoothing (
+    emacs_skia_image_transform_t *transform);
+
+  /* ============================================================
+     Canvas (drawing context)
+     ============================================================ */
+
+  /* State save/restore */
+  void emacs_skia_canvas_save (emacs_skia_canvas_t *canvas);
+  void emacs_skia_canvas_save_layer (emacs_skia_canvas_t *canvas,
+				     const emacs_skia_rect_t *bounds);
+  void emacs_skia_canvas_restore (emacs_skia_canvas_t *canvas);
+  int emacs_skia_canvas_get_save_count (emacs_skia_canvas_t *canvas);
+  void
+  emacs_skia_canvas_restore_to_count (emacs_skia_canvas_t *canvas,
+				      int count);
+
+  /* Clear entire canvas */
+  void emacs_skia_canvas_clear (emacs_skia_canvas_t *canvas,
+				emacs_skia_color_t color);
+
+  /* Clipping */
+  void emacs_skia_canvas_clip_rect (emacs_skia_canvas_t *canvas,
+				    const emacs_skia_rect_t *rect);
+  void emacs_skia_canvas_clip_irect (emacs_skia_canvas_t *canvas,
+				     const emacs_skia_irect_t *rect);
+
+  /* Get current clip bounds */
+  void emacs_skia_canvas_get_clip_bounds (emacs_skia_canvas_t *canvas,
+					  emacs_skia_rect_t *bounds);
+
+  /* Transform */
+  void emacs_skia_canvas_translate (emacs_skia_canvas_t *canvas,
+				    float dx, float dy);
+  void emacs_skia_canvas_scale (emacs_skia_canvas_t *canvas, float sx,
+				float sy);
+
+  /* Drawing primitives */
+  void emacs_skia_canvas_draw_rect (emacs_skia_canvas_t *canvas,
+				    const emacs_skia_rect_t *rect,
+				    emacs_skia_paint_t *paint);
+
+  void emacs_skia_canvas_draw_irect (emacs_skia_canvas_t *canvas,
+				     const emacs_skia_irect_t *rect,
+				     emacs_skia_paint_t *paint);
+
+  void emacs_skia_canvas_draw_line (emacs_skia_canvas_t *canvas,
+				    float x0, float y0, float x1,
+				    float y1,
+				    emacs_skia_paint_t *paint);
+
+  /* Draw glyphs at specified positions */
+  void emacs_skia_canvas_draw_glyphs (
+    emacs_skia_canvas_t *canvas, int count,
+    const emacs_skia_glyph_t *glyphs,
+    const emacs_skia_point_t *positions, emacs_skia_point_t origin,
+    emacs_skia_font_t *font, emacs_skia_paint_t *paint);
+
+  /* Draw image */
+  void emacs_skia_canvas_draw_image (emacs_skia_canvas_t *canvas,
+				     emacs_skia_image_t *image,
+				     float x, float y,
+				     emacs_skia_paint_t *paint);
+
+  void emacs_skia_canvas_draw_image_rect (
+    emacs_skia_canvas_t *canvas, emacs_skia_image_t *image,
+    const emacs_skia_rect_t *src, const emacs_skia_rect_t *dst,
+    emacs_skia_paint_t *paint);
+
+  /* Draw image with transformation applied.  */
+  void emacs_skia_canvas_draw_image_transformed (
+    emacs_skia_canvas_t *canvas, emacs_skia_image_t *image,
+    emacs_skia_image_transform_t *transform, float x, float y,
+    emacs_skia_paint_t *paint);
+
+  /* Draw a path */
+  void emacs_skia_canvas_draw_path (emacs_skia_canvas_t *canvas,
+				    emacs_skia_path_t *path,
+				    emacs_skia_paint_t *paint);
+
+  /* Draw an arc (part of an ellipse)
+     oval: bounding rectangle of the ellipse
+     start_angle: starting angle in degrees (0 = 3 o'clock)
+     sweep_angle: arc extent in degrees (positive = clockwise)
+     use_center: if true, draws pie slice; if false, draws arc */
+  void emacs_skia_canvas_draw_arc (emacs_skia_canvas_t *canvas,
+				   const emacs_skia_rect_t *oval,
+				   float start_angle,
+				   float sweep_angle, bool use_center,
+				   emacs_skia_paint_t *paint);
+
+  /* Draw image with a mask (for stipples and transparency masks)
+     image: the image to draw
+     mask: alpha mask image (A8 format preferred)
+     x, y: destination position */
+  void emacs_skia_canvas_draw_image_with_mask (
+    emacs_skia_canvas_t *canvas, emacs_skia_image_t *image,
+    emacs_skia_image_t *mask, float x, float y,
+    emacs_skia_paint_t *paint);
+
+  /* ============================================================
+     Path (for complex shapes)
+     ============================================================ */
+
+  emacs_skia_path_t *emacs_skia_path_create (void);
+  void emacs_skia_path_destroy (emacs_skia_path_t *path);
+
+  /* Reset path to empty */
+  void emacs_skia_path_reset (emacs_skia_path_t *path);
+
+  /* Path construction */
+  void emacs_skia_path_move_to (emacs_skia_path_t *path, float x,
+				float y);
+  void emacs_skia_path_line_to (emacs_skia_path_t *path, float x,
+				float y);
+  void emacs_skia_path_rel_line_to (emacs_skia_path_t *path, float dx,
+				    float dy);
+
+  /* Add an arc to the path
+     oval: bounding rectangle of the ellipse
+     start_angle: starting angle in degrees
+     sweep_angle: arc extent in degrees */
+  void emacs_skia_path_arc_to (emacs_skia_path_t *path,
+			       const emacs_skia_rect_t *oval,
+			       float start_angle, float sweep_angle,
+			       bool force_move_to);
+
+  /* Close the current contour */
+  void emacs_skia_path_close (emacs_skia_path_t *path);
+
+  /* Add a rectangle to the path */
+  void emacs_skia_path_add_rect (emacs_skia_path_t *path,
+				 const emacs_skia_rect_t *rect);
+
+  /* Clip canvas to path */
+  void emacs_skia_canvas_clip_path (emacs_skia_canvas_t *canvas,
+				    emacs_skia_path_t *path);
+
+  /* ============================================================
+     Paint (style and color)
+     ============================================================ */
+
+  emacs_skia_paint_t *emacs_skia_paint_create (void);
+  void emacs_skia_paint_destroy (emacs_skia_paint_t *paint);
+
+  void emacs_skia_paint_set_color (emacs_skia_paint_t *paint,
+				   emacs_skia_color_t color);
+  emacs_skia_color_t
+  emacs_skia_paint_get_color (emacs_skia_paint_t *paint);
+
+  void emacs_skia_paint_set_alpha (emacs_skia_paint_t *paint,
+				   uint8_t alpha);
+
+  void emacs_skia_paint_set_antialias (emacs_skia_paint_t *paint,
+				       bool antialias);
+  void emacs_skia_paint_set_blend_mode (emacs_skia_paint_t *paint,
+					emacs_skia_blend_mode_t mode);
+
+  /* Fill vs stroke */
+  void emacs_skia_paint_set_stroke (emacs_skia_paint_t *paint,
+				    bool stroke);
+  void emacs_skia_paint_set_stroke_width (emacs_skia_paint_t *paint,
+					  float width);
+
+  /* Dash pattern for strokes.
+     intervals: array of on/off lengths (must have even count)
+     count: number of elements in intervals array
+     phase: offset into the dash pattern */
+  void emacs_skia_paint_set_dash (emacs_skia_paint_t *paint,
+				  const float *intervals, int count,
+				  float phase);
+
+  /* Clear dash pattern (solid line) */
+  void emacs_skia_paint_clear_dash (emacs_skia_paint_t *paint);
+
+  /* ============================================================
+     Font and Typeface
+     ============================================================ */
+
+  /* Load a typeface from a file path */
+  emacs_skia_typeface_t *
+  emacs_skia_typeface_create_from_file (const char *path);
+
+  /* Load a typeface from memory */
+  emacs_skia_typeface_t *
+  emacs_skia_typeface_create_from_data (const void *data,
+					size_t size);
+
+  /* Load a typeface by family name and style */
+  emacs_skia_typeface_t *emacs_skia_typeface_create_from_name (
+    const char *family_name, int weight, int width, int slant);
+
+#ifdef HAVE_FONTCONFIG
+  /* Forward declaration for FcPattern */
+  struct _FcPattern;
+  typedef struct _FcPattern FcPattern;
+
+  /* Load a typeface from a fontconfig pattern
+     (replacement for cairo_ft_font_face_create_for_pattern) */
+  emacs_skia_typeface_t *
+  emacs_skia_typeface_create_from_fc_pattern (FcPattern *pattern);
+#endif
+
+  void emacs_skia_typeface_destroy (emacs_skia_typeface_t *typeface);
+
+  /* Get the font file path from a typeface (if available) */
+  const char *
+  emacs_skia_typeface_get_path (emacs_skia_typeface_t *typeface);
+
+  /* Get FreeType face index within the font file */
+  int emacs_skia_typeface_get_index (emacs_skia_typeface_t *typeface);
+
+  /* Create a font from a typeface at given size */
+  emacs_skia_font_t *
+  emacs_skia_font_create (emacs_skia_typeface_t *typeface,
+			  float size);
+  void emacs_skia_font_destroy (emacs_skia_font_t *font);
+
+  void emacs_skia_font_set_size (emacs_skia_font_t *font, float size);
+  float emacs_skia_font_get_size (emacs_skia_font_t *font);
+
+  void emacs_skia_font_set_hinting (emacs_skia_font_t *font,
+				    emacs_skia_hinting_t hinting);
+  void emacs_skia_font_set_edging (emacs_skia_font_t *font,
+				   emacs_skia_antialias_t edging);
+  void emacs_skia_font_set_subpixel (emacs_skia_font_t *font,
+				     bool subpixel);
+
+  /* Get font metrics */
+  void
+  emacs_skia_font_get_metrics (emacs_skia_font_t *font,
+			       emacs_skia_font_metrics_t *metrics);
+
+  /* Get font extents (replacement for cairo_scaled_font_extents) */
+  void
+  emacs_skia_font_get_extents (emacs_skia_font_t *font,
+			       emacs_skia_font_extents_t *extents);
+
+  /* Get glyph extents (replacement for
+     cairo_scaled_font_glyph_extents) Returns extents for each glyph
+     in the array. */
+  void emacs_skia_font_get_glyph_extents (
+    emacs_skia_font_t *font, const emacs_skia_glyph_t *glyphs,
+    int count, emacs_skia_glyph_extents_t *extents);
+
+  /* Get bounds for multiple glyphs (array version) */
+  void emacs_skia_font_get_glyph_bounds (
+    emacs_skia_font_t *font, const emacs_skia_glyph_t *glyphs,
+    int count, emacs_skia_rect_t *bounds);
+
+  /* Convert text to glyphs (UTF-8 input) */
+  int emacs_skia_font_text_to_glyphs (emacs_skia_font_t *font,
+				      const char *text,
+				      size_t byte_length,
+				      emacs_skia_glyph_t *glyphs,
+				      int max_glyphs);
+
+  /* Convert single Unicode codepoint to glyph */
+  emacs_skia_glyph_t
+  emacs_skia_font_char_to_glyph (emacs_skia_font_t *font,
+				 int32_t codepoint);
+
+  /* Get glyph advance widths */
+  void emacs_skia_font_get_widths (emacs_skia_font_t *font,
+				   const emacs_skia_glyph_t *glyphs,
+				   int count, float *widths);
+
+  /* Measure text width */
+  float emacs_skia_font_measure_text (emacs_skia_font_t *font,
+				      const char *text,
+				      size_t byte_length);
+
+  /* ============================================================
+     Image
+     ============================================================ */
+
+  /* Create an image from pixel data (RGBA format) */
+  emacs_skia_image_t *emacs_skia_image_create_from_pixels (
+    int width, int height, const void *pixels, size_t row_bytes,
+    bool has_alpha);
+
+  /* Create an image from BGRA pixel data (Cairo/native format).
+     This is the format used by Cairo (ARGB32 in native byte order),
+     which on little-endian machines is BGRA when viewed as bytes. */
+  emacs_skia_image_t *emacs_skia_image_create_from_bgra_pixels (
+    int width, int height, const void *pixels, size_t row_bytes,
+    bool has_alpha);
+
+  /* Create an image from encoded data (PNG, JPEG, etc.) */
+  emacs_skia_image_t *
+  emacs_skia_image_create_from_encoded (const void *data,
+					size_t size);
+
+  void emacs_skia_image_destroy (emacs_skia_image_t *image);
+
+  int emacs_skia_image_get_width (emacs_skia_image_t *image);
+  int emacs_skia_image_get_height (emacs_skia_image_t *image);
+
+  /* Create a 1-bit (A1 format) image from packed bitmap data.
+     This is used for fringe bitmaps and stipple patterns.
+     data: packed 1-bit data, MSB first, rows padded to byte boundary
+     width, height: dimensions in pixels
+     stride: bytes per row (0 = calculate from width) */
+  emacs_skia_image_t *emacs_skia_image_create_from_bitmap (
+    const unsigned char *data, int width, int height, int stride);
+
+  /* Create a tiled shader/pattern from an image for stipple fills.
+     Returns a paint configured with the tiled image. */
+  void emacs_skia_paint_set_image_shader (emacs_skia_paint_t *paint,
+					  emacs_skia_image_t *image);
+
+  /* Clear the shader from paint (return to solid color) */
+  void emacs_skia_paint_clear_shader (emacs_skia_paint_t *paint);
+
+  /* ============================================================
+     Document Export (PDF/SVG)
+     ============================================================ */
+
+  /* Opaque document type for multi-page PDF output */
+  typedef struct emacs_skia_document emacs_skia_document_t;
+
+  /* Create a PDF document that writes to a callback.
+     width/height are the initial page dimensions in points.  */
+  emacs_skia_document_t *
+  emacs_skia_document_create_pdf (emacs_skia_write_fn write_fn,
+				  void *write_ctx, float width,
+				  float height);
+
+  /* Begin a new page in the document.
+     Returns a canvas to draw on.  */
+  emacs_skia_canvas_t *
+  emacs_skia_document_begin_page (emacs_skia_document_t *doc,
+				  float width, float height);
+
+  /* End the current page.  */
+  void emacs_skia_document_end_page (emacs_skia_document_t *doc);
+
+  /* Close and finalize the document.
+     This must be called to complete the output.  */
+  void emacs_skia_document_close (emacs_skia_document_t *doc);
+
+  /* Create an SVG canvas that writes to a callback.
+     SVG is a single-page format, so there's no document concept.
+     The returned canvas should be destroyed with
+     emacs_skia_svg_canvas_finish() when done.  */
+  emacs_skia_canvas_t *
+  emacs_skia_svg_canvas_create (emacs_skia_write_fn write_fn,
+				void *write_ctx, float width,
+				float height);
+
+  /* Finish and close the SVG canvas.
+     This writes the closing SVG tags and finalizes output.  */
+  void emacs_skia_svg_canvas_finish (emacs_skia_canvas_t *canvas);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* EMACS_SKIA_H */
-- 
2.52.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0002-Add-with-skia-configure-option-and-build-system-supp.patch

From 474d36197608da5228acfee9fa6daaaf04a5559f Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:05:15 +0100
Subject: [PATCH 2/6] Add --with-skia configure option and build system support

This adds the build infrastructure for the Skia graphics backend without
yet enabling its use.  The Skia C wrapper is compiled but Emacs continues
to use Cairo for rendering.

* configure.ac: Add --with-skia option for Skia graphics backend.
(HAVE_SKIA, SKIA_CFLAGS, SKIA_LIBS, SKIA_CXX_OBJ): New substitutions.
Check for Skia library via pkg-config or manual detection.
Check for OpenGL support via libepoxy for GPU acceleration.
Make Cairo optional when Skia is enabled for PGTK.

* src/Makefile.in (SKIA_CXX_OBJ, SKIA_CFLAGS, SKIA_LIBS): New variables.
(.cpp.o): New suffix rule for C++ compilation.
(obj): Add SKIA_CXX_OBJ.
(LIBES): Add SKIA_LIBS.
(skia/emacs_skia.o): New target for Skia wrapper.
(temacs): Link with C++ when HAVE_SKIA.
(mostlyclean): Clean skia/*.o files.
---
 configure.ac    | 183 +++++++++++++++++++++++++++++++++++++++++++-----
 src/Makefile.in |  27 ++++++-
 2 files changed, 190 insertions(+), 20 deletions(-)

diff --git a/configure.ac b/configure.ac
index 0d7c58d8020..f40abe80ee9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -553,6 +553,7 @@ AC_DEFUN
 OPTION_DEFAULT_ON([libsystemd],[don't compile with libsystemd support])
 OPTION_DEFAULT_ON([cairo],[don't compile with Cairo drawing])
 OPTION_DEFAULT_OFF([cairo-xcb], [use XCB surfaces for Cairo support])
+OPTION_DEFAULT_OFF([skia],[use Skia for GPU-accelerated drawing (experimental)])
 OPTION_DEFAULT_ON([xml2],[don't compile with XML parsing support])
 OPTION_DEFAULT_OFF([imagemagick],[compile with ImageMagick image support])
 OPTION_DEFAULT_ON([native-image-api], [don't use native image APIs (GDI+ on Windows)])
@@ -4431,7 +4432,8 @@ AC_DEFUN
 
 HAVE_CAIRO=no
 if test "${HAVE_X11}" = "yes"; then
-  if test "${with_cairo}" != "no"; then
+  dnl Skip Cairo check if Skia is explicitly enabled (Skia replaces Cairo)
+  if test "${with_cairo}" != "no" && test "${with_skia}" != "yes"; then
     CAIRO_REQUIRED=1.8.0
     CAIRO_MODULE="cairo >= $CAIRO_REQUIRED"
     EMACS_CHECK_MODULES([CAIRO], [$CAIRO_MODULE])
@@ -4471,7 +4473,8 @@ AC_DEFUN
     fi
     HAVE_XWIDGETS=$HAVE_WEBKIT
     XWIDGETS_OBJ="xwidget.o"
-    if test "$HAVE_X_WINDOWS" = "yes" && test "${with_cairo}" = "no"; then
+    dnl xwidgets on X11 requires cairo-xlib for compositing, even when using Skia
+    if test "$HAVE_X_WINDOWS" = "yes" && test "$HAVE_CAIRO" != "yes"; then
       CAIRO_XLIB_MODULES="cairo >= 1.8.0 cairo-xlib >= 1.8.0"
       EMACS_CHECK_MODULES([CAIRO_XLIB], [$CAIRO_XLIB_MODULES])
       if test $HAVE_CAIRO_XLIB = "yes"; then
@@ -4514,20 +4517,149 @@ AC_DEFUN
 fi
 AC_SUBST([XWIDGETS_OBJ])
 
-if test "$window_system" = "pgtk"; then
-  CAIRO_REQUIRED=1.12.0
-  CAIRO_MODULE="cairo >= $CAIRO_REQUIRED"
-  EMACS_CHECK_MODULES([CAIRO], [$CAIRO_MODULE])
-  if test $HAVE_CAIRO = yes; then
-    AC_DEFINE([USE_CAIRO], [1], [Define to 1 if using cairo.])
+dnl Skia support (experimental, for PGTK)
+HAVE_SKIA=no
+if test "${with_skia}" = "yes"; then
+  dnl Check for Skia library
+  AC_MSG_CHECKING([for Skia])
+
+  dnl Allow user to specify paths via environment variables
+  AC_ARG_VAR([SKIA_DIR], [Path to Skia source/installation])
+  AC_ARG_VAR([SKIA_CFLAGS], [C compiler flags for Skia])
+  AC_ARG_VAR([SKIA_LIBS], [linker flags for Skia])
+
+  dnl First check if SKIA_CFLAGS and SKIA_LIBS are already set (e.g., from Nix)
+  if test -n "$SKIA_CFLAGS" && test -n "$SKIA_LIBS"; then
+    HAVE_SKIA=yes
   else
-    AC_MSG_ERROR([cairo required but not found.])
+    dnl Try pkg-config
+    PKG_CHECK_EXISTS([skia], [
+      PKG_CHECK_MODULES([SKIA], [skia], [HAVE_SKIA=yes], [HAVE_SKIA=no])
+    ], [
+      dnl Manual detection - check for header file existence
+      if test -n "$SKIA_DIR"; then
+        dnl Check for Nix-style layout (include/skia/core/SkCanvas.h)
+        if test -f "$SKIA_DIR/include/skia/core/SkCanvas.h"; then
+          HAVE_SKIA=yes
+          SKIA_CFLAGS="-I$SKIA_DIR/include/skia"
+          if test -f "$SKIA_DIR/lib/libskia.so"; then
+            SKIA_LIBS="-L$SKIA_DIR/lib -lskia"
+          else
+            SKIA_LIBS="-lskia"
+          fi
+        dnl Check for source-build layout (include/core/SkCanvas.h)
+        elif test -f "$SKIA_DIR/include/core/SkCanvas.h"; then
+          HAVE_SKIA=yes
+          SKIA_CFLAGS="-I$SKIA_DIR"
+          if test -f "$SKIA_DIR/out/Release/libskia.a"; then
+            SKIA_LIBS="-L$SKIA_DIR/out/Release -lskia"
+          elif test -f "$SKIA_DIR/out/Release/libskia.so"; then
+            SKIA_LIBS="-L$SKIA_DIR/out/Release -lskia"
+          else
+            SKIA_LIBS="-L$SKIA_DIR/out/Release -lskia"
+            AC_MSG_WARN([Skia library not found at $SKIA_DIR/out/Release - you need to build Skia first])
+          fi
+        else
+          HAVE_SKIA=no
+        fi
+      else
+        dnl Try system paths
+        save_CPPFLAGS="$CPPFLAGS"
+        AC_CHECK_HEADER([skia/core/SkCanvas.h], [
+          HAVE_SKIA=yes
+          SKIA_CFLAGS=""
+          SKIA_LIBS="-lskia"
+        ], [HAVE_SKIA=no])
+        CPPFLAGS="$save_CPPFLAGS"
+      fi
+    ])
   fi
 
-  CFLAGS="$CFLAGS $CAIRO_CFLAGS"
-  LIBS="$LIBS $CAIRO_LIBS"
-  AC_SUBST([CAIRO_CFLAGS])
-  AC_SUBST([CAIRO_LIBS])
+  if test "$HAVE_SKIA" = "yes"; then
+    AC_DEFINE([USE_SKIA], [1], [Define to 1 if using Skia for drawing.])
+    CFLAGS="$CFLAGS $SKIA_CFLAGS"
+    CXXFLAGS="$CXXFLAGS $SKIA_CFLAGS"
+    dnl Ensure we link with C++ standard library
+    case "$SKIA_LIBS" in
+      *-lstdc++*) ;;
+      *) SKIA_LIBS="$SKIA_LIBS -lstdc++" ;;
+    esac
+
+    dnl Auto-detect OpenGL support for Skia GPU acceleration
+    dnl We use libepoxy which GTK3 already depends on
+    dnl SK_GANESH is required by Skia to enable the Ganesh GPU backend,
+    dnl without it SK_GL gets undefined by SkTypes.h
+    HAVE_SKIA_GL=no
+    AC_MSG_CHECKING([for OpenGL support (libepoxy) for Skia GPU acceleration])
+    PKG_CHECK_EXISTS([epoxy], [
+      PKG_CHECK_MODULES([EPOXY], [epoxy], [
+        HAVE_SKIA_GL=yes
+        AC_DEFINE([SK_GL], [1], [Define to 1 if Skia GL backend is available.])
+        AC_DEFINE([SK_GANESH], [1], [Define to 1 to enable Skia Ganesh GPU backend.])
+        SKIA_CFLAGS="$SKIA_CFLAGS -DSK_GL=1 -DSK_GANESH=1 $EPOXY_CFLAGS"
+        SKIA_LIBS="$SKIA_LIBS $EPOXY_LIBS"
+        AC_MSG_RESULT([yes])
+      ], [
+        AC_MSG_RESULT([no (libepoxy not usable)])
+      ])
+    ], [
+      dnl Try direct GL detection as fallback
+      AC_CHECK_LIB([GL], [glClear], [
+        HAVE_SKIA_GL=yes
+        AC_DEFINE([SK_GL], [1], [Define to 1 if Skia GL backend is available.])
+        AC_DEFINE([SK_GANESH], [1], [Define to 1 to enable Skia Ganesh GPU backend.])
+        SKIA_CFLAGS="$SKIA_CFLAGS -DSK_GL=1 -DSK_GANESH=1"
+        SKIA_LIBS="$SKIA_LIBS -lGL"
+        AC_MSG_RESULT([yes (direct libGL)])
+      ], [
+        AC_MSG_RESULT([no])
+      ])
+    ])
+
+    dnl OpenGL is required for Skia - fail if not found
+    if test "$HAVE_SKIA_GL" != "yes"; then
+      AC_MSG_ERROR([Skia backend requires OpenGL support (libepoxy or libGL). Install libepoxy-dev or mesa-dev.])
+    fi
+    AC_SUBST([HAVE_SKIA_GL])
+
+    AC_SUBST([SKIA_CFLAGS])
+    AC_SUBST([SKIA_LIBS])
+    AC_MSG_RESULT([yes])
+  else
+    AC_MSG_RESULT([no])
+    AC_MSG_WARN([Skia requested but not found. Install Skia or set SKIA_DIR/SKIA_CFLAGS/SKIA_LIBS.])
+  fi
+fi
+AC_SUBST([HAVE_SKIA])
+
+dnl Skia C++ objects (must come after HAVE_SKIA is set)
+SKIA_CXX_OBJ=
+if test "$HAVE_SKIA" = "yes"; then
+  SKIA_CXX_OBJ="skia/emacs_skia.o"
+fi
+AC_SUBST([SKIA_CXX_OBJ])
+
+if test "$window_system" = "pgtk"; then
+  dnl For PGTK, either Cairo or Skia is required
+  if test "$HAVE_SKIA" = "yes"; then
+    dnl Using Skia for PGTK
+    AC_MSG_NOTICE([Using Skia for PGTK rendering])
+  else
+    dnl Fall back to Cairo
+    CAIRO_REQUIRED=1.12.0
+    CAIRO_MODULE="cairo >= $CAIRO_REQUIRED"
+    EMACS_CHECK_MODULES([CAIRO], [$CAIRO_MODULE])
+    if test $HAVE_CAIRO = yes; then
+      AC_DEFINE([USE_CAIRO], [1], [Define to 1 if using cairo.])
+    else
+      AC_MSG_ERROR([Either cairo or skia required for PGTK but neither found.])
+    fi
+
+    CFLAGS="$CFLAGS $CAIRO_CFLAGS"
+    LIBS="$LIBS $CAIRO_LIBS"
+    AC_SUBST([CAIRO_CFLAGS])
+    AC_SUBST([CAIRO_LIBS])
+  fi
 fi
 
 if test "${HAVE_BE_APP}" = "yes"; then
@@ -4554,19 +4686,27 @@ AC_DEFUN
 ### Start of font-backend (under X11) section.
 is_xft_version_outdated=no
 if test "${HAVE_X11}" = "yes"; then
-  if test $HAVE_CAIRO = yes; then
+  if test "$HAVE_SKIA" = "yes" || test "$HAVE_CAIRO" = "yes"; then
+    dnl Skia and Cairo both use ftfont, which requires freetype/fontconfig.
     dnl Strict linkers fail with
     dnl ftfont.o: undefined reference to symbol 'FT_New_Face'
     dnl if -lfreetype is not specified.
     dnl The following is needed to set FREETYPE_LIBS.
     EMACS_CHECK_MODULES([FREETYPE], [freetype2])
 
-    test "$HAVE_FREETYPE" = "no" && AC_MSG_ERROR([cairo requires libfreetype])
+    if test "$HAVE_SKIA" = "yes"; then
+      test "$HAVE_FREETYPE" = "no" && AC_MSG_ERROR([skia requires libfreetype])
+    else
+      test "$HAVE_FREETYPE" = "no" && AC_MSG_ERROR([cairo requires libfreetype])
+    fi
 
     EMACS_CHECK_MODULES([FONTCONFIG], [fontconfig >= 2.2.0])
 
-    test "$HAVE_FONTCONFIG" = "no" &&
-      AC_MSG_ERROR([cairo requires libfontconfig])
+    if test "$HAVE_SKIA" = "yes"; then
+      test "$HAVE_FONTCONFIG" = "no" && AC_MSG_ERROR([skia requires libfontconfig])
+    else
+      test "$HAVE_FONTCONFIG" = "no" && AC_MSG_ERROR([cairo requires libfontconfig])
+    fi
     dnl For the "Does Emacs use" message at the end.
     HAVE_XFT=no
   else
@@ -4646,6 +4786,10 @@ AC_DEFUN
   if test "${HAVE_FREETYPE}" = "yes"; then
     AC_DEFINE([HAVE_FREETYPE], [1],
 	      [Define to 1 if using the freetype and fontconfig libraries.])
+    if test "${HAVE_FONTCONFIG}" = "yes"; then
+      AC_DEFINE([HAVE_FONTCONFIG], [1],
+		[Define to 1 if using the fontconfig library.])
+    fi
     OLD_CFLAGS=$CFLAGS
     OLD_LIBS=$LIBS
     CFLAGS="$CFLAGS $FREETYPE_CFLAGS"
@@ -4693,6 +4837,8 @@ AC_DEFUN
     HAVE_LIBOTF=no
     AC_DEFINE([HAVE_FREETYPE], [1],
 	      [Define to 1 if using the freetype and fontconfig libraries.])
+    AC_DEFINE([HAVE_FONTCONFIG], [1],
+	      [Define to 1 if using the fontconfig library.])
     if test "${with_libotf}" != "no"; then
       EMACS_CHECK_MODULES([LIBOTF], [libotf])
       if test "$HAVE_LIBOTF" = "yes"; then
@@ -7227,6 +7373,7 @@ AC_DEFUN
   XMENU_OBJ=xmenu.o
   XOBJ="xterm.o xfns.o xselect.o xrdb.o xsmfns.o xsettings.o"
   FONT_OBJ=xfont.o
+  dnl Note: skiafont.o added in later commit; for now Skia falls through to Cairo fonts
   if test "$HAVE_CAIRO" = "yes"; then
     FONT_OBJ="$FONT_OBJ ftfont.o ftcrfont.o"
   elif test "$HAVE_XFT" = "yes"; then
@@ -7237,6 +7384,7 @@ AC_DEFUN
 fi
 
 if test "${window_system}" = "pgtk"; then
+   dnl Note: skiafont.o added in later commit; for now use ftcrfont.o
    FONT_OBJ="ftfont.o ftcrfont.o"
 fi
 
@@ -7669,6 +7817,7 @@ AC_DEFUN
   Does Emacs use -lwebp?                                  ${HAVE_WEBP}
   Does Emacs use -lsqlite3?                               ${HAVE_SQLITE3}
   Does Emacs use cairo?                                   ${HAVE_CAIRO}
+  Does Emacs use skia?                                    ${HAVE_SKIA}
   Does Emacs use -llcms2?                                 ${HAVE_LCMS2}
   Does Emacs use imagemagick?                             ${HAVE_IMAGEMAGICK}
   Does Emacs use native APIs for images?                  ${NATIVE_IMAGE_API}
diff --git a/src/Makefile.in b/src/Makefile.in
index 111f94fb0ba..464582e2602 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -380,6 +380,10 @@ HAIKU_CXX_OBJ =
 HAIKU_LIBS = @HAIKU_LIBS@
 HAIKU_CFLAGS = @HAIKU_CFLAGS@
 
+SKIA_CXX_OBJ = @SKIA_CXX_OBJ@
+SKIA_CFLAGS = @SKIA_CFLAGS@
+SKIA_LIBS = @SKIA_LIBS@
+
 ANDROID_OBJ = @ANDROID_OBJ@
 ANDROID_LIBS = @ANDROID_LIBS@
 ANDROID_LDFLAGS = @ANDROID_LDFLAGS@
@@ -392,6 +396,7 @@ CHECK_STRUCTS =
 HAVE_PDUMPER = @HAVE_PDUMPER@
 
 HAVE_BE_APP = @HAVE_BE_APP@
+HAVE_SKIA = @HAVE_SKIA@
 
 ## ARM Macs require that all code have a valid signature.  Since pdump
 ## invalidates the signature, we must re-sign to fix it.
@@ -439,13 +444,15 @@ ALL_OBJC_CFLAGS =
 ALL_CXX_CFLAGS = $(EMACS_CFLAGS) \
   $(filter-out $(NON_CXX_CFLAGS),$(WARN_CFLAGS)) $(CXXFLAGS)
 
-.SUFFIXES: .c .m .cc
+.SUFFIXES: .c .m .cc .cpp
 .c.o:
 	$(AM_V_CC)$(CC) -c $(CPPFLAGS) $(ALL_CFLAGS) $(PROFILING_CFLAGS) $<
 .m.o:
 	$(AM_V_CC)$(CC) -c $(CPPFLAGS) $(ALL_OBJC_CFLAGS) $(PROFILING_CFLAGS) $<
 .cc.o:
 	$(AM_V_CXX)$(CXX) -c $(CPPFLAGS) $(ALL_CXX_CFLAGS) $(PROFILING_CFLAGS) $<
+.cpp.o:
+	$(AM_V_CXX)$(CXX) -c $(CPPFLAGS) $(ALL_CXX_CFLAGS) $(PROFILING_CFLAGS) $<
 
 base_obj = dispnew.o frame.o scroll.o xdisp.o menu.o $(XMENU_OBJ) window.o     \
 	charset.o coding.o category.o ccl.o character.o chartab.o bidi.o       \
@@ -468,7 +475,7 @@ base_obj =
 	$(W32_OBJ) $(WINDOW_SYSTEM_OBJ) $(XGSELOBJ)			       \
 	$(HAIKU_OBJ) $(PGTK_OBJ) $(ANDROID_OBJ)
 doc_obj = $(base_obj) $(NS_OBJC_OBJ)
-obj = $(doc_obj) $(HAIKU_CXX_OBJ)
+obj = $(doc_obj) $(HAIKU_CXX_OBJ) $(SKIA_CXX_OBJ)
 
 ## Object files used on some machine or other.
 ## These go in the DOC file on all machines in case they are needed.
@@ -577,7 +584,7 @@ LIBES =
    $(EUIDACCESS_LIBGEN) $(TIMER_TIME_LIB) $(DBUS_LIBS) \
    $(LIB_EXECINFO) $(XRANDR_LIBS) $(XINERAMA_LIBS) $(XFIXES_LIBS) \
    $(XDBE_LIBS) $(XSYNC_LIBS) \
-   $(LIBXML2_LIBS) $(LIBGPM) $(LIBS_SYSTEM) $(CAIRO_LIBS) \
+   $(LIBXML2_LIBS) $(LIBGPM) $(LIBS_SYSTEM) $(CAIRO_LIBS) $(SKIA_LIBS) \
    $(LIBS_TERMCAP) $(GETLOADAVG_LIBS) $(SETTINGS_LIBS) $(LIBSELINUX_LIBS) \
    $(FREETYPE_LIBS) $(FONTCONFIG_LIBS) $(HARFBUZZ_LIBS) $(LIBOTF_LIBS) $(M17N_FLT_LIBS) \
    $(LIBGNUTLS_LIBS) $(LIB_PTHREAD) $(GETADDRINFO_A_LIBS) $(LCMS2_LIBS) \
@@ -688,6 +695,15 @@ globals.h:
 
 $(ALLOBJS): globals.h
 
+## Skia C++ wrapper
+ifneq ($(SKIA_CXX_OBJ),)
+skia:
+	$(MKDIR_P) skia
+
+skia/emacs_skia.o: $(srcdir)/skia/emacs_skia.cpp skia $(config_h)
+	$(AM_V_CXX)$(CXX) -c $(CPPFLAGS) $(ALL_CXX_CFLAGS) $(SKIA_CFLAGS) -o $@ $<
+endif
+
 LIBEGNU_ARCHIVE = $(lib)/libgnu.a
 
 $(LIBEGNU_ARCHIVE): $(config_h)
@@ -709,6 +725,10 @@ temacs$(EXEEXT):
 	$(AM_V_CXXLD)$(CXX) -o $@.tmp \
 	  $(ALL_CFLAGS) $(TEMACS_LDFLAGS) $(LDFLAGS) \
 	  $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(W32_RES_LINK) $(LIBES) -lstdc++
+else ifeq ($(HAVE_SKIA),yes)
+	$(AM_V_CXXLD)$(CXX) -o $@.tmp \
+	  $(ALL_CFLAGS) $(TEMACS_LDFLAGS) $(LDFLAGS) \
+	  $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(W32_RES_LINK) $(LIBES) -lstdc++
 else
 	$(AM_V_CCLD)$(CC) -o $@.tmp \
 	  $(ALL_CFLAGS) $(CXXFLAGS) $(TEMACS_LDFLAGS) $(LDFLAGS) \
@@ -794,6 +814,7 @@ .PHONY:
 mostlyclean:
 	rm -f android-emacs libemacs.so
 	rm -f temacs$(EXEEXT) core ./*.core \#* ./*.o build-counter.c
+	rm -f skia/*.o
 	rm -f dmpstruct.h
 	rm -f emacs.pdmp
 	rm -f ../etc/DOC
-- 
2.52.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0003-Add-Skia-type-definitions-and-declarations-to-header.patch

From 55ac1cf641ad93b79db9c9b9ad24edff020c116d Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:06:13 +0100
Subject: [PATCH 3/6] Add Skia type definitions and declarations to headers

This adds the necessary type definitions, struct fields, and function
declarations to support the Skia graphics backend.  No functional
changes yet - these are just declarations.

* src/dispextern.h (Emacs_Pixmap, Emacs_Pix_Container)
(Emacs_Pix_Context): Extend conditionals for USE_SKIA.
(HAVE_NATIVE_TRANSFORMS): Include USE_SKIA.
(struct image): Add skia_data and skia_transform fields.

* src/pgtkterm.h: Include skia/emacs_skia.h when USE_SKIA.
(struct pgtk_bitmap_record): Add skia_image field.
(struct pgtk_output): Add Skia surface, canvas, GL context, and
related fields for GPU rendering.
(FRAME_SKIA_SURFACE, FRAME_SKIA_CANVAS, FRAME_SKIA_GL_CONTEXT)
(FRAME_SKIA_PAINT, FRAME_GL_AREA, FRAME_GDK_GL_CONTEXT): New macros.

* src/font.h (skiafont_driver, skiahbfont_driver, syms_of_skiafont):
New declarations for Skia font backend.

* src/ftfont.h (struct font_info): Add Skia-specific fields for
FT_Face access and metrics caching.
---
 src/dispextern.h | 23 +++++++++------
 src/font.h       |  7 +++++
 src/ftfont.h     |  7 +++++
 src/pgtkterm.h   | 74 ++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 99 insertions(+), 12 deletions(-)

diff --git a/src/dispextern.h b/src/dispextern.h
index 30785b9ccdf..b0bcd14ce70 100644
--- a/src/dispextern.h
+++ b/src/dispextern.h
@@ -38,7 +38,7 @@ #define DISPEXTERN_H_INCLUDED
 typedef XColor Emacs_Color;
 typedef Cursor Emacs_Cursor;
 #define No_Cursor (None)
-#ifndef USE_CAIRO
+#if !defined (USE_CAIRO) && !defined (USE_SKIA)
 typedef Pixmap Emacs_Pixmap;
 #endif
 typedef XRectangle Emacs_Rectangle;
@@ -113,14 +113,14 @@ xstrcasecmp (char const *a, char const *b)
 #ifdef HAVE_X_WINDOWS
 #include <X11/Xresource.h> /* for XrmDatabase */
 typedef struct x_display_info Display_Info;
-#ifndef USE_CAIRO
+#if !defined (USE_CAIRO) && !defined (USE_SKIA)
 typedef XImage *Emacs_Pix_Container;
 typedef XImage *Emacs_Pix_Context;
-#endif	/* !USE_CAIRO */
+#endif	/* !USE_CAIRO && !USE_SKIA */
 #define NativeRectangle XRectangle
 #endif
 
-#ifdef USE_CAIRO
+#if defined (USE_CAIRO) || defined (USE_SKIA)
 /* Minimal version of XImage.  */
 typedef struct
 {
@@ -3166,9 +3166,8 @@ reset_mouse_highlight (Mouse_HLInfo *hlinfo)
 
 #ifdef HAVE_WINDOW_SYSTEM
 
-# if (defined USE_CAIRO || defined HAVE_XRENDER				\
-      || defined HAVE_NS || defined HAVE_NTGUI || defined HAVE_HAIKU	\
-      || defined HAVE_ANDROID)
+# if (defined HAVE_X_WINDOWS || defined USE_CAIRO || defined USE_SKIA \
+     || defined HAVE_HAIKU || defined HAVE_NS || defined HAVE_ANDROID)
 #  define HAVE_NATIVE_TRANSFORMS
 # endif
 
@@ -3188,6 +3187,12 @@ reset_mouse_highlight (Mouse_HLInfo *hlinfo)
 #ifdef USE_CAIRO
   void *cr_data;
 #endif
+#ifdef USE_SKIA
+  /* Skia image for this image (separate from cr_data for hybrid builds).  */
+  void *skia_data;
+  /* Skia image transformation (for rotation/scaling).  */
+  void *skia_transform;
+#endif
 #ifdef HAVE_X_WINDOWS
   /* X images of the image, corresponding to the above Pixmaps.
      Non-NULL means it and its Pixmap counterpart may be out of sync
@@ -3712,8 +3717,8 @@ #define TRY_WINDOW_IGNORE_FONTS_CHANGE	(1 << 1)
 ptrdiff_t lookup_image (struct frame *, Lisp_Object, int);
 Lisp_Object image_spec_value (Lisp_Object, Lisp_Object, bool *);
 
-#if defined HAVE_X_WINDOWS || defined USE_CAIRO || defined HAVE_NS \
-  || defined HAVE_HAIKU || defined HAVE_ANDROID
+#if defined HAVE_X_WINDOWS || defined USE_CAIRO || defined USE_SKIA \
+  || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_ANDROID
 #define RGB_PIXEL_COLOR unsigned long
 #endif
 
diff --git a/src/font.h b/src/font.h
index b6db84aa10b..c7d139f0100 100644
--- a/src/font.h
+++ b/src/font.h
@@ -993,6 +993,13 @@ valid_font_driver (struct font_driver const *d)
 #endif	/* HAVE_HARFBUZZ */
 extern void syms_of_ftcrfont (void);
 #endif
+#ifdef USE_SKIA
+extern struct font_driver const skiafont_driver;
+#ifdef HAVE_HARFBUZZ
+extern struct font_driver skiahbfont_driver;
+#endif	/* HAVE_HARFBUZZ */
+extern void syms_of_skiafont (void);
+#endif
 
 #ifndef FONT_DEBUG
 #define FONT_DEBUG
diff --git a/src/ftfont.h b/src/ftfont.h
index ee56e2d7608..36636ab58e3 100644
--- a/src/ftfont.h
+++ b/src/ftfont.h
@@ -76,6 +76,13 @@ #define EMACS_FTFONT_H
   /* Font metrics cache.  */
   struct font_metrics **metrics;
   short metrics_nrows;
+/* USE_SKIA uses pure Skia + FreeType for font rendering.  */
+#elif defined (USE_SKIA)
+  /* Pure Skia path: use FreeType directly for FT_Face access.  */
+  FT_Face ft_face;	/* Direct FreeType face for metrics.  */
+  double bitmap_position_unit;
+  struct font_metrics **metrics;
+  short metrics_nrows;
 #else
   /* These are used by the XFT backend.  */
   Display *display;
diff --git a/src/pgtkterm.h b/src/pgtkterm.h
index abf16b3aef1..03d41e80e2d 100644
--- a/src/pgtkterm.h
+++ b/src/pgtkterm.h
@@ -40,12 +40,20 @@ #define _PGTKTERM_H_
 #include <cairo-svg.h>
 #endif
 
+# ifdef USE_SKIA
+#  include "skia/emacs_skia.h"
+# endif
+
 struct pgtk_bitmap_record
 {
   char *file;
   int refcount;
   int height, width, depth;
+# ifdef USE_SKIA
+  emacs_skia_image_t *skia_image;
+# else
   cairo_pattern_t *pattern;
+# endif
 };
 
 struct pgtk_device_t
@@ -412,12 +420,33 @@ #define BLUE_FROM_ULONG(color)	((color) & 0xff)
      Zero if not using an external tool bar or if tool bar is horizontal.  */
   int toolbar_left_width, toolbar_right_width;
 
-#ifdef USE_CAIRO
-  /* Cairo drawing contexts.  */
+#if defined (USE_CAIRO) || defined (USE_SKIA)
+  /* Cairo drawing contexts (also used for PDF/SVG export with Skia).  */
   cairo_t *cr_context, *cr_active;
   int cr_surface_desired_width, cr_surface_desired_height;
+#endif
+#ifdef USE_CAIRO
   /* Cairo surface for double buffering */
   cairo_surface_t *cr_surface_visible_bell;
+#endif
+#ifdef USE_SKIA
+  /* Skia drawing contexts.  */
+  emacs_skia_surface_t *skia_surface;
+  emacs_skia_canvas_t *skia_canvas;
+  emacs_skia_gl_context_t *skia_gl_context;
+  int skia_surface_desired_width, skia_surface_desired_height;
+  emacs_skia_paint_t *skia_paint; /* Reusable paint object */
+  emacs_skia_surface_t *skia_surface_visible_bell;
+  bool skia_gl_initialized;
+  /* Skia GL rendering support.  */
+  GtkWidget *gl_area;
+  GdkGLContext *gdk_gl_context;
+  unsigned int gl_framebuffer;
+  unsigned int gl_texture;
+  unsigned int gl_stencil;
+  gint64 last_render_time;
+  /* Track when GL state needs reset - avoids unnecessary resetContext calls.  */
+  bool skia_gl_state_dirty;
 #endif
   struct atimer *atimer_visible_bell;
 
@@ -523,10 +552,33 @@ #define FIRST_CHAR_POSITION(f)				\
   (! (FRAME_HAS_VERTICAL_SCROLL_BARS_ON_LEFT (f)) ? 0	\
    : FRAME_SCROLL_BAR_COLS (f))
 
+#if defined (USE_CAIRO) || defined (USE_SKIA)
 #define FRAME_CR_SURFACE_DESIRED_WIDTH(f)		\
   ((f)->output_data.pgtk->cr_surface_desired_width)
 #define FRAME_CR_SURFACE_DESIRED_HEIGHT(f) \
   ((f)->output_data.pgtk->cr_surface_desired_height)
+#endif
+
+#ifdef USE_SKIA
+# define FRAME_SKIA_SURFACE(f) ((f)->output_data.pgtk->skia_surface)
+# define FRAME_SKIA_CANVAS(f) ((f)->output_data.pgtk->skia_canvas)
+# define FRAME_SKIA_GL_CONTEXT(f) \
+    ((f)->output_data.pgtk->skia_gl_context)
+# define FRAME_SKIA_PAINT(f) ((f)->output_data.pgtk->skia_paint)
+# define FRAME_SKIA_SURFACE_DESIRED_WIDTH(f) \
+    ((f)->output_data.pgtk->skia_surface_desired_width)
+# define FRAME_SKIA_SURFACE_DESIRED_HEIGHT(f) \
+    ((f)->output_data.pgtk->skia_surface_desired_height)
+# define FRAME_SKIA_GL_INITIALIZED(f) \
+     ((f)->output_data.pgtk->skia_gl_initialized)
+# define FRAME_GL_AREA(f) ((f)->output_data.pgtk->gl_area)
+# define FRAME_GDK_GL_CONTEXT(f) ((f)->output_data.pgtk->gdk_gl_context)
+# define FRAME_GL_FRAMEBUFFER(f) ((f)->output_data.pgtk->gl_framebuffer)
+# define FRAME_GL_TEXTURE(f) ((f)->output_data.pgtk->gl_texture)
+# define FRAME_GL_STENCIL(f) ((f)->output_data.pgtk->gl_stencil)
+# define FRAME_LAST_RENDER_TIME(f) ((f)->output_data.pgtk->last_render_time)
+# define FRAME_SKIA_GL_STATE_DIRTY(f) ((f)->output_data.pgtk->skia_gl_state_dirty)
+#endif
 
 
 /* If a struct input_event has a kind which is SELECTION_REQUEST_EVENT
@@ -607,7 +659,9 @@ #define SELECTION_EVENT_TIME(eventp)	\
 extern void pgtk_set_no_accept_focus (struct frame *, Lisp_Object, Lisp_Object);
 extern void pgtk_set_z_group (struct frame *, Lisp_Object, Lisp_Object);
 
-/* Cairo related functions implemented in pgtkterm.c */
+#if defined (USE_CAIRO) || defined (USE_SKIA)
+/* Cairo related functions implemented in pgtkterm.c
+   (also needed as bridge for Skia rendering to GTK).  */
 extern void pgtk_cr_update_surface_desired_size (struct frame *, int, int, bool);
 extern cairo_t *pgtk_begin_cr_clip (struct frame *);
 extern void pgtk_end_cr_clip (struct frame *);
@@ -617,6 +671,20 @@ #define SELECTION_EVENT_TIME(eventp)	\
 extern void pgtk_cr_draw_frame (cairo_t *, struct frame *);
 extern void pgtk_cr_destroy_frame_context (struct frame *);
 extern Lisp_Object pgtk_cr_export_frames (Lisp_Object , cairo_surface_type_t);
+#endif
+
+#ifdef USE_SKIA
+/* Skia related functions implemented in pgtkterm.c */
+extern void pgtk_skia_update_surface_desired_size (struct frame *, int, int, bool);
+extern emacs_skia_canvas_t *pgtk_begin_skia_clip (struct frame *);
+extern void pgtk_end_skia_clip (struct frame *);
+extern void pgtk_skia_set_paint_foreground (struct frame *, Emacs_GC *);
+extern void pgtk_skia_set_paint_background (struct frame *, Emacs_GC *);
+extern void pgtk_skia_set_paint_color (struct frame *, unsigned long, bool);
+extern void pgtk_skia_draw_frame (struct frame *);
+extern Lisp_Object pgtk_skia_export_frames (Lisp_Object frames, Lisp_Object type);
+extern void pgtk_skia_destroy_frame_context (struct frame *);
+#endif
 
 /* Defined in pgtkmenu.c */
 extern Lisp_Object pgtk_popup_dialog (struct frame *, Lisp_Object, Lisp_Object);
-- 
2.52.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment; filename=0004-Add-Skia-font-driver.patch

From ebebb20005b8daf20f44b17e5b3563b3e167948a Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:06:52 +0100
Subject: [PATCH 4/6] Add Skia font driver

This adds the Skia font driver which uses FreeType for font discovery
and Skia for glyph rendering.  The driver implements the font_driver
interface with HarfBuzz support when available.

* src/skiafont.c: New file.  Skia font driver using FreeType for font
discovery and Skia for rendering.  Implements font_driver interface
with HarfBuzz support when available.

* src/font.c (syms_of_font): Register Skia font driver when USE_SKIA.

* configure.ac: Add skiafont.o to FONT_OBJ when HAVE_SKIA for both
X11 and PGTK builds.
---
 configure.ac   |  12 +-
 src/font.c     |   8 +-
 src/skiafont.c | 774 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 788 insertions(+), 6 deletions(-)
 create mode 100644 src/skiafont.c

diff --git a/configure.ac b/configure.ac
index f40abe80ee9..e775707ff6f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7373,8 +7373,9 @@ AC_DEFUN
   XMENU_OBJ=xmenu.o
   XOBJ="xterm.o xfns.o xselect.o xrdb.o xsmfns.o xsettings.o"
   FONT_OBJ=xfont.o
-  dnl Note: skiafont.o added in later commit; for now Skia falls through to Cairo fonts
-  if test "$HAVE_CAIRO" = "yes"; then
+  if test "$HAVE_SKIA" = "yes"; then
+    FONT_OBJ="$FONT_OBJ ftfont.o skiafont.o"
+  elif test "$HAVE_CAIRO" = "yes"; then
     FONT_OBJ="$FONT_OBJ ftfont.o ftcrfont.o"
   elif test "$HAVE_XFT" = "yes"; then
     FONT_OBJ="$FONT_OBJ ftfont.o xftfont.o"
@@ -7384,8 +7385,11 @@ AC_DEFUN
 fi
 
 if test "${window_system}" = "pgtk"; then
-   dnl Note: skiafont.o added in later commit; for now use ftcrfont.o
-   FONT_OBJ="ftfont.o ftcrfont.o"
+   if test "${HAVE_SKIA}" = "yes"; then
+     FONT_OBJ="ftfont.o skiafont.o"
+   else
+     FONT_OBJ="ftfont.o ftcrfont.o"
+   fi
 fi
 
 if test "${HAVE_BE_APP}" = "yes" ; then
diff --git a/src/font.c b/src/font.c
index fed90084219..e4e2fad5a97 100644
--- a/src/font.c
+++ b/src/font.c
@@ -6055,7 +6055,9 @@ syms_of_font (void)
   syms_of_ftfont ();
 #ifdef HAVE_X_WINDOWS
   syms_of_xfont ();
-#ifdef USE_CAIRO
+#ifdef USE_SKIA
+  syms_of_skiafont ();
+#elif defined USE_CAIRO
   syms_of_ftcrfont ();
 #else
 #ifdef HAVE_XFT
@@ -6063,7 +6065,9 @@ syms_of_font (void)
 #endif  /* HAVE_XFT */
 #endif  /* not USE_CAIRO */
 #else	/* not HAVE_X_WINDOWS */
-#ifdef USE_CAIRO
+#ifdef USE_SKIA
+  syms_of_skiafont ();
+#elif defined USE_CAIRO
   syms_of_ftcrfont ();
 #endif
 #endif	/* not HAVE_X_WINDOWS */
diff --git a/src/skiafont.c b/src/skiafont.c
new file mode 100644
index 00000000000..8430343d84e
--- /dev/null
+++ b/src/skiafont.c
@@ -0,0 +1,774 @@
+/* skiafont.c -- FreeType font driver with Skia rendering.
+   Copyright (C) 2024-2026 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs 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.
+
+GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. */
+
+/* This font driver uses FreeType for font discovery and Skia for
+   rendering.  It uses Skia for all font metrics and rendering.  */
+
+#include <config.h>
+
+#ifdef USE_SKIA
+
+#include <stdio.h>
+
+/* Debug logging for Skia font driver failures.
+   Define SKIA_DEBUG at compile time to enable verbose error messages.  */
+#ifdef SKIA_DEBUG
+#define SKIAFONT_LOG_ERROR(fmt, ...) \
+  fprintf (stderr, "skiafont: " fmt "\n", ##__VA_ARGS__)
+#else
+#define SKIAFONT_LOG_ERROR(fmt, ...) ((void)0)
+#endif
+
+# include <fontconfig/fontconfig.h>
+# include <ft2build.h>
+# include <math.h>
+# include FT_FREETYPE_H
+
+# include "lisp.h"
+# include "blockinput.h"
+# include "charset.h"
+# include "composite.h"
+# include "dispextern.h"
+# include "font.h"
+# include "ftfont.h"
+# include "pdumper.h"
+# include "pgtkterm.h"
+# include "xsettings.h"
+
+/* We extend font_info to include Skia-specific data.  */
+struct skia_font_info
+{
+  /* The base font_info from ftfont.  */
+  struct font_info base;
+
+  /* Skia typeface for this font.  */
+  emacs_skia_typeface_t *skia_typeface;
+
+  /* Skia font object (typeface + size).  */
+  emacs_skia_font_t *skia_font;
+};
+
+# define METRICS_NCOLS_PER_ROW (128)
+
+enum metrics_status
+{
+  METRICS_INVALID = -1, /* metrics entry is invalid */
+};
+
+# define METRICS_STATUS(metrics) \
+   ((metrics)->ascent + (metrics)->descent)
+# define METRICS_SET_STATUS(metrics, status) \
+   ((metrics)->ascent = 0, (metrics)->descent = (status))
+
+static int
+skiafont_glyph_extents (struct font *font, unsigned glyph,
+			struct font_metrics *metrics)
+{
+  struct skia_font_info *skiafont_info
+    = (struct skia_font_info *) font;
+  struct font_info *ftfont_info = &skiafont_info->base;
+  int row, col;
+  struct font_metrics *cache;
+
+  row = glyph / METRICS_NCOLS_PER_ROW;
+  col = glyph % METRICS_NCOLS_PER_ROW;
+  if (row >= ftfont_info->metrics_nrows)
+    {
+      ftfont_info->metrics
+	= xrealloc (ftfont_info->metrics,
+		    sizeof (struct font_metrics *) * (row + 1));
+      memset (ftfont_info->metrics + ftfont_info->metrics_nrows, 0,
+	      (sizeof (struct font_metrics *)
+	       * (row + 1 - ftfont_info->metrics_nrows)));
+      ftfont_info->metrics_nrows = row + 1;
+    }
+  if (ftfont_info->metrics[row] == NULL)
+    {
+      struct font_metrics *new;
+      int i;
+
+      new = xmalloc (sizeof (struct font_metrics)
+		     * METRICS_NCOLS_PER_ROW);
+      for (i = 0; i < METRICS_NCOLS_PER_ROW; i++)
+	METRICS_SET_STATUS (new + i, METRICS_INVALID);
+      ftfont_info->metrics[row] = new;
+    }
+  cache = ftfont_info->metrics[row] + col;
+
+  if (METRICS_STATUS (cache) == METRICS_INVALID)
+    {
+      /* Get glyph extents from Skia.  */
+      if (skiafont_info->skia_font)
+	{
+	  emacs_skia_glyph_t skia_glyph = glyph;
+	  emacs_skia_glyph_extents_t extents;
+
+	  emacs_skia_font_get_glyph_extents (skiafont_info->skia_font,
+					     &skia_glyph, 1,
+					     &extents);
+	  cache->lbearing = floor (extents.x_bearing);
+	  cache->rbearing = ceil (extents.width + extents.x_bearing);
+	  cache->width = lround (extents.x_advance);
+	  cache->ascent = ceil (-(double)extents.y_bearing - 1.0 / 256);
+	  cache->descent = ceil (extents.height + extents.y_bearing);
+	}
+      else
+	{
+	  /* Fallback: return zero metrics if no Skia font.  */
+	  cache->lbearing = 0;
+	  cache->rbearing = 0;
+	  cache->width = 0;
+	  cache->ascent = 0;
+	  cache->descent = 0;
+	}
+    }
+
+  if (metrics)
+    *metrics = *cache;
+
+  return cache->width;
+}
+
+static Lisp_Object
+skiafont_list (struct frame *f, Lisp_Object spec)
+{
+  return ftfont_list2 (f, spec, Qskia);
+}
+
+static Lisp_Object
+skiafont_match (struct frame *f, Lisp_Object spec)
+{
+  return ftfont_match2 (f, spec, Qskia);
+}
+
+/* FreeType library handle for direct font access.
+   Thread safety: This global is initialized once from the main thread
+   during font driver setup.  Emacs display code runs single-threaded,
+   so no synchronization is needed.  If Emacs ever becomes multi-threaded
+   for display operations, this would need mutex protection.  */
+static FT_Library ft_library;
+static bool ft_library_initialized;
+
+static bool
+skiafont_init_freetype (void)
+{
+  if (!ft_library_initialized)
+    {
+      if (FT_Init_FreeType (&ft_library) == 0)
+	ft_library_initialized = true;
+    }
+  return ft_library_initialized;
+}
+
+static Lisp_Object
+skiafont_open (struct frame *f, Lisp_Object entity, int pixel_size)
+{
+  FcResult result;
+  Lisp_Object val, filename, font_object;
+  FcPattern *pat, *match;
+  struct skia_font_info *skiafont_info;
+  struct font *font;
+  double size = 0;
+  char *filename_str;
+  int font_index = 0;
+  FT_Face ft_face = NULL;
+
+  val = assq_no_quit (QCfont_entity, AREF (entity, FONT_EXTRA_INDEX));
+  if (!CONSP (val))
+    return Qnil;
+  val = XCDR (val);
+  filename = XCAR (val);
+  size = XFIXNUM (AREF (entity, FONT_SIZE_INDEX));
+  if (size == 0)
+    size = pixel_size;
+
+  block_input ();
+
+  pat = ftfont_entity_pattern (entity, pixel_size);
+  FcConfigSubstitute (NULL, pat, FcMatchPattern);
+  FcDefaultSubstitute (pat);
+  match = FcFontMatch (NULL, pat, &result);
+  ftfont_fix_match (pat, match);
+  FcPatternDestroy (pat);
+
+  /* Get font index from the match.  */
+  FcPatternGetInteger (match, FC_INDEX, 0, &font_index);
+
+  font_object = font_build_object (VECSIZE (struct skia_font_info),
+				   Qskia, entity, size);
+  skiafont_info
+    = (struct skia_font_info *) XFONT_OBJECT (font_object);
+  font = &skiafont_info->base.font;
+  font->pixel_size = size;
+  font->driver = &skiafont_driver;
+  font->encoding_charset = -1;
+  font->repertory_charset = -1;
+  font->default_ascent = 0;
+  font->vertical_centering = false;
+  font->baseline_offset = 0;
+  font->relative_compose = 0;
+
+  skiafont_info->base.metrics = NULL;
+  skiafont_info->base.metrics_nrows = 0;
+  skiafont_info->base.bitmap_position_unit = 0;
+  skiafont_info->base.ft_face = NULL;
+
+  /* Create Skia typeface and font.  */
+  filename_str = SSDATA (filename);
+  skiafont_info->skia_typeface
+    = emacs_skia_typeface_create_from_file (filename_str);
+  if (skiafont_info->skia_typeface)
+    {
+      skiafont_info->skia_font
+	= emacs_skia_font_create (skiafont_info->skia_typeface, size);
+      if (skiafont_info->skia_font)
+	{
+	  emacs_skia_font_set_subpixel (skiafont_info->skia_font,
+					true);
+	  emacs_skia_font_set_hinting (skiafont_info->skia_font,
+				       EMACS_SKIA_HINTING_NORMAL);
+	}
+    }
+  else
+    skiafont_info->skia_font = NULL;
+
+  /* Get font metrics from Skia.  */
+  if (skiafont_info->skia_font)
+    {
+      emacs_skia_font_extents_t extents;
+      emacs_skia_font_get_extents (skiafont_info->skia_font,
+				   &extents);
+      font->ascent = lround (extents.ascent);
+      font->descent = lround (extents.descent);
+      font->height = lround (extents.height);
+
+      /* Calculate average_width properly by measuring printable ASCII
+	 characters, similar to ftfont.c.  Using max_x_advance would give
+	 the maximum character width, which is incorrect for proportional
+	 fonts and causes issues with image scaling.  */
+      {
+	int total_width = 0, n = 0, min_w = 0, space_w = 0;
+
+	for (int c = 32; c < 127; c++)
+	  {
+	    emacs_skia_glyph_t glyph
+	      = emacs_skia_font_char_to_glyph (skiafont_info->skia_font, c);
+	    if (glyph != 0)
+	      {
+		emacs_skia_glyph_extents_t glyph_ext;
+		emacs_skia_font_get_glyph_extents (skiafont_info->skia_font,
+						   &glyph, 1, &glyph_ext);
+		int w = lround (glyph_ext.x_advance);
+		if (w > 0)
+		  {
+		    total_width += w;
+		    n++;
+		    if (min_w == 0 || w < min_w)
+		      min_w = w;
+		    if (c == 32)
+		      space_w = w;
+		  }
+	      }
+	  }
+
+	if (n > 0)
+	  {
+	    font->average_width = total_width / n;
+	    font->min_width = min_w;
+	    font->space_width = space_w > 0 ? space_w : font->average_width;
+	  }
+	else
+	  {
+	    /* Fallback to max_x_advance if we couldn't measure characters.  */
+	    font->min_width = font->average_width = font->space_width
+	      = lround (extents.max_x_advance);
+	  }
+      }
+    }
+  else
+    {
+      /* Fallback to default metrics if Skia font creation failed.  */
+      SKIAFONT_LOG_ERROR ("failed to create Skia font for '%s', "
+			  "using fallback metrics", filename_str);
+      font->ascent = pixel_size;
+      font->descent = pixel_size / 4;
+      font->height = font->ascent + font->descent;
+      font->min_width = font->average_width = font->space_width
+	= pixel_size / 2;
+    }
+
+  /* Get underline metrics from FreeType directly.  */
+  if (skiafont_init_freetype ()
+      && FT_New_Face (ft_library, filename_str, font_index, &ft_face)
+	   == 0)
+    {
+      if (ft_face->face_flags & FT_FACE_FLAG_SCALABLE)
+	{
+	  font->underline_position = -ft_face->underline_position
+				     * size / ft_face->units_per_EM;
+	  font->underline_thickness = ft_face->underline_thickness
+				      * size / ft_face->units_per_EM;
+	}
+      else
+	{
+	  font->underline_position = -1;
+	  font->underline_thickness = 1;
+	}
+      /* Store ft_face for later use (encode_char, etc.).  */
+      skiafont_info->base.ft_face = ft_face;
+    }
+  else
+    {
+      /* Default underline metrics.  */
+      font->underline_position = -1;
+      font->underline_thickness = 1;
+    }
+
+  FcPatternDestroy (match);
+  unblock_input ();
+
+  return font_object;
+}
+
+static void
+skiafont_close (struct font *font)
+{
+  struct skia_font_info *skiafont_info
+    = (struct skia_font_info *) font;
+  int i;
+
+  if (skiafont_info->skia_font)
+    {
+      emacs_skia_font_destroy (skiafont_info->skia_font);
+      skiafont_info->skia_font = NULL;
+    }
+  if (skiafont_info->skia_typeface)
+    {
+      emacs_skia_typeface_destroy (skiafont_info->skia_typeface);
+      skiafont_info->skia_typeface = NULL;
+    }
+
+  block_input ();
+
+  /* Close the FreeType face.  */
+  if (skiafont_info->base.ft_face)
+    {
+      FT_Done_Face (skiafont_info->base.ft_face);
+      skiafont_info->base.ft_face = NULL;
+    }
+
+  if (skiafont_info->base.metrics)
+    {
+      for (i = 0; i < skiafont_info->base.metrics_nrows; i++)
+	if (skiafont_info->base.metrics[i])
+	  xfree (skiafont_info->base.metrics[i]);
+      xfree (skiafont_info->base.metrics);
+      skiafont_info->base.metrics = NULL;
+    }
+
+  unblock_input ();
+}
+
+static int
+skiafont_has_char (Lisp_Object font, int c)
+{
+  if (FONT_ENTITY_P (font))
+    return ftfont_has_char (font, c);
+
+  struct charset *cs = NULL;
+
+  if (EQ (AREF (font, FONT_ADSTYLE_INDEX), Qja)
+      && charset_jisx0208 >= 0)
+    cs = CHARSET_FROM_ID (charset_jisx0208);
+  else if (EQ (AREF (font, FONT_ADSTYLE_INDEX), Qko)
+	   && charset_ksc5601 >= 0)
+    cs = CHARSET_FROM_ID (charset_ksc5601);
+  if (cs)
+    return (ENCODE_CHAR (cs, c) != CHARSET_INVALID_CODE (cs));
+
+  return -1;
+}
+
+static unsigned
+skiafont_encode_char (struct font *font, int c)
+{
+  struct skia_font_info *skiafont_info
+    = (struct skia_font_info *) font;
+
+  /* Use Skia if available.  */
+  if (skiafont_info->skia_font)
+    {
+      unsigned glyph
+	= emacs_skia_font_char_to_glyph (skiafont_info->skia_font, c);
+      /* Glyph index 0 means the character is not in the font.
+	 Return FONT_INVALID_CODE so Emacs will look for a fallback font.  */
+      if (glyph)
+	return glyph;
+      return FONT_INVALID_CODE;
+    }
+
+  /* Fall back to FreeType directly.  */
+  struct font_info *ftfont_info = &skiafont_info->base;
+  if (ftfont_info->ft_face)
+    {
+      unsigned glyph = FT_Get_Char_Index (ftfont_info->ft_face, c);
+      if (glyph)
+	return glyph;
+    }
+
+  return FONT_INVALID_CODE;
+}
+
+static void
+skiafont_text_extents (struct font *font, const unsigned int *code,
+		       int nglyphs, struct font_metrics *metrics)
+{
+  int i, width = 0;
+
+  memset (metrics, 0, sizeof (*metrics));
+
+  for (i = 0; i < nglyphs; i++)
+    {
+      struct font_metrics m;
+      int glyph_width = skiafont_glyph_extents (font, code[i], &m);
+
+      if (i == 0)
+	{
+	  metrics->lbearing = m.lbearing;
+	  metrics->rbearing = m.rbearing;
+	}
+      else
+	{
+	  if (metrics->lbearing > m.lbearing + width)
+	    metrics->lbearing = m.lbearing + width;
+	  if (metrics->rbearing < m.rbearing + width)
+	    metrics->rbearing = m.rbearing + width;
+	}
+      if (metrics->ascent < m.ascent)
+	metrics->ascent = m.ascent;
+      if (metrics->descent < m.descent)
+	metrics->descent = m.descent;
+
+      width += glyph_width;
+    }
+
+  metrics->width = width;
+}
+
+static int
+skiafont_draw (struct glyph_string *s, int from, int to, int x, int y,
+	       bool with_background)
+{
+  struct frame *f = s->f;
+  struct skia_font_info *skiafont_info
+    = (struct skia_font_info *) s->font;
+  int len = to - from;
+  int i;
+
+  block_input ();
+
+  emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+  if (!canvas)
+    {
+      unblock_input ();
+      return 0;
+    }
+
+  /* Apply clipping from glyph string to prevent drawing outside
+     bounds.  */
+  {
+    XRectangle clip_rects[2];
+    int n = get_glyph_string_clip_rects (s, clip_rects, 2);
+    for (int j = 0; j < n; j++)
+      {
+	emacs_skia_irect_t clip
+	  = { clip_rects[j].x, clip_rects[j].y,
+	      clip_rects[j].x + clip_rects[j].width,
+	      clip_rects[j].y + clip_rects[j].height };
+	emacs_skia_canvas_clip_irect (canvas, &clip);
+      }
+  }
+
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+
+  if (with_background)
+    {
+      pgtk_skia_set_paint_color (f, s->xgcv.background,
+				 s->hl != DRAW_CURSOR);
+      emacs_skia_irect_t rect
+	= { x, y - FONT_BASE (s->font), x + s->width,
+	    y - FONT_BASE (s->font) + FONT_HEIGHT (s->font) };
+      emacs_skia_canvas_draw_irect (canvas, &rect, paint);
+    }
+
+  /* Set foreground color for text.  */
+  pgtk_skia_set_paint_color (f, s->xgcv.foreground, false);
+
+  /* Draw glyphs using Skia if we have a Skia font, otherwise fall
+     back to Cairo-style rendering via the raster surface.  */
+  if (skiafont_info->skia_font)
+    {
+      /* Allocate glyph and position arrays.  */
+      emacs_skia_glyph_t *glyphs
+	= alloca (sizeof (emacs_skia_glyph_t) * len);
+      emacs_skia_point_t *positions
+	= alloca (sizeof (emacs_skia_point_t) * len);
+
+      float current_x = 0;
+      for (i = 0; i < len; i++)
+	{
+	  glyphs[i] = s->char2b[from + i];
+	  positions[i].x = current_x;
+	  positions[i].y = 0;
+	  current_x += (s->padding_p
+			  ? 1
+			  : skiafont_glyph_extents (s->font,
+						    glyphs[i], NULL));
+	}
+
+      emacs_skia_point_t origin = { x, y };
+      emacs_skia_canvas_draw_glyphs (canvas, len, glyphs, positions,
+				     origin, skiafont_info->skia_font,
+				     paint);
+    }
+  else
+    {
+      /* Fallback: draw each glyph as a rectangle (placeholder).  */
+      float current_x = x;
+      for (i = 0; i < len; i++)
+	{
+	  int glyph_width
+	    = skiafont_glyph_extents (s->font, s->char2b[from + i],
+				      NULL);
+	  /* For fallback, we could potentially use Cairo here,
+	     but for now just advance.  */
+	  current_x += (s->padding_p ? 1 : glyph_width);
+	}
+    }
+
+  pgtk_end_skia_clip (f);
+  unblock_input ();
+
+  return len;
+}
+
+/* These functions use the stored FT_Face directly for FreeType
+ * operations.  */
+
+static int
+skiafont_get_bitmap (struct font *font, unsigned int code,
+		     struct font_bitmap *bitmap, int bits_per_pixel)
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return -1;
+
+  ftfont_info->ft_size = ft_face->size;
+  int result = ftfont_get_bitmap (font, code, bitmap, bits_per_pixel);
+  ftfont_info->ft_size = NULL;
+
+  return result;
+}
+
+static int
+skiafont_anchor_point (struct font *font, unsigned int code, int idx,
+		       int *x, int *y)
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return -1;
+
+  ftfont_info->ft_size = ft_face->size;
+  int result = ftfont_anchor_point (font, code, idx, x, y);
+  ftfont_info->ft_size = NULL;
+
+  return result;
+}
+
+# ifdef HAVE_LIBOTF
+static Lisp_Object
+skiafont_otf_capability (struct font *font)
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return Qnil;
+
+  ftfont_info->ft_size = ft_face->size;
+  Lisp_Object result = ftfont_otf_capability (font);
+  ftfont_info->ft_size = NULL;
+
+  return result;
+}
+# endif
+
+# if defined HAVE_M17N_FLT && defined HAVE_LIBOTF
+static Lisp_Object
+skiafont_shape (Lisp_Object lgstring, Lisp_Object direction)
+{
+  struct font *font
+    = CHECK_FONT_GET_OBJECT (LGSTRING_FONT (lgstring));
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return Qnil;
+
+  ftfont_info->ft_size = ft_face->size;
+  Lisp_Object result = ftfont_shape (lgstring, direction);
+  ftfont_info->ft_size = NULL;
+
+  return result;
+}
+# endif
+
+# if defined HAVE_OTF_GET_VARIATION_GLYPHS \
+   || defined HAVE_FT_FACE_GETCHARVARIANTINDEX
+static int
+skiafont_variation_glyphs (struct font *font, int c,
+			   unsigned variations[256])
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return 0;
+
+  ftfont_info->ft_size = ft_face->size;
+  int result = ftfont_variation_glyphs (font, c, variations);
+  ftfont_info->ft_size = NULL;
+
+  return result;
+}
+# endif
+
+# ifdef HAVE_HARFBUZZ
+
+static Lisp_Object
+skiahbfont_list (struct frame *f, Lisp_Object spec)
+{
+  return ftfont_list2 (f, spec, Qskiahb);
+}
+
+static Lisp_Object
+skiahbfont_match (struct frame *f, Lisp_Object spec)
+{
+  return ftfont_match2 (f, spec, Qskiahb);
+}
+
+static hb_font_t *
+skiahbfont_begin_hb_font (struct font *font, double *position_unit)
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+  FT_Face ft_face = ftfont_info->ft_face;
+
+  if (!ft_face)
+    return NULL;
+
+  ftfont_info->ft_size = ft_face->size;
+  hb_font_t *hb_font = fthbfont_begin_hb_font (font, position_unit);
+  if ((hb_version_atleast (5, 2, 0) || !hb_version_atleast (5, 0, 0))
+      && ftfont_info->bitmap_position_unit)
+    *position_unit = ftfont_info->bitmap_position_unit;
+
+  return hb_font;
+}
+
+static void
+skiahbfont_end_hb_font (struct font *font, hb_font_t *hb_font)
+{
+  struct font_info *ftfont_info = (struct font_info *) font;
+
+  eassert (hb_font == ftfont_info->hb_font);
+  hb_font_destroy (ftfont_info->hb_font);
+  ftfont_info->hb_font = NULL;
+  ftfont_info->ft_size = NULL;
+}
+
+# endif /* HAVE_HARFBUZZ */
+
+static void syms_of_skiafont_for_pdumper (void);
+
+struct font_driver const skiafont_driver = {
+  .type = LISPSYM_INITIALLY (Qskia),
+  .get_cache = ftfont_get_cache,
+  .list = skiafont_list,
+  .match = skiafont_match,
+  .list_family = ftfont_list_family,
+  .open_font = skiafont_open,
+  .close_font = skiafont_close,
+  .has_char = skiafont_has_char,
+  .encode_char = skiafont_encode_char,
+  .text_extents = skiafont_text_extents,
+  .draw = skiafont_draw,
+  .get_bitmap = skiafont_get_bitmap,
+  .anchor_point = skiafont_anchor_point,
+# ifdef HAVE_LIBOTF
+  .otf_capability = skiafont_otf_capability,
+# endif
+# if defined HAVE_M17N_FLT && defined HAVE_LIBOTF
+  .shape = skiafont_shape,
+# endif
+# if defined HAVE_OTF_GET_VARIATION_GLYPHS \
+   || defined HAVE_FT_FACE_GETCHARVARIANTINDEX
+  .get_variation_glyphs = skiafont_variation_glyphs,
+# endif
+  .filter_properties = ftfont_filter_properties,
+  .combining_capability = ftfont_combining_capability,
+};
+
+# ifdef HAVE_HARFBUZZ
+struct font_driver skiahbfont_driver;
+# endif
+
+void
+syms_of_skiafont (void)
+{
+  DEFSYM (Qskia, "skia");
+# ifdef HAVE_HARFBUZZ
+  DEFSYM (Qskiahb, "skiahb");
+  Fput (Qskia, Qfont_driver_superseded_by, Qskiahb);
+# endif
+  pdumper_do_now_and_after_load (syms_of_skiafont_for_pdumper);
+}
+
+static void
+syms_of_skiafont_for_pdumper (void)
+{
+  register_font_driver (&skiafont_driver, NULL);
+# ifdef HAVE_HARFBUZZ
+  skiahbfont_driver = skiafont_driver;
+  skiahbfont_driver.type = Qskiahb;
+  skiahbfont_driver.list = skiahbfont_list;
+  skiahbfont_driver.match = skiahbfont_match;
+  skiahbfont_driver.otf_capability = hbfont_otf_capability;
+  skiahbfont_driver.shape = hbfont_shape;
+  skiahbfont_driver.combining_capability
+    = hbfont_combining_capability;
+  skiahbfont_driver.begin_hb_font = skiahbfont_begin_hb_font;
+  skiahbfont_driver.end_hb_font = skiahbfont_end_hb_font;
+  register_font_driver (&skiahbfont_driver, NULL);
+# endif
+}
+
+#endif /* USE_SKIA */
-- 
2.52.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0005-Implement-Skia-rendering-for-PGTK.patch

From 696f6d44d694a222cb00400eb225b0a920e87d99 Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:07:12 +0100
Subject: [PATCH 5/6] Implement Skia rendering for PGTK

This implements the Skia rendering backend for PGTK, providing
GPU-accelerated drawing for all Emacs primitives including text,
rectangles, images, and cursors.

* src/pgtkterm.c: Major changes to support Skia rendering.
Add Skia drawing implementations for all primitives: rectangles,
lines, fringe bitmaps, glyphs, composite glyphs, cursors, and images.
Implement GtkGLArea integration for GPU-accelerated rendering.
Add surface management with GL framebuffer support.

* src/pgtkfns.c (Fx_export_frames): Support Skia PDF/SVG/PNG export.
(update_watched_scale_factor): Use Skia surface update when USE_SKIA.
(Fx_create_frame): Register Skia font drivers when USE_SKIA.

* src/gtkutil.c (xg_tool_item_stale_p, update_frame_tool_bar):
Handle Skia image data for toolbar icons.

* src/image.c: Extend image handling for Skia backend.
(image_create_pix_container): Use Skia stride calculation.
(skia_put_image_to_skia_data): New function to convert pixel
containers to Skia images with proper alpha handling.
---
 src/gtkutil.c  |   19 +-
 src/image.c    |  255 +++++-
 src/pgtkfns.c  |   86 +-
 src/pgtkterm.c | 2141 +++++++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 2343 insertions(+), 158 deletions(-)

diff --git a/src/gtkutil.c b/src/gtkutil.c
index 9645bbad9c3..a463501e826 100644
--- a/src/gtkutil.c
+++ b/src/gtkutil.c
@@ -511,7 +511,8 @@ xg_get_image_for_pixmap (struct frame *f,
 {
 #ifdef USE_CAIRO
   cairo_surface_t *surface;
-#else
+#endif
+#ifdef HAVE_X_WINDOWS
   GdkPixbuf *icon_buf;
 #endif
 
@@ -568,7 +569,7 @@ xg_get_image_for_pixmap (struct frame *f,
 	}
 #endif	/* !HAVE_GTK3 */
     }
-#else
+#elif defined HAVE_X_WINDOWS
   /* This is a workaround to make icons look good on pseudo color
      displays.  Apparently GTK expects the images to have an alpha
      channel.  If they don't, insensitive and activated icons will
@@ -5796,7 +5797,11 @@ xg_tool_item_stale_p (GtkWidget *wbutton, const char *stock_name,
       void *old_img = (void *) gold_img;
       if (old_img != img->cr_data)
 	return 1;
-#else
+#elif defined USE_SKIA
+      void *old_img = (void *) gold_img;
+      if (old_img != img->skia_data)
+	return 1;
+#elif defined HAVE_X_WINDOWS
       Pixmap old_img = (Pixmap) gold_img;
       if (old_img != img->pixmap)
 	return 1;
@@ -6104,6 +6109,8 @@ update_frame_tool_bar (struct frame *f)
           if (img->load_failed_p
 #ifdef USE_CAIRO
 	      || img->cr_data == NULL
+#elif defined USE_SKIA
+	      || img->skia_data == NULL
 #else
 	      || img->pixmap == None
 #endif
@@ -6162,8 +6169,12 @@ update_frame_tool_bar (struct frame *f)
               g_object_set_data (G_OBJECT (w), XG_TOOL_BAR_IMAGE_DATA,
 #ifdef USE_CAIRO
                                  (gpointer)img->cr_data
-#else
+#elif defined USE_SKIA
+                                 (gpointer)img->skia_data
+#elif defined HAVE_X_WINDOWS
                                  (gpointer)img->pixmap
+#else
+                                 (gpointer)NULL
 #endif
 				 );
             }
diff --git a/src/image.c b/src/image.c
index 119287db899..056788ef298 100644
--- a/src/image.c
+++ b/src/image.c
@@ -64,6 +64,10 @@ Copyright (C) 1989-2026 Free Software Foundation, Inc.
 #include TERM_HEADER
 #endif /* HAVE_WINDOW_SYSTEM */
 
+#ifdef USE_SKIA
+# include "skia/emacs_skia.h"
+#endif
+
 #ifdef HAVE_X_WINDOWS
 typedef struct x_bitmap_record Bitmap_Record;
 #ifndef USE_CAIRO
@@ -76,9 +80,9 @@ #define PIX_MASK_DRAW	1
 #endif	/* !USE_CAIRO */
 #endif /* HAVE_X_WINDOWS */
 
-#if defined(USE_CAIRO) || defined(HAVE_NS)
+#if defined (USE_CAIRO) || defined (USE_SKIA) || defined (HAVE_NS)
 #define RGB_TO_ULONG(r, g, b) (((r) << 16) | ((g) << 8) | (b))
-#ifndef HAVE_NS
+#if defined (USE_CAIRO) && !defined (HAVE_NS)
 #define ARGB_TO_ULONG(a, r, g, b) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b))
 #endif
 #define RED_FROM_ULONG(color)	(((color) >> 16) & 0xff)
@@ -89,7 +93,7 @@ #define GREEN16_FROM_ULONG(color)	(GREEN_FROM_ULONG (color) * 0x101)
 #define BLUE16_FROM_ULONG(color)	(BLUE_FROM_ULONG (color) * 0x101)
 #endif
 
-#ifdef USE_CAIRO
+#if defined (USE_CAIRO) || defined (USE_SKIA)
 #define GET_PIXEL image_pix_context_get_pixel
 #define PUT_PIXEL image_pix_container_put_pixel
 #define NO_PIXMAP 0
@@ -99,7 +103,7 @@ #define PIX_MASK_DRAW	255
 
 static unsigned long image_alloc_image_color (struct frame *, struct image *,
 					      Lisp_Object, unsigned long);
-#endif	/* USE_CAIRO */
+#endif	/* USE_CAIRO || USE_SKIA */
 
 #if defined HAVE_PGTK && defined HAVE_IMAGEMAGICK
 /* In pgtk, we don't want to create scaled image.  If we create scaled
@@ -228,7 +232,7 @@ #define n_planes n_image_planes
 static void anim_prune_animation_cache (Lisp_Object);
 #endif
 
-#ifdef USE_CAIRO
+#if defined (USE_CAIRO) || defined (USE_SKIA)
 
 static Emacs_Pix_Container
 image_create_pix_container (unsigned int width, unsigned int height,
@@ -240,10 +244,17 @@ image_create_pix_container (unsigned int width, unsigned int height,
   pimg->width = width;
   pimg->height = height;
   pimg->bits_per_pixel = depth == 1 ? 8 : 32;
+#ifdef USE_CAIRO
   pimg->bytes_per_line = cairo_format_stride_for_width ((depth == 1
 							 ? CAIRO_FORMAT_A8
 							 : CAIRO_FORMAT_RGB24),
 							width);
+#elif defined (USE_SKIA)
+  /* Use Skia's stride calculation when Cairo is not available.
+     Format 0 = A8 (1 byte/pixel), 1 = RGB24/ARGB32 (4 bytes/pixel).  */
+  pimg->bytes_per_line
+    = emacs_skia_format_stride_for_width (depth == 1 ? 0 : 1, width);
+#endif
   pimg->data = xmalloc (pimg->bytes_per_line * height);
 
   return pimg;
@@ -268,6 +279,7 @@ image_pix_context_get_pixel (Emacs_Pix_Context image, int x, int y)
     return ((uint8_t *)(image->data + y * image->bytes_per_line))[x];
 }
 
+#ifdef USE_CAIRO
 static Emacs_Pix_Container
 image_pix_container_create_from_bitmap_data (char *data, unsigned int width,
 					     unsigned int height,
@@ -287,7 +299,6 @@ image_pix_container_create_from_bitmap_data (char *data, unsigned int width,
 
   return pimg;
 }
-
 static cairo_surface_t *
 cr_create_surface_from_pix_containers (Emacs_Pix_Container pimg,
 				       Emacs_Pix_Container mask)
@@ -352,6 +363,67 @@ cr_put_image_to_cr_data (struct image *img)
 
 #endif	/* USE_CAIRO */
 
+#endif	/* USE_CAIRO || USE_SKIA */
+
+#ifdef USE_SKIA
+/* Create a Skia image from pixel containers.
+   This converts the pixel data into a Skia image for rendering.  */
+static void
+skia_put_image_to_skia_data (struct image *img)
+{
+  if (img->pixmap && img->pixmap->data)
+    {
+      int width = img->pixmap->width;
+      int height = img->pixmap->height;
+      int stride = img->pixmap->bytes_per_line;
+      unsigned char *data = (unsigned char *) img->pixmap->data;
+
+      /* Create a copy of the data since Skia needs to own it.
+	 For Skia, we create a copy with premultiplied alpha.  */
+      size_t data_size = stride * height;
+      unsigned char *data_copy = xmalloc (data_size);
+      memcpy (data_copy, data, data_size);
+
+      /* If we have a mask, apply it to create premultiplied alpha.
+	 Otherwise, set alpha to 0xFF for Skia to render correctly.  */
+      if (img->mask && img->mask->data)
+	{
+	  unsigned char *mask_data = (unsigned char *) img->mask->data;
+	  int mask_stride = img->mask->bytes_per_line;
+	  for (int y = 0; y < height; y++)
+	    {
+	      uint32_t *row = (uint32_t *) (data_copy + y * stride);
+	      uint8_t *mask_row = mask_data + y * mask_stride;
+	      for (int x = 0; x < width; x++)
+		{
+		  uint8_t alpha = mask_row[x];
+		  uint32_t pixel = row[x];
+		  int r = ((pixel >> 16) & 0xFF) * alpha / 255;
+		  int g = ((pixel >> 8) & 0xFF) * alpha / 255;
+		  int b = (pixel & 0xFF) * alpha / 255;
+		  row[x] = (alpha << 24) | (r << 16) | (g << 8) | b;
+		}
+	    }
+	}
+      else
+	{
+	  /* No mask - set alpha to fully opaque.  */
+	  for (int y = 0; y < height; y++)
+	    {
+	      uint32_t *row = (uint32_t *) (data_copy + y * stride);
+	      for (int x = 0; x < width; x++)
+		row[x] = row[x] | 0xFF000000;
+	    }
+	}
+
+      bool has_alpha = (img->mask && img->mask->data);
+      img->skia_data = emacs_skia_image_create_from_bgra_pixels (
+	width, height, data_copy, stride, has_alpha);
+      xfree (data_copy);
+    }
+}
+#endif /* USE_SKIA */
+
 #ifdef HAVE_NS
 /* Use with images created by ns_image_for_XPM.  */
 static unsigned long
@@ -477,7 +549,7 @@ image_reference_bitmap (struct frame *f, ptrdiff_t id)
   ++FRAME_DISPLAY_INFO (f)->bitmaps[id - 1].refcount;
 }
 
-#ifdef HAVE_PGTK
+#if defined HAVE_PGTK && defined USE_CAIRO
 
 /* Create a Cairo pattern from the bitmap BITS, which should be WIDTH
    and HEIGHT in size.  BITS's fill order is LSB first, meaning that
@@ -572,7 +644,7 @@ image_bitmap_to_cr_pattern (char *bits, int width, int height)
   return pattern;
 }
 
-#endif /* HAVE_PGTK */
+#endif /* HAVE_PGTK && USE_CAIRO */
 
 /* Create a bitmap for frame F from a HEIGHT x WIDTH array of bits at BITS.  */
 
@@ -639,11 +711,11 @@ image_create_bitmap_from_data (struct frame *f, char *bits,
       return -1;
 #endif
 
-#ifdef HAVE_PGTK
+#if defined HAVE_PGTK && defined USE_CAIRO
   cairo_pattern_t *pattern;
 
   pattern = image_bitmap_to_cr_pattern (bits, width, height);
-#endif /* HAVE_PGTK */
+#endif /* HAVE_PGTK && USE_CAIRO */
 
 #ifdef HAVE_HAIKU
   void *bitmap, *stipple;
@@ -677,7 +749,11 @@ image_create_bitmap_from_data (struct frame *f, char *bits,
 
 #ifdef HAVE_PGTK
   dpyinfo->bitmaps[id - 1].depth = 1;
+# ifdef USE_SKIA
+  dpyinfo->bitmaps[id - 1].skia_image = NULL; /* Created on demand */
+# else
   dpyinfo->bitmaps[id - 1].pattern = pattern;
+# endif
 #endif
 
 #ifdef HAVE_HAIKU
@@ -861,8 +937,15 @@ image_create_bitmap_from_file (struct frame *f, Lisp_Object file)
   dpyinfo->bitmaps[id - 1].file = xlispstrdup (file);
   dpyinfo->bitmaps[id - 1].height = width;
   dpyinfo->bitmaps[id - 1].width = height;
+# ifdef USE_SKIA
+  dpyinfo->bitmaps[id - 1].skia_image
+    = emacs_skia_image_create_from_bitmap ((unsigned char *) data,
+					   width, height,
+					   (width + 7) / 8);
+# else
   dpyinfo->bitmaps[id - 1].pattern
     = image_bitmap_to_cr_pattern (data, width, height);
+# endif
   xfree (contents);
   xfree (data);
   return id;
@@ -1116,8 +1199,13 @@ free_bitmap_record (Display_Info *dpyinfo, Bitmap_Record *bm)
 #endif
 
 #ifdef HAVE_PGTK
+# ifdef USE_SKIA
+  if (bm->skia_image != NULL)
+    emacs_skia_image_destroy (bm->skia_image);
+# else
   if (bm->pattern != NULL)
     cairo_pattern_destroy (bm->pattern);
+# endif
 #endif
 
 #ifdef HAVE_HAIKU
@@ -1860,6 +1948,12 @@ prepare_image_for_display (struct frame *f, struct image *img)
 	     we have img->pixmap->data/img->mask->data.  */
 	  IMAGE_BACKGROUND (img, f, img->pixmap);
 	  IMAGE_BACKGROUND_TRANSPARENT (img, f, img->mask);
+# ifdef USE_SKIA
+	  /* Create Skia image BEFORE cr_put_image_to_cr_data,
+	     which frees the pixel data.  */
+	  if (img->skia_data == NULL)
+	    skia_put_image_to_skia_data (img);
+# endif
 	  cr_put_image_to_cr_data (img);
 	  if (img->cr_data == NULL)
 	    {
@@ -1869,6 +1963,17 @@ prepare_image_for_display (struct frame *f, struct image *img)
 	}
       unblock_input ();
     }
+#elif defined (USE_SKIA)
+  /* For Skia without Cairo, create Skia image from pixel containers.  */
+  if (!img->load_failed_p)
+    {
+      block_input ();
+      IMAGE_BACKGROUND (img, f, img->pixmap);
+      IMAGE_BACKGROUND_TRANSPARENT (img, f, img->mask);
+      if (img->skia_data == NULL)
+	skia_put_image_to_skia_data (img);
+      unblock_input ();
+    }
 #elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID
   if (!img->load_failed_p)
     {
@@ -2124,6 +2229,19 @@ image_clear_image_1 (struct frame *f, struct image *img, int flags)
       img->cr_data = NULL;
     }
 #endif	/* USE_CAIRO */
+
+#ifdef USE_SKIA
+  if (img->skia_data)
+    {
+      emacs_skia_image_destroy (img->skia_data);
+      img->skia_data = NULL;
+    }
+  if (img->skia_transform)
+    {
+      emacs_skia_image_transform_destroy (img->skia_transform);
+      img->skia_transform = NULL;
+    }
+#endif /* USE_SKIA */
 }
 
 /* Free X resources of image IMG which is used on frame F.  */
@@ -2889,6 +3007,8 @@ compute_image_size (struct frame *f, double width, double height,
 
 typedef double matrix3x3[3][3];
 
+#if defined USE_CAIRO || defined HAVE_XRENDER || defined HAVE_NTGUI \
+  || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_ANDROID
 static void
 matrix3x3_mult (matrix3x3 a, matrix3x3 b, matrix3x3 result)
 {
@@ -2901,6 +3021,7 @@ matrix3x3_mult (matrix3x3 a, matrix3x3 b, matrix3x3 result)
 	result[i][j] = sum;
       }
 }
+#endif
 
 static void
 compute_image_rotation (struct image *img, double *rotation)
@@ -3077,8 +3198,8 @@ image_set_transform (struct frame *f, struct image *img)
   /* Determine flipping.  */
   flip = !NILP (image_spec_value (img->spec, QCflip, NULL));
 
-# if defined USE_CAIRO || defined HAVE_XRENDER || defined HAVE_NS || defined HAVE_HAIKU \
-  || defined HAVE_ANDROID || defined HAVE_NTGUI
+# if defined USE_CAIRO || defined USE_SKIA || defined HAVE_XRENDER || defined HAVE_NS \
+  || defined HAVE_HAIKU || defined HAVE_ANDROID || defined HAVE_NTGUI
   /* We want scale up operations to use a nearest neighbor filter to
      show real pixels instead of munging them, but scale down
      operations to use a blended filter, to avoid aliasing and the like.  */
@@ -3350,6 +3471,44 @@ image_set_transform (struct frame *f, struct image *img)
      drawing time, so store it for later.  */
   ns_image_set_transform (img->pixmap, matrix);
   ns_image_set_smoothing (img->pixmap, smoothing);
+# elif defined USE_SKIA
+  /* Store transformation in Skia-native format.  */
+  {
+    emacs_skia_image_transform_t *transform
+      = emacs_skia_image_transform_create ();
+    if (transform)
+      {
+	/* Convert 3x3 matrix to Skia's 6-element format [a,b,c,d,e,f].  */
+	float skia_matrix[6] = {
+	  (float) matrix[0][0], /* a = scale x */
+	  (float) matrix[0][1], /* b = skew y */
+	  (float) matrix[1][0], /* c = skew x */
+	  (float) matrix[1][1], /* d = scale y */
+	  (float) matrix[2][0], /* e = translate x */
+	  (float) matrix[2][1]  /* f = translate y */
+	};
+	emacs_skia_image_transform_set_matrix (transform, skia_matrix);
+	emacs_skia_image_transform_set_smoothing (transform, smoothing);
+
+	/* Free any existing transform.  */
+	if (img->skia_transform)
+	  emacs_skia_image_transform_destroy (img->skia_transform);
+	img->skia_transform = transform;
+      }
+  }
+#  ifdef USE_CAIRO
+  /* For hybrid builds, also store in Cairo format for compatibility.  */
+  {
+    cairo_matrix_t cr_matrix = {matrix[0][0], matrix[0][1], matrix[1][0],
+				matrix[1][1], matrix[2][0], matrix[2][1]};
+    cairo_pattern_t *pattern = cairo_pattern_create_rgb (0, 0, 0);
+    cairo_pattern_set_matrix (pattern, &cr_matrix);
+    cairo_pattern_set_filter (pattern, smoothing
+			      ? CAIRO_FILTER_BEST : CAIRO_FILTER_NEAREST);
+    /* Dummy solid color pattern just to record pattern matrix.  */
+    img->cr_data = pattern;
+  }
+#  endif
 # elif defined USE_CAIRO
   cairo_matrix_t cr_matrix = {matrix[0][0], matrix[0][1], matrix[1][0],
 			      matrix[1][1], matrix[2][0], matrix[2][1]};
@@ -4040,7 +4199,7 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d
                                    Emacs_Pix_Container *pimg,
                                    Emacs_Pixmap *pixmap, Picture *picture)
 {
-#ifdef USE_CAIRO
+#if defined USE_CAIRO || defined USE_SKIA
   eassert (input_blocked_p ());
 
   /* Allocate a pixmap of the same size.  */
@@ -4204,11 +4363,11 @@ image_destroy_x_image (Emacs_Pix_Container pimg)
   eassert (input_blocked_p ());
   if (pimg)
     {
-#if defined USE_CAIRO || defined HAVE_HAIKU || defined HAVE_NS
+#if defined USE_CAIRO || defined USE_SKIA || defined HAVE_HAIKU || defined HAVE_NS
       /* On these systems, Emacs_Pix_Containers always point to the same
 	 data as pixmaps in `struct image', and therefore must never be
 	 freed separately.  */
-#endif	/* USE_CAIRO || HAVE_HAIKU || HAVE_NS */
+#endif	/* USE_CAIRO || USE_SKIA || HAVE_HAIKU || HAVE_NS */
 #ifdef HAVE_NTGUI
       /* Data will be freed by DestroyObject.  */
       pimg->data = NULL;
@@ -4227,7 +4386,7 @@ image_destroy_x_image (Emacs_Pix_Container pimg)
 gui_put_x_image (struct frame *f, Emacs_Pix_Container pimg,
                  Emacs_Pixmap pixmap, int width, int height)
 {
-#if defined USE_CAIRO || defined HAVE_HAIKU || defined HAVE_NS
+#if defined USE_CAIRO || defined USE_SKIA || defined HAVE_HAIKU || defined HAVE_NS
   eassert (pimg == pixmap);
 #elif defined HAVE_X_WINDOWS
   GC gc;
@@ -4343,7 +4502,7 @@ image_unget_x_image_or_dc (struct image *img, bool mask_p,
 static Emacs_Pix_Container
 image_get_x_image (struct frame *f, struct image *img, bool mask_p)
 {
-#if defined USE_CAIRO || defined (HAVE_HAIKU)
+#if defined USE_CAIRO || defined USE_SKIA || defined (HAVE_HAIKU)
   return !mask_p ? img->pixmap : img->mask;
 #elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID
   XImage *ximg_in_img = !mask_p ? img->ximg : img->mask_img;
@@ -5439,8 +5598,8 @@ #define Display xpm_Display
 #endif /* not HAVE_NTGUI */
 #endif /* HAVE_XPM */
 
-#if defined HAVE_XPM || defined USE_CAIRO || defined HAVE_NS	\
-  || defined HAVE_HAIKU || defined HAVE_ANDROID
+#if defined HAVE_XPM || defined USE_CAIRO || defined USE_SKIA \
+  || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_ANDROID
 
 /* Indices of image specification fields in xpm_format, below.  */
 
@@ -5799,7 +5958,7 @@ x_create_bitmap_from_xpm_data (struct frame *f, const char **bits)
 /* Load image IMG which will be displayed on frame F.  Value is
    true if successful.  */
 
-#if defined HAVE_XPM && !defined USE_CAIRO
+#if defined HAVE_XPM && !defined USE_CAIRO && !defined USE_SKIA
 
 static bool
 xpm_load (struct frame *f, struct image *img)
@@ -6108,9 +6267,10 @@ xpm_load (struct frame *f, struct image *img)
   return rc == XpmSuccess;
 }
 
-#endif /* HAVE_XPM && !USE_CAIRO */
+#endif /* HAVE_XPM && !USE_CAIRO && !USE_SKIA */
 
 #if (defined USE_CAIRO && defined HAVE_XPM)	\
+  || (defined USE_SKIA && defined HAVE_XPM)	\
   || (defined HAVE_NS && !defined HAVE_XPM)	\
   || (defined HAVE_HAIKU && !defined HAVE_XPM)  \
   || (defined HAVE_PGTK && !defined HAVE_XPM)	\
@@ -6507,7 +6667,7 @@ #define expect_ident(IDENT)					\
     }
 
   unsigned long frame_fg = FRAME_FOREGROUND_PIXEL (f);
-#ifdef USE_CAIRO
+#if defined USE_CAIRO || defined USE_SKIA
   {
     Emacs_Color color = {.pixel = frame_fg};
     FRAME_TERMINAL (f)->query_colors (f, &color, 1);
@@ -6617,7 +6777,7 @@ xpm_load (struct frame *f,
   return success_p;
 }
 
-#endif /* HAVE_NS && !HAVE_XPM */
+#endif /* USE_CAIRO || USE_SKIA || HAVE_NS || HAVE_HAIKU || HAVE_PGTK || HAVE_ANDROID (internal XPM parser) */
 
 
 
@@ -6878,8 +7038,8 @@ lookup_rgb_color (struct frame *f, int r, int g, int b)
 {
 #ifdef HAVE_NTGUI
   return PALETTERGB (r >> 8, g >> 8, b >> 8);
-#elif defined USE_CAIRO || defined HAVE_NS || defined HAVE_HAIKU	\
-  || defined HAVE_ANDROID
+#elif defined USE_CAIRO || defined USE_SKIA || defined HAVE_NS	\
+  || defined HAVE_HAIKU || defined HAVE_ANDROID
   return RGB_TO_ULONG (r >> 8, g >> 8, b >> 8);
 #else
   xsignal1 (Qfile_error,
@@ -6952,8 +7112,8 @@ image_to_emacs_colors (struct frame *f, struct image *img, bool rgb_p)
   p = colors;
   for (y = 0; y < img->height; ++y)
     {
-#if !defined USE_CAIRO && !defined HAVE_NS && !defined HAVE_HAIKU	\
-  && !defined HAVE_ANDROID
+#if !defined USE_CAIRO && !defined USE_SKIA && !defined HAVE_NS	\
+  && !defined HAVE_HAIKU && !defined HAVE_ANDROID
       Emacs_Color *row = p;
       for (x = 0; x < img->width; ++x, ++p)
 	p->pixel = GET_PIXEL (ximg, x, y);
@@ -6961,7 +7121,7 @@ image_to_emacs_colors (struct frame *f, struct image *img, bool rgb_p)
         {
           FRAME_TERMINAL (f)->query_colors (f, row, img->width);
         }
-#else  /* USE_CAIRO || HAVE_NS || HAVE_HAIKU || HAVE_ANDROID */
+#else  /* USE_CAIRO || USE_SKIA || HAVE_NS || HAVE_HAIKU || HAVE_ANDROID */
       for (x = 0; x < img->width; ++x, ++p)
 	{
 	  p->pixel = GET_PIXEL (ximg, x, y);
@@ -6972,7 +7132,7 @@ image_to_emacs_colors (struct frame *f, struct image *img, bool rgb_p)
 	      p->blue = BLUE16_FROM_ULONG (p->pixel);
 	    }
 	}
-#endif	/* USE_CAIRO || HAVE_NS || HAVE_ANDROID */
+#endif	/* USE_CAIRO || USE_SKIA || HAVE_NS || HAVE_ANDROID */
     }
 
   image_unget_x_image_or_dc (img, 0, ximg, prev);
@@ -7203,8 +7363,8 @@ image_edge_detection (struct frame *f, struct image *img,
 }
 
 
-#if defined HAVE_X_WINDOWS || defined USE_CAIRO || defined HAVE_HAIKU	\
-  || defined HAVE_ANDROID
+#if defined HAVE_X_WINDOWS || defined USE_CAIRO || defined USE_SKIA \
+  || defined HAVE_HAIKU || defined HAVE_ANDROID
 
 static void
 image_pixmap_draw_cross (struct frame *f, Emacs_Pixmap pixmap,
@@ -7231,6 +7391,29 @@ image_pixmap_draw_cross (struct frame *f, Emacs_Pixmap pixmap,
   cairo_set_line_width (cr, 1);
   cairo_stroke (cr);
   cairo_destroy (cr);
+#elif defined USE_SKIA
+  /* For Skia without Cairo, draw the cross directly on the pixel data.  */
+  if (pixmap && pixmap->data && pixmap->bits_per_pixel == 32)
+    {
+      uint32_t *data = (uint32_t *) pixmap->data;
+      int stride = pixmap->bytes_per_line / 4;
+      uint32_t pixel = 0xFF000000 | (color & 0xFFFFFF); /* ARGB */
+
+      /* Draw diagonal line from top-left to bottom-right.  */
+      for (unsigned int i = 0; i < width && i < height; i++)
+	{
+	  int px = x + i, py = y + i;
+	  if (px >= 0 && px < pixmap->width && py >= 0 && py < pixmap->height)
+	    data[py * stride + px] = pixel;
+	}
+      /* Draw diagonal line from bottom-left to top-right.  */
+      for (unsigned int i = 0; i < width && i < height; i++)
+	{
+	  int px = x + i, py = y + height - 1 - i;
+	  if (px >= 0 && px < pixmap->width && py >= 0 && py < pixmap->height)
+	    data[py * stride + px] = pixel;
+	}
+    }
 #elif HAVE_X_WINDOWS
   Display *dpy = FRAME_X_DISPLAY (f);
   GC gc = XCreateGC (dpy, pixmap, 0, NULL);
@@ -7299,17 +7482,17 @@ image_disable_image (struct frame *f, struct image *img)
 #ifndef HAVE_NTGUI
 #ifndef HAVE_NS  /* TODO: NS support, however this not needed for toolbars */
 
-#if !defined USE_CAIRO && !defined HAVE_HAIKU && !defined HAVE_ANDROID
+#if !defined USE_CAIRO && !defined USE_SKIA && !defined HAVE_HAIKU && !defined HAVE_ANDROID
 #define CrossForeground(f) BLACK_PIX_DEFAULT (f)
 #define MaskForeground(f)  WHITE_PIX_DEFAULT (f)
-#else  /* USE_CAIRO || HAVE_HAIKU */
+#else  /* USE_CAIRO || USE_SKIA || HAVE_HAIKU */
 #define CrossForeground(f) 0
 #define MaskForeground(f)  PIX_MASK_DRAW
-#endif	/* USE_CAIRO || HAVE_HAIKU */
+#endif	/* USE_CAIRO || USE_SKIA || HAVE_HAIKU */
 
-#if !defined USE_CAIRO && !defined HAVE_HAIKU
+#if !defined USE_CAIRO && !defined USE_SKIA && !defined HAVE_HAIKU
       image_sync_to_pixmaps (f, img);
-#endif	/* !USE_CAIRO && !HAVE_HAIKU */
+#endif	/* !USE_CAIRO && !USE_SKIA && !HAVE_HAIKU */
       image_pixmap_draw_cross (f, img->pixmap, 0, 0, img->width, img->height,
 			       CrossForeground (f));
       if (img->mask)
diff --git a/src/pgtkfns.c b/src/pgtkfns.c
index c336ce36d58..8127039cf84 100644
--- a/src/pgtkfns.c
+++ b/src/pgtkfns.c
@@ -890,7 +890,9 @@ DEFUN ("x-export-frames", Fx_export_frames, Sx_export_frames, 0, 2, 0,
      (Lisp_Object frames, Lisp_Object type)
 {
   Lisp_Object rest, tmp;
+#ifdef USE_CAIRO
   cairo_surface_type_t surface_type;
+#endif
 
   if (!CONSP (frames))
     frames = list1 (frames);
@@ -908,6 +910,12 @@ DEFUN ("x-export-frames", Fx_export_frames, Sx_export_frames, 0, 2, 0,
     }
   frames = Fnreverse (tmp);
 
+#ifdef USE_SKIA
+  /* Skia export supports pdf, svg, and png.  */
+  if (NILP (type) || EQ (type, Qpdf) || EQ (type, Qsvg) || EQ (type, Qpng))
+    return pgtk_skia_export_frames (frames, type);
+  error ("Skia export supports pdf, svg, and png types");
+#else /* USE_CAIRO */
 #ifdef CAIRO_HAS_PDF_SURFACE
   if (NILP (type) || EQ (type, Qpdf))
     surface_type = CAIRO_SURFACE_TYPE_PDF;
@@ -940,6 +948,7 @@ DEFUN ("x-export-frames", Fx_export_frames, Sx_export_frames, 0, 2, 0,
     error ("Unsupported export type");
 
   return pgtk_cr_export_frames (frames, surface_type);
+#endif /* USE_CAIRO */
 }
 
 extern frame_parm_handler pgtk_frame_parm_handlers[];
@@ -1122,10 +1131,17 @@ update_watched_scale_factor (struct atimer *timer)
   if (scale_factor != FRAME_X_OUTPUT (f)->watched_scale_factor)
     {
       FRAME_X_OUTPUT (f)->watched_scale_factor = scale_factor;
+#ifdef USE_SKIA
+      pgtk_skia_update_surface_desired_size (f,
+					     FRAME_SKIA_SURFACE_DESIRED_WIDTH (f),
+					     FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f),
+					     true);
+#else
       pgtk_cr_update_surface_desired_size (f,
 					   FRAME_CR_SURFACE_DESIRED_WIDTH (f),
 					   FRAME_CR_SURFACE_DESIRED_HEIGHT (f),
 					   true);
+#endif
     }
 }
 
@@ -1366,10 +1382,17 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, 1, 1, 0,
       specbind (Qx_resource_name, name);
     }
 
+#ifdef USE_SKIA
+  register_font_driver (&skiafont_driver, f);
+# ifdef HAVE_HARFBUZZ
+  register_font_driver (&skiahbfont_driver, f);
+# endif /* HAVE_HARFBUZZ */
+#else	/* !USE_SKIA */
   register_font_driver (&ftcrfont_driver, f);
-#ifdef HAVE_HARFBUZZ
+# ifdef HAVE_HARFBUZZ
   register_font_driver (&ftcrhbfont_driver, f);
-#endif	/* HAVE_HARFBUZZ */
+# endif /* HAVE_HARFBUZZ */
+#endif	/* !USE_SKIA */
 
   gui_default_parameter (f, parms, Qfont_backend, Qnil,
 			 "fontBackend", "FontBackend", RES_TYPE_STRING);
@@ -1718,7 +1741,12 @@ #define INSTALL_CURSOR(FIELD, NAME) \
 
   FRAME_X_OUTPUT (f)->border_color_css_provider = NULL;
 
+#ifdef USE_CAIRO
   FRAME_X_OUTPUT (f)->cr_surface_visible_bell = NULL;
+#endif
+#ifdef USE_SKIA
+  FRAME_X_OUTPUT (f)->skia_surface_visible_bell = NULL;
+#endif
   FRAME_X_OUTPUT (f)->atimer_visible_bell = NULL;
   FRAME_X_OUTPUT (f)->watched_scale_factor = 1.0;
   struct timespec ts = make_timespec (1, 0);
@@ -2659,10 +2687,17 @@ pgtk_create_tip_frame (struct pgtk_display_info *dpyinfo, Lisp_Object parms, str
       specbind (Qx_resource_name, name);
     }
 
+#ifdef USE_SKIA
+  register_font_driver (&skiafont_driver, f);
+# ifdef HAVE_HARFBUZZ
+  register_font_driver (&skiahbfont_driver, f);
+# endif /* HAVE_HARFBUZZ */
+#else	/* !USE_SKIA */
   register_font_driver (&ftcrfont_driver, f);
-#ifdef HAVE_HARFBUZZ
+# ifdef HAVE_HARFBUZZ
   register_font_driver (&ftcrhbfont_driver, f);
-#endif	/* HAVE_HARFBUZZ */
+# endif /* HAVE_HARFBUZZ */
+#endif	/* !USE_SKIA */
 
   gui_default_parameter (f, parms, Qfont_backend, Qnil,
                          "fontBackend", "FontBackend", RES_TYPE_STRING);
@@ -3273,7 +3308,11 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0,
 
   unblock_input ();
 
+#ifdef USE_SKIA
+  pgtk_skia_update_surface_desired_size (tip_f, width, height, false);
+#else
   pgtk_cr_update_surface_desired_size (tip_f, width, height, false);
+#endif
 
   w->must_be_updated_p = true;
   update_single_window (w);
@@ -3506,6 +3545,7 @@ position (0, 0) of the selected frame's terminal. */)
 }
 
 
+#ifdef USE_CAIRO
 DEFUN ("pgtk-page-setup-dialog", Fpgtk_page_setup_dialog,
        Spgtk_page_setup_dialog, 0, 0, 0,
        doc: /* Pop up a page setup dialog.
@@ -3518,7 +3558,20 @@ DEFUN ("pgtk-page-setup-dialog", Fpgtk_page_setup_dialog,
 
   return Qnil;
 }
+#elif defined USE_SKIA
+DEFUN ("pgtk-page-setup-dialog", Fpgtk_page_setup_dialog,
+       Spgtk_page_setup_dialog, 0, 0, 0,
+       doc: /* Pop up a page setup dialog.
+The current page setup can be obtained using `x-get-page-setup'.
+Note: Print dialogs are not available when Emacs is built with Skia.  */)
+  (void)
+{
+  error ("Print dialogs are not available in Skia builds; rebuild with Cairo for printing support");
+  return Qnil;
+}
+#endif
 
+#ifdef USE_CAIRO
 DEFUN ("pgtk-get-page-setup", Fpgtk_get_page_setup,
        Spgtk_get_page_setup, 0, 0, 0,
        doc: /* Return the value of the current page setup.
@@ -3548,7 +3601,19 @@ DEFUN ("pgtk-get-page-setup", Fpgtk_get_page_setup,
 
   return result;
 }
+#elif defined USE_SKIA
+DEFUN ("pgtk-get-page-setup", Fpgtk_get_page_setup,
+       Spgtk_get_page_setup, 0, 0, 0,
+       doc: /* Return the value of the current page setup.
+Note: Print dialogs are not available when Emacs is built with Skia.  */)
+  (void)
+{
+  error ("Print dialogs are not available in Skia builds; rebuild with Cairo for printing support");
+  return Qnil;
+}
+#endif
 
+#ifdef USE_CAIRO
 DEFUN ("pgtk-print-frames-dialog", Fpgtk_print_frames_dialog, Spgtk_print_frames_dialog, 0, 1, "",
        doc: /* Pop up a print dialog to print the current contents of FRAMES.
 FRAMES should be nil (the selected frame), a frame, or a list of
@@ -3583,6 +3648,17 @@ frames (each of which corresponds to one page).  Each frame should be
 
   return Qnil;
 }
+#elif defined USE_SKIA
+DEFUN ("pgtk-print-frames-dialog", Fpgtk_print_frames_dialog, Spgtk_print_frames_dialog, 0, 1, "",
+       doc: /* Pop up a print dialog to print the current contents of FRAMES.
+Note: Print dialogs are not available when Emacs is built with Skia.  */)
+  (Lisp_Object frames)
+{
+  (void) frames;
+  error ("Print dialogs are not available in Skia builds; rebuild with Cairo for printing support");
+  return Qnil;
+}
+#endif
 
 static void
 clean_up_dialog (void)
@@ -3829,9 +3905,11 @@ syms_of_pgtkfns (void)
   defsubr (&Sx_hide_tip);
 
   defsubr (&Sx_export_frames);
+#if defined USE_CAIRO || defined USE_SKIA
   defsubr (&Spgtk_page_setup_dialog);
   defsubr (&Spgtk_get_page_setup);
   defsubr (&Spgtk_print_frames_dialog);
+#endif
   defsubr (&Spgtk_backend_display_class);
 
   defsubr (&Spgtk_set_monitor_scale_factor);
diff --git a/src/pgtkterm.c b/src/pgtkterm.c
index bf482590bc5..b0f34396aff 100644
--- a/src/pgtkterm.c
+++ b/src/pgtkterm.c
@@ -26,6 +26,16 @@ Copyright (C) 1989, 1993-1994, 2005-2006, 2008-2026 Free Software
 #endif
 
 #include <cairo.h>
+#ifdef CAIRO_HAS_PDF_SURFACE
+# include <cairo-pdf.h>
+#endif
+#ifdef CAIRO_HAS_PS_SURFACE
+# include <cairo-ps.h>
+#endif
+#ifdef CAIRO_HAS_SVG_SURFACE
+# include <cairo-svg.h>
+#endif
+#include <errno.h>
 #include <fcntl.h>
 #include <math.h>
 #include <pthread.h>
@@ -69,9 +79,32 @@ Copyright (C) 1989, 1993-1994, 2005-2006, 2008-2026 Free Software
 #include <gdk/gdkwayland.h>
 #endif
 
+#ifdef USE_SKIA
+# include "skia/emacs_skia.h"
+# include <epoxy/gl.h> /* For GL types and functions */
+
+/* Convert Emacs pixel color (0xRRGGBB) to Skia color (0xAARRGGBB). */
+static inline emacs_skia_color_t
+pgtk_color_to_skia (unsigned long color)
+{
+  return EMACS_SKIA_COLOR_RGB ((color >> 16) & 0xff,
+			       (color >> 8) & 0xff, color & 0xff);
+}
+
+/* Convert Emacs pixel color with explicit alpha to Skia color.  */
+static inline emacs_skia_color_t
+pgtk_color_to_skia_alpha (unsigned long color, uint8_t alpha)
+{
+  return EMACS_SKIA_COLOR (alpha, (color >> 16) & 0xff,
+			   (color >> 8) & 0xff, color & 0xff);
+}
+#endif
+
+#ifndef USE_SKIA
 #define FRAME_CR_CONTEXT(f)		((f)->output_data.pgtk->cr_context)
 #define FRAME_CR_ACTIVE_CONTEXT(f)	((f)->output_data.pgtk->cr_active)
 #define FRAME_CR_SURFACE(f)		(cairo_get_target (FRAME_CR_CONTEXT (f)))
+#endif
 
 /* Non-zero means that a HELP_EVENT has been generated since Emacs
    start.  */
@@ -112,8 +145,29 @@ #define FRAME_CR_SURFACE(f)		(cairo_get_target (FRAME_CR_CONTEXT (f)))
 static void pgtk_clear_frame_area (struct frame *, int, int, int, int);
 static void pgtk_fill_rectangle (struct frame *, unsigned long, int, int,
 				 int, int, bool);
+#ifdef USE_SKIA
+static void pgtk_skia_fill_rectangle (struct frame *, unsigned long,
+				      int, int, int, int, bool);
+static void pgtk_skia_draw_rectangle (struct frame *, unsigned long,
+				      int, int, int, int, bool);
+static void pgtk_skia_clip_to_row (struct window *,
+				   struct glyph_row *,
+				   enum glyph_row_area,
+				   emacs_skia_canvas_t *);
+static void pgtk_skia_set_clip_rectangles (struct frame *,
+					   emacs_skia_canvas_t *,
+					   XRectangle *, int);
+static void
+pgtk_skia_set_glyph_string_clipping (struct glyph_string *,
+				     emacs_skia_canvas_t *);
+static void
+pgtk_skia_set_glyph_string_clipping_exactly (struct glyph_string *,
+					     struct glyph_string *,
+					     emacs_skia_canvas_t *);
+#else
 static void pgtk_clip_to_row (struct window *, struct glyph_row *,
 			      enum glyph_row_area, cairo_t *);
+#endif
 static struct frame *pgtk_any_window_to_frame (GdkWindow *);
 static void pgtk_regenerate_devices (struct pgtk_display_info *);
 
@@ -266,6 +320,7 @@ pgtk_get_device_for_event (struct pgtk_display_info *dpyinfo,
    resize or other fundamental change.  This is called when that
    context's surface has completed drawing.  */
 
+#ifndef USE_SKIA
 static void
 flip_cr_context (struct frame *f)
 {
@@ -281,6 +336,7 @@ flip_cr_context (struct frame *f)
     }
   unblock_input ();
 }
+#endif
 
 
 static void
@@ -524,11 +580,20 @@ #define CLEAR_IF_EQ(FIELD)	\
 
   gtk_widget_destroy (FRAME_WIDGET (f));
 
+#ifdef USE_SKIA
+  if (FRAME_X_OUTPUT (f)->skia_surface_visible_bell != NULL)
+    {
+      emacs_skia_surface_destroy (
+	FRAME_X_OUTPUT (f)->skia_surface_visible_bell);
+      FRAME_X_OUTPUT (f)->skia_surface_visible_bell = NULL;
+    }
+#else
   if (FRAME_X_OUTPUT (f)->cr_surface_visible_bell != NULL)
     {
       cairo_surface_destroy (FRAME_X_OUTPUT (f)->cr_surface_visible_bell);
       FRAME_X_OUTPUT (f)->cr_surface_visible_bell = NULL;
     }
+#endif
 
   if (FRAME_X_OUTPUT (f)->atimer_visible_bell != NULL)
     {
@@ -1228,6 +1293,36 @@ pgtk_set_glyph_string_gc (struct glyph_string *s)
 /* Set clipping for output of glyph string S.  S may be part of a mode
    line or menu if we don't have X toolkit support.  */
 
+#ifdef USE_SKIA
+static void
+pgtk_skia_set_glyph_string_clipping (struct glyph_string *s,
+				     emacs_skia_canvas_t *canvas)
+{
+  XRectangle r[2];
+  int n = get_glyph_string_clip_rects (s, r, 2);
+
+  if (n > 0)
+    pgtk_skia_set_clip_rectangles (s->f, canvas, r, n);
+}
+
+static void
+pgtk_skia_set_glyph_string_clipping_exactly (
+  struct glyph_string *src, struct glyph_string *dst,
+  emacs_skia_canvas_t *canvas)
+{
+  dst->clip[0].x = src->x;
+  dst->clip[0].y = src->y;
+  dst->clip[0].width = src->width;
+  dst->clip[0].height = src->height;
+  dst->num_clips = 1;
+
+  emacs_skia_rect_t rect
+    = { src->x, src->y, src->x + src->width, src->y + src->height };
+  emacs_skia_canvas_clip_rect (canvas, &rect);
+}
+#endif
+
+#ifdef USE_CAIRO
 static void
 pgtk_set_glyph_string_clipping (struct glyph_string *s, cairo_t *cr)
 {
@@ -1261,6 +1356,7 @@ pgtk_set_glyph_string_clipping_exactly (struct glyph_string *src,
   cairo_rectangle (cr, src->x, src->y, src->width, src->height);
   cairo_clip (cr);
 }
+#endif
 
 /* RIF:
    Compute left and right overhang of glyph string S.  */
@@ -1317,6 +1413,51 @@ pgtk_clear_glyph_string_rect (struct glyph_string *s, int x, int y,
 fill_background_by_face (struct frame *f, struct face *face, int x, int y,
 			 int width, int height)
 {
+#ifdef USE_SKIA
+  if (face->stipple != 0)
+    {
+      /* Full Skia stipple implementation.  */
+      emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+      emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+      uint8_t alpha = (uint8_t) (f->alpha_background * 255);
+
+      emacs_skia_canvas_save (canvas);
+      emacs_skia_rect_t clip = { x, y, x + width, y + height };
+      emacs_skia_canvas_clip_rect (canvas, &clip);
+
+      /* Draw background color.  */
+      emacs_skia_paint_set_color (
+	paint, pgtk_color_to_skia_alpha (face->background, alpha));
+      emacs_skia_paint_set_blend_mode (paint, EMACS_SKIA_BLEND_SRC);
+      emacs_skia_paint_set_stroke (paint, false);
+      emacs_skia_canvas_draw_rect (canvas, &clip, paint);
+
+      /* Draw stipple pattern with foreground color.  */
+      emacs_skia_image_t *stipple_image
+	= FRAME_DISPLAY_INFO (f)
+	    ->bitmaps[face->stipple - 1]
+	    .skia_image;
+      if (stipple_image)
+	{
+	  emacs_skia_paint_set_color (
+	    paint,
+	    pgtk_color_to_skia_alpha (face->foreground, alpha));
+	  emacs_skia_paint_set_image_shader (paint, stipple_image);
+	  emacs_skia_canvas_draw_rect (canvas, &clip, paint);
+	  emacs_skia_paint_clear_shader (paint);
+	}
+
+      emacs_skia_paint_set_blend_mode (paint,
+				       EMACS_SKIA_BLEND_SRC_OVER);
+      emacs_skia_canvas_restore (canvas);
+    }
+  else
+    {
+      /* Simple solid fill - use Skia.  */
+      pgtk_skia_fill_rectangle (f, face->background, x, y, width,
+				height, true);
+    }
+#else
   cairo_t *cr = pgtk_begin_cr_clip (f);
   double r, g, b, a;
 
@@ -1344,6 +1485,7 @@ fill_background_by_face (struct frame *f, struct face *face, int x, int y,
     }
 
   pgtk_end_cr_clip (f);
+#endif
 }
 
 static void
@@ -1396,6 +1538,10 @@ pgtk_draw_glyph_string_background (struct glyph_string *s, bool force_p)
 pgtk_draw_rectangle (struct frame *f, unsigned long color, int x, int y,
 		     int width, int height, bool respect_alpha_background)
 {
+#ifdef USE_SKIA
+  pgtk_skia_draw_rectangle (f, color, x, y, width, height,
+			    respect_alpha_background);
+#else
   cairo_t *cr;
 
   cr = pgtk_begin_cr_clip (f);
@@ -1404,6 +1550,7 @@ pgtk_draw_rectangle (struct frame *f, unsigned long color, int x, int y,
   cairo_set_line_width (cr, 1);
   cairo_stroke (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* Draw the foreground of glyph string S.  */
@@ -1719,6 +1866,24 @@ pgtk_compute_lighter_color (struct frame *f, unsigned long *pixel,
 pgtk_fill_trapezoid_for_relief (struct frame *f, unsigned long color, int x,
 				int y, int width, int height, int top_p)
 {
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  emacs_skia_path_t *path = emacs_skia_path_create ();
+
+  emacs_skia_paint_set_color (paint, pgtk_color_to_skia (color));
+  emacs_skia_paint_set_stroke (paint, false);
+
+  emacs_skia_path_move_to (path, top_p ? x : x + height, y);
+  emacs_skia_path_line_to (path, x, y + height);
+  emacs_skia_path_line_to (path, top_p ? x + width - height : x + width,
+			   y + height);
+  emacs_skia_path_line_to (path, x + width, y);
+  emacs_skia_path_close (path);
+
+  emacs_skia_canvas_draw_path (canvas, path, paint);
+  emacs_skia_path_destroy (path);
+#else
   cairo_t *cr;
 
   cr = pgtk_begin_cr_clip (f);
@@ -1729,6 +1894,7 @@ pgtk_fill_trapezoid_for_relief (struct frame *f, unsigned long color, int x,
   cairo_line_to (cr, x + width, y);
   cairo_fill (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 enum corners
@@ -1745,6 +1911,48 @@ pgtk_erase_corners_for_relief (struct frame *f, unsigned long color, int x,
 			       int y, int width, int height, double radius,
 			       double margin, int corners)
 {
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  emacs_skia_path_t *clip_path = emacs_skia_path_create ();
+  int i;
+
+  /* Build clipping path from corner arcs */
+  for (i = 0; i < CORNER_LAST; i++)
+    if (corners & (1 << i))
+      {
+	double xm, ym, xc, yc;
+	emacs_skia_rect_t oval;
+
+	if (i == CORNER_TOP_LEFT || i == CORNER_BOTTOM_LEFT)
+	  xm = x - margin, xc = xm + radius;
+	else
+	  xm = x + width + margin, xc = xm - radius;
+	if (i == CORNER_TOP_LEFT || i == CORNER_TOP_RIGHT)
+	  ym = y - margin, yc = ym + radius;
+	else
+	  ym = y + height + margin, yc = ym - radius;
+
+	emacs_skia_path_move_to (clip_path, xm, ym);
+	/* Arc bounding box */
+	oval.left = xc - radius;
+	oval.top = yc - radius;
+	oval.right = xc + radius;
+	oval.bottom = yc + radius;
+	/* Convert from radians to degrees: i * 90 degrees */
+	emacs_skia_path_arc_to (clip_path, &oval, i * 90.0, 90.0,
+				false);
+      }
+
+  emacs_skia_canvas_save (canvas);
+  emacs_skia_canvas_clip_path (canvas, clip_path);
+  emacs_skia_paint_set_color (paint, pgtk_color_to_skia (color));
+  emacs_skia_paint_set_stroke (paint, false);
+  emacs_skia_rect_t rect = { x, y, x + width, y + height };
+  emacs_skia_canvas_draw_rect (canvas, &rect, paint);
+  emacs_skia_canvas_restore (canvas);
+  emacs_skia_path_destroy (clip_path);
+#else
   cairo_t *cr;
   int i;
 
@@ -1771,6 +1979,7 @@ pgtk_erase_corners_for_relief (struct frame *f, unsigned long color, int x,
   cairo_rectangle (cr, x, y, width, height);
   cairo_fill (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 static void
@@ -1822,6 +2031,27 @@ pgtk_setup_relief_colors (struct glyph_string *s)
     }
 }
 
+#ifdef USE_SKIA
+static void
+pgtk_skia_set_clip_rectangles (struct frame *f,
+			       emacs_skia_canvas_t *canvas,
+			       XRectangle *rectangles, int n)
+{
+  if (n > 0)
+    {
+      for (int i = 0; i < n; i++)
+	{
+	  emacs_skia_rect_t rect
+	    = { rectangles[i].x, rectangles[i].y,
+		rectangles[i].x + rectangles[i].width,
+		rectangles[i].y + rectangles[i].height };
+	  emacs_skia_canvas_clip_rect (canvas, &rect);
+	}
+    }
+}
+#endif
+
+#ifdef USE_CAIRO
 static void
 pgtk_set_clip_rectangles (struct frame *f, cairo_t *cr,
 			  XRectangle *rectangles, int n)
@@ -1834,6 +2064,7 @@ pgtk_set_clip_rectangles (struct frame *f, cairo_t *cr,
       cairo_clip (cr);
     }
 }
+#endif
 
 /* Draw a relief on frame F inside the rectangle given by LEFT_X,
    TOP_Y, RIGHT_X, and BOTTOM_Y.  WIDTH is the thickness of the relief
@@ -1853,8 +2084,6 @@ pgtk_draw_relief_rect (struct frame *f,
   unsigned long top_left_color, bottom_right_color;
   int corners = 0;
 
-  cairo_t *cr = pgtk_begin_cr_clip (f);
-
   if (raised_p)
     {
       top_left_color = FRAME_X_OUTPUT (f)->white_relief.xgcv.foreground;
@@ -1866,7 +2095,14 @@ pgtk_draw_relief_rect (struct frame *f,
       bottom_right_color = FRAME_X_OUTPUT (f)->white_relief.xgcv.foreground;
     }
 
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_canvas_save (canvas);
+  pgtk_skia_set_clip_rectangles (f, canvas, clip_rect, 1);
+#else
+  cairo_t *cr = pgtk_begin_cr_clip (f);
   pgtk_set_clip_rectangles (f, cr, clip_rect, 1);
+#endif
 
   if (left_p)
     {
@@ -1907,8 +2143,8 @@ pgtk_draw_relief_rect (struct frame *f,
 					right_x + 1 - left_x, hwidth, 0);
     }
   if (left_p && vwidth > 1)
-    pgtk_fill_rectangle (f, bottom_right_color, left_x, top_y,
-			 1, bottom_y + 1 - top_y, false);
+    pgtk_fill_rectangle (f, bottom_right_color, left_x, top_y, 1,
+			 bottom_y + 1 - top_y, false);
   if (top_p && hwidth > 1)
     pgtk_fill_rectangle (f, bottom_right_color, left_x, top_y,
 			 right_x + 1 - left_x, 1, false);
@@ -1917,7 +2153,12 @@ pgtk_draw_relief_rect (struct frame *f,
 				   top_y, right_x - left_x + 1,
 				   bottom_y - top_y + 1, 6, 1, corners);
 
+
+#ifdef USE_SKIA
+  emacs_skia_canvas_restore (canvas);
+#else
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* Draw a box on frame F inside the rectangle given by LEFT_X, TOP_Y,
@@ -1935,12 +2176,17 @@ pgtk_draw_box_rect (struct glyph_string *s, int left_x,
 {
   unsigned long foreground_backup;
 
-  cairo_t *cr = pgtk_begin_cr_clip (s->f);
-
   foreground_backup = s->xgcv.foreground;
   s->xgcv.foreground = s->face->box_color;
 
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (s->f);
+  emacs_skia_canvas_save (canvas);
+  pgtk_skia_set_clip_rectangles (s->f, canvas, clip_rect, 1);
+#else
+  cairo_t *cr = pgtk_begin_cr_clip (s->f);
   pgtk_set_clip_rectangles (s->f, cr, clip_rect, 1);
+#endif
 
   /* Top.  */
   pgtk_fill_rectangle (s->f, s->xgcv.foreground,
@@ -1966,7 +2212,11 @@ pgtk_draw_box_rect (struct glyph_string *s, int left_x,
 
   s->xgcv.foreground = foreground_backup;
 
+#ifdef USE_SKIA
+  emacs_skia_canvas_restore (canvas);
+#else
   pgtk_end_cr_clip (s->f);
+#endif
 }
 
 
@@ -2021,6 +2271,48 @@ pgtk_draw_glyph_string_box (struct glyph_string *s)
 pgtk_draw_horizontal_wave (struct frame *f, unsigned long color, int x, int y,
 			   int width, int height, int wave_length)
 {
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  emacs_skia_path_t *path = emacs_skia_path_create ();
+  double dx = wave_length, dy = height - 1;
+  int xoffset, n;
+
+  emacs_skia_canvas_save (canvas);
+  emacs_skia_rect_t clip = { x, y, x + width, y + height };
+  emacs_skia_canvas_clip_rect (canvas, &clip);
+
+  if (x >= 0)
+    {
+      xoffset = x % (wave_length * 2);
+      if (xoffset == 0)
+	xoffset = wave_length * 2;
+    }
+  else
+    xoffset = x % (wave_length * 2) + wave_length * 2;
+  n = (width + xoffset) / wave_length + 1;
+  if (xoffset > wave_length)
+    {
+      xoffset -= wave_length;
+      --n;
+      y += height - 1;
+      dy = -dy;
+    }
+
+  emacs_skia_path_move_to (path, x - xoffset + 0.5, y + 0.5);
+  while (--n >= 0)
+    {
+      emacs_skia_path_rel_line_to (path, dx, dy);
+      dy = -dy;
+    }
+
+  emacs_skia_paint_set_color (paint, pgtk_color_to_skia (color));
+  emacs_skia_paint_set_stroke (paint, true);
+  emacs_skia_paint_set_stroke_width (paint, 1.0);
+  emacs_skia_canvas_draw_path (canvas, path, paint);
+  emacs_skia_canvas_restore (canvas);
+  emacs_skia_path_destroy (path);
+#else
   cairo_t *cr;
   double dx = wave_length, dy = height - 1;
   int xoffset, n;
@@ -2056,6 +2348,7 @@ pgtk_draw_horizontal_wave (struct frame *f, unsigned long color, int x, int y,
   cairo_set_line_width (cr, 1);
   cairo_stroke (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 static void
@@ -2172,6 +2465,56 @@ pgtk_draw_glyph_string_bg_rect (struct glyph_string *s, int x, int y, int w,
     pgtk_clear_glyph_string_rect (s, x, y, w, h);
 }
 
+#ifdef USE_SKIA
+/* Draw an image using Skia.
+   image: Skia image to draw
+   src_x, src_y: source position within the image
+   width, height: dimensions to draw
+   dest_x, dest_y: destination position on the frame
+   overlay_p: if true, draw on top of existing content; if false, fill
+   background first */
+static void
+pgtk_skia_draw_image (struct frame *f, Emacs_GC *gc,
+		      emacs_skia_image_t *image, int src_x, int src_y,
+		      int width, int height, int dest_x, int dest_y,
+		      bool overlay_p)
+{
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+
+  emacs_skia_canvas_save (canvas);
+
+  /* Clip to destination rectangle */
+  emacs_skia_rect_t clip_rect
+    = { dest_x, dest_y, dest_x + width, dest_y + height };
+  emacs_skia_canvas_clip_rect (canvas, &clip_rect);
+
+  /* Fill background if not overlay mode */
+  if (!overlay_p)
+    {
+      emacs_skia_paint_set_color (paint, pgtk_color_to_skia (
+					   gc->background));
+      emacs_skia_paint_set_stroke (paint, false);
+      emacs_skia_canvas_draw_rect (canvas, &clip_rect, paint);
+    }
+
+  /* Draw the image */
+  emacs_skia_rect_t src_rect
+    = { src_x, src_y, src_x + width, src_y + height };
+  emacs_skia_rect_t dst_rect
+    = { dest_x, dest_y, dest_x + width, dest_y + height };
+
+  /* Reset paint to default for image drawing */
+  emacs_skia_paint_set_color (paint,
+			      EMACS_SKIA_COLOR (255, 255, 255, 255));
+  emacs_skia_canvas_draw_image_rect (canvas, image, &src_rect,
+				     &dst_rect, paint);
+
+  emacs_skia_canvas_restore (canvas);
+}
+#endif /* USE_SKIA */
+
+#ifdef USE_CAIRO
 static void
 pgtk_cr_draw_image (struct frame *f, Emacs_GC *gc, cairo_pattern_t *image,
 		    int src_x, int src_y, int width, int height,
@@ -2207,6 +2550,7 @@ pgtk_cr_draw_image (struct frame *f, Emacs_GC *gc, cairo_pattern_t *image,
 
   pgtk_end_cr_clip (f);
 }
+#endif /* USE_CAIRO */
 
 /* Draw foreground of image glyph string S.  */
 
@@ -2230,6 +2574,56 @@ pgtk_draw_image_foreground (struct glyph_string *s)
   if (s->slice.y == 0)
     y += s->img->vmargin;
 
+#ifdef USE_SKIA
+  if (s->img->skia_data)
+    {
+      emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (s->f);
+
+      emacs_skia_canvas_save (canvas);
+
+      /* Set up clipping */
+      if (s->num_clips > 0)
+	{
+	  for (int i = 0; i < s->num_clips; i++)
+	    {
+	      emacs_skia_rect_t clip_rect
+		= { s->clip[i].x, s->clip[i].y,
+		    s->clip[i].x + s->clip[i].width,
+		    s->clip[i].y + s->clip[i].height };
+	      emacs_skia_canvas_clip_rect (canvas, &clip_rect);
+	    }
+	}
+
+      pgtk_skia_draw_image (s->f, &s->xgcv, s->img->skia_data,
+			    s->slice.x, s->slice.y, s->slice.width,
+			    s->slice.height, x, y, true);
+
+      if (!s->img->mask)
+	{
+	  /* When the image has a mask, we can expect that at
+	     least part of a mouse highlight or a block cursor will
+	     be visible.  If the image doesn't have a mask, make
+	     a block cursor visible by drawing a rectangle around
+	     the image.  I believe it's looking better if we do
+	     nothing here for mouse-face.  */
+	  if (s->hl == DRAW_CURSOR)
+	    {
+	      int relief = eabs (s->img->relief);
+	      pgtk_draw_rectangle (s->f, s->xgcv.foreground,
+				   x - relief, y - relief,
+				   s->slice.width + relief * 2 - 1,
+				   s->slice.height + relief * 2 - 1,
+				   false);
+	    }
+	}
+      emacs_skia_canvas_restore (canvas);
+    }
+  else
+    /* Draw a rectangle if image could not be loaded.  */
+    pgtk_draw_rectangle (s->f, s->xgcv.foreground, x, y,
+			 s->slice.width - 1, s->slice.height - 1,
+			 false);
+#else  /* USE_CAIRO */
   if (s->img->cr_data)
     {
       cairo_t *cr = pgtk_begin_cr_clip (s->f);
@@ -2259,6 +2653,7 @@ pgtk_draw_image_foreground (struct glyph_string *s)
     /* Draw a rectangle if image could not be loaded.  */
     pgtk_draw_rectangle (s->f, s->xgcv.foreground, x, y,
 			 s->slice.width - 1, s->slice.height - 1, false);
+#endif /* USE_SKIA */
 }
 
 /* Draw image glyph string S.
@@ -2389,6 +2784,20 @@ pgtk_draw_stretch_glyph_string (struct glyph_string *s)
 	  else
 	    color = s->face->background;
 
+#ifdef USE_SKIA
+	  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (s->f);
+	  emacs_skia_canvas_save (canvas);
+
+	  get_glyph_string_clip_rect (s, &r);
+	  pgtk_skia_set_clip_rectangles (s->f, canvas, &r, 1);
+
+	  if (s->face->stipple)
+	    fill_background (s, x, y, w, h);
+	  else
+	    pgtk_fill_rectangle (s->f, color, x, y, w, h, true);
+
+	  emacs_skia_canvas_restore (canvas);
+#else
 	  cairo_t *cr = pgtk_begin_cr_clip (s->f);
 
 	  get_glyph_string_clip_rect (s, &r);
@@ -2401,6 +2810,7 @@ pgtk_draw_stretch_glyph_string (struct glyph_string *s)
 				 true);
 
 	  pgtk_end_cr_clip (s->f);
+#endif
 	}
     }
   else if (!s->background_filled_p)
@@ -2434,6 +2844,21 @@ pgtk_draw_dash (struct frame *f, struct glyph_string *s,
 		unsigned long foreground, int width,
 		char segment, int offset, int thickness)
 {
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  float sk_segment = (float) segment;
+  float y_center = s->ybase + offset + (thickness / 2.0);
+  float intervals[2] = { sk_segment, sk_segment };
+
+  emacs_skia_paint_set_color (paint, pgtk_color_to_skia (foreground));
+  emacs_skia_paint_set_stroke (paint, true);
+  emacs_skia_paint_set_stroke_width (paint, thickness);
+  emacs_skia_paint_set_dash (paint, intervals, 2, (float) s->x);
+  emacs_skia_canvas_draw_line (canvas, s->x, y_center, s->x + width,
+			       y_center, paint);
+  emacs_skia_paint_clear_dash (paint);
+#else
   cairo_t *cr;
   double cr_segment, y_center;
 
@@ -2448,6 +2873,7 @@ pgtk_draw_dash (struct frame *f, struct glyph_string *s,
   cairo_line_to (cr, s->x + width, y_center);
   cairo_stroke (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* Draw an underline of STYLE onto F at an offset of POSITION from the
@@ -2495,6 +2921,11 @@ pgtk_fill_underline (struct frame *f, struct glyph_string *s,
 pgtk_draw_glyph_string (struct glyph_string *s)
 {
   bool relief_drawn_p = false;
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas;
+#else
+  cairo_t *cr;
+#endif
 
   /* If S draws into the background of its successors, draw the
      background of the successors first so that S can draw into it.
@@ -2509,22 +2940,36 @@ pgtk_draw_glyph_string (struct glyph_string *s)
 	   width += next->width, next = next->next)
 	if (next->first_glyph->type != IMAGE_GLYPH)
 	  {
-	    cairo_t *cr = pgtk_begin_cr_clip (next->f);
+#ifdef USE_SKIA
+	    canvas = pgtk_begin_skia_clip (next->f);
+	    pgtk_set_glyph_string_gc (next);
+	    pgtk_skia_set_glyph_string_clipping (next, canvas);
+#else
+	    cr = pgtk_begin_cr_clip (next->f);
 	    pgtk_set_glyph_string_gc (next);
 	    pgtk_set_glyph_string_clipping (next, cr);
+#endif
 	    if (next->first_glyph->type == STRETCH_GLYPH)
 	      pgtk_draw_stretch_glyph_string (next);
 	    else
 	      pgtk_draw_glyph_string_background (next, true);
 	    next->num_clips = 0;
+#ifdef USE_SKIA
+	    pgtk_end_skia_clip (next->f);
+#else
 	    pgtk_end_cr_clip (next->f);
+#endif
 	  }
     }
 
   /* Set up S->gc, set clipping and draw S.  */
   pgtk_set_glyph_string_gc (s);
 
-  cairo_t *cr = pgtk_begin_cr_clip (s->f);
+#ifdef USE_SKIA
+  canvas = pgtk_begin_skia_clip (s->f);
+#else
+  cr = pgtk_begin_cr_clip (s->f);
+#endif
 
   /* Draw relief (if any) in advance for char/composition so that the
      glyph string can be drawn over it.  */
@@ -2534,10 +2979,17 @@ pgtk_draw_glyph_string (struct glyph_string *s)
 	  || s->first_glyph->type == COMPOSITE_GLYPH))
 
     {
+#ifdef USE_SKIA
+      pgtk_skia_set_glyph_string_clipping (s, canvas);
+      pgtk_draw_glyph_string_background (s, true);
+      pgtk_draw_glyph_string_box (s);
+      pgtk_skia_set_glyph_string_clipping (s, canvas);
+#else
       pgtk_set_glyph_string_clipping (s, cr);
       pgtk_draw_glyph_string_background (s, true);
       pgtk_draw_glyph_string_box (s);
       pgtk_set_glyph_string_clipping (s, cr);
+#endif
       relief_drawn_p = true;
     }
   else if (!s->clip_head	/* draw_glyphs didn't specify a clip mask. */
@@ -2547,9 +2999,15 @@ pgtk_draw_glyph_string (struct glyph_string *s)
     /* We must clip just this glyph.  left_overhang part has already
        drawn when s->prev was drawn, and right_overhang part will be
        drawn later when s->next is drawn. */
+#ifdef USE_SKIA
+    pgtk_skia_set_glyph_string_clipping_exactly (s, s, canvas);
+  else
+    pgtk_skia_set_glyph_string_clipping (s, canvas);
+#else
     pgtk_set_glyph_string_clipping_exactly (s, s, cr);
   else
     pgtk_set_glyph_string_clipping (s, cr);
+#endif
 
   switch (s->first_glyph->type)
     {
@@ -2744,15 +3202,25 @@ pgtk_draw_glyph_string (struct glyph_string *s)
 
 		prev->hl = s->hl;
 		pgtk_set_glyph_string_gc (prev);
+#ifdef USE_SKIA
+		emacs_skia_canvas_save (canvas);
+		pgtk_skia_set_glyph_string_clipping_exactly (s, prev,
+							     canvas);
+#else
 		cairo_save (cr);
 		pgtk_set_glyph_string_clipping_exactly (s, prev, cr);
+#endif
 		if (prev->first_glyph->type == CHAR_GLYPH)
 		  pgtk_draw_glyph_string_foreground (prev);
 		else
 		  pgtk_draw_composite_glyph_string_foreground (prev);
 		prev->hl = save;
 		prev->num_clips = 0;
+#ifdef USE_SKIA
+		emacs_skia_canvas_restore (canvas);
+#else
 		cairo_restore (cr);
+#endif
 	      }
 	}
 
@@ -2770,13 +3238,23 @@ pgtk_draw_glyph_string (struct glyph_string *s)
 
 		next->hl = s->hl;
 		pgtk_set_glyph_string_gc (next);
+#ifdef USE_SKIA
+		emacs_skia_canvas_save (canvas);
+		pgtk_skia_set_glyph_string_clipping_exactly (s, next,
+							     canvas);
+#else
 		cairo_save (cr);
 		pgtk_set_glyph_string_clipping_exactly (s, next, cr);
+#endif
 		if (next->first_glyph->type == CHAR_GLYPH)
 		  pgtk_draw_glyph_string_foreground (next);
 		else
 		  pgtk_draw_composite_glyph_string_foreground (next);
+#ifdef USE_SKIA
+		emacs_skia_canvas_restore (canvas);
+#else
 		cairo_restore (cr);
+#endif
 		next->hl = save;
 		next->num_clips = 0;
 		next->clip_head = s->next;
@@ -2790,7 +3268,11 @@ pgtk_draw_glyph_string (struct glyph_string *s)
     s->row->stipple_p = s->face->stipple;
 
   /* Reset clipping.  */
+#ifdef USE_SKIA
+  pgtk_end_skia_clip (s->f);
+#else
   pgtk_end_cr_clip (s->f);
+#endif
   s->num_clips = 0;
 }
 
@@ -2823,8 +3305,8 @@ pgtk_after_update_window_line (struct window *w,
   if (windows_or_buffers_changed
       && desired_row->full_width_p
       && (f = XFRAME (w->frame),
-	  width = FRAME_INTERNAL_BORDER_WIDTH (f),
-	  width != 0) && (height = desired_row->visible_height, height > 0))
+	  width = FRAME_INTERNAL_BORDER_WIDTH (f), width != 0)
+      && (height = desired_row->visible_height, height > 0))
     {
       int y = WINDOW_TO_FRAME_PIXEL_Y (w, max (0, desired_row->y));
 
@@ -2861,11 +3343,6 @@ pgtk_draw_hollow_cursor (struct window *w, struct glyph_row *row)
   get_phys_cursor_geometry (w, row, cursor_glyph, &x, &y, &h);
   wd = w->phys_cursor_width - 1;
 
-  /* The foreground of cursor_gc is typically the same as the normal
-     background color, which can cause the cursor box to be invisible.  */
-  cairo_t *cr = pgtk_begin_cr_clip (f);
-  pgtk_set_cr_source_with_color (f, FRAME_X_OUTPUT (f)->cursor_color, false);
-
   /* When on R2L character, show cursor at the right edge of the
      glyph, unless the cursor box is as wide as the glyph or wider
      (the latter happens when x-stretch-cursor is non-nil).  */
@@ -2876,11 +3353,40 @@ pgtk_draw_hollow_cursor (struct window *w, struct glyph_row *row)
       if (wd > 0)
 	wd -= 1;
     }
+
+#ifdef USE_SKIA
+  /* Use Skia directly with Skia clipping.  */
+  emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+  if (canvas)
+    {
+      pgtk_skia_clip_to_row (w, row, TEXT_AREA, canvas);
+      pgtk_skia_set_paint_color (f, FRAME_X_OUTPUT (f)->cursor_color,
+				 false);
+
+      emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+      emacs_skia_paint_set_stroke (paint, true);
+      emacs_skia_paint_set_stroke_width (paint, 1.0f);
+
+      emacs_skia_rect_t rect
+	= { x + 0.5f, y + 0.5f, x + wd + 0.5f, y + h - 1 + 0.5f };
+      emacs_skia_canvas_draw_rect (canvas, &rect, paint);
+
+      emacs_skia_paint_set_stroke (paint, false);
+      pgtk_end_skia_clip (f);
+    }
+#else
+  /* The foreground of cursor_gc is typically the same as the normal
+     background color, which can cause the cursor box to be invisible.
+   */
+  cairo_t *cr = pgtk_begin_cr_clip (f);
+  pgtk_set_cr_source_with_color (f, FRAME_X_OUTPUT (f)->cursor_color,
+				 false);
   /* Set clipping, draw the rectangle, and reset clipping again.  */
   pgtk_clip_to_row (w, row, TEXT_AREA, cr);
   pgtk_draw_rectangle (f, FRAME_X_OUTPUT (f)->cursor_color,
 		       x, y, wd, h - 1, false);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* Draw a bar cursor on window W in glyph row ROW.
@@ -2922,8 +3428,6 @@ pgtk_draw_bar_cursor (struct window *w, struct glyph_row *row, int width,
       struct face *face = FACE_FROM_ID (f, cursor_glyph->face_id);
       unsigned long color;
 
-      cairo_t *cr = pgtk_begin_cr_clip (f);
-
       /* If the glyph's background equals the color we normally draw
          the bars cursor in, the bar cursor in its normal color is
          invisible.  Use the glyph's foreground color instead in this
@@ -2934,6 +3438,72 @@ pgtk_draw_bar_cursor (struct window *w, struct glyph_row *row, int width,
       else
 	color = FRAME_X_OUTPUT (f)->cursor_color;
 
+#ifdef USE_SKIA
+      emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+      if (canvas)
+	{
+	  pgtk_skia_clip_to_row (w, row, TEXT_AREA, canvas);
+	  pgtk_skia_set_paint_color (f, color, false);
+
+	  if (kind == BAR_CURSOR)
+	    {
+	      int x
+		= WINDOW_TEXT_TO_FRAME_PIXEL_X (w, w->phys_cursor.x);
+
+	      if (width < 0)
+		width = FRAME_CURSOR_WIDTH (f);
+	      width = min (cursor_glyph->pixel_width, width);
+
+	      w->phys_cursor_width = width;
+
+	      /* If the character under cursor is R2L, draw the bar
+		 cursor on the right of its glyph, rather than on the
+		 left.  */
+	      if ((cursor_glyph->resolved_level & 1) != 0)
+		x += cursor_glyph->pixel_width - width;
+
+	      emacs_skia_irect_t rect
+		= { x, WINDOW_TO_FRAME_PIXEL_Y (w, w->phys_cursor.y),
+		    x + width,
+		    WINDOW_TO_FRAME_PIXEL_Y (w, w->phys_cursor.y)
+		      + row->height };
+	      emacs_skia_canvas_draw_irect (canvas, &rect,
+					    FRAME_SKIA_PAINT (f));
+	    }
+	  else /* HBAR_CURSOR */
+	    {
+	      int dummy_x, dummy_y, dummy_h;
+	      int x
+		= WINDOW_TEXT_TO_FRAME_PIXEL_X (w, w->phys_cursor.x);
+
+	      if (width < 0)
+		width = row->height;
+
+	      width = min (row->height, width);
+
+	      get_phys_cursor_geometry (w, row, cursor_glyph,
+					&dummy_x, &dummy_y, &dummy_h);
+
+	      if ((cursor_glyph->resolved_level & 1) != 0
+		  && cursor_glyph->pixel_width
+		       > w->phys_cursor_width - 1)
+		x += cursor_glyph->pixel_width - w->phys_cursor_width
+		     + 1;
+
+	      int y = WINDOW_TO_FRAME_PIXEL_Y (w, w->phys_cursor.y
+						    + row->height
+						    - width);
+	      emacs_skia_irect_t rect
+		= { x, y, x + w->phys_cursor_width - 1, y + width };
+	      emacs_skia_canvas_draw_irect (canvas, &rect,
+					    FRAME_SKIA_PAINT (f));
+	    }
+
+	  pgtk_end_skia_clip (f);
+	}
+#else
+      cairo_t *cr = pgtk_begin_cr_clip (f);
+
       pgtk_clip_to_row (w, row, TEXT_AREA, cr);
 
       if (kind == BAR_CURSOR)
@@ -2978,6 +3548,7 @@ pgtk_draw_bar_cursor (struct window *w, struct glyph_row *row, int width,
 	}
 
       pgtk_end_cr_clip (f);
+#endif
     }
 }
 
@@ -3050,18 +3621,49 @@ pgtk_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, int x,
 pgtk_copy_bits (struct frame *f, cairo_rectangle_t *src_rect,
 		cairo_rectangle_t *dst_rect)
 {
-  cairo_t *cr;
-  cairo_surface_t *surface;	/* temporary surface */
+#ifdef USE_SKIA
+  /* Skia version: snapshot the source region and draw to destination.  */
+  emacs_skia_surface_t *skia_surface = FRAME_SKIA_SURFACE (f);
+  if (skia_surface)
+    {
+      emacs_skia_irect_t src_irect
+	= { (int) src_rect->x, (int) src_rect->y,
+	    (int) (src_rect->x + src_rect->width),
+	    (int) (src_rect->y + src_rect->height) };
 
-  surface
-    = cairo_surface_create_similar (FRAME_CR_SURFACE (f),
-				    CAIRO_CONTENT_COLOR_ALPHA,
-				    (int) src_rect->width,
-				    (int) src_rect->height);
+      emacs_skia_image_t *snapshot
+	= emacs_skia_surface_make_image_snapshot_rect (skia_surface,
+						       &src_irect);
+      if (snapshot)
+	{
+	  emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+	  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
 
-  cr = cairo_create (surface);
-  cairo_set_source_surface (cr, FRAME_CR_SURFACE (f), -src_rect->x,
-			    -src_rect->y);
+	  /* Set up paint for direct copy (SRC blend mode).  */
+	  emacs_skia_paint_set_blend_mode (paint,
+					   EMACS_SKIA_BLEND_SRC);
+
+	  /* Draw the snapshot at the destination position.  */
+	  emacs_skia_canvas_draw_image (canvas, snapshot,
+					(float) dst_rect->x,
+					(float) dst_rect->y, paint);
+
+	  pgtk_end_skia_clip (f);
+	  emacs_skia_image_destroy (snapshot);
+	}
+    }
+#else
+  cairo_t *cr;
+  cairo_surface_t *surface; /* temporary surface */
+
+  surface = cairo_surface_create_similar (FRAME_CR_SURFACE (f),
+					  CAIRO_CONTENT_COLOR_ALPHA,
+					  (int) src_rect->width,
+					  (int) src_rect->height);
+
+  cr = cairo_create (surface);
+  cairo_set_source_surface (cr, FRAME_CR_SURFACE (f), -src_rect->x,
+			    -src_rect->y);
   cairo_rectangle (cr, 0, 0, src_rect->width, src_rect->height);
   cairo_clip (cr);
   cairo_paint (cr);
@@ -3077,6 +3679,7 @@ pgtk_copy_bits (struct frame *f, cairo_rectangle_t *src_rect,
   pgtk_end_cr_clip (f);
 
   cairo_surface_destroy (surface);
+#endif
 }
 
 /* Scroll part of the display as described by RUN.  */
@@ -3324,18 +3927,21 @@ pgtk_draw_vertical_window_border (struct window *w, int x, int y0, int y1)
 {
   struct frame *f = XFRAME (WINDOW_FRAME (w));
   struct face *face;
-  cairo_t *cr;
-
-  cr = pgtk_begin_cr_clip (f);
 
   face = FACE_FROM_ID_OR_NULL (f, VERTICAL_BORDER_FACE_ID);
-  if (face)
-    pgtk_set_cr_source_with_color (f, face->foreground, false);
+  unsigned long color
+    = face ? face->foreground : FRAME_FOREGROUND_PIXEL (f);
 
+#ifdef USE_SKIA
+  pgtk_skia_fill_rectangle (f, color, x, y0, 1, y1 - y0, false);
+#else
+  cairo_t *cr;
+  cr = pgtk_begin_cr_clip (f);
+  pgtk_set_cr_source_with_color (f, color, false);
   cairo_rectangle (cr, x, y0, 1, y1 - y0);
   cairo_fill (cr);
-
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* Draw a window divider from (x0,y0) to (x1,y1)  */
@@ -3356,6 +3962,35 @@ pgtk_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1)
   unsigned long color_last = (face_last
 			      ? face_last->foreground
 			      : FRAME_FOREGROUND_PIXEL (f));
+  bool alpha = f->borders_respect_alpha_background;
+
+#ifdef USE_SKIA
+  if (y1 - y0 > x1 - x0 && x1 - x0 > 2)
+    /* Vertical.  */
+    {
+      pgtk_skia_fill_rectangle (f, color_first, x0, y0, 1, y1 - y0,
+				alpha);
+      pgtk_skia_fill_rectangle (f, color, x0 + 1, y0, x1 - x0 - 2,
+				y1 - y0, alpha);
+      pgtk_skia_fill_rectangle (f, color_last, x1 - 1, y0, 1, y1 - y0,
+				alpha);
+    }
+  else if (x1 - x0 > y1 - y0 && y1 - y0 > 3)
+    /* Horizontal.  */
+    {
+      pgtk_skia_fill_rectangle (f, color_first, x0, y0, x1 - x0, 1,
+				alpha);
+      pgtk_skia_fill_rectangle (f, color, x0, y0 + 1, x1 - x0,
+				y1 - y0 - 2, alpha);
+      pgtk_skia_fill_rectangle (f, color_last, x0, y1 - 1, x1 - x0, 1,
+				alpha);
+    }
+  else
+    {
+      pgtk_skia_fill_rectangle (f, color, x0, y0, x1 - x0, y1 - y0,
+				alpha);
+    }
+#else
   cairo_t *cr = pgtk_begin_cr_clip (f);
 
   if (y1 - y0 > x1 - x0 && x1 - x0 > 2)
@@ -3399,6 +4034,7 @@ pgtk_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1)
     }
 
   pgtk_end_cr_clip (f);
+#endif
 }
 
 /* End update of frame F.  This function is installed as a hook in
@@ -3416,11 +4052,50 @@ pgtk_frame_up_to_date (struct frame *f)
 {
   block_input ();
   FRAME_MOUSE_UPDATE (f);
+
+#ifdef USE_SKIA
+  /* For Skia GL rendering, bypass the buffer_flipping_blocked check
+     since we don't use Cairo's double-buffering mechanism.  */
+  if (FRAME_GDK_GL_CONTEXT (f) && FRAME_GL_TEXTURE (f))
+    {
+      /* Frame pacing: limit to ~60 FPS (16.6ms) to reduce flickering
+	 over waypipe.  Skip frame if we rendered too recently.  */
+      gint64 now = g_get_monotonic_time ();
+      gint64 elapsed = now - FRAME_LAST_RENDER_TIME (f);
+      /* 16000 microseconds = 16ms ~ 60 FPS.  Use 8ms for smoother
+	 response.  */
+      const gint64 min_frame_interval = 8000;
+
+      if (FRAME_LAST_RENDER_TIME (f) > 0
+	  && elapsed < min_frame_interval)
+	{
+	  /* Too soon - skip this frame.  The next
+	     pgtk_frame_up_to_date call will trigger a render.  */
+	  unblock_input ();
+	  return;
+	}
+
+      FRAME_LAST_RENDER_TIME (f) = now;
+
+      /* Flush Skia first.  */
+      if (FRAME_SKIA_SURFACE (f))
+	emacs_skia_surface_flush (FRAME_SKIA_SURFACE (f));
+      if (FRAME_SKIA_GL_CONTEXT (f))
+	emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+
+      /* Queue a render on the GtkGLArea.  */
+      if (FRAME_GL_AREA (f))
+	gtk_gl_area_queue_render (GTK_GL_AREA (FRAME_GL_AREA (f)));
+      unblock_input ();
+      return;
+    }
+#else /* USE_CAIRO */
   if (!buffer_flipping_blocked_p ())
     {
       flip_cr_context (f);
       gtk_widget_queue_draw (FRAME_GTK_WIDGET (f));
     }
+#endif
   unblock_input ();
 }
 
@@ -3433,13 +4108,14 @@ pgtk_frame_up_to_date (struct frame *f)
    position on the scroll bar.
 
    If the mouse movement started elsewhere, set *FP to the frame the
-   mouse is on, *BAR_WINDOW to nil, and *X and *Y to the character cell
-   the mouse is over.
+   mouse is on, *BAR_WINDOW to nil, and *X and *Y to the character
+   cell the mouse is over.
 
    Set *TIMESTAMP to the server time-stamp for the time at which the mouse
    was at this position.
 
-   Don't store anything if we don't have a valid set of values to report.
+   Don't store anything if we don't have a valid set of values to
+   report.
 
    This clears the mouse_moved flag, so we can wait for the next mouse
    movement.  */
@@ -3529,45 +4205,94 @@ pgtk_mouse_position (struct frame **fp, int insist, Lisp_Object * bar_window,
 /* Fringe bitmaps.  */
 
 static int max_fringe_bmp = 0;
+#ifdef USE_SKIA
+static emacs_skia_image_t **fringe_bmp_skia = 0;
+#else
 static cairo_pattern_t **fringe_bmp = 0;
+#endif
 
 static void
 pgtk_define_fringe_bitmap (int which, unsigned short *bits, int h, int wd)
 {
-  int i, stride;
-  cairo_surface_t *surface;
-  unsigned char *data;
-  cairo_pattern_t *pattern;
+  int i;
 
   if (which >= max_fringe_bmp)
     {
       i = max_fringe_bmp;
       max_fringe_bmp = which + 20;
+#ifdef USE_SKIA
+      fringe_bmp_skia
+	= xrealloc (fringe_bmp_skia,
+		    max_fringe_bmp * sizeof (emacs_skia_image_t *));
+#else
       fringe_bmp
-	= xrealloc (fringe_bmp, max_fringe_bmp * sizeof (cairo_pattern_t *));
+	= xrealloc (fringe_bmp,
+		    max_fringe_bmp * sizeof (cairo_pattern_t *));
+#endif
       while (i < max_fringe_bmp)
-	fringe_bmp[i++] = 0;
+	{
+#ifdef USE_SKIA
+	  fringe_bmp_skia[i] = 0;
+#else
+	  fringe_bmp[i] = 0;
+#endif
+	  i++;
+	}
     }
 
   block_input ();
 
-  surface = cairo_image_surface_create (CAIRO_FORMAT_A1, wd, h);
-  stride = cairo_image_surface_get_stride (surface);
-  data = cairo_image_surface_get_data (surface);
+#ifdef USE_SKIA
+  {
+    /* Convert the bits to a format suitable for Skia.
+       The bits are 16-bit values representing each row of the fringe.
+     */
+    int stride = (wd + 7) / 8;
+    unsigned char *bitmap_data = xmalloc (stride * h);
 
-  for (i = 0; i < h; i++)
-    {
-      *((unsigned short *) data) = bits[i];
-      data += stride;
-    }
+    for (i = 0; i < h; i++)
+      {
+	/* Copy the low bytes of each 16-bit value.  */
+	if (stride >= 2)
+	  {
+	    bitmap_data[i * stride] = bits[i] & 0xff;
+	    bitmap_data[i * stride + 1] = (bits[i] >> 8) & 0xff;
+	  }
+	else
+	  bitmap_data[i * stride] = bits[i] & 0xff;
+      }
 
-  cairo_surface_mark_dirty (surface);
-  pattern = cairo_pattern_create_for_surface (surface);
-  cairo_surface_destroy (surface);
+    fringe_bmp_skia[which]
+      = emacs_skia_image_create_from_bitmap (bitmap_data, wd, h,
+					     stride);
+    xfree (bitmap_data);
+  }
+#else /* USE_CAIRO */
+  {
+    int stride;
+    cairo_surface_t *surface;
+    unsigned char *data;
+    cairo_pattern_t *pattern;
 
-  unblock_input ();
+    surface = cairo_image_surface_create (CAIRO_FORMAT_A1, wd, h);
+    stride = cairo_image_surface_get_stride (surface);
+    data = cairo_image_surface_get_data (surface);
+
+    for (i = 0; i < h; i++)
+      {
+	*((unsigned short *) data) = bits[i];
+	data += stride;
+      }
 
-  fringe_bmp[which] = pattern;
+    cairo_surface_mark_dirty (surface);
+    pattern = cairo_pattern_create_for_surface (surface);
+    cairo_surface_destroy (surface);
+
+    fringe_bmp[which] = pattern;
+  }
+#endif
+
+  unblock_input ();
 }
 
 static void
@@ -3576,15 +4301,26 @@ pgtk_destroy_fringe_bitmap (int which)
   if (which >= max_fringe_bmp)
     return;
 
+  block_input ();
+
+#ifdef USE_SKIA
+  if (fringe_bmp_skia[which])
+    {
+      emacs_skia_image_destroy (fringe_bmp_skia[which]);
+      fringe_bmp_skia[which] = 0;
+    }
+#else
   if (fringe_bmp[which])
     {
-      block_input ();
       cairo_pattern_destroy (fringe_bmp[which]);
-      unblock_input ();
+      fringe_bmp[which] = 0;
     }
-  fringe_bmp[which] = 0;
+#endif
+
+  unblock_input ();
 }
 
+#ifdef USE_CAIRO
 static void
 pgtk_clip_to_row (struct window *w, struct glyph_row *row,
 		  enum glyph_row_area area, cairo_t * cr)
@@ -3603,6 +4339,28 @@ pgtk_clip_to_row (struct window *w, struct glyph_row *row,
   cairo_rectangle (cr, rect.x, rect.y, rect.width, rect.height);
   cairo_clip (cr);
 }
+#endif
+
+#ifdef USE_SKIA
+static void
+pgtk_skia_clip_to_row (struct window *w, struct glyph_row *row,
+		       enum glyph_row_area area,
+		       emacs_skia_canvas_t *canvas)
+{
+  int window_x, window_y, window_width;
+  emacs_skia_rect_t rect;
+
+  window_box (w, area, &window_x, &window_y, &window_width, 0);
+
+  rect.left = window_x;
+  rect.top = WINDOW_TO_FRAME_PIXEL_Y (w, max (0, row->y));
+  rect.top = max (rect.top, window_y);
+  rect.right = rect.left + window_width;
+  rect.bottom = rect.top + row->visible_height;
+
+  emacs_skia_canvas_clip_rect (canvas, &rect);
+}
+#endif
 
 static void
 pgtk_draw_fringe_bitmap (struct window *w, struct glyph_row *row,
@@ -3611,6 +4369,63 @@ pgtk_draw_fringe_bitmap (struct window *w, struct glyph_row *row,
   struct frame *f = XFRAME (WINDOW_FRAME (w));
   struct face *face = p->face;
 
+#ifdef USE_SKIA
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+
+  emacs_skia_canvas_save (canvas);
+
+  /* Must clip because of partially visible lines.  */
+  pgtk_skia_clip_to_row (w, row, ANY_AREA, canvas);
+
+  if (p->bx >= 0 && !p->overlay_p)
+    fill_background_by_face (f, face, p->bx, p->by, p->nx, p->ny);
+
+  if (p->which && p->which < max_fringe_bmp
+      && p->which < max_used_fringe_bitmap)
+    {
+      unsigned long foreground
+	= (p->cursor_p
+	     ? (p->overlay_p ? face->background
+			     : FRAME_X_OUTPUT (f)->cursor_color)
+	     : face->foreground);
+
+      if (!fringe_bmp_skia[p->which])
+	gui_define_fringe_bitmap (f, p->which);
+
+      if (fringe_bmp_skia[p->which])
+	{
+	  emacs_skia_image_t *img = fringe_bmp_skia[p->which];
+
+	  /* Draw background if not overlay */
+	  if (!p->overlay_p)
+	    {
+	      emacs_skia_rect_t bg_rect
+		= { p->x, p->y, p->x + p->wd, p->y + p->h };
+	      emacs_skia_paint_set_color (paint, pgtk_color_to_skia (
+						   face->background));
+	      emacs_skia_paint_set_stroke (paint, false);
+	      emacs_skia_canvas_draw_rect (canvas, &bg_rect, paint);
+	    }
+
+	  /* Draw the fringe bitmap as a mask with foreground color */
+	  emacs_skia_paint_set_color (paint, pgtk_color_to_skia (
+					       foreground));
+	  emacs_skia_paint_set_image_shader (paint, img);
+	  emacs_skia_rect_t fringe_rect
+	    = { p->x, p->y, p->x + p->wd, p->y + p->h };
+	  emacs_skia_canvas_save (canvas);
+	  emacs_skia_canvas_translate (canvas, p->x, p->y - p->dh);
+	  emacs_skia_canvas_clip_rect (canvas, &fringe_rect);
+	  emacs_skia_rect_t img_rect = { 0, 0, p->wd, p->h + p->dh };
+	  emacs_skia_canvas_draw_rect (canvas, &img_rect, paint);
+	  emacs_skia_canvas_restore (canvas);
+	  emacs_skia_paint_clear_shader (paint);
+	}
+    }
+
+  emacs_skia_canvas_restore (canvas);
+#else
   cairo_t *cr = pgtk_begin_cr_clip (f);
 
   /* Must clip because of partially visible lines.  */
@@ -3646,6 +4461,7 @@ pgtk_draw_fringe_bitmap (struct window *w, struct glyph_row *row,
     }
 
   pgtk_end_cr_clip (f);
+#endif
 }
 
 static struct atimer *hourglass_atimer = NULL;
@@ -3762,11 +4578,20 @@ recover_from_visible_bell (struct atimer *timer)
 {
   struct frame *f = timer->client_data;
 
+#ifdef USE_SKIA
+  if (FRAME_X_OUTPUT (f)->skia_surface_visible_bell != NULL)
+    {
+      emacs_skia_surface_destroy (
+	FRAME_X_OUTPUT (f)->skia_surface_visible_bell);
+      FRAME_X_OUTPUT (f)->skia_surface_visible_bell = NULL;
+    }
+#else
   if (FRAME_X_OUTPUT (f)->cr_surface_visible_bell != NULL)
     {
       cairo_surface_destroy (FRAME_X_OUTPUT (f)->cr_surface_visible_bell);
       FRAME_X_OUTPUT (f)->cr_surface_visible_bell = NULL;
     }
+#endif
 
   if (FRAME_X_OUTPUT (f)->atimer_visible_bell != NULL)
     FRAME_X_OUTPUT (f)->atimer_visible_bell = NULL;
@@ -3777,6 +4602,112 @@ recover_from_visible_bell (struct atimer *timer)
 static void
 pgtk_flash (struct frame *f)
 {
+#ifdef USE_SKIA
+  emacs_skia_surface_t *surface_orig = FRAME_SKIA_SURFACE (f);
+  emacs_skia_surface_t *surface;
+  emacs_skia_canvas_t *canvas;
+  emacs_skia_paint_t *paint;
+  emacs_skia_image_t *snapshot;
+  int width, height, flash_height, flash_left, flash_right;
+  struct timespec delay;
+
+  if (!surface_orig)
+    return;
+
+  block_input ();
+
+  width = FRAME_CR_SURFACE_DESIRED_WIDTH (f);
+  height = FRAME_CR_SURFACE_DESIRED_HEIGHT (f);
+
+  /* Create a new surface for the flash effect */
+  surface = emacs_skia_surface_create_raster (width, height);
+  if (!surface)
+    {
+      unblock_input ();
+      return;
+    }
+
+  canvas = emacs_skia_surface_get_canvas (surface);
+  paint = emacs_skia_paint_create ();
+
+  /* Copy original surface content */
+  snapshot = emacs_skia_surface_make_image_snapshot (surface_orig);
+  if (snapshot)
+    {
+      emacs_skia_canvas_draw_image (canvas, snapshot, 0, 0, NULL);
+      emacs_skia_image_destroy (snapshot);
+    }
+
+  /* Set up for DIFFERENCE blend mode with white color */
+  emacs_skia_paint_set_color (paint,
+			      EMACS_SKIA_COLOR_RGB (255, 255, 255));
+  emacs_skia_paint_set_blend_mode (paint,
+				   EMACS_SKIA_BLEND_DIFFERENCE);
+  emacs_skia_paint_set_stroke (paint, false);
+
+  /* Get the height not including a menu bar widget.  */
+  height = FRAME_PIXEL_HEIGHT (f);
+  /* Height of each line to flash.  */
+  flash_height = FRAME_LINE_HEIGHT (f);
+  /* These will be the left and right margins of the rectangles.  */
+  flash_left = FRAME_INTERNAL_BORDER_WIDTH (f);
+  flash_right
+    = (FRAME_PIXEL_WIDTH (f) - FRAME_INTERNAL_BORDER_WIDTH (f));
+  width = flash_right - flash_left;
+
+  /* If window is tall, flash top and bottom line.  */
+  if (height > 3 * FRAME_LINE_HEIGHT (f))
+    {
+      emacs_skia_rect_t rect1
+	= { flash_left,
+	    FRAME_INTERNAL_BORDER_WIDTH (f)
+	      + FRAME_TOP_MARGIN_HEIGHT (f),
+	    flash_left + width,
+	    FRAME_INTERNAL_BORDER_WIDTH (f)
+	      + FRAME_TOP_MARGIN_HEIGHT (f) + flash_height };
+      emacs_skia_canvas_draw_rect (canvas, &rect1, paint);
+
+      emacs_skia_rect_t rect2
+	= { flash_left,
+	    height - flash_height - FRAME_INTERNAL_BORDER_WIDTH (f)
+	      - FRAME_BOTTOM_MARGIN_HEIGHT (f),
+	    flash_left + width,
+	    height - FRAME_INTERNAL_BORDER_WIDTH (f)
+	      - FRAME_BOTTOM_MARGIN_HEIGHT (f) };
+      emacs_skia_canvas_draw_rect (canvas, &rect2, paint);
+    }
+  else
+    {
+      /* If it is short, flash it all.  */
+      emacs_skia_rect_t rect
+	= { flash_left, FRAME_INTERNAL_BORDER_WIDTH (f),
+	    flash_left + width,
+	    height - FRAME_INTERNAL_BORDER_WIDTH (f) };
+      emacs_skia_canvas_draw_rect (canvas, &rect, paint);
+    }
+
+  emacs_skia_paint_destroy (paint);
+
+  /* Store the flash surface for later restoration */
+  if (FRAME_X_OUTPUT (f)->skia_surface_visible_bell)
+    emacs_skia_surface_destroy (
+      FRAME_X_OUTPUT (f)->skia_surface_visible_bell);
+  FRAME_X_OUTPUT (f)->skia_surface_visible_bell = surface;
+
+  delay = make_timespec (0, 50 * 1000 * 1000);
+
+  if (FRAME_X_OUTPUT (f)->atimer_visible_bell != NULL)
+    {
+      cancel_atimer (FRAME_X_OUTPUT (f)->atimer_visible_bell);
+      FRAME_X_OUTPUT (f)->atimer_visible_bell = NULL;
+    }
+
+  FRAME_X_OUTPUT (f)->atimer_visible_bell
+    = start_atimer (ATIMER_RELATIVE, delay, recover_from_visible_bell,
+		    f);
+
+  unblock_input ();
+#else
   cairo_surface_t *surface_orig, *surface;
   cairo_t *cr;
   int width, height, flash_height, flash_left, flash_right;
@@ -3862,6 +4793,7 @@ pgtk_flash (struct frame *f)
 
   cairo_destroy (cr);
   unblock_input ();
+#endif
 }
 
 /* Make audible bell.  */
@@ -4820,8 +5752,25 @@ pgtk_new_focus_frame (struct pgtk_display_info *dpyinfo, struct frame *frame)
 pgtk_buffer_flipping_unblocked_hook (struct frame *f)
 {
   block_input ();
-  flip_cr_context (f);
-  gtk_widget_queue_draw (FRAME_GTK_WIDGET (f));
+#ifdef USE_SKIA
+  /* For Skia GL, queue a render on the GtkGLArea.  */
+  if (FRAME_SKIA_SURFACE (f))
+    {
+      /* Flush Skia first.  */
+      emacs_skia_surface_flush (FRAME_SKIA_SURFACE (f));
+      if (FRAME_SKIA_GL_CONTEXT (f))
+	emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+      if (FRAME_GL_AREA (f))
+	gtk_gl_area_queue_render (GTK_GL_AREA (FRAME_GL_AREA (f)));
+    }
+  else
+#endif
+    {
+#ifdef USE_CAIRO
+      flip_cr_context (f);
+#endif
+      gtk_widget_queue_draw (FRAME_GTK_WIDGET (f));
+    }
   unblock_input ();
 }
 
@@ -4994,12 +5943,17 @@ pgtk_handle_event (GtkWidget *widget, GdkEvent *event, gpointer *data)
 pgtk_fill_rectangle (struct frame *f, unsigned long color, int x, int y,
 		     int width, int height, bool respect_alpha_background)
 {
+#ifdef USE_SKIA
+  pgtk_skia_fill_rectangle (f, color, x, y, width, height,
+			    respect_alpha_background);
+#else
   cairo_t *cr;
   cr = pgtk_begin_cr_clip (f);
   pgtk_set_cr_source_with_color (f, color, respect_alpha_background);
   cairo_rectangle (cr, x, y, width, height);
   cairo_fill (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
 void
@@ -5058,22 +6012,30 @@ pgtk_handle_draw (GtkWidget *widget, cairo_t *cr, gpointer *data)
 
   GdkWindow *win = gtk_widget_get_window (widget);
 
-  if (win != NULL)
+  if (win == NULL)
+    return FALSE;
+
+  f = pgtk_any_window_to_frame (win);
+  if (f == NULL)
+    return FALSE;
+
+#ifdef USE_SKIA
+  /* For Skia with GL rendering, GtkGLArea handles all display via its
+     render callback.  This draw callback on the GtkFixed widget is not
+     used.  Return FALSE to indicate we didn't handle the draw.  */
+  (void) cr;  /* Suppress unused parameter warning.  */
+  return FALSE;
+#else /* USE_CAIRO */
+  cairo_surface_t *src = NULL;
+  src = FRAME_X_OUTPUT (f)->cr_surface_visible_bell;
+  if (src == NULL && FRAME_CR_ACTIVE_CONTEXT (f) != NULL)
+    src = cairo_get_target (FRAME_CR_ACTIVE_CONTEXT (f));
+  if (src != NULL)
     {
-      cairo_surface_t *src = NULL;
-      f = pgtk_any_window_to_frame (win);
-      if (f != NULL)
-	{
-	  src = FRAME_X_OUTPUT (f)->cr_surface_visible_bell;
-	  if (src == NULL && FRAME_CR_ACTIVE_CONTEXT (f) != NULL)
-	    src = cairo_get_target (FRAME_CR_ACTIVE_CONTEXT (f));
-	}
-      if (src != NULL)
-	{
-	  cairo_set_source_surface (cr, src, 0, 0);
-	  cairo_paint (cr);
-	}
+      cairo_set_source_surface (cr, src, 0, 0);
+      cairo_paint (cr);
     }
+#endif
   return FALSE;
 }
 
@@ -5089,7 +6051,19 @@ size_allocate (GtkWidget *widget, GtkAllocation *alloc,
   if (f)
     {
       xg_frame_resized (f, alloc->width, alloc->height);
-      pgtk_cr_update_surface_desired_size (f, alloc->width, alloc->height, false);
+#ifdef USE_SKIA
+      /* Resize the GtkGLArea to fill the frame.  */
+      if (FRAME_GL_AREA (f))
+	{
+	  gtk_widget_set_size_request (FRAME_GL_AREA (f),
+				       alloc->width, alloc->height);
+	}
+      pgtk_skia_update_surface_desired_size (f, alloc->width,
+					     alloc->height, false);
+#else
+      pgtk_cr_update_surface_desired_size (f, alloc->width,
+					   alloc->height, false);
+#endif
     }
 }
 
@@ -6783,7 +7757,13 @@ pgtk_monitors_changed_cb (GdkScreen *screen, gpointer user_data)
 
 static gboolean pgtk_selection_event (GtkWidget *, GdkEvent *, gpointer);
 
-
+#ifdef USE_SKIA
+/* Forward declarations for GtkGLArea callbacks.  */
+static void pgtk_gl_area_realize (GtkGLArea *, gpointer);
+static gboolean pgtk_gl_area_render (GtkGLArea *, GdkGLContext *, gpointer);
+static void pgtk_gl_area_resize (GtkGLArea *, gint, gint, gpointer);
+static bool pgtk_setup_gl_framebuffer (struct frame *, int, int);
+#endif
 
 void
 pgtk_set_event_handler (struct frame *f)
@@ -6844,10 +7824,45 @@ pgtk_set_event_handler (struct frame *f)
 		    G_CALLBACK (drag_motion), NULL);
   g_signal_connect (G_OBJECT (FRAME_GTK_WIDGET (f)), "drag-drop",
 		    G_CALLBACK (drag_drop), NULL);
+
+#ifdef USE_SKIA
+  /* For Skia GL rendering, create a GtkGLArea widget.  */
+  {
+    GtkWidget *gl_area = gtk_gl_area_new ();
+    FRAME_GL_AREA (f) = gl_area;
+
+    /* Request OpenGL 3.2 core profile for Skia compatibility.  */
+    gtk_gl_area_set_required_version (GTK_GL_AREA (gl_area), 3, 2);
+    gtk_gl_area_set_has_depth_buffer (GTK_GL_AREA (gl_area), FALSE);
+    /* Skia needs stencil buffer for clip mask operations.  */
+    gtk_gl_area_set_has_stencil_buffer (GTK_GL_AREA (gl_area), TRUE);
+    gtk_gl_area_set_auto_render (GTK_GL_AREA (gl_area), FALSE);
+
+    /* Connect GtkGLArea signals.  */
+    g_signal_connect (G_OBJECT (gl_area), "realize",
+		      G_CALLBACK (pgtk_gl_area_realize), f);
+    g_signal_connect (G_OBJECT (gl_area), "render",
+		      G_CALLBACK (pgtk_gl_area_render), f);
+    g_signal_connect (G_OBJECT (gl_area), "resize",
+		      G_CALLBACK (pgtk_gl_area_resize), f);
+
+    /* Add GtkGLArea to the GtkFixed at position (0,0).  */
+    gtk_fixed_put (GTK_FIXED (FRAME_GTK_WIDGET (f)), gl_area, 0, 0);
+
+    /* Make it fill the entire frame.  This will be updated on resize.  */
+    gtk_widget_set_size_request (gl_area, 1, 1);
+    gtk_widget_set_hexpand (gl_area, TRUE);
+    gtk_widget_set_vexpand (gl_area, TRUE);
+    gtk_widget_show (gl_area);
+  }
+#else
+  /* For Cairo, use the draw callback.  */
   g_signal_connect (G_OBJECT (FRAME_GTK_WIDGET (f)), "draw",
 		    G_CALLBACK (pgtk_handle_draw), NULL);
   g_signal_connect (G_OBJECT (FRAME_GTK_WIDGET (f)), "property-notify-event",
 		    G_CALLBACK (pgtk_selection_event), NULL);
+#endif
+
   g_signal_connect (G_OBJECT (FRAME_GTK_WIDGET (f)), "selection-clear-event",
 		    G_CALLBACK (pgtk_selection_event), NULL);
   g_signal_connect (G_OBJECT (FRAME_GTK_WIDGET (f)), "selection-request-event",
@@ -7413,62 +8428,779 @@ pgtk_query_color (struct frame *f, Emacs_Color * color)
 void
 pgtk_clear_area (struct frame *f, int x, int y, int width, int height)
 {
-  cairo_t *cr;
-
   eassert (width > 0 && height > 0);
 
+#ifdef USE_SKIA
+  pgtk_skia_fill_rectangle (f, FRAME_X_OUTPUT (f)->background_color,
+			    x, y, width, height, true);
+#else
+  cairo_t *cr;
+
   cr = pgtk_begin_cr_clip (f);
-  pgtk_set_cr_source_with_color (f, FRAME_X_OUTPUT (f)->background_color,
+  pgtk_set_cr_source_with_color (f,
+				 FRAME_X_OUTPUT (f)->background_color,
 				 true);
   cairo_rectangle (cr, x, y, width, height);
   cairo_fill (cr);
   pgtk_end_cr_clip (f);
+#endif
 }
 
+#ifdef USE_SKIA
+/* ============================================================
+   Skia drawing functions
+   ============================================================ */
 
-void
-syms_of_pgtkterm (void)
+/* GtkGLArea "realize" callback - set up GL resources.  */
+static void
+pgtk_gl_area_realize (GtkGLArea *gl_area, gpointer user_data)
 {
-  DEFSYM (Qmodifier_value, "modifier-value");
-  DEFSYM (Qalt, "alt");
-  DEFSYM (Qhyper, "hyper");
-  DEFSYM (Qmeta, "meta");
-  DEFSYM (Qsuper, "super");
-  DEFSYM (Qcontrol, "control");
-  DEFSYM (QUTF8_STRING, "UTF8_STRING");
-  /* Referenced in gtkutil.c.  */
-  DEFSYM (Qtheme_name, "theme-name");
-  DEFSYM (Qfile_name_sans_extension, "file-name-sans-extension");
+  struct frame *f = (struct frame *) user_data;
 
-  DEFSYM (Qfile, "file");
-  DEFSYM (Qurl, "url");
+  /* Make the GtkGLArea's context current.  */
+  gtk_gl_area_make_current (gl_area);
 
-  DEFSYM (Qlatin_1, "latin-1");
+  GError *gl_error = gtk_gl_area_get_error (gl_area);
+  if (gl_error != NULL)
+    return;
 
-  xg_default_icon_file
-    = build_string ("icons/hicolor/scalable/apps/emacs.svg");
-  staticpro (&xg_default_icon_file);
+  /* Get the GDK GL context from GtkGLArea.  */
+  GdkGLContext *gl_context = gtk_gl_area_get_context (gl_area);
+  if (!gl_context)
+    return;
 
-  DEFSYM (Qx_gtk_map_stock, "x-gtk-map-stock");
+  FRAME_GDK_GL_CONTEXT (f) = gl_context;
 
-  DEFSYM (Qcopy, "copy");
-  DEFSYM (Qmove, "move");
-  DEFSYM (Qlink, "link");
-  DEFSYM (Qprivate, "private");
+  /* Create Skia GL context using the native GL interface.  */
+  FRAME_SKIA_GL_CONTEXT (f) = emacs_skia_gl_context_create_native ();
 
-  Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
-  Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
-  Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
-  Fput (Qsuper, Qmodifier_value, make_fixnum (super_modifier));
-  Fput (Qcontrol, Qmodifier_value, make_fixnum (ctrl_modifier));
+  if (!FRAME_SKIA_GL_CONTEXT (f))
+    {
+      FRAME_GDK_GL_CONTEXT (f) = NULL;
+      return;
+    }
 
-  DEFVAR_LISP ("x-ctrl-keysym", Vx_ctrl_keysym,
-	       doc: /* SKIP: real doc in xterm.c.  */);
-  Vx_ctrl_keysym = Qnil;
+  /* Create GL framebuffer and texture for offscreen rendering.  */
+  glGenFramebuffers (1, &FRAME_GL_FRAMEBUFFER (f));
+  glGenTextures (1, &FRAME_GL_TEXTURE (f));
 
-  DEFVAR_LISP ("x-alt-keysym", Vx_alt_keysym,
-	       doc: /* SKIP: real doc in xterm.c.  */);
-  Vx_alt_keysym = Qnil;
+  /* Initialize GL state as dirty so Skia resets on first use.  */
+  FRAME_SKIA_GL_STATE_DIRTY (f) = true;
+
+  FRAME_SKIA_GL_INITIALIZED (f) = true;
+
+  /* Get the initial size and set up the FBO.  */
+  GtkAllocation alloc;
+  gtk_widget_get_allocation (GTK_WIDGET (gl_area), &alloc);
+  if (alloc.width > 0 && alloc.height > 0)
+    {
+      pgtk_setup_gl_framebuffer (f, alloc.width, alloc.height);
+      FRAME_SKIA_SURFACE_DESIRED_WIDTH (f) = alloc.width;
+      FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f) = alloc.height;
+    }
+}
+
+/* GtkGLArea "render" callback - blit FBO to screen.  */
+static gboolean
+pgtk_gl_area_render (GtkGLArea *gl_area, GdkGLContext *context,
+		     gpointer user_data)
+{
+  struct frame *f = (struct frame *) user_data;
+  emacs_skia_surface_t *skia_surface;
+
+  /* Use visible bell surface if active, otherwise main surface.  */
+  if (FRAME_X_OUTPUT (f)->skia_surface_visible_bell)
+    skia_surface = FRAME_X_OUTPUT (f)->skia_surface_visible_bell;
+  else
+    skia_surface = FRAME_SKIA_SURFACE (f);
+
+  /* No FBO at all - just clear to background.  */
+  if (!FRAME_GL_FRAMEBUFFER (f))
+    {
+      unsigned long bg = FRAME_X_OUTPUT (f)->background_color;
+      float r = RED_FROM_ULONG (bg) / 255.0f;
+      float g = GREEN_FROM_ULONG (bg) / 255.0f;
+      float b = BLUE_FROM_ULONG (bg) / 255.0f;
+      glClearColor (r, g, b, 1.0f);
+      glClear (GL_COLOR_BUFFER_BIT);
+      return TRUE;
+    }
+
+  /* Ensure GtkGLArea's buffers are attached.  */
+  gtk_gl_area_attach_buffers (gl_area);
+
+  /* Flush Skia rendering if we have an active surface.  */
+  if (skia_surface)
+    {
+      emacs_skia_surface_flush (skia_surface);
+      if (FRAME_SKIA_GL_CONTEXT (f))
+	emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+      glFinish ();
+    }
+
+  /* Get source dimensions from Skia surface if available, otherwise
+     from the FBO texture (for resize case where surface is destroyed
+     but FBO has preserved content).  */
+  int src_width, src_height;
+  if (skia_surface)
+    {
+      src_width = emacs_skia_surface_get_width (skia_surface);
+      src_height = emacs_skia_surface_get_height (skia_surface);
+    }
+  else
+    {
+      /* Query FBO texture size directly.  */
+      glBindTexture (GL_TEXTURE_2D, FRAME_GL_TEXTURE (f));
+      glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH,
+				&src_width);
+      glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT,
+				&src_height);
+      glBindTexture (GL_TEXTURE_2D, 0);
+    }
+
+  /* Get the actual viewport size (may differ due to HiDPI).  */
+  GLint viewport[4];
+  glGetIntegerv (GL_VIEWPORT, viewport);
+  int dst_width = viewport[2];
+  int dst_height = viewport[3];
+
+  /* Disable blending for opaque blit.  */
+  glDisable (GL_BLEND);
+  glDisable (GL_SCISSOR_TEST);
+
+  /* Bind our FBO as the read framebuffer.  The GtkGLArea's FBO is
+     already bound as the draw framebuffer.  */
+  glBindFramebuffer (GL_READ_FRAMEBUFFER, FRAME_GL_FRAMEBUFFER (f));
+
+  /* Blit the FBO to GtkGLArea's framebuffer.  Both FBOs use the same
+     OpenGL coordinate system (origin at bottom-left), so no Y-flip
+     is needed.  */
+  glBlitFramebuffer (0, 0, src_width, src_height,
+		     0, 0, dst_width, dst_height,
+		     GL_COLOR_BUFFER_BIT, GL_LINEAR);
+
+  /* Check for GL errors (silently ignore).  */
+  glGetError ();
+
+  /* Restore framebuffer bindings.  */
+  glBindFramebuffer (GL_READ_FRAMEBUFFER, 0);
+
+  return TRUE;
+}
+
+/* Resize FBO while preserving existing content.  This blits the old
+   content to a temp buffer, resizes the main FBO, clears to background,
+   and blits the preserved content back.  */
+static void
+pgtk_resize_fbo_preserve_content (struct frame *f, int old_width,
+				  int old_height, int new_width,
+				  int new_height)
+{
+  /* Create a temporary FBO to hold the old content.  */
+  GLuint temp_fbo, temp_texture;
+  glGenFramebuffers (1, &temp_fbo);
+  glGenTextures (1, &temp_texture);
+
+  /* Copy old texture to temp texture.  */
+  glBindTexture (GL_TEXTURE_2D, temp_texture);
+  glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, old_width, old_height,
+		0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+  glBindFramebuffer (GL_FRAMEBUFFER, temp_fbo);
+  glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+			  GL_TEXTURE_2D, temp_texture, 0);
+
+  /* Blit old content to temp.  */
+  glBindFramebuffer (GL_READ_FRAMEBUFFER, FRAME_GL_FRAMEBUFFER (f));
+  glBindFramebuffer (GL_DRAW_FRAMEBUFFER, temp_fbo);
+  glBlitFramebuffer (0, 0, old_width, old_height,
+		     0, 0, old_width, old_height,
+		     GL_COLOR_BUFFER_BIT, GL_NEAREST);
+
+  /* Resize main FBO to new size.  */
+  pgtk_setup_gl_framebuffer (f, new_width, new_height);
+
+  /* Clear new FBO to background color first.  */
+  glBindFramebuffer (GL_FRAMEBUFFER, FRAME_GL_FRAMEBUFFER (f));
+  unsigned long bg = FRAME_X_OUTPUT (f)->background_color;
+  float r = RED_FROM_ULONG (bg) / 255.0f;
+  float g = GREEN_FROM_ULONG (bg) / 255.0f;
+  float b = BLUE_FROM_ULONG (bg) / 255.0f;
+  glClearColor (r, g, b, 1.0f);
+  glClear (GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+  /* Blit preserved content back.  Use min of old/new dimensions
+     to handle both grow and shrink.  */
+  int blit_width = old_width < new_width ? old_width : new_width;
+  int blit_height = old_height < new_height ? old_height : new_height;
+  glBindFramebuffer (GL_READ_FRAMEBUFFER, temp_fbo);
+  glBindFramebuffer (GL_DRAW_FRAMEBUFFER, FRAME_GL_FRAMEBUFFER (f));
+  glBlitFramebuffer (0, 0, blit_width, blit_height,
+		     0, 0, blit_width, blit_height,
+		     GL_COLOR_BUFFER_BIT, GL_NEAREST);
+  glFinish ();
+
+  /* Clean up temp resources.  */
+  glDeleteFramebuffers (1, &temp_fbo);
+  glDeleteTextures (1, &temp_texture);
+  glBindFramebuffer (GL_FRAMEBUFFER, 0);
+}
+
+/* GtkGLArea "resize" callback - resize FBO to match widget.  */
+static void
+pgtk_gl_area_resize (GtkGLArea *gl_area, gint width, gint height,
+		     gpointer user_data)
+{
+  struct frame *f = (struct frame *) user_data;
+
+  if (width <= 0 || height <= 0)
+    return;
+
+  /* Make context current before GL operations.  */
+  gtk_gl_area_make_current (gl_area);
+
+  /* Destroy the old Skia surface if size changed.  The surface will
+     be recreated on next draw with the new size.  */
+  if (FRAME_SKIA_SURFACE_DESIRED_WIDTH (f) != width
+      || FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f) != height)
+    {
+      /* Destroy only the Skia surface, keep the GL context.  */
+      if (FRAME_SKIA_SURFACE (f))
+	{
+	  if (FRAME_SKIA_GL_CONTEXT (f))
+	    {
+	      emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+	      glFinish ();
+	    }
+	  emacs_skia_surface_destroy (FRAME_SKIA_SURFACE (f));
+	  FRAME_SKIA_SURFACE (f) = NULL;
+	  FRAME_SKIA_CANVAS (f) = NULL;
+	}
+
+      /* Resize the FBO texture, preserving existing content.  */
+      if (FRAME_GL_FRAMEBUFFER (f) && FRAME_GL_TEXTURE (f))
+	{
+	  int old_width = FRAME_SKIA_SURFACE_DESIRED_WIDTH (f);
+	  int old_height = FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f);
+	  pgtk_resize_fbo_preserve_content (f, old_width, old_height,
+					    width, height);
+	}
+
+      FRAME_SKIA_SURFACE_DESIRED_WIDTH (f) = width;
+      FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f) = height;
+
+      /* Mark the frame as needing a full redraw.  */
+      SET_FRAME_GARBAGED (f);
+    }
+}
+
+/* Create and set up GL context for frame F (fallback when no GtkGLArea).  */
+static bool
+pgtk_init_gl_area (struct frame *f)
+{
+  GtkWidget *fixed;
+  GdkWindow *gdk_window;
+  GdkGLContext *gl_context;
+  GError *error = NULL;
+
+  if (FRAME_GDK_GL_CONTEXT (f))
+    return true; /* Already initialized.  */
+
+  fixed = FRAME_GTK_WIDGET (f);
+
+  /* Make the fixed widget app-paintable so it doesn't draw background.  */
+  gtk_widget_set_app_paintable (fixed, TRUE);
+
+  /* Get the GdkWindow from the widget.  */
+  gdk_window = gtk_widget_get_window (fixed);
+  if (!gdk_window)
+    return false;
+
+  /* Create a GL context directly from the GdkWindow.  */
+  gl_context = gdk_window_create_gl_context (gdk_window, &error);
+  if (!gl_context)
+    {
+      if (error)
+	g_error_free (error);
+      return false;
+    }
+
+  /* Realize the context (required before use).  */
+  if (!gdk_gl_context_realize (gl_context, &error))
+    {
+      if (error)
+	g_error_free (error);
+      g_object_unref (gl_context);
+      return false;
+    }
+
+  FRAME_GDK_GL_CONTEXT (f) = gl_context;
+
+  /* Make the context current.  */
+  gdk_gl_context_make_current (gl_context);
+
+  /* Create Skia GL context using the native GL interface.  */
+  FRAME_SKIA_GL_CONTEXT (f) = emacs_skia_gl_context_create_native ();
+
+  if (!FRAME_SKIA_GL_CONTEXT (f))
+    {
+      g_object_unref (gl_context);
+      FRAME_GDK_GL_CONTEXT (f) = NULL;
+      return false;
+    }
+
+  /* Create GL framebuffer and texture for offscreen rendering.  */
+  glGenFramebuffers (1, &FRAME_GL_FRAMEBUFFER (f));
+  glGenTextures (1, &FRAME_GL_TEXTURE (f));
+
+  /* Initialize GL state as dirty so Skia resets on first use.  */
+  FRAME_SKIA_GL_STATE_DIRTY (f) = true;
+
+  FRAME_SKIA_GL_INITIALIZED (f) = true;
+  /* No GtkGLArea - we use direct GdkGLContext.  */
+  FRAME_GL_AREA (f) = NULL;
+
+  return true;
+}
+
+/* Legacy init function - now just calls pgtk_init_gl_area.  */
+static bool
+pgtk_init_gl_context (struct frame *f)
+{
+  if (FRAME_SKIA_GL_INITIALIZED (f))
+    return FRAME_GDK_GL_CONTEXT (f) != NULL;
+
+  return pgtk_init_gl_area (f);
+}
+
+/* Set up GL framebuffer for rendering at given size.  */
+static bool
+pgtk_setup_gl_framebuffer (struct frame *f, int width, int height)
+{
+  if (!FRAME_GDK_GL_CONTEXT (f))
+    return false;
+
+  /* Make the GL context current.  */
+  gdk_gl_context_make_current (FRAME_GDK_GL_CONTEXT (f));
+
+  /* Bind and configure the texture.  */
+  glBindTexture (GL_TEXTURE_2D, FRAME_GL_TEXTURE (f));
+  glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA,
+		GL_UNSIGNED_BYTE, NULL);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+  /* Create or resize the stencil renderbuffer.  Skia needs this for
+     clip mask operations.  */
+  if (!FRAME_GL_STENCIL (f))
+    glGenRenderbuffers (1, &FRAME_GL_STENCIL (f));
+  glBindRenderbuffer (GL_RENDERBUFFER, FRAME_GL_STENCIL (f));
+  glRenderbufferStorage (GL_RENDERBUFFER, GL_STENCIL_INDEX8, width, height);
+
+  /* Bind the framebuffer and attach the texture and stencil.  */
+  glBindFramebuffer (GL_FRAMEBUFFER, FRAME_GL_FRAMEBUFFER (f));
+  glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+			  GL_TEXTURE_2D, FRAME_GL_TEXTURE (f), 0);
+  glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
+			     GL_RENDERBUFFER, FRAME_GL_STENCIL (f));
+
+  GLenum status = glCheckFramebufferStatus (GL_FRAMEBUFFER);
+  if (status != GL_FRAMEBUFFER_COMPLETE)
+    return false;
+
+  /* Clear the FBO to black initially to avoid garbage data.
+     The actual background color will be set when Skia draws.  */
+  glClearColor (0.0f, 0.0f, 0.0f, 1.0f);
+  glClear (GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+  glFinish ();
+
+  /* Unbind the framebuffer.  */
+  glBindFramebuffer (GL_FRAMEBUFFER, 0);
+
+  /* Mark GL state as dirty so Skia will reset its cached state.  */
+  FRAME_SKIA_GL_STATE_DIRTY (f) = true;
+
+  return true;
+}
+
+/* Clean up GL resources for frame F.  */
+static void
+pgtk_cleanup_gl_context (struct frame *f)
+{
+  if (FRAME_SKIA_GL_CONTEXT (f))
+    {
+      emacs_skia_gl_context_destroy (FRAME_SKIA_GL_CONTEXT (f));
+      FRAME_SKIA_GL_CONTEXT (f) = NULL;
+    }
+
+  if (FRAME_GDK_GL_CONTEXT (f))
+    {
+      gdk_gl_context_make_current (FRAME_GDK_GL_CONTEXT (f));
+
+      if (FRAME_GL_FRAMEBUFFER (f))
+	{
+	  glDeleteFramebuffers (1, &FRAME_GL_FRAMEBUFFER (f));
+	  FRAME_GL_FRAMEBUFFER (f) = 0;
+	}
+      if (FRAME_GL_TEXTURE (f))
+	{
+	  glDeleteTextures (1, &FRAME_GL_TEXTURE (f));
+	  FRAME_GL_TEXTURE (f) = 0;
+	}
+      if (FRAME_GL_STENCIL (f))
+	{
+	  glDeleteRenderbuffers (1, &FRAME_GL_STENCIL (f));
+	  FRAME_GL_STENCIL (f) = 0;
+	}
+
+      /* We created this context ourselves, so unref it.  */
+      gdk_gl_context_clear_current ();
+      g_object_unref (FRAME_GDK_GL_CONTEXT (f));
+      FRAME_GDK_GL_CONTEXT (f) = NULL;
+    }
+
+  /* No GtkGLArea to destroy - we use direct GdkGLContext.  */
+  FRAME_GL_AREA (f) = NULL;
+
+  FRAME_SKIA_GL_INITIALIZED (f) = false;
+}
+
+/* Forward declaration.  */
+static void pgtk_skia_destroy_surface_only (struct frame *f);
+
+void
+pgtk_skia_update_surface_desired_size (struct frame *f, int width,
+				       int height, bool force)
+{
+  if (FRAME_SKIA_SURFACE_DESIRED_WIDTH (f) != width
+      || FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f) != height || force)
+    {
+      /* Only destroy the Skia surface, preserve the GtkGLArea and GL
+	 context.  This avoids recreating the entire GL setup on every
+	 resize, which causes flickering.  */
+      pgtk_skia_destroy_surface_only (f);
+      FRAME_SKIA_SURFACE_DESIRED_WIDTH (f) = width;
+      FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f) = height;
+      SET_FRAME_GARBAGED (f);
+    }
+}
+
+emacs_skia_canvas_t *
+pgtk_begin_skia_clip (struct frame *f)
+{
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+
+  if (!canvas)
+    {
+      int width = FRAME_SKIA_SURFACE_DESIRED_WIDTH (f);
+      int height = FRAME_SKIA_SURFACE_DESIRED_HEIGHT (f);
+
+      if (width <= 0)
+	width = 1;
+      if (height <= 0)
+	height = 1;
+
+      /* Use GtkGLArea's context if available.  */
+      if (FRAME_GL_AREA (f) && FRAME_GDK_GL_CONTEXT (f))
+	{
+	  /* Make the GtkGLArea's context current.  */
+	  gtk_gl_area_make_current (GTK_GL_AREA (FRAME_GL_AREA (f)));
+
+	  /* Set up FBO if not already done, or resize if size changed.
+	     This handles cases where size_allocate fires before the
+	     GtkGLArea resize signal (e.g., tiling WM fullscreen).  */
+	  if (!FRAME_GL_FRAMEBUFFER (f))
+	    pgtk_setup_gl_framebuffer (f, width, height);
+	  else
+	    {
+	      /* Check if FBO needs resizing by querying the texture size.  */
+	      GLint tex_width = 0, tex_height = 0;
+	      glBindTexture (GL_TEXTURE_2D, FRAME_GL_TEXTURE (f));
+	      glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH,
+					&tex_width);
+	      glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT,
+					&tex_height);
+	      glBindTexture (GL_TEXTURE_2D, 0);
+
+	      if (tex_width != width || tex_height != height)
+		pgtk_resize_fbo_preserve_content (f, tex_width, tex_height,
+						  width, height);
+	    }
+
+	  /* Create Skia surface if needed.  */
+	  if (!FRAME_SKIA_SURFACE (f) && FRAME_GL_FRAMEBUFFER (f))
+	    {
+	      FRAME_SKIA_SURFACE (f)
+		= emacs_skia_surface_create_gl (FRAME_SKIA_GL_CONTEXT (f),
+						width, height,
+						FRAME_GL_FRAMEBUFFER (f),
+						GL_RGBA8);
+	    }
+	}
+      /* Fallback: create offscreen GL context if no GtkGLArea.  */
+      else if (pgtk_init_gl_context (f)
+	       && pgtk_setup_gl_framebuffer (f, width, height))
+	{
+	  /* Make GL context current.  */
+	  gdk_gl_context_make_current (FRAME_GDK_GL_CONTEXT (f));
+	  FRAME_SKIA_SURFACE (f)
+	    = emacs_skia_surface_create_gl (FRAME_SKIA_GL_CONTEXT (f),
+					    width, height,
+					    FRAME_GL_FRAMEBUFFER (f),
+					    GL_RGBA8);
+	}
+
+      if (!FRAME_SKIA_SURFACE (f))
+	return NULL;
+
+      canvas = emacs_skia_surface_get_canvas (FRAME_SKIA_SURFACE (f));
+      FRAME_SKIA_CANVAS (f) = canvas;
+
+      /* Create a reusable paint object.  */
+      if (!FRAME_SKIA_PAINT (f))
+	FRAME_SKIA_PAINT (f) = emacs_skia_paint_create ();
+
+      /* Clear the newly created surface with the background color.
+	 This is critical for GL surfaces where the FBO starts with
+	 undefined contents.  Without this, the first readback may
+	 show garbage data causing flickering.  */
+      {
+	unsigned long bg = FRAME_X_OUTPUT (f)->background_color;
+	Emacs_Color col;
+	col.pixel = bg;
+	pgtk_query_color (f, &col);
+	uint8_t r = col.red >> 8;
+	uint8_t g = col.green >> 8;
+	uint8_t b = col.blue >> 8;
+	emacs_skia_canvas_clear (canvas,
+				 EMACS_SKIA_COLOR (255, r, g, b));
+	/* Flush the clear operation for GL surfaces and reset Skia's
+	   GL state tracking since we just set up the GL context.  */
+	if (FRAME_SKIA_GL_CONTEXT (f))
+	  {
+	    emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+	    glFinish ();
+	    /* Reset Skia state after initial setup.  */
+	    emacs_skia_gl_context_reset (FRAME_SKIA_GL_CONTEXT (f));
+	    FRAME_SKIA_GL_STATE_DIRTY (f) = false;
+	  }
+      }
+    }
+  else if (FRAME_GDK_GL_CONTEXT (f))
+    {
+      /* For GL surfaces, ensure the GL context is current before any
+	 drawing operations.  Skia's GL backend requires this.  */
+      if (FRAME_GL_AREA (f))
+	gtk_gl_area_make_current (GTK_GL_AREA (FRAME_GL_AREA (f)));
+      else
+	gdk_gl_context_make_current (FRAME_GDK_GL_CONTEXT (f));
+
+      /* Tell Skia to re-query GL state since GTK/GDK may have modified
+	 it between frames.  Without this, Skia's cached GL state may be
+	 stale and rendering may go to the wrong target.  */
+      if (FRAME_SKIA_GL_CONTEXT (f))
+	emacs_skia_gl_context_reset (FRAME_SKIA_GL_CONTEXT (f));
+    }
+
+  emacs_skia_canvas_save (canvas);
+
+  return canvas;
+}
+
+void
+pgtk_end_skia_clip (struct frame *f)
+{
+  emacs_skia_canvas_t *canvas = FRAME_SKIA_CANVAS (f);
+  if (canvas)
+    emacs_skia_canvas_restore (canvas);
+}
+
+void
+pgtk_skia_set_paint_color (struct frame *f, unsigned long color,
+			   bool respects_alpha_background)
+{
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  if (!paint)
+    return;
+
+  Emacs_Color col;
+  col.pixel = color;
+  pgtk_query_color (f, &col);
+
+  uint8_t r = col.red >> 8;
+  uint8_t g = col.green >> 8;
+  uint8_t b = col.blue >> 8;
+  uint8_t a;
+
+  if (!respects_alpha_background)
+    {
+      a = 255;
+      emacs_skia_paint_set_blend_mode (paint,
+				       EMACS_SKIA_BLEND_SRC_OVER);
+    }
+  else
+    {
+      a = (uint8_t) (f->alpha_background * 255.0);
+      emacs_skia_paint_set_blend_mode (paint, EMACS_SKIA_BLEND_SRC);
+    }
+
+  emacs_skia_paint_set_color (paint, EMACS_SKIA_COLOR (a, r, g, b));
+}
+
+/* Destroy only the Skia surface, preserving GL context and GtkGLArea.
+   Used during resize to avoid recreating the entire GL setup.  */
+static void
+pgtk_skia_destroy_surface_only (struct frame *f)
+{
+  if (FRAME_SKIA_PAINT (f))
+    {
+      emacs_skia_paint_destroy (FRAME_SKIA_PAINT (f));
+      FRAME_SKIA_PAINT (f) = NULL;
+    }
+
+  /* Canvas is owned by surface, so just NULL it.  */
+  FRAME_SKIA_CANVAS (f) = NULL;
+
+  if (FRAME_SKIA_SURFACE (f))
+    {
+      /* For GL surfaces, make context current and flush before
+	 destroying to ensure any pending operations complete and
+	 the GrDirectContext state is clean.  */
+      if (FRAME_GDK_GL_CONTEXT (f))
+	{
+	  gdk_gl_context_make_current (FRAME_GDK_GL_CONTEXT (f));
+	  if (FRAME_SKIA_GL_CONTEXT (f))
+	    {
+	      emacs_skia_gl_context_flush (FRAME_SKIA_GL_CONTEXT (f));
+	      glFinish ();
+	    }
+	}
+      emacs_skia_surface_destroy (FRAME_SKIA_SURFACE (f));
+      FRAME_SKIA_SURFACE (f) = NULL;
+
+      /* Reset the GrDirectContext state after destroying the surface.
+	 This clears Skia's internal caches that may reference the
+	 old surface's backend render target.  */
+      if (FRAME_SKIA_GL_CONTEXT (f))
+	{
+	  emacs_skia_gl_context_reset (FRAME_SKIA_GL_CONTEXT (f));
+	}
+    }
+}
+
+void
+pgtk_skia_destroy_frame_context (struct frame *f)
+{
+  pgtk_skia_destroy_surface_only (f);
+
+  /* Clean up GL resources (includes Skia GL context, GDK GL context,
+     and GL framebuffer/texture).  */
+  pgtk_cleanup_gl_context (f);
+}
+
+/* Skia version of fill rectangle.  */
+static void
+pgtk_skia_fill_rectangle (struct frame *f, unsigned long color, int x,
+			  int y, int width, int height,
+			  bool respect_alpha_background)
+{
+  emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+  if (!canvas)
+    return;
+
+  pgtk_skia_set_paint_color (f, color, respect_alpha_background);
+
+  emacs_skia_irect_t rect = { x, y, x + width, y + height };
+  emacs_skia_canvas_draw_irect (canvas, &rect, FRAME_SKIA_PAINT (f));
+
+  pgtk_end_skia_clip (f);
+}
+
+/* Skia version of draw rectangle (stroked outline).  */
+static void
+pgtk_skia_draw_rectangle (struct frame *f, unsigned long color, int x,
+			  int y, int width, int height,
+			  bool respect_alpha_background)
+{
+  emacs_skia_canvas_t *canvas = pgtk_begin_skia_clip (f);
+  if (!canvas)
+    return;
+
+  emacs_skia_paint_t *paint = FRAME_SKIA_PAINT (f);
+  pgtk_skia_set_paint_color (f, color, respect_alpha_background);
+  emacs_skia_paint_set_stroke (paint, true);
+  emacs_skia_paint_set_stroke_width (paint, 1.0f);
+
+  /* Use float rect for proper stroke alignment (0.5 offset for crisp
+   * lines).  */
+  emacs_skia_rect_t rect
+    = { x + 0.5f, y + 0.5f, x + width + 0.5f, y + height + 0.5f };
+  emacs_skia_canvas_draw_rect (canvas, &rect, paint);
+
+  /* Reset to fill mode for subsequent operations.  */
+  emacs_skia_paint_set_stroke (paint, false);
+
+  pgtk_end_skia_clip (f);
+}
+
+#endif /* USE_SKIA */
+
+#ifdef USE_SKIA
+DEFUN ("pgtk-skia-gl-enabled-p", Fpgtk_skia_gl_enabled_p,
+       Spgtk_skia_gl_enabled_p, 0, 1, 0,
+       doc: /* Return non-nil if Skia GL acceleration is active for FRAME.
+If FRAME is nil, use the selected frame.  */)
+(Lisp_Object frame)
+{
+  struct frame *f = decode_window_system_frame (frame);
+  if (FRAME_GDK_GL_CONTEXT (f) && FRAME_SKIA_GL_CONTEXT (f))
+    return Qt;
+  return Qnil;
+}
+#endif
+
+void
+syms_of_pgtkterm (void)
+{
+  DEFSYM (Qmodifier_value, "modifier-value");
+  DEFSYM (Qalt, "alt");
+  DEFSYM (Qhyper, "hyper");
+  DEFSYM (Qmeta, "meta");
+  DEFSYM (Qsuper, "super");
+  DEFSYM (Qcontrol, "control");
+  DEFSYM (QUTF8_STRING, "UTF8_STRING");
+  /* Referenced in gtkutil.c.  */
+  DEFSYM (Qtheme_name, "theme-name");
+  DEFSYM (Qfile_name_sans_extension, "file-name-sans-extension");
+
+  DEFSYM (Qfile, "file");
+  DEFSYM (Qurl, "url");
+
+  DEFSYM (Qlatin_1, "latin-1");
+
+  xg_default_icon_file
+    = build_string ("icons/hicolor/scalable/apps/emacs.svg");
+  staticpro (&xg_default_icon_file);
+
+  DEFSYM (Qx_gtk_map_stock, "x-gtk-map-stock");
+
+  DEFSYM (Qcopy, "copy");
+  DEFSYM (Qmove, "move");
+  DEFSYM (Qlink, "link");
+  DEFSYM (Qprivate, "private");
+
+  Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
+  Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
+  Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
+  Fput (Qsuper, Qmodifier_value, make_fixnum (super_modifier));
+  Fput (Qcontrol, Qmodifier_value, make_fixnum (ctrl_modifier));
+
+  DEFVAR_LISP ("x-ctrl-keysym", Vx_ctrl_keysym,
+	       doc: /* SKIP: real doc in xterm.c.  */);
+  Vx_ctrl_keysym = Qnil;
+
+  DEFVAR_LISP ("x-alt-keysym", Vx_alt_keysym,
+	       doc: /* SKIP: real doc in xterm.c.  */);
+  Vx_alt_keysym = Qnil;
 
   DEFVAR_LISP ("x-hyper-keysym", Vx_hyper_keysym,
 	       doc: /* SKIP: real doc in xterm.c.  */);
@@ -7514,10 +9246,15 @@ syms_of_pgtkterm (void)
   window_being_scrolled = Qnil;
   staticpro (&window_being_scrolled);
 
+#ifdef USE_SKIA
+  defsubr (&Spgtk_skia_gl_enabled_p);
+#endif
+
   /* Tell Emacs about this window system.  */
   Fprovide (Qpgtk, Qnil);
 }
 
+#ifdef USE_CAIRO
 /* Cairo does not allow resizing a surface/context after it is
    created, so we need to trash the old context, create a new context
    on the next cr_clip_begin with the new dimensions and request a
@@ -7729,10 +9466,186 @@ pgtk_cr_export_frames (Lisp_Object frames, cairo_surface_type_t surface_type)
       cairo_surface_flush (surface);
       cairo_surface_write_to_png_stream (surface, pgtk_cr_accumulate_data, &acc);
     }
-#endif
+# endif
   unblock_input ();
 
   unbind_to (count, Qnil);
 
   return CALLN (Fapply, Qconcat, Fnreverse (acc));
 }
+#endif /* USE_CAIRO */
+
+#ifdef USE_SKIA
+/* Skia-based frame export.
+   Export types: pdf, svg (PNG would need additional work).  */
+
+/* Callback adapter for Skia write function.  */
+struct skia_export_accumulator
+{
+  Lisp_Object data;
+};
+
+static size_t
+pgtk_skia_accumulate_data (void *ctx, const void *data, size_t size)
+{
+  struct skia_export_accumulator *acc
+    = (struct skia_export_accumulator *) ctx;
+  acc->data = Fcons (make_unibyte_string ((const char *) data, size),
+		     acc->data);
+  return size;
+}
+
+/* Export frames to PDF, SVG, or PNG using Skia.
+   Returns the exported data as a unibyte string.  */
+Lisp_Object
+pgtk_skia_export_frames (Lisp_Object frames, Lisp_Object type)
+{
+  struct frame *f;
+  int width, height;
+  struct skia_export_accumulator acc = { Qnil };
+  specpdl_ref count = SPECPDL_INDEX ();
+  bool is_pdf = NILP (type) || EQ (type, Qpdf);
+  bool is_svg = EQ (type, Qsvg);
+  bool is_png = EQ (type, Qpng);
+
+  if (!is_pdf && !is_svg && !is_png)
+    error ("Skia export supports pdf, svg, and png types");
+
+  if ((is_svg || is_png) && !NILP (XCDR (frames)))
+    error ("SVG and PNG export cannot handle multiple frames");
+
+  redisplay_preserve_echo_area (31);
+
+  f = XFRAME (XCAR (frames));
+  frames = XCDR (frames);
+  width = FRAME_PIXEL_WIDTH (f);
+  height = FRAME_PIXEL_HEIGHT (f);
+
+  block_input ();
+
+  if (is_pdf)
+    {
+      /* Create PDF document.  */
+      emacs_skia_document_t *doc
+	= emacs_skia_document_create_pdf (pgtk_skia_accumulate_data,
+					  &acc, width, height);
+      if (!doc)
+	{
+	  unblock_input ();
+	  error ("Failed to create PDF document");
+	}
+
+      while (1)
+	{
+	  emacs_skia_canvas_t *canvas
+	    = emacs_skia_document_begin_page (doc, width, height);
+	  if (!canvas)
+	    {
+	      emacs_skia_document_close (doc);
+	      unblock_input ();
+	      error ("Failed to begin PDF page");
+	    }
+
+	  /* Save current Skia canvas and use document page.  */
+	  emacs_skia_canvas_t *saved_canvas = FRAME_SKIA_CANVAS (f);
+	  FRAME_SKIA_CANVAS (f) = canvas;
+
+	  /* Clear and redraw the frame.  */
+	  emacs_skia_canvas_clear (canvas,
+				   EMACS_SKIA_COLOR_RGB (255, 255,
+							 255));
+	  expose_frame (f, 0, 0, width, height);
+
+	  FRAME_SKIA_CANVAS (f) = saved_canvas;
+	  emacs_skia_document_end_page (doc);
+
+	  if (NILP (frames))
+	    break;
+
+	  f = XFRAME (XCAR (frames));
+	  frames = XCDR (frames);
+	  width = FRAME_PIXEL_WIDTH (f);
+	  height = FRAME_PIXEL_HEIGHT (f);
+
+	  unblock_input ();
+	  maybe_quit ();
+	  block_input ();
+	}
+
+      emacs_skia_document_close (doc);
+    }
+  else if (is_svg)
+    {
+      /* Create SVG canvas.  */
+      emacs_skia_canvas_t *canvas
+	= emacs_skia_svg_canvas_create (pgtk_skia_accumulate_data,
+					&acc, width, height);
+      if (!canvas)
+	{
+	  unblock_input ();
+	  error ("Failed to create SVG canvas");
+	}
+
+      /* Save current Skia canvas and use SVG canvas.  */
+      emacs_skia_canvas_t *saved_canvas = FRAME_SKIA_CANVAS (f);
+      FRAME_SKIA_CANVAS (f) = canvas;
+
+      /* Clear and redraw the frame.  */
+      emacs_skia_canvas_clear (canvas,
+			       EMACS_SKIA_COLOR_RGB (255, 255, 255));
+      expose_frame (f, 0, 0, width, height);
+
+      FRAME_SKIA_CANVAS (f) = saved_canvas;
+      emacs_skia_svg_canvas_finish (canvas);
+    }
+  else if (is_png)
+    {
+      /* Create a temporary raster surface for PNG export.  */
+      emacs_skia_surface_t *png_surface
+	= emacs_skia_surface_create_raster (width, height);
+      if (!png_surface)
+	{
+	  unblock_input ();
+	  error ("Failed to create PNG surface");
+	}
+
+      emacs_skia_canvas_t *canvas
+	= emacs_skia_surface_get_canvas (png_surface);
+      if (!canvas)
+	{
+	  emacs_skia_surface_destroy (png_surface);
+	  unblock_input ();
+	  error ("Failed to get PNG canvas");
+	}
+
+      /* Save current Skia canvas and use PNG canvas.  */
+      emacs_skia_canvas_t *saved_canvas = FRAME_SKIA_CANVAS (f);
+      FRAME_SKIA_CANVAS (f) = canvas;
+
+      /* Clear and redraw the frame.  */
+      emacs_skia_canvas_clear (canvas,
+			       EMACS_SKIA_COLOR_RGB (255, 255, 255));
+      expose_frame (f, 0, 0, width, height);
+
+      FRAME_SKIA_CANVAS (f) = saved_canvas;
+
+      /* Flush the surface and encode to PNG.  */
+      emacs_skia_surface_flush (png_surface);
+      if (!emacs_skia_surface_write_to_png (png_surface,
+					    pgtk_skia_accumulate_data,
+					    &acc))
+	{
+	  emacs_skia_surface_destroy (png_surface);
+	  unblock_input ();
+	  error ("Failed to encode PNG");
+	}
+
+      emacs_skia_surface_destroy (png_surface);
+    }
+
+  unblock_input ();
+  unbind_to (count, Qnil);
+
+  return CALLN (Fapply, Qconcat, Fnreverse (acc.data));
+}
+#endif /* USE_SKIA */
-- 
2.52.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0006-Document-with-skia-configure-option.patch

From 5ac0b7169cc00f2bc751cdee26e41d00e3e3780b Mon Sep 17 00:00:00 2001
From: Arthur Heymans <arthur@HIDDEN>
Date: Tue, 27 Jan 2026 08:07:24 +0100
Subject: [PATCH 6/6] Document --with-skia configure option

* INSTALL: Document --with-skia option, how to obtain and build Skia,
and the required environment variables.

* etc/NEWS: Announce the new --with-skia configure option for
GPU-accelerated rendering in PGTK builds.
---
 INSTALL  | 27 +++++++++++++++++++++++++++
 etc/NEWS | 11 ++++++++++-
 2 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/INSTALL b/INSTALL
index 4558f706f06..ee22a5a2cc1 100644
--- a/INSTALL
+++ b/INSTALL
@@ -439,6 +439,33 @@ faster when running over X connections with high latency, it is likely
 to crash when a new frame is created on a display connection opened
 after a display connection is closed.
 
+Use --with-skia to compile Emacs with Skia drawing instead of Cairo.
+This option is only available for PGTK builds (--with-pgtk) and provides
+GPU-accelerated rendering via OpenGL.  Skia is Google's 2D graphics
+library used in Chrome and Android.  Skia must be built and installed
+separately before configuring Emacs.
+
+  To obtain Skia:
+    git clone https://skia.googlesource.com/skia.git
+    cd skia
+    python3 tools/git-sync-deps
+
+  To build Skia (example for Linux):
+    bin/gn gen out/Release --args='is_official_build=true skia_use_system_freetype2=true'
+    ninja -C out/Release
+
+  Set environment variables to point to your Skia installation:
+    export SKIA_DIR=/path/to/skia
+    export SKIA_CFLAGS="-I$SKIA_DIR/include -I$SKIA_DIR"
+    export SKIA_LIBS="-L$SKIA_DIR/out/Release -lskia"
+
+  Alternatively, if using the Nix package manager, the repository's
+  flake.nix provides a development shell with Skia pre-configured:
+    nix develop
+
+  Then configure with:
+    ./configure --with-pgtk --with-skia
+
 Use --with-modules to build Emacs with support for dynamic modules.
 This needs a C compiler that supports '__attribute__ ((cleanup (...)))',
 as in GCC 3.4 and later.
diff --git a/etc/NEWS b/etc/NEWS
index 9d36f6c3d96..99e209467e0 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -48,7 +48,16 @@ incorrectly in rare cases.
 This allows to specify the directory where the user unit file for
 systemd is installed; default is '${prefix}/usr/lib/systemd/user'.
 
-
+---
+** New configure option '--with-skia' for GPU-accelerated rendering.
+When configured with '--with-skia', the PGTK build of Emacs uses
+Google's Skia graphics library for all drawing operations instead of
+Cairo.  This provides GPU-accelerated rendering via OpenGL.  Requires
+OpenGL support (libepoxy or libGL) and is mutually exclusive with
+'--with-cairo'.  The Skia library must be installed separately; see
+the INSTALL file for details on obtaining and building Skia.
+
+
 * Startup Changes in Emacs 31.1
 
 ** In compatible terminals, 'xterm-mouse-mode' is turned on by default.
-- 
2.52.0


--=-=-=
Content-Type: text/plain


This patch series adds Skia as a GPU-accelerated rendering backend for
the PGTK build of Emacs.  Skia is Google's 2D graphics library used in
Chrome and Android.
When configured with --with-skia, Emacs uses Skia for all drawing
operations instead of Cairo, providing GPU-accelerated rendering via
OpenGL through GtkGLArea.
The series is split into logical commits for easier review:
  1. Skia C wrapper - standalone C API around Skia's C++ library
  2. Build system - configure.ac and Makefile.in support
  3. Header definitions - type definitions and declarations
  4. Font driver - Skia font rendering with FreeType/HarfBuzz
  5. Rendering implementation - all drawing primitives
  6. Documentation - INSTALL and NEWS updates
Tested with both --with-pgtk (Cairo) and --with-pgtk --with-skia.

I've send the copyright assignement request.
Disclaimer: I did use an LLM to help with this work.

Arthur Heymans (6):
  Add Skia C wrapper library
  Add --with-skia configure option and build system support
  Add Skia type definitions and declarations to headers
  Add Skia font driver
  Implement Skia rendering for PGTK
  Document --with-skia configure option

 INSTALL                 |   27 +
 configure.ac            |  191 +++-
 etc/NEWS                |   11 +-
 src/Makefile.in         |   27 +-
 src/dispextern.h        |   23 +-
 src/font.c              |    8 +-
 src/font.h              |    7 +
 src/ftfont.h            |    7 +
 src/gtkutil.c           |   19 +-
 src/image.c             |  255 ++++-
 src/pgtkfns.c           |   86 +-
 src/pgtkterm.c          | 2141 ++++++++++++++++++++++++++++++++++--
 src/pgtkterm.h          |   74 +-
 src/skia/emacs_skia.cpp | 2271 +++++++++++++++++++++++++++++++++++++++
 src/skia/emacs_skia.h   |  650 +++++++++++
 src/skiafont.c          |  774 +++++++++++++
 16 files changed, 6376 insertions(+), 195 deletions(-)
 create mode 100644 src/skia/emacs_skia.cpp
 create mode 100644 src/skia/emacs_skia.h
 create mode 100644 src/skiafont.c

-- 
2.52.0



--=-=-=--




Acknowledgement sent to Arthur Heymans <arthur@HIDDEN>:
New bug report received and forwarded. Copy sent to bug-gnu-emacs@HIDDEN. Full text available.
Report forwarded to bug-gnu-emacs@HIDDEN:
bug#80272; Package emacs. Full text available.
Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.
Last modified: Tue, 27 Jan 2026 08:00:02 UTC

GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997 nCipher Corporation Ltd, 1994-97 Ian Jackson.