Planet Igalia WebKit

October 06, 2026

WPE WebKit Blog

WPEPlatform: the new WPE API

With the 2.54 release, WPEPlatform became the default way of integrating WPE WebKit with the underlying platform, and its API is now considered stable. At the same time, the libwpe-based API that WPE has relied on since its beginnings is now deprecated. In this article we explain why this change was made, how applications use the new API, and what is involved in migrating an existing application from libwpe.

Back in 2022 we published an overview of the WPE WebKit project, describing the different components of WPE and how they fit together. Most of what that article explains about WebKit and the WPE API is still valid, but the way WPE connects to the platform has changed considerably since then, so this is a good moment to revisit that part of the picture.

How WPE used to integrate with the platform

Since its beginnings, WPE has delegated graphics and input to code that lives outside of WebKit. libwpe defined a generic interface for these, and a backend, loaded at runtime, implemented that interface for a given platform. WPEBackend-fdo, based on Wayland and other freedesktop.org technologies, was the reference backend, while Cog provided a convenience layer on top for those writing browsers.

This design achieved its main goal: hardware vendors and integrators could support WPE on their platforms without having to modify WebKit itself. However, it also meant that a good part of the platform work was left to the application. To show a web page on screen using WPEBackend-fdo, an application had to:

  • Load a libwpe backend with wpe_loader_init() and initialize it, usually by passing it an EGL display.
  • Create a view backend through the “exportable” API of WPEBackend-fdo, wrap it in a WebKitWebViewBackend, and pass that to the web view.
  • Receive, in a set of callbacks, the buffers exported by WebKit for each frame, present them on screen, release them, and notify WebKit when the frame had been displayed.
  • Collect input events from the windowing system and forward them to WebKit with the wpe_view_backend_dispatch_*_event() family of functions.

As a result, even a simple browser required a fair amount of knowledge of the graphics pipeline, and the code involved was split across three repositories (WebKit, libwpe, and WPEBackend-fdo) that had to be kept in sync. Much of this code was the same from one application to the next, and Cog provided a ready-made implementation of it for applications that did not want to write their own.

What WPEPlatform changes

WPEPlatform is a GObject-based library that lives in the WebKit repository and is built as part of WPE WebKit. It replaces both libwpe and the backends written against it, and it moves the responsibility of rendering and input handling away from the application and into WebKit and the platform implementation.

The library is only used by WebKit’s UI process. As before, the web process renders the page into buffers that are shared with the UI process (DMA-BUF buffers on Linux, AHardwareBuffer on Android, or shared memory when rendering in software). The difference is that WebKit now hands these buffers directly to the platform implementation, which takes care of presenting them, without the application having to take part in the process.

The API is built around four classes:

  • WPEDisplay: the connection to the platform, and the object that creates all the others.
  • WPEToplevel: a top-level surface, which is normally a window, or the whole output on platforms that have no windows.
  • WPEView: the surface where a single web view is rendered, hosted in a toplevel. It also receives the input events for that web view.
  • WPEBuffer: the pixel data produced by WebKit and handed to the view, with a subclass for each type of buffer.

These are complemented by smaller classes for screens, keymaps, settings, input methods, gestures, gamepads, the clipboard, and accessibility. Several of them, such as screens, settings, input methods, and gestures, had no equivalent in libwpe.

WPE WebKit ships with three built-in platform implementations: Wayland, DRM/KMS (for devices that drive the display directly, without a compositor), and headless (for testing and offscreen rendering). Other implementations can be developed out of tree and installed as modules, which WPE WebKit discovers at runtime. wpe-platform-gtk, which embeds WPE web views in GTK 4 applications, is an example of this.

A diagram of the WPE architecture: the application uses WPE WebKit, which includes the WebKit engine and API and the WPEPlatform API. WPEPlatform has built-in Wayland, DRM/KMS, and headless implementations, and custom implementations can be provided for other platforms.

Using WPEPlatform in an application

For most applications, WPEPlatform is not visible at all. A WebKitWebView created without a backend selects a platform automatically: WPE WebKit tries the available platform implementations in order of priority and uses the first one that connects successfully. The following program is a complete, if minimal, browser:

#include <wpe/webkit.h>
#include <wpe/wpe-platform.h>

static void
on_view_closed (WPEView *view, gpointer user_data)
{
    g_main_loop_quit (user_data);
}

int
main (int argc, char *argv[])
{
    g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
    g_autoptr(WebKitWebView) web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW, NULL);

    WPEView *view = webkit_web_view_get_wpe_view (web_view);
    g_signal_connect (view, "closed", G_CALLBACK (on_view_closed), loop);

    webkit_web_view_load_uri (web_view, argc > 1 ? argv[1] : "https://wpewebkit.org");
    g_main_loop_run (loop);

    return 0;
}

It only needs the wpe-webkit-2.0 pkg-config module to build, which pulls in wpe-platform-2.0 when WPE WebKit is built with WPEPlatform support (the default since 2.54). Note that the closed signal is emitted when the user closes the window, and of the built-in platforms, only Wayland emits it. On DRM and headless the application decides by itself when to exit.

The platform API comes into play when an application needs more than these defaults. The most common cases are the following.

Choosing a platform. The WPE_PLATFORM environment variable (wayland, drm, or headless) forces a given platform without any changes to the code. An application that is meant to run on a single platform can also create the display itself and pass it to the web view:

#include <wpe/wayland/wpe-wayland.h>

g_autoptr(GError) error = NULL;
g_autoptr(WPEDisplay) display = wpe_display_wayland_new ();
if (!wpe_display_connect (display, &error))
    g_error ("Could not connect to Wayland: %s", error->message);

WebKitWebView *web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW,
                                        "display", display,
                                        NULL);

The Wayland-specific API is available with the same wpe-webkit-2.0 module, but an application that depends on it can check for the wpe-platform-wayland-2.0 pkg-config module, which is only installed when WPE WebKit is built with the Wayland platform enabled. The DRM and headless implementations work the same way, with wpe_display_drm_new() and wpe_display_headless_new().

Handling input. Input events are delivered to the WPEView through the event signal before they reach the web page. An application can connect to it to implement keyboard shortcuts, returning TRUE to stop the event from reaching the page:

static gboolean
on_view_event (WPEView *view, WPEEvent *event, gpointer user_data)
{
    if (wpe_event_get_event_type (event) == WPE_EVENT_KEYBOARD_KEY_DOWN
        && (wpe_event_get_modifiers (event) & WPE_MODIFIER_KEYBOARD_CONTROL)
        && wpe_event_keyboard_get_keyval (event) == WPE_KEY_q) {
        g_main_loop_quit (user_data);
        return TRUE;
    }
    return FALSE;
}

g_signal_connect (view, "event", G_CALLBACK (on_view_event), loop);

Controlling the window. The toplevel that hosts the view is available through wpe_view_get_toplevel(), and provides functions to set the title (wpe_toplevel_set_title()), resize, maximize, or switch to fullscreen (wpe_toplevel_fullscreen()). Changes in its state are reported by the toplevel-state-changed signal of the view. How much of this is supported depends on the platform: Wayland implements all of it, the headless implementation only keeps track of size and fullscreen state, and DRM does not support them, since its toplevel always covers the whole screen.

Finally, wpe_display_get_settings() gives access to WPESettings, where applications can override platform settings such as the dark mode preference, reduced motion, or contrast. The prefers-color-scheme, prefers-reduced-motion, and prefers-contrast CSS media queries follow these settings.

Migrating from libwpe

For an application, most of the migration consists of removing code. With WPEBackend-fdo, creating a web view looked roughly like this, with most of the actual work happening in the callbacks of exportable_client:

struct wpe_view_backend_exportable_fdo *exportable =
    wpe_view_backend_exportable_fdo_egl_create (&exportable_client, app, width, height);
struct wpe_view_backend *wpe_backend =
    wpe_view_backend_exportable_fdo_get_view_backend (exportable);
WebKitWebViewBackend *backend =
    webkit_web_view_backend_new (wpe_backend, destroy_backend, exportable);
WebKitWebView *web_view = webkit_web_view_new (backend);

With WPEPlatform, the same is achieved with a single line:

WebKitWebView *web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW, NULL);

webkit_web_view_new() is only available when WPE WebKit is built with the legacy API, so applications need to switch to g_object_new(). The rest of the application code that uses the WebKit API (settings, network sessions, navigation, and so on) does not need any changes. The diagram below summarizes what moves out of the application:

A diagram comparing the two architectures. Before: the application, often through Cog, uses WPE WebKit, libwpe, and WPEBackend-fdo, and takes care of loading the backend, handling exported buffers, and dispatching input. After: the application only uses the WebKit API, and WPEPlatform, inside WPE WebKit, handles rendering and input.

The rest of the libwpe code in an application maps to WPEPlatform as follows:

libwpe / WPEBackend-fdo WPEPlatform
wpe_loader_init() and backend initialization Automatic, or WPE_PLATFORM, or creating a WPEDisplay
Exportable client callbacks, buffer release, frame completion Handled by WebKit and the platform implementation
wpe_view_backend_dispatch_*_event() Handled by the platform; intercept with the WPEView::event signal
Fullscreen handler wpe_toplevel_fullscreen() and the toplevel state

There are a few things in libwpe and WPEBackend-fdo that have no equivalent in WPEPlatform. The process provider API of libwpe is gone, since WebKit is again in charge of launching its auxiliary processes. The only exception is Android, which has its own WPEProcessManager API. The audio extension of WPEBackend-fdo has not been supported since 2.46, and the video plane extension, used to display video frames on hardware overlay planes, has no equivalent yet.

Those who maintain a custom backend for WPEBackend-fdo, rather than an application, will need to port it to a WPEPlatform implementation. This means subclassing WPEDisplay, WPEToplevel, and WPEView, and optionally the input method, keymap, and screen classes. The tutorial on writing a platform implementation covers this in detail. In many cases, extending one of the built-in implementations might be enough: the Wayland one, for example, exposes its wl_display, wl_compositor, and wl_surface objects, which makes it possible to add support for additional Wayland protocols.

On the build side, the legacy API is still built by default in 2.54 and will continue to be maintained while applications migrate. Once an application no longer needs it, it can be disabled at build time with the ENABLE_WPE_LEGACY_API=OFF CMake option. As for Cog, it will not have further stable releases beyond the 0.18.x series, so we recommend that new applications use the WebKit and WPEPlatform APIs directly.

Further reading

The reference documentation for WPEPlatform has been considerably extended for this release, and is the best place to continue from here:

For an example of a platform implementation that lives entirely outside of WebKit, Alex has written about WPE WebKit on Android with WPEPlatform, which shows how far the API can be taken.

If you run into problems while migrating, or find that something you relied on in libwpe is missing, please let us know, either by filing a bug in WebKit’s Bugzilla, on the webkit-wpe mailing list, or in the #wpe Matrix channel.

October 06, 2026 12:00 AM

October 05, 2026

Igalia WebKit Team

WebKit Igalia Periodical #80

Update on what happened in WebKit in the week from September 28 to October 5.

Another week populated with updates! This time we sweep through a new crypto backend, more layer-based SVG engine updates, better filters, and some miscellaneous updates.

Cross-Port 🐱

Add moving steps handling to map, img, label and radio input elements to handle their internal book keeping. This also handles custom element registries.

Initial support to build WPE and WebKitGTK with the OpenSSL crypto backend has landed. This is still in development and not yet intended for use, but progress on bringing it to parity with the libgcrypt backend is ongoing.

Graphics 🖼️

Fixed stacked backdrop filters in the Skia compositor, so an element with a backdrop-filter placed over an earlier element with its own backdrop-filter now shows that earlier element with its filter applied, instead of without it.

Removed redundant save/restore pairs when painting transformed SVG shapes in the Layer-Based SVG Engine (LBSE), so each shape now uses a single pair, just like the legacy SVG engine. When DOM rendering happens in the GPU process, every state change is an IPC message, so this cuts LBSE from about 8.5 to about 6.5 messages per shape and closes the gap to the legacy engine.

Fixed transformed <clipPath> elements that use clipPathUnits="objectBoundingBox" in the Layer-Based SVG Engine (LBSE), whose transform was wrongly applied in bounding box units instead of user space. Rendering and hit testing now apply it in user space, matching the legacy SVG engine.

Fixed hit testing and repainting of non-scaling strokes in the Layer-Based SVG Engine (LBSE), which kept using the old stroke width after the transform of the shape or one of its ancestors changed. A vector-effect: non-scaling-stroke stroke depends on every transform up to the outermost <svg> element, so its cached bounds are now cleared whenever one of those transforms changes.

Fixed the source area of filters and backdrop filters: a filter's offscreen surface only covered the visible part of its layer, so when a layer was partly scrolled out of the viewport, its drop shadow was cut off even where the shadow itself fell inside the viewport. The surface now also covers the area the filter reads from outside the visible part, as in Chromium, and a backdrop-filter blur is mirrored at the border box as the spec requires, which fixes several web-platform-tests.

Fixed viewport clipping of nested <svg> elements with overflow: hidden in the Layer-Based SVG Engine (LBSE), which did not clip descendants that have their own layer, such as a transformed <g>. Containers that clip their overflow now get their own layer, like containers using clip-path or mask already do, so layered descendants are clipped correctly.

Fixed misplaced child layers of SVG elements with a filter in the Layer-Based SVG Engine (LBSE), which were drawn in the wrong position whenever an ancestor layer had a transform applied.

WPE WebKit 📟

Restored the Direct Rendering Manager (DRM) vertical blank monitor for the legacy WPE API, so rendering is once again paced by the display's actual refresh cycle instead of a fixed 60 Hz timer. Two regressions had silently disabled it: a missing display ID forced the timer fallback, and a shadowed variable broke the display controller lookup.

That’s all for this week!

by Igalia WebKit Team at October 05, 2026 07:21 PM

September 29, 2026

Igalia WebKit Team

WebKit Igalia Periodical #79

Update on what happened in WebKit in the week from September 21 to September 28.

Following the stable releases last week, we have the first security advisory covering fixes included in them; the flurry of fixes and polish for the Skia compositor continues; SharedArrayBuffer gets re-enabled, and more.

Cross-Port 🐱

Fixed a crash on exit in WebKitGTK and WPE WebKit, caused by the EGL display being torn down in an atexit() handler while other threads were still using it, by now destroying it on the main thread once the GPU process has stopped its WebGL thread and the web process has closed its pages.

Fixed moving steps for source element to account for the <media> and <model> elements.

JavaScriptCore 🐟

The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.

SharedArrayBuffer is now enabled for the WPE and GTK ports. SharedArrayBuffer is exposed when the origin is cross-origin isolated. We have enabled 35 associated cross-origin isolation tests. Initialization was designed to work with Android service-launched processes, too.

Graphics 🖼️

Fixed a bug with 3D-transformed elements that are tilted so far away from the viewer that part of them ends up behind the camera, where WebKit got the visible area of the element wrong and computed the hidden part instead. As a result, the visible part of such elements is no longer left blank in WebKitGTK and WPE—painting and hit-testing works properly now.

Fixed oversized damage regions for 3D-transformed layers that cross the camera plane, by clipping their projected polygon against the clip bounds so the damage now matches what the layer actually paints.

GPU renderer small paths now use Skia distance fields. Bitmap atlas entries depended on the transform and subpixel position, causing animated paths to be re-rasterized and uploaded every frame. With distance field indexed entries we can reuse them across transforms. We are improving MotionMark Suits results on Raspberry Pi 4 and desktop.

Fixed the paint order of preserve-3d layers that cross the camera plane in the Skia-based compositor. Layer corners behind the camera used to be flipped to the far side of the screen when projected—layers were sorted against the wrong geometry and far layers painted over near ones. Now the part of a layer that lies behind the camera is clipped away before sorting, which makes complex 3D scenes like the CSS FPS demo render correctly.

Aligned the filter surface with the device pixel grid in the Skia-based compositor, so layers with CSS filters that end up at fractional device positions are no longer resampled when the filtered result is drawn. This fixes wrong colors in the outermost rows and columns of pixels of such layers, which now render the same as they would without a filter.

Fixed SMIL-animated viewBox changes being ignored on the outermost <svg> element in the Layer-Based SVG Engine (LBSE), bringing it in line with the legacy SVG engine.

Fixed edge artifacts on scaled or transformed non-repeating background images in the Skia-based compositor, which painted all composited background images as if they repeat, so a thin line of the opposite edge showed up along the border. Only tiles that actually repeat are painted that way now, which removes the yellow line that showed up above the gun in the CSS FPS demo. This fixes the last known artifact in that demo.

Fixed rendering artifacts in WebKitGTK and WPE WebKit for 3D-transformed layers that extend behind the camera under a perspective (for example a large element under rotateX(90deg)), whose re-composited area came out too small because corners behind the camera were projected onto the wrong side of the frame, and are now clipped away before mapping.

WebKitGTK 🖥️

Fixed touch positions in the GTK port, which were offset by the surface transform, the room left for the client-side decoration shadows, because it was not subtracted from the raw GdkEvent position, so every touch landed away from the finger unless the window was maximized. The same change cancels the touch sequences that were stranded when the web view is unmapped or a dialog was shown, which left the page with a finger that was never released.

WPE WebKit 📟

WPE Platform API 🧩

New, modern platform API that supersedes usage of libwpe and WPE backends.

The ENABLE_WPE_PLATFORM build option has been removed. The new WPE Platform API, which was enabled by default since 2.54, is now always enabled.

Releases 📦️

A new security advisory, WSA-2026-0006, has been published for WebKitGTK and for WPE. This covers issues fixed in the recent 2.54.0 releases, and for the first time includes security issues fixed in the bundled ANGLE and Skia libraries. Updating to the latest stable release is highly recommended.

That’s all for this week!

by Igalia WebKit Team at September 29, 2026 12:40 AM

September 21, 2026

Igalia WebKit Team

WebKit Igalia Periodical #78

Update on what happened in WebKit in the week from September 14 to September 21.

Following up to last week, we have a handful of exciting fixes to WebKitGTK and WPE WebKit covering Sysprof, rendering of CSS "filter" and "backdrop-filter", and the Layer-Based SVG Engine. We also have optimizations, stable releases, and new blog posts!

Cross-Port 🐱

Fixed the pairing of begin and end marks in SysprofAnnotator, which kept only one begin per mark and therefore mistimed recursive, concurrent and overlapping marks, so Sysprof traces of WebKitGTK and WPE WebKit now show correct durations for nested work.

Graphics 🖼️

Fixed CSS filter and backdrop-filter rendering on transformed elements in the Skia compositor used by WebKitGTK and WPE WebKit, where drop-shadow always fell in the same direction on screen instead of turning with the element, blur was scaled by neither the transform nor the device scale factor, and perspective was not handled at all. The filtered subtree is now painted with the layer transform on the canvas into a surface in the layer's own plane at a fixed scale, with the filter applied there and the result drawn back through the layer transform, as Chromium does.

Stopped rasterizing the parts of a tile that the clip discards in the Skia backing store used by the GTK and WPE ports. Both the drawn quad and its source rectangle are now cut down to the clip first, so the rasterizer no longer fills pixels that are immediately thrown away when a tile is larger than the clip.

Fixed the transform attribute on the outermost <svg> element, which the legacy SVG engine ignored entirely and the Layer-Based SVG Engine (LBSE) wrongly applied inside viewBox space. The attribute is now turned into the CSS transform property, one CSS function per SVG function, so that it transforms the element's CSS box, matching the behaviour of Chromium and Firefox.

Releases 📦️

After another six-month development cycle, WebKitGTK 2.54.0 and WPE WebKit 2.54.0 have been published. These are the first stable releases of the 2.54 series, that as a highlight include a completely new compositor based on Skia. As it is becoming tradition with each major release, two blog posts go into details of what's new in WPE WebKit 2.54, and in WebKitGTK 2.54.

This release is a milestone for WPE WebKit: after a couple of years in the incubator, the new WPEPlatform embedding API is now considered stable and recommended for new developments. As part of the stabilization, the WPEPlatform manual now includes not one, but two introductory tutorials, and a migration guide to aid migration from the (now) legacy libwpe-based embedding API.

Note that the libwpe-based legacy API is still available and will be kept in light maintenance mode for the foreseeable future.

Community & Events 🤝

Coinciding with the first stable releases, Carlos García has written about the new Skia compositor that is debuting in the 2.54 series. It includes plenty of details on why and how the new compositor has been built to replace the aging TextureMapper, and measurements that demonstrate how it is bringing graphics performance improvements across the board.

At the same time, Nikolas Zimmermann published a companion article about damage handling in the new Skia compositor that explains how the feature it has been rewritten with a focus on correctness while ensuring that it saves doing unnecessary work without incurring on excessive busywork that would cancel the benefits.

That’s all for this week!

by Igalia WebKit Team at September 21, 2026 08:15 PM

Carlos García Campos

Skia compositor for WPE WebKit and WebKitGTK

WPE WebKit and WebKitGTK 2.54 have been released with a bunch of improvements and new APIs as usual, but there’s one point that kept the Igalia WebKit graphics team busy for the whole cycle: the new Skia-based compositor. The replacement of Cairo with Skia for content rendering has been a success and it’s already well integrated and optimized. We thought we could try to use Skia for the composition too and replace TextureMapper with Skia. TextureMapper was introduced in 2010 for the Qt port and later adopted by other ports. It uses the OpenGL ES API and maintains a collection of shader programs to paint different content. Nowadays TextureMapper is mostly the same code and shader programs, and it’s unmaintained and missing features. However, the performance was good and it has served us really well all these years. So, this time the goal was not to get better results in benchmarks, but to modernize the implementation, reduce the amount of code to maintain ourselves (like all shader programs) and make it easier to implement the missing features and fix existing bugs. This post is a summary of all the work we have done this cycle to implement the new Skia compositor.

SkiaCompositingLayer

The first step was adding an SkiaCompositingLayer class to replace TextureMapperLayer and adapt all the code to use one or the other depending on an environment variable. The initial implementation was based on the TextureMapper one for the things that are common like iterating the layer tree, computing transformations, etc. The way layers produced their contents didn’t change, so we were receiving textures for tiled content, video buffers, WebGL, accelerated 2D canvas, etc. SkiaCompositingLayer created a Ganesh Skia surface to draw those textures using SkCanvas::drawImageRect(). This initial implementation was enough to run the default MotionMark test suite, since it doesn’t use other composition features. Even though performance was not the goal, we had to make sure we didn’t regress. This initial implementation was neutral in MotionMark. We needed tests to implement those features and measure performance at the same time, so we decided to add a new set of tests to MotionMark, just extending the existing tests to require composition, which makes sure that filters, masks, path clipping, transformations, etc. were done by the compositor.

Filters

We first tried implementing filters using an intermediate surface like TextureMapper does. It worked, but the MotionMark score in the filters test was much worse. We realized that with Skia we could implement most of the filters without using an intermediate surface. All filter types except blur and drop shadow can be simplified to an SkColorFilter with SkImageFilter::asAColorFilter() which can be implemented without an intermediate surface, just by setting the color filter in the SkPaint we pass to SkCanvas::drawImageRect(). This not only fixed the performance regression, but also gave better results than TextureMapper, which always needs an intermediate surface.

Masks

There are two different kinds of masks: image mask, where the source mask is an image already, and clip path, where the mask is represented by a path to be clipped. In TextureMapper both are implemented the same way using intermediate surfaces. The mask is painted into a surface and then the masked layer creates an intermediate surface where its contents are first painted and then the mask contents on top using DstIn blend mode. Skia has APIs that allowed us to implement both cases in a much simpler and more efficient way. In the case of image masks, where we already have an image, we paint the mask contents once and keep it cached, and then the masked layer creates an SkShader for the image mask that is passed to SkCanvas::clipShader() without having to paint into an intermediate surface. Clip path masks are even easier, because we can just take the path we get and build an SkPath we can pass to SkCanvas::clipPath(), without having to paint the mask as an image at all or use any other intermediate surface. Once again, masks were not only easier to implement but they ended up being more performant too.

Grouped bar chart of ten MotionMark composition subtests, comparing TextureMapper with the Skia compositor. Filters, clipping and mask tests are three to four times faster with Skia; the three leaves tests are about 20% slower.
MotionMark composition suite, WPE with GPU rendering on a Raspberry Pi 4, comparing TextureMapper (312400@main) with the Skia compositor (313600@main). TextureMapper never implemented blend modes, so its high score on bouncing blend circles is the score for not doing the work.

3D contexts

The implementation of 3D layer contexts is fairly independent of TextureMapper and OpenGL, so we could just take it almost as it was, using SkPath to build the clips and a few other adaptations. We could also fix existing bugs like the z-ordering that has always been broken in TextureMapper.

Two screenshots side by side of the same page. Under TextureMapper a small red box sits flat on top of a green plane rotated in 3D. Under the Skia compositor the red box is much taller and is cut by the plane: a sliver shows past the left edge, the middle is hidden behind the plane, and the right part is drawn in front of it.
The same page rendered by TextureMapper (left) and by the Skia compositor (right), WPE on the same build. The red box intersects the rotated green plane. TextureMapper draws the box flat against the plane, so the intersection is lost; the Skia compositor splits it, drawing the part in front of the plane and hiding the part behind it.

Blend modes

TextureMapper never supported blend modes and they were easy to implement with Skia just using the SkPaint property for it. This made several layout tests start passing.

Batched painting

After implementing all the features we were at a point in which we had the same or better performance in all tests except for three MotionMark compositing tests that were giving much worse results. Those tests use small layers and give a high result which means we end up adding a lot of layers to the scene before we start skipping frames. The root cause was the large number of layers filling the command queue of Ganesh. Skia Ganesh queues the GL drawing operations instead of sending them to the GPU right away. When the surface is flushed for whatever reason, the queued GL drawing operations are then processed and sent to the GPU. This allows Skia to apply nice optimizations like merging several tasks and reducing the amount of draw operations we end up sending to the GPU. In those tests where a lot of layers are created and painted to the compositor Skia surface the internal command queue ends up being huge too. Processing and analyzing such a long queue to optimize what we send to the GPU required more CPU work than what we save by optimizing the GL draw operations. Skia provides an API that allows us to do the batching ourselves. Since the compositor already has information to decide what operations could be merged together, we could reduce the internal queue size in many cases. We can merge SkCanvas::drawImageRect() operations as long as they share the same color filter, blend modes and sampling options. In the best case scenario we could reduce the whole internal queue to just one operation. This time the change improved the results of those tests getting them to about 93% of the TextureMapper score, but still a bit behind.

Promise images

The Skia Ganesh backend requires that an SkImage backed by a texture is created for the current thread GrDirectContext, even if it’s borrowing an existing texture. In WebKit all textures are created with a sharing GL context so that they can be accessed and destroyed from different threads with the same sharing GL context. So, for a layer whose content is an image we had to create a texture in the compositing thread to upload the pixels if the image was not accelerated, or for accelerated images get the texture identifier of the image, and then create another SkImage from the compositing thread borrowing the texture for the current GrDirectContext. The Skia Ganesh backend provides an API to create promise images, which can be created from any thread but targeting a specific thread, providing a fulfill callback that will be called on the target thread when the SkImage is first used to retrieve the wrapped texture. This way we can create the SkImage from the main thread for the compositing thread without using OpenGL at creation time. For non-accelerated images we realized we don’t need to manually create the texture and upload the pixels in the compositor, we can just pass the unaccelerated SkImage to the compositor SkCanvas and Skia will handle it internally much more efficiently than we did. And this change improved those compositing tests much further than we expected. The reason turned out to be the batching from the previous section: Skia merges the entries of an image set by comparing texture proxy pointers, and until now we were wrapping the texture in a new SkImage on every frame for every layer, so hundreds of layers drawing the very same image produced hundreds of different proxies that Skia could not merge. Passing the same SkImage every time collapses all of them into a single draw operation, which is the best case we described above. With batched painting and promise images together we could beat TextureMapper significantly.

Line chart of the three MotionMark leaves subtests by WebKit revision. All three step up sharply at revision 314626 when batched painting landed, and again at revision 315529 when promise images landed.
MotionMark composition suite, leaves subtests, WPE with GPU rendering. Score per revision; higher is better. The same two steps appear with CPU rendering.

Deferred Display Lists (DDL)

When we switched to Skia for painting, we kept the threaded rendering model, just using a separate smaller queue for GPU rendering workers. The GPU workers created their own GrDirectContext to paint the layer tiles. The resulting textures were re-wrapped in the compositing thread for the compositor GrDirectContext using fences for the proper synchronization. We knew this was not the recommended way to use Skia Ganesh from multiple threads, but with TextureMapper we had no other option. However, with the Skia compositor we can do it the recommended way by using a single GrDirectContext in the compositing thread and use Deferred Display Lists (DDL) and promise images to paint the tiles. With DDL, GPU workers no longer use GL at all and they don’t need a GrDirectContext, they paint tiles into a display list that records the GL drawing operations, but without touching GL. For image drawing operations recorded into the DDL, promise images are used too. Since this is now all CPU work we can remove the smaller GPU worker queue and use a single queue with more workers. The compositor replays the DDL into an SkSurface that is then passed to the compositor SkCanvas.

This change fixed rendering glitches on Android and was performance neutral for the whole composition suite and for most of the MotionMark tests, but in MotionMark 1.3 at 15fps it cost 29% in Suits and 14% in Leaves, while improving Images by 9%. Correctness and the other benefits of DDL made us accept those regressions.

Line chart of the MotionMark suits score by WebKit revision. The score drops from about 470 to about 335 when deferred display lists are enabled, returns to 470 when they are disabled, and drops again when they are re-enabled, staying there.
MotionMark 1.3 at 15fps, suits subtest, WPE with GPU rendering on a Raspberry Pi 4. Shaded regions are where deferred display lists were enabled by default. CPU rendering moves less than 1% at all three switches, since it has no GPU worker threads for DDL to change.

Damage

TextureMapper already supported using damage information to optimize the painting while compositing, but it has always been disabled at run time because there were issues we never managed to fix. With the Skia compositor we decided to start from scratch and properly handle the damage information while compositing to render only the parts of the frame that actually changed. I’m not going to go into detail here because Nikolas Zimmermann has written an amazing blog post about it with all the details.

Current situation

The Skia compositor is finished and enabled by default in 2.54. Even though it was not the main goal, it performs better than TextureMapper in most of the benchmarks we run: the composition suite we added is 45% faster, and MotionMark 1.3.1 is 35% faster. The exception is MotionMark 1.3 at 15fps with GPU rendering, which comes out flat, because the Suits and Leaves tests are still about 26% and 10% behind due to the deferred display lists trade-off described above.

We are already working on fixing existing issues in composition that we never fixed in TextureMapper. In the main branch TextureMapper is now disabled by default at build time, and support will be removed soon for the GTK and WPE ports. In 2.54 it’s still a run-time decision so if you find any issue with 2.54, you can check if it’s a Skia compositor regression by trying TextureMapper with WEBKIT_USE_SKIA_FOR_COMPOSITION=0 environment variable.

Bar chart of overall benchmark scores today relative to the TextureMapper baseline. The composition suite is 45% ahead with GPU rendering, MotionMark 1.3.1 is 35% ahead, and MotionMark 1.3 at 15fps is level with GPU rendering and 10% ahead with CPU rendering.
Overall (geometric mean) score, WPE on a Raspberry Pi 4: 320000@main and later against the TextureMapper baseline at 312400-313296@main. Bars start at the baseline. Part of the gain in the MotionMark suites is Skia rendering work rather than the compositor.
Bar chart of MotionMark 1.3 at 15fps subtests, showing the change from the TextureMapper baseline to today. Suits is 26% behind and leaves 10% behind with GPU rendering, while both are well ahead with CPU rendering; every other subtest is level or ahead.
WPE on a Raspberry Pi 4, change from the TextureMapper baseline (312400-313296@main) to 320000@main and later. Suits and leaves are the deferred display lists trade-off, not the compositor switch, which was neutral in this suite.

Future plans

We are already working on further improvements like using promise images for all external textures we have to pass to the compositor. We will explore the possibility of using Vulkan with the Ganesh backend instead of GL and eventually try the new Graphite backend. And of course we will continue fixing any existing issues related to the compositor.

by carlos garcia campos at September 21, 2026 08:40 AM

Nikolas Zimmermann

Bringing damage to the Skia compositor

Teaching the new Skia compositor to recomposite only what changed

September 21, 2026 12:00 AM

September 16, 2026

WPE WebKit Blog

WPE WebKit 2.54 highlights

The WebKit team at Igalia is happy to announce a new release series of WPE WebKit. This release has two main highlights: the new WPEPlatform API, now stable and enabled by default, and a web process compositor built on the Skia graphics library, replacing the TextureMapper-based one. Read on for the details on both, along with a summary of the other most noteworthy changes from the latest release cycle.

WPEPlatform: the new WPE API

WPEPlatform, the API that has been in the works for the past several release cycles, takes center stage in 2.54: it is now enabled by default and its API is considered stable, so applications can build on it without expecting breaking changes in future releases. Consequently, the traditional libwpe-based API is officially deprecated: it remains available and maintained, but new code should target WPEPlatform, and existing embedders are encouraged to plan their migration. This also applies to Cog, which will not have stable releases beyond the 0.18.x series.

The best part of the new API is how much simpler it is. Under libwpe, an application had to create a view backend (usually through WPEBackend-fdo’s “exportable” backend) and drive rendering, buffer management, and input dispatch itself through its callbacks. WPEPlatform moves all of that into WebKit and the platform implementation, so migrating an application is mostly a matter of deleting code: in the common case, the application constructs its WebKitWebView without a backend, WebKit selects a suitable platform automatically, and everything built on top of the web view carries over unchanged. The platform API only surfaces when an application wants more than the defaults: pinning to a particular platform, handling raw input events, or driving the toplevel window. Each of those is a few lines against a small GObject API rather than a set of C callbacks to implement.

To help embedders make the move, WPEPlatform is now extensively documented. The reference documentation includes an overview of the platform API (covering its relationship to libwpe and what is intentionally not part of the new API), a guide on compiling against it, a tutorial on writing a browser, and a tutorial on writing a WPE platform implementation. A guide on migrating from libwpe, with a symbol-by-symbol mapping table, walks embedders through the process step by step.

Since WPEPlatform is now built by default, the wpe-platform-2.0 pkg-config module is available in regular builds. Conversely, the legacy API can be disabled at build time with ENABLE_WPE_LEGACY_API=OFF when it is not needed.

Additions and final adjustments

New platform APIs added this cycle include:

  • WPEProcessManager and WPEProcessLaunchOptions, which allow the embedder to control how the auxiliary WebKit processes are launched and terminated. This is particularly important on Android, where each process is a service that must be started with bindService(), and it removes the last reason a WPEPlatform-only build could not work there. Note that, unlike the rest of WPEPlatform, the process management API is only built when targeting Android and remains experimental: it is not yet generic enough to be enabled everywhere, and it may still change in future releases.
  • Gamepad rumble support, through wpe_gamepad_has_rumble() and wpe_gamepad_rumble(), with a built-in implementation based on libmanette. This enables the Gamepad API vibrationActuator for web content.
  • A new WPE_SETTING_OVERLAY_SCROLLBARS setting, enabled by default, which may be disabled by applications to opt into classic, always-visible scrollbars.
  • A new WPE_INPUT_PURPOSE_SEARCH input purpose, allowing input methods to detect when the values of an input field are expected to be search terms.

A few final adjustments were made to the API before declaring it stable, which may require updates to platform implementations and applications developed against the earlier previews:

  • The WPE_SETTING_DISABLE_ANIMATIONS setting has been replaced by WPE_SETTING_REDUCED_MOTION, matching the dedicated reduced-motion setting introduced in GNOME 50, and a new tri-state WPE_SETTING_INTERFACE_CONTRAST setting has been added. Together with the existing WPE_SETTING_DARK_MODE setting, these make the prefers-reduced-motion, prefers-contrast, and prefers-color-scheme media queries follow the platform settings.
  • wpe_gesture_controller_handle_event() now returns a boolean indicating whether the event was consumed.
  • The WPE_DMABUF_BUFFER_FORMAT environment variable has been renamed to WPE_BUFFER_FORMAT.

Beyond Linux: Android

A good measure of the new API’s maturity is wpe-android, which has been rebuilt this year as a WPEPlatform platform implementation living entirely outside the WebKit tree: Android’s display system now looks to the engine like any other WPE platform, and applications get a convenience Java API modeled after android.webkit.WebView. The new WPEProcessManager API removed the last dependency on libwpe there. You can read more about it in this post.

On the WebKit API side

The shared WebKit API has also seen additions this cycle:

Graphics improvements

A new Skia-based compositor

This cycle brings the largest overhaul of the rendering architecture since the adoption of Skia for 2D rendering: the web process compositor now uses the Skia API instead of the venerable TextureMapper. Layers are composed into the final frame using Skia, which allows sharing a single rendering infrastructure across the whole graphics stack and enables several optimizations:

  • Tile contents are recorded into deferred display lists and replayed on the compositor thread, so painting worker threads no longer need to touch the GPU at all.
  • Batched painting groups the drawing of many layers into a single Skia call, which improves performance on pages with many layers that can be painted in the same operation.
  • Unnecessary clip operations are avoided whenever possible, keeping the batched paths effective.

Beyond raw performance, expressing compositing as Skia draw calls made several features simpler and faster: filters and masks no longer require intermediate offscreen surfaces in most cases, and CSS blend modes, which TextureMapper never implemented, now work in composited layers. And since Skia can target both OpenGL and Vulkan, the compositor no longer stands in the way of Vulkan-based rendering in the future.

The new compositor also works when using the legacy libwpe-based API.

The consolidation on Skia goes beyond compositing: the option to use Cairo for 2D rendering has been removed, making Skia the only 2D rendering implementation. Rendering tiles in the main thread is no longer supported either, so threaded rendering is now the only tile painting path.

Damage-aware compositing

Damage tracking has seen substantial work this cycle. The damage is the region of the view that changed since the previous frame and therefore requires repainting; Paweł Lampe’s introduction to damage propagation covers the concept in depth. Compositing itself now uses this information: each draw the compositor issues is restricted to the damaged rectangles, in a way that preserves the batched painting described above, and damage propagation to the platform is now enabled by default. A new DamageRectangleThreshold preference allows embedders to tune the balance between damage precision and bookkeeping cost.

The performance impact

What drove the compositor rewrite was making it simpler and easier to maintain, as Carlos García Campos explained at the Web Engines Hackfest 2026; performance came later, since TextureMapper started out ahead after more than a decade of tuning. By now the optimization work has more than closed that gap. The public WPE performance dashboard, which continuously runs benchmarks on Raspberry Pi 4 devices, tells the story: comparing the last TextureMapper-based revisions against the current ones, and thus measuring the cumulative effect of the graphics work described in this section, the MotionMark score is up by around 36%. On the composition-focused variant of the benchmark, where the compositor dominates the workload, the score is up by around 45%.

Bar chart comparing MotionMark scores on a Raspberry Pi 4: MotionMark 1.3.1 at 30 FPS goes from 246 with TextureMapper to 334 with the Skia compositor (+36%), and the Composition variant from 131 to 189 (+45%)

The dashboard also records the GPU load during each run, and there the difference is even more telling. MotionMark increases scene complexity until the browser can no longer sustain the target frame rate, so a higher score already means more work per frame; the Skia compositor delivers that while keeping the GPU markedly less busy. On embedded devices, where the GPU is typically the scarcest resource, this headroom translates directly into smoother pages.

Bar chart comparing GPU load during the same benchmark runs: 56% with TextureMapper against 39% with the Skia compositor on MotionMark 1.3.1, and 47% against 26% on the Composition variant

One part of the work goes largely unmeasured here, though. MotionMark animates nearly the entire viewport, so there is hardly anything for damage tracking to save. Real-world content behaves differently: usually a small area of the page is changing while everything else stays still, and skipping all of that quiet area cuts the per-frame GPU work to a fraction.

Other rendering improvements

A GPU atlas is now used for batched raster image uploads, regardless of the compositor in use, and it is reused across frames when the image set does not change, avoiding needless texture allocation and pixel uploads.

Asynchronous scrolling is smoother: several synchronization issues between the main, scrolling, and compositing threads that caused glitches while scrolling have been fixed, and the scrolling thread no longer blocks the compositor to flush its state, removing input-latency stalls on pages with many layers.

Finally, more animations can now run on the compositing thread: CSS animations using the steps() and linear() timing functions no longer force main-thread animation.

Better multimedia on embedded hardware, and a WebRTC transition

Let’s start with the transition: the GStreamer-based WebRTC backend is being replaced with a LibWebRTC-based implementation, which is expected to be available in the next release cycle. As a consequence, WebRTC support, which in previous releases required building with experimental features enabled, is disabled in 2.54.

The rest of the multimedia work moved forward at full speed, with a strong focus on embedded hardware:

  • Hardware video decoding and encoding for platforms with a Qualcomm GPU has been added, leveraging the qtic2vdec and qtic2venc GStreamer elements.
  • Video decoding limits are now respected in MediaCapabilities queries, and can be overridden with the WEBKIT_GST_VIDEO_DECODING_LIMIT environment variable, so a single build can serve devices with different capabilities.
  • Resource usage on pages containing many videos has been improved, by stopping the pipelines of muted, invisible video elements.
  • A new feature flag, enabled by default, allows low-end devices to skip caching pages with multimedia content in the back-forward cache, so that a suspended pipeline cannot hold a scarce hardware decoder hostage.
  • Media capability reporting is more accurate: non-AAC mp4a codecs (MP3, AC-3, E-AC-3) are correctly reported as supported when decoders are present, xHE-AAC support is auto-detected, and Dolby AC-4 is advertised for MSE on systems that support it.
  • Experimental support for SourceBuffer.changeType() has been added to the MSE backend, along with a fix for playback stalling at ad transitions on Twitch.
  • Persistent licenses are now supported in the Thunder CDM for encrypted media.

On top of this, the GStreamer backend has received a substantial amount of memory-safety and lifetime-correctness work that translates into a more stable multimedia experience.

WebXR

The OpenXR-based WebXR implementation continues to progress. The main highlight is support for the WebXR Layers API: quad, cylinder, equirect, and cube layers are now implemented, in addition to the already supported projection layers. Layers can be backed by texture arrays, and XRSession.maxRenderLayers lets content query the compositor’s layer budget.

The backend has also been decoupled from OpenGL ES through an abstract graphics binding, paving the way for a future Vulkan-based binding.

WebXR support remains a build-time option, enabled with the ENABLE_WEBXR=ON CMake option; the Layers support additionally requires ENABLE_WEBXR_LAYERS=ON.

Other improvements to the WPE port

Several long-standing gaps in the WPE port have been closed this cycle:

  • A built-in popup menu is now used as the default implementation for <select> elements when the WebKitWebView::show-option-menu signal is left unhandled, so option menus work out of the box.
  • Initial drag-and-drop support has been added: web views now handle drags driven by the mouse events they already receive.
  • Spell checking is now supported using the Enchant library, enabled by default, and can be toggled at build time with the ENABLE_SPELLCHECKING CMake option.
  • The on-screen keyboard is no longer shown when an element is focused programmatically; it only appears as a result of user interaction.
  • Initial support for the Pointer Lock API can be enabled at build time with the ENABLE_POINTER_LOCK CMake option.
  • Saving files from the Web Inspector now works in WPE.

Web Platform support

As usual, this list is not exhaustive as WebKit continuously progresses in its support for new standards. Some of the highlights for this release are:

What’s new for WebKit developers?

WebKit can now use mimalloc as its memory allocator, as an alternative to its own bmalloc. For now it is the default only on some architectures (32-bit ARM, MIPS, RISC-V, and builds supporting 64 KB memory pages); everywhere else bmalloc remains the default, and mimalloc can be enabled with the USE_MIMALLOC build option.

Logging now falls back to the standard error output when journald is not reachable, which is common in minimal containers, and the new WEBKIT_DEBUG_OUTPUT environment variable allows choosing the log destination explicitly.

Profile-guided optimization is now supported in regular CMake builds with Clang, through the ENABLE_LLVM_PROFILE_GENERATION and USE_PGO_PROFILE options.

Finally, a note for packagers: building WPE WebKit now requires Ninja, as the CMake Makefile generator is no longer supported.

Looking forward to 2.56

The 2.56 release series will bring even more improvements, and we expect it to be released during the spring of 2027. Until then!

September 16, 2026 12:00 AM

September 14, 2026

Igalia WebKit Team

WebKit Igalia Periodical #77

Update on what happened in WebKit in the week from September 7 to September 14.

In this week's edition, the PNG image decoder received a much needed cleanup, as well as another swipe of unassorted graphics improvements, most notably in the Layer-Based SVG Engine. Finally, we also have an exciting and informative article about how to use the WPE Platform API on Raspberry Pi.

Cross-Port 🐱

Graphics 🖼️

Cleaned up the PNG image decoder a little bit, by removing conditional code that was used to support libpng versions older than 1.5.0, and requiring that as the minimum version. Given that version 1.5.0 was released back in 2011, it is expected that every system where current WebKit works will have much newer versions of libpng anyway.

Fixed an assertion failure for an outermost <svg> with non-visible overflow in the Layer-Based SVG Engine (LBSE) by refreshing its scroll dimensions at the end of layout, a first step towards reworking <svg> scrolling, which is not yet spec compliant.

Fixed an assertion failure when computing filter outsets for <feGaussianBlur> and <feDropShadow> with a negative stdDeviation, which turns the primitive off but was still passed on to the outset calculation, affecting both SVG filters and CSS reference filters.

Stopped reporting damage every frame for composited layers that use CSS filter in the Skia based compositor, so a blurred or drop-shadowed element is now only repainted when its subtree, or the applied (possibly animated) filter value, actually changed.

Fixed the bug that composited layers with clipping or masking and fully covered by opaque child layers were treated as opaque layers.

Fixed two debug assertion failures in the Layer-Based SVG Engine (LBSE), one where an SVG transform change left ancestor layer repaint rects stale and one where repainting on a compositing change during layout tripped an over-strict check on the paint offset cache.

Fixed the white noised image issue on the combination of i915 driver and Intel Arc after 308458@main introduced image atlas uploading.

Community & Events 🤝

Published a blog post describing the steps to try WPE Platform API on Raspberry Pi with latest WebKit main branch.

That’s all for this week!

by Igalia WebKit Team at September 14, 2026 07:07 PM

September 13, 2026

Hironori Fujii

Asynchronous scrolling for touch events in WPE and WebKitGTK

The WPE and GTK ports have supported touch events for a long time, but asynchronous scrolling only worked for wheel events. Scrolling driven by touch still depended on the web process main thread. This change puts touch events on the asynchronous scrolling path too.

Let’s start with the background: what asynchronous scrolling is and why it needs to be built differently here.

What is asynchronous scrolling? #

Why it is needed #

A naive implementation of scrolling looks like this:

  1. The UI process receives an input event (wheel or touch).
  2. It sends the event to the web process main thread.
  3. The main thread runs the page’s JavaScript event listeners.
  4. If nothing called preventDefault(), the scroll position is updated.
  5. The page is rendered at the new scroll position.

Step 3 is the problem. The main thread is easily blocked for hundreds of milliseconds by JavaScript execution or layout, and scrolling is frozen for that whole time. Your finger moves, the screen does not. That is what synchronous scrolling feels like.

Asynchronous scrolling moves the work off the main thread: the scrolling thread updates the scroll position, and the compositor thread composites and presents the frame. Neither needs the main thread, so scrolling keeps running at 60fps even when the main thread is busy.

The scrolling tree and event regions #

Two data structures make this possible.

The scrolling tree is a tree of the scrollable areas of a page. The main frame, overflow: scroll elements, position: fixed/sticky elements and so on each become a node holding its own scroll position and a reference to its layer. The scrolling thread updates scroll positions by looking only at this tree, and the compositor thread then draws the layers at their new positions — the main thread is not involved in either step.

But there are places where scrolling on its own would be wrong, because the page might call preventDefault() from an addEventListener("touchstart", ...) handler. That is what event regions are for.

An event region records, per layer and at rendering time, “this rectangle has a listener for this kind of event”. EventRegion keeps separate regions for touchstart, touchmove, pointerdown, mousedown and friends, and for any given point it yields a TrackingType:

enum class TrackingType : uint8_t {
NotTracking = 0, // No listener. The event does not even need to be delivered.
Asynchronous = 1, // Passive listeners only. Scroll now, notify the page later.
Synchronous = 2 // A non-passive listener may call preventDefault(). We must wait.
};

The passive distinction is what makes Asynchronous possible. preventDefault() cancels an event only if the listener was registered with passive: false; from a passive listener it does nothing. And on window, document and document.body, touchstart/touchmove (and wheel) default to passive: true — see MDN: Using passive listeners. So Asynchronous is the case “there are listeners, but none of them can cancel the scroll”: start scrolling now, deliver the event to the main thread afterwards.

Because this information travels to the scrolling thread along with the layer tree, an incoming input event can be classified without waking the main thread. That is the heart of asynchronous scrolling.

Why the iOS implementation could not be reused #

The iOS port already implements asynchronous scrolling for touch events, but it could not be reused, because the process layout is different.

iOS:
  UI process:  platform layer tree + scrolling tree + touch event input
  Web process: main thread (DOM, layout)

WPE / GTK (Coordinated Graphics):
  UI process:  touch event input only
  Web process: main thread (DOM, layout)
               + EventDispatcher thread / scrolling thread
               + platform layer tree + scrolling tree

On iOS both the platform layer tree and the scrolling tree live in the UI process — the very process that receives touch events — so the classification and the scroll both happen right there. On WPE and GTK we use Coordinated Graphics, and both trees live in the web process instead. The classification therefore has to happen after sending the event to the web process, but before touching the main thread.

Fortunately the same problem was already solved for wheel events. The web process has an EventDispatcher thread that receives wheel events from the UI process without going through the main thread and consults the scrolling tree directly. This change builds the same shape for touch events.

How a touch becomes a scroll in WPE #

One more piece of background: in WPE a touch does not scroll the page directly. Touch events are first offered to the page; only if the page does not consume them does the UI process turn the touch sequence into scrolling.

The decision point is PageClientImpl::doneWithTouchEvent(). If the page handled the event, gesture detection is cancelled with wpe_gesture_controller_cancel() so the engine does not also act on it. If it was not handled, the event is fed to the WPE platform gesture controller via ViewPlatform::handleGesture(), and a recognized WPE_GESTURE_DRAG is turned into a synthetic scroll event pushed back into the page as a wheel event:

GRefPtr<WPEEvent> simulatedScrollEvent = adoptGRef(wpe_event_scroll_new(
m_wpeView.get(), WPE_INPUT_SOURCE_TOUCHSCREEN, 0, static_cast<WPEModifiers>(0), dx, dy, TRUE, FALSE, x, y));
page().handleNativeWheelEvent(WebKit::NativeWebWheelEvent::create(simulatedScrollEvent.get(), phase));

That TRUE is precise_deltas: touch-driven scrolling in WPE reaches the engine as precise-delta wheel events, which becomes relevant later.

The important consequence is this: the UI process cannot start scrolling until it knows whether the page is going to consume the touch. That answer used to come from the web process main thread — so when the main thread was busy, scrolling did not start. That is the problem this change fixes.

The change #

Enabling touch event regions #

A new ENABLE(COORDINATED_TOUCH_EVENTS) is introduced in PlatformEnableGlib.h, and it turns on ENABLE(TOUCH_EVENT_REGIONS) whenever touch events are enabled on WPE/GTK. The AlwaysUseTouchEventRegions preference now defaults to true under that flag, so Document::shouldUseTouchEventRegions() returns true and touch regions are actually recorded on the layers during rendering.

UI process: send to the EventDispatcher instead of the main thread #

The old WebPageProxy::handleTouchEvent() consulted a touchEventTracking state kept in the UI process and sent Messages::WebPage::TouchEvent, i.e. straight to the web process main thread.

The new version delegates all classification to the web process and only queues events and delivers answers. One event is in flight at a time; the next is sent when the reply arrives. The flood of touchmove events produced while a finger moves is coalesced into the newest queued event when that is also a touchmove, and the coalesced events are flushed to doneWithTouchEvent() together with the reply.

The destination is now Messages::EventDispatcher::TouchEvent.

Web process: classification on the EventDispatcher thread #

EventDispatcher::touchEvent() runs on the EventDispatcher thread, where it looks up the page’s scrolling tree, asks it for a TrackingType, and splits three ways:

  • NotTracking — no listeners. Reply handled = false immediately, without bothering the main thread at all. The UI process can start scrolling right away.
  • Asynchronous — passive listeners only, so nothing can cancel the event. Reply handled = false first so scrolling starts, then deliver the event to the main thread.
  • Synchronous — a non-passive listener may call preventDefault(), so wait for the main thread result as before.

Replying without a main thread round trip is possible because the new TouchEvent message is declared AnyThread in EventDispatcher.messages.in. The iOS equivalent is MainThreadCallback, which always replies from the main thread.

If there is no scrolling tree for the page yet, the event goes to the main thread as before.

Classifying a touch in the scrolling tree #

The classification itself is ScrollingTreeCoordinated::eventTrackingTypeForTouchEvent(). It works in two stages.

First, for each newly pressed touch point: convert the point from view to contents coordinates, hit test the layer tree down from the root contents layer, take the frontmost layer whose event region contains the point, and query that region. It is queried for many event types, because a touch fires more than the DOM touch* events — pointer*, compatibility mouse events and gesture* too, and a non-passive listener for any of them forces synchronous handling. The results are folded into a small TouchEventTracking struct with four fields: start, move, end and force-change.

Second, the tracking type of the event as a whole is derived from the touch point states, merging the per-field values. Merging picks the stronger of two types (NotTracking < Asynchronous < Synchronous), so if any single point needs synchronous handling, the whole event is synchronous.

TouchEventTracking persists for the lifetime of a touch sequence and is reset once all points are released, so the hit test done at touchstart is reused for the following touchmove/touchend. That guarantees a sequence never flips from synchronous to asynchronous halfway through just because a finger moved off a listener’s area.

<input type=range> #

A slider handles touches internally even with no JavaScript listener, so looking at the event region alone would classify it as NotTracking. HTMLInputElement::updateTouchEventHandler() now sets the HasInternalTouchEventHandling flag on EventTarget for range inputs, and StyleAdjuster turns that flag into the full set of touch region types for the element.

Keeping the animation running on the scrolling thread #

The last piece is in ScrollingEffectsController::handleWheelEvent(). As shown above, WPE synthesizes wheel events from touch gestures with precise deltas. Precise-delta events only need immediateScrollBy() to move the scroll position — but then nothing drives screen updates while the main thread is busy.

The fix is that, while a scroll gesture is in progress, a scroll animation is also started — from one ULP short of the destination (std::nextafter()) to the destination. Visually it finishes instantly — the real scroll is still done by immediateScrollBy() — but a scroll animation is now running, which starts display link monitoring and keeps compositing driven regardless of the main thread.

The event flow, summarized #

Before:

The UI process sends the touch event to the web process main thread, which may
be blocked by JavaScript or layout. Only after the reply arrives does gesture
recognition synthesize a wheel event and scrolling
start.

After (no listeners, or passive listeners only):

The UI process sends the touch event to the EventDispatcher thread of the web
process, which asks the scrolling tree and replies immediately without the main
thread, so gesture recognition synthesizes a wheel event and scrolling starts
right away. If passive listeners exist, the event is also delivered to the main
thread afterwards.

Where a non-passive listener exists, we still wait for the main thread as before. The spec requires preventDefault() to be honoured, so that is unavoidable.

Layout test updates #

As a side effect, the tests under fast/events/touch/ had to be updated.

Event regions are computed during a rendering update and propagated to the scrolling tree via the platform layer tree. Which means a test like this:

target.addEventListener("touchstart", handler);
tapSoon(20, 20); // ← the region has not been updated yet!

taps immediately after registering the listener, while the scrolling tree still believes there is no listener and returns NotTracking. The event never reaches the main thread and the test fails.

A new UIHelper.renderingComplete() was added for this:

static async renderingComplete()
{
// Wait for the platform layer tree to be updated
await UIHelper.animationFrame();
await UIHelper.animationFrame();
}

Two animation frames are needed because the first one runs the rendering update that computes the regions, and a second is needed for the result to reach the layer tree.

Summary #

  • On WPE and GTK both the layer tree and the scrolling tree live in the web process (Coordinated Graphics), so the iOS touch asynchronous scrolling implementation could not be reused directly.
  • Instead, touch events were given the same shape that already works for wheel events: ask the scrolling tree from the EventDispatcher thread.
  • The keys were enabling touch event regions, and making the IPC reply AnyThread so it can be sent without waiting for the main thread.
  • Anywhere the page has no non-passive listener, scrolling now starts regardless of what the main thread is doing.

Acknowledgements #

Many thanks to Alejandro G. Castro and Carlos Garcia Campos for their insightful reviews of this work, and to Claude for writing this blog post.

September 13, 2026 12:00 AM

September 08, 2026

Pawel Lampe

Trying WPE Platform API on Raspberry Pi

WPE Platform API (also known as “new API”) is a redesigned, GObject-based platform-integration layer for WPE WebKit that replaces the older libwpe backend model. A few months ago, Kate and Simon published two closely related blog posts about it. The first one focuses more on the API and browser implementation details, while the second one focuses more on writing and integrating the browser within Linux distribution.

This article builds on top of the above ones, and showcases how to build and try a minimal WPE browser using WPE Platform API on Raspberry Pi. Moreover, as the WPE Platform API still evolves to some degree, this article also explains how to use and stick to the latest WPE WebKit from main branch. This way one can play with all the latest features straight on embedded hardware.

Before going further, one should be aware that in case of a simple release build (instead of one using latest main branch) it’s better to follow official instructions instead of this article.

Setup #

This article focuses on a certain setup using Raspberry Pi 3B but it should be fairly easy to adapt the config to any other Raspberry Pi model.

As for the work environment: the Linux-based host with ability to run containers was used along with WebKit Container SDK. The SDK version was precisely 2.53-v6-d535e88 as it uses Ubuntu 24.04.4 LTS that works well with Yocto scarthgap.

The Yocto scarthgap has been used to increase the chances that the config and commands demonstrated in this article will remain buildable for many years to follow.

Preparing the image #

The preparation of the image starts with a series of commands that create a main directory and clone important Yocto repositories along with some meta layer repositories. At this point already, it’s important to have the working directory shared between host and SDK.

# host
mkdir wpe-upstream
cd wpe-upstream
git clone https://git.yoctoproject.org/git/poky -b scarthgap
git clone git@github.com:openembedded/meta-openembedded.git -b scarthgap
git clone https://git.yoctoproject.org/git/meta-raspberrypi -b scarthgap
git clone https://github.com/Igalia/meta-webkit -b scarthgap
source poky/oe-init-build-env build

Once the build directory is created, it’s necessary to configure the meta layers in the build/conf/bblayers.conf file the following way:

# POKY_BBLAYERS_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes incompatibly
POKY_BBLAYERS_CONF_VERSION = "2"

BBPATH = "${TOPDIR}"
BSPDIR := "${@os.path.abspath(os.path.dirname(d.getVar('FILE', True)) + '/../..')}"

BBFILES ?= ""
BBLAYERS ?= " \
${BSPDIR}/poky/meta \
${BSPDIR}/poky/meta-poky \
${BSPDIR}/poky/meta-yocto-bsp \
${BSPDIR}/meta-openembedded/meta-oe \
${BSPDIR}/meta-openembedded/meta-multimedia \
${BSPDIR}/meta-openembedded/meta-python \
${BSPDIR}/meta-raspberrypi \
${BSPDIR}/meta-webkit \
"

With the above, the recipes from the meta layers cloned earlier will be considered by bitbake.

Next, the most important configuration step is appending the following to build/conf/local.conf:

MACHINE = "raspberrypi3-64" 
MACHINE_FEATURES:append = " vc4graphics"
GPU_MEM_256 = "128"
GPU_MEM_512 = "196"
GPU_MEM_1024 = "396"
DISTRO_FEATURES:append = " opengl egl wayland"
EXTRA_IMAGE_FEATURES = "debug-tweaks"
IMAGE_FEATURES:append = " ssh-server-dropbear hwcodecs"
IMAGE_INSTALL:append = " wpewebkit wpe-browser"
PREFERRED_VERSION_wpewebkit = "latest"
LICENSE_FLAGS_ACCEPTED = "synaptics-killswitch"

With that, wpewebkit latest will be preferred and installed in the image along with a dummy browser called wpe-browser.

To make the wpewebkit latest work, one needs to create meta-webkit/recipes-browser/wpewebkit/wpewebkit_latest.bb:

SUMMARY = "Lightweight WebKit port for embedded devices with OpenGL-ES acceleration"
DESCRIPTION = "WPE WebKit port pairs the WebKit engine with OpenGL-ES (OpenGL for Embedded Systems), \
allowing embedders to create simple and performant systems based on Web platform technologies. \
It is designed with hardware acceleration in mind, relying on EGL, and OpenGL ES."

HOMEPAGE = "https://wpewebkit.org/"
BUGTRACKER = "https://bugs.webkit.org/"
LICENSE = "BSD-2-Clause & LGPL-2.0-or-later"
LIC_FILES_CHKSUM = "file://Source/WebCore/LICENSE-LGPL-2.1;md5=a778a33ef338abbaf8b8a7c36b6eec80 "

REQUIRED_DISTRO_FEATURES = "opengl"

DEPENDS:append = " \
libsoup \
bison-native gperf-native harfbuzz-native libxml2-native ccache-native ninja-native ruby-native \
fontconfig freetype glib-2.0 harfbuzz icu jpeg pcre sqlite3 zlib libpng libtasn1 \
libwebp libxml2 libxslt virtual/egl virtual/libgles2 libepoxy libgcrypt \
unifdef-native \
"


inherit cmake features_check pkgconfig perlnative python3native

export WK_USE_CCACHE = "NO"

PACKAGECONFIG ??= "accessibility avif dfg-jit gbm gpu-process \
jit jpegxl libbacktrace \
mediasource mediastream \
remote-inspector \
sysprof \
${@' system-sysprof' \
if bb.utils.contains('BBFILE_COLLECTIONS', 'meta-gnome', True, False, d) \
else '' }
\
unified-builds video webaudio woff2 wpe-platform \
${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'journald', '' ,d)} \
"


PACKAGECONFIG[reduce-size] = "-DCMAKE_BUILD_TYPE=MinSizeRel,-DCMAKE_BUILD_TYPE=Release,,"
PACKAGECONFIG[release-with-debug-info] = "-DCMAKE_BUILD_TYPE=RelWithDebInfo,-DCMAKE_BUILD_TYPE=Release,,"

# WPE features
PACKAGECONFIG[accessibility] = "-DUSE_ATK=ON,-DUSE_ATK=OFF,atk at-spi2-atk"
PACKAGECONFIG[avif] = "-DUSE_AVIF=ON,-DUSE_AVIF=OFF,libavif"
PACKAGECONFIG[bubblewrap] = "-DENABLE_BUBBLEWRAP_SANDBOX=ON -DBWRAP_EXECUTABLE=${bindir}/bwrap -DDBUS_PROXY_EXECUTABLE=${bindir}/xdg-dbus-proxy,-DENABLE_BUBBLEWRAP_SANDBOX=OFF,bubblewrap xdg-dbus-proxy libseccomp"
PACKAGECONFIG[developer-mode] = "-DDEVELOPER_MODE=ON,-DDEVELOPER_MODE=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[deviceorientation] = "-DENABLE_DEVICE_ORIENTATION=ON,-DENABLE_DEVICE_ORIENTATION=OFF,"
PACKAGECONFIG[dfg-jit] = "-DENABLE_DFG_JIT=ON,-DENABLE_DFG_JIT=OFF,"
PACKAGECONFIG[documentation] = "-DENABLE_DOCUMENTATION=ON,-DENABLE_DOCUMENTATION=OFF, gi-docgen-native gi-docgen"
PACKAGECONFIG[encryptedmedia] = "-DENABLE_ENCRYPTED_MEDIA=ON,-DENABLE_ENCRYPTED_MEDIA=OFF,libgcrypt"
PACKAGECONFIG[experimental-features] = "-DENABLE_EXPERIMENTAL_FEATURES=ON,-DENABLE_EXPERIMENTAL_FEATURES=OFF,libavif libjxl"
PACKAGECONFIG[gamepad] = "-DENABLE_GAMEPAD=ON,-DENABLE_GAMEPAD=OFF,libmanette"
PACKAGECONFIG[gbm] = "-DUSE_GBM=ON,-DUSE_GBM=OFF,libdrm"
PACKAGECONFIG[geolocation] = "-DENABLE_GEOLOCATION=ON,-DENABLE_GEOLOCATION=OFF,geoclue"
PACKAGECONFIG[gpu-process] = "-DENABLE_GPU_PROCESS=ON,-DENABLE_GPU_PROCESS=OFF,"
PACKAGECONFIG[hyphen] = "-DUSE_LIBHYPHEN=ON,-DUSE_LIBHYPHEN=OFF,hyphen"
PACKAGECONFIG[introspection] = "-DENABLE_INTROSPECTION=ON,-DENABLE_INTROSPECTION=OFF, gobject-introspection-native"
PACKAGECONFIG[jit] = "-DENABLE_JIT=ON -DENABLE_C_LOOP=OFF,-DENABLE_JIT=OFF -DENABLE_C_LOOP=ON,"
PACKAGECONFIG[jpegxl] = "-DUSE_JPEGXL=ON,-DUSE_JPEGXL=OFF,libjxl"
PACKAGECONFIG[journald] = "-DENABLE_JOURNALD_LOG=ON,-DENABLE_JOURNALD_LOG=OFF,"
PACKAGECONFIG[lcms] = "-DUSE_LCMS=ON,-DUSE_LCMS=OFF,"
PACKAGECONFIG[spellcheck] = "-DENABLE_SPELLCHECK=ON,-DENABLE_SPELLCHECK=OFF,enchant"
PACKAGECONFIG[wpe-legacy-api] = "-DENABLE_WPE_LEGACY_API=ON,-DENABLE_WPE_LEGACY_API=OFF,libwpe virtual/wpebackend,"
PACKAGECONFIG[libbacktrace] = "-DUSE_LIBBACKTRACE=ON,-DUSE_LIBBACKTRACE=OFF,libbacktrace"
PACKAGECONFIG[minibrowser] = "-DENABLE_MINIBROWSER=ON,-DENABLE_MINIBROWSER=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[mediasource] = "-DENABLE_MEDIA_SOURCE=ON,-DENABLE_MEDIA_SOURCE=OFF,gstreamer1.0 gstreamer1.0-plugins-good"
PACKAGECONFIG[mediastream] = "-DENABLE_MEDIA_STREAM=ON,-DENABLE_MEDIA_STREAM=OFF,gstreamer1.0 gstreamer1.0-plugins-bad"
PACKAGECONFIG[pdfjs] = "-DENABLE_PDFJS=ON,-DENABLE_PDFJS=OFF,"

PACKAGECONFIG[speech-synthesis] = "-DENABLE_SPEECH_SYNTHESIS=ON,-DENABLE_SPEECH_SYNTHESIS=OFF,flite"

PACKAGECONFIG[sysprof] = "-DUSE_SYSPROF_CAPTURE=ON, -DUSE_SYSPROF_CAPTURE=OFF,"
PACKAGECONFIG[system-sysprof] = "-DUSE_SYSTEM_SYSPROF_CAPTURE=ON, -DUSE_SYSTEM_SYSPROF_CAPTURE=OFF, sysprof"
PACKAGECONFIG[video] = "-DENABLE_VIDEO=ON,-DENABLE_VIDEO=OFF,gstreamer1.0 gstreamer1.0-plugins-base"
PACKAGECONFIG[webaudio] = "-DENABLE_WEB_AUDIO=ON,-DENABLE_WEB_AUDIO=OFF,gstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good"
PACKAGECONFIG[woff2] = "-DUSE_WOFF2=ON,-DUSE_WOFF2=OFF,woff2"
PACKAGECONFIG[remote-inspector] = "-DENABLE_REMOTE_INSPECTOR=ON,-DENABLE_REMOTE_INSPECTOR=OFF,"
PACKAGECONFIG[webrtc] = "-DENABLE_WEB_RTC=ON,-DENABLE_WEB_RTC=OFF,libvpx libevent libopus openh264"
PACKAGECONFIG[qtwpe] = "-DENABLE_WPE_QT_API=ON ${CMAKE_QT_OECONF},-DENABLE_WPE_QT_API=OFF,qtbase-native qtbase qtdeclarative libepoxy wpebackend-fdo ${QT_BUILD_DEPS}"
PACKAGECONFIG[unified-builds] = "-DENABLE_UNIFIED_BUILDS=ON,-DENABLE_UNIFIED_BUILDS=OFF,"
PACKAGECONFIG[thunder] = "-DENABLE_THUNDER=ON,-DENABLE_THUNDER=OFF,virtual/open-cdm"
PACKAGECONFIG[webxr] = "-DENABLE_WEBXR=ON,-DENABLE_WEBXR=OFF,openxr"

# Build option for WPE API 1.1
PACKAGECONFIG[wpe-1-1-api] = "-DENABLE_WPE_1_1_API:BOOL=ON,-DENABLE_WPE_1_1_API:BOOL=OFF,"

# Build option for WPE platform API
PACKAGECONFIG[wpe-platform] = "-DENABLE_WPE_PLATFORM=ON,-DENABLE_WPE_PLATFORM=OFF,libinput libxkbcommon wayland-native"

EXTRA_OECMAKE = " -DPORT=WPE -G Ninja"

# TODO: documentation and introspection are disabled by default because the are
# causing cross-compiling build errors
# PACKAGECONFIG:append = " ${@bb.utils.contains('DISTRO_FEATURES', 'api-documentation', 'documentation', '' ,d)} introspection"

# If SSE code compiles, assume it runs successfully (it can't actually run
# because of cross compiling)
EXTRA_OECMAKE:append:x86 = " -DHAVE_SSE2_EXTENSIONS_EXITCODE=0"
# Javascript JIT is not supported on ppc/arm/RISCV32/mips64
PACKAGECONFIG:remove:powerpc = "jit"
PACKAGECONFIG:remove:powerpc64 = "jit"
PACKAGECONFIG:remove:powerpc64le = "jit"
PACKAGECONFIG:remove:armv4 = "jit"
PACKAGECONFIG:remove:armv5 = "jit"
PACKAGECONFIG:remove:armv6 = "jit"
PACKAGECONFIG:remove:armv7a = "jit"
PACKAGECONFIG:remove:armv7ve = "jit"
PACKAGECONFIG:remove:riscv32 = "jit"
PACKAGECONFIG:remove:riscv64 = "jit"
PACKAGECONFIG:remove:mipsarchn64 = "jit"
PACKAGECONFIG:remove:mipsarchn32 = "jit"
PACKAGECONFIG:remove:loongarch64 = "jit"

# Javascript JIT is not supported on x86
PACKAGECONFIG:remove:x86 = "jit"

LDFLAGS:append:riscv64 = " -pthread"

FULL_OPTIMIZATION:remove = "-g"

LEAD_SONAME = "libWPEWebKit.so"
PACKAGES =+ "${PN}-web-inspector-plugin ${PN}-qtwpe-qml-plugin"
FILES:${PN} += "${libdir}/wpe-webkit*/injected-bundle/libWPEInjectedBundle.so"
FILES:${PN}-web-inspector-plugin += "${datadir}/wpe-webkit-*/inspector.gresource"
# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-web-inspector-plugin = "dev-so"

# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-qtwpe-qml-plugin = "dev-so"

# JSC JIT on ARMv7 is better supported with Thumb2 instruction set.
ARM_INSTRUCTION_SET:armv7a = "thumb"
ARM_INSTRUCTION_SET:armv7r = "thumb"
ARM_INSTRUCTION_SET:armv7m = "thumb"
ARM_INSTRUCTION_SET:armv7ve = "thumb"

# Extra runtime depends
# nooelint: oelint.vars.dependsordered - ignored for convenience
RDEPENDS:${PN} += "\
${@bb.utils.contains('PACKAGECONFIG', 'remote-inspector', '${PN}-web-inspector-plugin', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'gst_gl', 'gstreamer1.0-plugins-base-opengl', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'mediasource', 'gstreamer1.0-plugins-good-isomp4', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'webaudio', 'gstreamer1.0-plugins-good-wavparse', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-app \
gstreamer1.0-plugins-base-audioconvert \
gstreamer1.0-plugins-base-audioresample \
gstreamer1.0-plugins-base-gio \
gstreamer1.0-plugins-base-playback \
gstreamer1.0-plugins-base-typefindfunctions \
gstreamer1.0-plugins-base-videoconvertscale \
gstreamer1.0-plugins-base-volume \
gstreamer1.0-plugins-good-audiofx \
gstreamer1.0-plugins-good-audioparsers \
gstreamer1.0-plugins-good-autodetect \
gstreamer1.0-plugins-good-avi \
gstreamer1.0-plugins-good-deinterlace \
gstreamer1.0-plugins-good-interleave \
', '', d)}
\
libgles2 \
"


RDEPENDS:${PN}-web-inspector-plugin += "\
shared-mime-info \
"


# Extra runtime recommends
RRECOMMENDS:${PN} += "\
ca-certificates \
ttf-dejavu-sans \
ttf-dejavu-sans-mono \
ttf-dejavu-serif \
${PN}-qtwpe-qml-plugin \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-meta gstreamer1.0-plugins-good-meta gstreamer1.0-plugins-bad-meta', '', d)} \
"


DEFAULT_PREFERENCE = "-1"

FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"

# https://commits.webkit.org/319279@main
PR = "r319279"
SRCREV = "93472ec12ee947b30c7ec3c176ca0e2fb6ba6ace"
SRC_URI = "git://github.com/WebKit/WebKit.git;protocol=https;branch=main \
file://0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch \
file://0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch \
"

S = "${WORKDIR}/git"

This file is self-contained on purpose; the idea is not to rely on any includes so that unpredictable behavior doesn’t happen in the future.

While the above file contains a lot of interesting details, the most interesting practical part is the last few lines. The SRCREV is set to point to the latest commit from the WebKit main branch (at the time of writing). Along with that comes SRC_URI that specifies two patches required to make WPE WebKit compile.

The problem with the patches is that advancing SRCREV will likely make them unusable due to merge conflicts. Therefore, when advancing the revision, it’s recommended to remove patches from SRC_URI and face the compilation problems from scratch as it’s very likely there will be new compilation problems anyway. Fortunately, nowadays LLMs can be used to fix any compilation problems by preparing custom patches just like the below ones (created by LLM as well). For example, most of the modern models should manage to prepare proper patches just by pointing them to the above recipe and the temp directory with the latest logs from build commands.

Assuming one uses the wpewebkit_latest.bb above, the first patch needs to be created in: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch with the following content:

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [libpas] Only include stdatomic.h when compiling with Clang

Some libpas .c files (e.g. jit_heap.c) are compiled as C++ via
set_source_files_properties(... PROPERTIES LANGUAGE CXX) in
Source/bmalloc/CMakeLists.txt, for TZone heap support. pas_utils.h
unconditionally does `#include <stdatomic.h>`, but GCC's own
<stdatomic.h> relies on the C11-only `_Atomic` keyword and has no
support for being included from C++ translation units (this was only
addressed in much newer GCC releases). Compiling any of the
CXX-tagged libpas .c files with GCC 13 therefore fails with:

    error: '_Atomic' does not name a type

The header is only actually needed here for the Clang-specific
__c11_atomic_* intrinsics guarded by `#elif PAS_COMPILER(CLANG)`
further down in this file; the non-Clang (GCC) path uses the
__atomic_* builtins instead and does not need any of the types or
macros from <stdatomic.h>. Guard the include accordingly so GCC
builds (both plain C and the CXX-tagged libpas sources) are
unaffected by GCC's non-C++-aware <stdatomic.h>.

Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
 Source/bmalloc/libpas/src/libpas/pas_utils.h | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/Source/bmalloc/libpas/src/libpas/pas_utils.h b/Source/bmalloc/libpas/src/libpas/pas_utils.h
index 962634080930..9b98fda54f43 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_utils.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_utils.h
@@ -42,7 +42,15 @@
 #endif
 
 #include <limits.h>
+#if PAS_COMPILER(CLANG)
+/* GCC's <stdatomic.h> relies on the C-only _Atomic keyword and is not
+ * usable when this header is included from a translation unit compiled
+ * as C++ (some libpas .c files are compiled as C++, see bmalloc's
+ * CMakeLists.txt). It is only needed here for the Clang-specific
+ * __c11_atomic_* intrinsics below; the GCC path uses __atomic_* builtins
+ * instead. */
 #include <stdatomic.h>
+#endif
 #include <stdbool.h>
 #include <stdint.h>
 #include <string.h>
--
2.43.0


The second patch should be: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch with:

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [WebDriver] Guard LOG_CHANNEL check with
 LOG_DISABLED/RELEASE_LOG_DISABLED

WebDriverService::handleRequest() unconditionally checks
LOG_CHANNEL(WebDriverClassic).state to decide whether it is worth
building the request/response log strings. However,
Source/WebDriver/Logging.h only declares the WebDriverClassic (and
other WebDriver) log channels inside:

    #if !LOG_DISABLED || !RELEASE_LOG_DISABLED

On a release build (NDEBUG, so LOG_DISABLED is true) without journald
support and without OS_LOG/Android (so RELEASE_LOG_DISABLED is also
true) - the common configuration for an embedded Linux build without
the "journald" PACKAGECONFIG - that guard is false, so the channel is
never declared, and this direct, unguarded use of LOG_CHANNEL() fails
to compile:

    error: 'LOG_CHANNEL_PREFIXWebDriverClassic' was not declared in this scope

RELEASE_LOG_INFO() itself already collapses to a no-op in that
configuration (see wtf/Assertions.h), so guard this manual
LOG_CHANNEL() state check with the same condition used to declare the
channel in Logging.h.

Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
 Source/WebDriver/WebDriverService.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Source/WebDriver/WebDriverService.cpp b/Source/WebDriver/WebDriverService.cpp
index 07ae385f33f4..2612d239a611 100644
--- a/Source/WebDriver/WebDriverService.cpp
+++ b/Source/WebDriver/WebDriverService.cpp
@@ -381,6 +381,7 @@ bool WebDriverService::findCommand(HTTPMethod method, const String& path, Comman
 void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler)
 {
     Function<void (HTTPRequestHandler::Response&&)> actualReplyHandler = WTF::move(replyHandler);
+#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
     if (LOG_CHANNEL(WebDriverClassic).state != WTFLogChannelState::Off) {
         RELEASE_LOG_INFO(WebDriverClassic, "HTTP request %s %s (body=%zu bytes)", request.method.utf8().data(), request.path.utf8().data(), request.dataLength);
         actualReplyHandler = [startTime = MonotonicTime::now(), replyHandler = WTF::move(actualReplyHandler)](HTTPRequestHandler::Response&& response) mutable {
@@ -388,6 +389,7 @@ void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Func
             replyHandler(WTF::move(response));
         };
     }
+#endif
 
     auto method = toCommandHTTPMethod(request.method);
     if (!method) {
--
2.43.0

Once the patches are added, wpewebkit latest should build correctly. However, to make use of it one needs a browser.

To demonstrate how easy the browser for WPE WebKit with Platform API can be, a very minimalistic one will be prepared below.

The first step is to create a directory:

mkdir meta-webkit/recipes-browser/wpe-browser/

Then a file: meta-webkit/recipes-browser/wpe-browser/main.cpp that implements the whole browser:

#include <wpe/webkit.h>

int main(int argc, const char *argv[]) {
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, false);
g_autoptr(WebKitWebView) view = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW,
nullptr));
webkit_web_view_load_uri(view,
(argc > 1) ? argv[1] : "https://wpewebkit.org");
g_main_loop_run(loop);
return EXIT_SUCCESS;
}

Then a file: meta-webkit/recipes-browser/wpe-browser/CMakeLists.txt that describes how to build it:

cmake_minimum_required(VERSION 3.16)
project(wpe-browser CXX)

set(CMAKE_CXX_STANDARD 17)

include(GNUInstallDirs)

find_package(PkgConfig REQUIRED)

# The Wayland WPE Platform already depends on wpe-platform-2.0
pkg_check_modules(WebKitDeps REQUIRED
IMPORTED_TARGET
wpe-webkit-2.0
wpe-platform-wayland-2.0
)

add_executable(wpe-browser main.cpp)

target_link_libraries(wpe-browser
PRIVATE
PkgConfig::WebKitDeps
)

install(TARGETS wpe-browser RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

and finally a recipe file: meta-webkit/recipes-browser/wpe-browser/wpe-browser_1.0.bb that allows bitbake to build the browser and install it in the image:

SUMMARY = "Minimal WPE WebKit browser launcher"
DESCRIPTION = "A minimal launcher built on the WPE Platform API, displaying \
a URL given as its only argument (defaults to https://wpewebkit.org). \
Based on https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"

HOMEPAGE = "https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

FILESEXTRAPATHS:prepend := "${THISDIR}:"

SRC_URI = "file://main.cpp \
file://CMakeLists.txt \
"


S = "${WORKDIR}"

DEPENDS = "wpewebkit"
RDEPENDS:${PN} += "wpewebkit"

inherit cmake pkgconfig features_check

REQUIRED_DISTRO_FEATURES = "opengl wayland"

After all those steps, everything is ready to perform a build. This time the commands are executed in the SDK:

source poky/oe-init-build-env build
bitbake core-image-weston
cd tmp/deploy/images/raspberrypi3-64/
# flash to SD card based on preference

Trying the image #

Once the image is built and flashed to SD card, and the SD card has been used to boot the Raspberry Pi, one can SSH into it, basically by:

ssh root@<IP>

Then, the browser should work out of the box. The first command to try is the one that doesn’t need the network and therefore is the simplest:

XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'webkit://gpu/stdout'

If the network is available, it’s worth starting simple and loading some HTTP page:

XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'http://info.cern.ch'

If that works as well, one can try the HTTPS one:

XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'https://igalia.com'

If there’s an issue with certificates, it’s likely due to broken date/time, so the correct one needs to be set using a command like:

date -s '2026-09-02 22:34:56'

Conclusions #

Since the Platform API is the future of WPE, it’s worth using it already. As the above sections demonstrate, it hasn’t ever been easier to create a WPE-powered browser for embedded hardware. Moreover, nowadays as LLMs are at play, using latest sources from main branch and quickly patching on demand is a possibility worth utilizing. With that, a broad sea of experimenting possibilities becomes wide open. However, it must not be forgotten that while main branch is great for testing new web platform features implemented in WebKit, it’s not necessarily ideal for testing performance. In such case, it’s better to rely on releases and proper browser engine fine tuning for particular hardware one plays with.

September 08, 2026 12:00 AM

September 07, 2026

Igalia WebKit Team

WebKit Igalia Periodical #76

Update on what happened in WebKit in the week from August 31 to September 7.

This was another week focused on ironing out graphics issues in preparation for the upcoming 2.54.x release series. Did we say release? Here we have another set of packaged release candidates as well!

Cross-Port 🐱

The “paint flushing” feature of the Web Inspector that shows the areas of the layers that were painted is now available. On the other hand, setting WEBKIT_SHOW_DAMAGE=1 in the environment will show the areas of the window that were rendered. For example, if the page is scrolled a little, the whole window is rendered, but no layer has to be repainted.

Graphics 🖼️

Fixed visible pixel snapping in the GTK and WPE ports' Skia compositor by choosing linear instead of nearest-neighbor sampling for backing store tiles whenever a layer's transform no longer maps tile pixels onto screen pixels 1:1, matching what the Texture Mapper backend already got from its always-linear OpenGL code path.

Fixed a clipping bug in the Skia-based compositor that caused layers with overflow: hidden set to not clip blur and box-shadow filter outsets.

Fixed a repaint bug where content changing behind an element with a software-rendered filter such as blur() could leave stale pixels on screen, as with the glow effect that YouTube draws around videos in dark/ambient mode. The fix restores proper tracking of which layers act as the repaint container for a pixel-moving filter, so the repaint area is expanded correctly again on WPE, GTK and macOS.

A few problems with Offscreen Canvas have been fixed.

Releases 📦️

Release candidates WebKitGTK 2.53.92 and WPE WebKit 2.53.92 have been published, and they include polishing and a number of fixes that ensure that there will not be noticeable regressions brought in by the new Skia-based compositor and the damage tracking support—two of the main features of the upcoming stable release series. As the stable release dates approaches, we encourage people who try these preview versions to report issues in Bugzilla.

That’s all for this week!

by Igalia WebKit Team at September 07, 2026 11:43 PM

August 31, 2026

Igalia WebKit Team

WebKit Igalia Periodical #75

Update on what happened in WebKit in the week from August 24 to August 31.

After such a packed edition last week, we can relax with an even more packed installment this week! The team has been moving full steam ahead with the Layer-Based SVG Engine, and graphics improvements in general, but we also had a handful of other updates, such as SDK updates, a new WebsitePolicies API, and more.

Cross-Port 🐱

Add new ChildChange types for moveBefore(). This addresses issues with the in-progress moveBefore() implementation where scripts would sometimes execute erroneously.

Added alpha channel support to color input choosers.

Fixed Service Worker static routes on non-Apple ports.

Added a WebsitePolicies:upgrade-to-https-policy property. When enabled this policy will automatically try using HTTPS even for HTTP websites (excluding localhost or IPs). The policy can be configured either to allow automatically falling back to HTTP if that fails, or to consider all HTTPS failures fatal for the best security.

Graphics 🖼️

Fixed SVG text being rasterized for the wrong resolution in zoomed standalone SVG documents in the Layer-Based SVG Engine (LBSE). vector-effect: non-scaling-stroke on <text> no longer comes out too thick, and the resulting metrics match the legacy SVG engine.

Fixed viewport clipping in the Layer-Based SVG Engine (LBSE), where a nested <svg> or a <marker> applied its viewport clip even when its content already fitted inside, and since that clip is not pixel-snapped its edge could fall between two device pixels and cut into whatever was drawn right at the viewport border. Painting now skips a clip that removes nothing, which eliminates a class of subtle pixel differences against the legacy SVG engine.

Fixed opacity animations not damaging descendant layers in the GTK and WPE ports, where a descendant that paints outside its parent's bounds kept its stale pixels on screen. The mask-specific damage handling was generalized into a single group-property path shared by opacity, filters, mask blend modes and replicas, which now damages the layer plus the overlap region of its whole subtree.

Added a cycle-analysis subcommand to webkit-sysprof, which draws every frame cycle of a capture as a bar of cells colored by the mark covering that moment on the main thread, showing whether a slow frame stalled on layout, a long timer or rasterization instead of only reporting that it was slow.

Removed redundant text updates for the text children of elements using display: contents, which previously got one on every style resolution regardless of whether their style actually changed. This removes dozens of useless updates per style recalculation in Web Component applications, where every <slot> uses display: contents.

Fixed tile image caching in the Skia compositor by regenerating the cached SkImage whenever a a tile's contents are updated and by adding a texture release callback that keeps the texture valid for as long as the image references it.

Switched video DMA-BUF buffers to Skia promise images in the Skia compositor, so the texture backing a video frame is only created at the point Skia actually draws it, which works for these buffers because the underlying DMABufBuffer can be kept alive until the promise image is released.

Fixed a hang in the Layer-Based SVG Engine (LBSE) when two SVG <pattern> elements reference each other through href, or one references itself: collecting the inherited pattern attributes now remembers which patterns it has already visited, the same cycle detection that gradients have always had. The walk also resolves every reference in the tree scope of the pattern it started from, so a pattern referenced from inside a shadow tree now inherits the right attributes.

Skipped anchor-positioning bookkeeping during style resolution when a document uses no anchor positioning at all, avoiding two hash lookups per styled element.

Cached the SVG viewport size used to resolve lengths in the Layer-Based SVG Engine (LBSE), instead of recomputing the nearest <svg> element's view box rectangle once per shape per frame. The viewport is invariant across a flush and identical for every shape under the same <svg>, so caching removes redundant work.

Removed the code path for GPU rendering without Deferred Display Lists (DDL) in the Skia compositor, so accelerated painting always records into a display list and no longer needs to create GL contexts on worker threads.

Fixed red and blue appearing swapped in non-accelerated video with the Skia compositor, by allocating the video frame's BitmapTexture with the BGRA layout flag and applying that flag when the buffer is turned into a Skia image.

Infrastructure 🏗️

Bumped the GTK and WPE developer SDK from v9 to v11, bringing GStreamer 1.28.5, sparkle-cdm 2026.2 and libsoup 3.7.2. Be sure to update your local wkdev-sdk container using wkdev-update to make sure your development environment matches what the CI is testing.

That’s all for this week!

by Igalia WebKit Team at August 31, 2026 08:12 PM

August 24, 2026

Igalia WebKit Team

WebKit Igalia Periodical #74

Update on what happened in WebKit in the week from August 17 to August 24.

Another periodical packed with updates on the graphics, multimedia, and tooling fronts. Which sure has to do with preparing for the upcoming 2.54.x release series, that now has release candidates published. Also, do not miss a new stable release being published with fixes for security issues, and make sure to update.

Cross-Port 🐱

Fixed flakiness in the Web Inspector heap snapshot tests.

Extended the webkit-sysprof analyze tool cover the remaining marks emitted along the rendering pipeline, from RenderTreeBuild and CompositingUpdate through FinalizeRenderingUpdate, RenderLayerTree and WaitForCompositionCompletion down to the individual tile marks, pulling the tile count and the dirty region out of the mark messages as statistics next to the durations. The statistics tables also gained mean and median columns, so a captured trace now shows where the time in a frame actually goes.

Fixed test fast/canvas/canvas-composite-text-alpha.html, and improved the state of several other tests which relied on setTimeout().

Implemented moving steps for <option> elements.

Add support for asynchronous scrolling with touch events.

Touch events are now dispatched to the EventDispatcher thread in the Web Process, enabling smooth scrolling even when the main thread is busy. Additionally, this change fixed a bug where precise scrolling delta wheel events, dispatched by touchpads, failed to trigger asynchronous scrolling.

Multimedia 🎥

GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.

Ensure that multimedia on pages restored from the back-forward cache is correctly processed without errors or reloading.

The robustness level is now queried from the CDM (if supported) when using Encrypted Media Extensions (EME).

Added video rendering support for Qualcomm hardware-accelerated decoders when the Skia compositor is in use, complementing the earlier TextureMapper-only support that left such videos blank with the new compositor.

Video frame processing now relies on the driver's implicit YUV to RGB conversion, steered by the colour space and sample range hints taken from the frame colorimetry, with a hint-free import as fallback.

Graphics 🖼️

Fixed how <feImage> paints its referenced element in the Layer-Based SVG Engine (LBSE), which still used a helper written for the legacy SVG engine, where SVG content never had layers, so it used to paint renderers directly and skipped any child that owns a RenderLayer under LBSE, silently dropping its opacity, mask, filter or 3D transform. The referenced content is now painted through the layer tree, the way <mask>,<clipPath>, <pattern> and <marker> content already is.

Made SVG mask no longer force a RenderLayer in the Layer-Based SVG Engine (LBSE), mirroring the earlier change for clip-path. A mask is now applied during painting by SVGNonLayerClippingAndMaskingScope, which opens one transparency layer capturing the renderer's foreground and composites the mask over it afterwards, and which also absorbed the clips that cannot be expressed as a path. A container with a mask keeps its layer, since the mask covers its whole subtree, while a leaf stays layer-free. This further reduces the layer overhead that has been holding LBSE back against the legacy SVG engine.

Skipped scroll coordination for composited layers that have no scrolling role, where the per-layer update previously walked every branch to detach roles the layer had never registered for, only to hand back the parent node ID unchanged. Cutting that work out shortens every compositing update, which matters for composition-heavy workloads.

Fixed rendering of an outermost <svg> with an empty viewBox in the Layer-Based SVG Engine (LBSE), which per the SVG specification should disable painting when the width or height is zero, matching what the legacy engine already did.

Skipped scroll coordination work for layers that have no scrolling role during compositing updates, where updateScrollCoordinationForLayer previously walked every branch to detach roles the layer had never registered for, only to hand back the unchanged parent node ID.

Releases 📦️

WebKitGTK 2.52.6 and WPE WebKit 2.52.6 have been released, including a number of fixes for security issues covered in the accompanying security advisory WSA-2026-0005 (GTK, WPE). It is recommended for everybody to update to these stable releases.

Stabilization for the upcoming 2.54.x release series for both the GTK and WPE ports is ongoing, with the first stable release, 2.54.0, expected around mid-September 2026. In the meantime release candidates WebKitGTK 2.53.91 and WPE WebKit 2.53.91 have been released.

Those interested in previewing the work done by the team in the last half year, including the new Skia-based compositor that is expected to eventually replace the aging TextureMapper, may want to give them a try and report any issues found in Bugzilla.

That’s all for this week!

by Igalia WebKit Team at August 24, 2026 11:30 PM

August 17, 2026

Igalia WebKit Team

WebKit Igalia Periodical #73

Update on what happened in WebKit in the week from August 10 to August 17.

Following an extra packed periodical, this week we get back to a more regular pace with two nice bugfixes, and a new tool to analyze WebKit performance on Linux!

Cross-Port 🐱

The webkit-sysprof toolkit landed in main thus introducing a set of tools for processing Sysprof .syscap capture files recorded from WebKit (GTK/WPE ports). It extracts marks (timeline events) and counters (time-series metrics) from a capture and lets one dump, summarize, analyze, or plot delta-time histograms for them.

Graphics 🖼️

Fixed filters specified on the outermost <svg> element in the Layer-Based SVG Engine (LBSE), where a filter: url(...) reference on an SVG root was silently dropped because the layer code skipped it, as the legacy engine used to apply it by itself. The filter region is now resolved against the SVG root's border box in its container's coordinate system, since the outermost <svg> is a replaced element in the CSS box tree, not part of the SVG user space its children live in.

Avoided serializing gradient and pattern transforms just to answer a presence check in the Layer-Based SVG Engine (LBSE). Asking hasAttribute() whether gradientTransform or patternTransform was specified forced the transform list to be serialized into the attribute map whenever the base value was changed through the SVG DOM, even though that string is never read back, so the check is now answered directly from the typed accessor.

That’s all for this week!

by Igalia WebKit Team at August 17, 2026 07:08 PM

August 10, 2026

Igalia WebKit Team

WebKit Igalia Periodical #72

Update on what happened in WebKit in the week from July 28 to August 10.

Quite a packed pair of weeks this time! The range of updates is big, but some highlights are the handful of Layer-Based SVG Engine updates, performance improvements, and the new WPE APIs. Finally, the Web Engines Hackfest recordings are now published!

Cross-Port 🐱

Fixed webkit_website_data_get_size() looking up sizes for localStorage, indexedDB, and the DOM Cache.

Enabled support for the File System and Storage APIs.

Added WEBKIT_WEBSITE_DATA_FILE_SYSTEM API to enable fetching/clearing File System data sites use.

Multimedia 🎥

GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.

Landed a follow-up fix for the h264 edit list support to prevent regressions on YouTube MSE Conformance Tests 2019 when using older versions of GStreamer.

Avoided a spurious seek to zero when seeking to the end of an audio/video when playback starts.

Graphics 🖼️

Recovered a MotionMark compositing regression in the Skia backend, where respecting damage information during compositing cost about 20% on the composition suite and more than 50% on two of its tests. Restricting a draw to the damaged area means splitting it into source-rect-to-destination-rect pieces or drawing it under a device-space clip, and neither is needed when the damage already covers the whole draw, which is the common case in those tests because a composited layer is much smaller than a damage grid cell. Those draws are now issued exactly as they would be with damage turned off, avoiding a clip path that flushed the image set batch and left it an order of magnitude smaller, and with the regressions gone, using damage information for compositing was enabled again along with unifying damaged regions that are sent to the system compositor.

Skipped a per-frame visual overflow recomputation in the container paint cull of the Layer-Based SVG Engine (LBSE). The cull used a cached overflow rect that was recomputed by unioning all descendant bounds on a miss, which happened every frame for containers with an animated transform. It now only runs when the rect is already cached.

Skipped the outline paint pass for SVG renderers without an outline in the Layer-Based SVG Engine (LBSE). Every shape used to be painted twice per frame, the second pass being a no-op in the common outline-free case, so guarding it behind hasOutline() removes a redundant traversal from every frame.

Fixed masked SVG content being cut off at the edges in the Layer-Based SVG Engine (LBSE), where the mask image was sized over the enclosing integer rect of the mask content bounds in device space while the transparency layer clip was computed differently, losing the outermost pixels. An SVG renderer that is a box (<text>, <foreignObject>) also used the CSS mask clip rect derived from the border box, which leaves out SVG content spilling outside it, and now uses the visual overflow rect instead

Fixed most of the remaining <mask> issues in the Layer-Based SVG Engine (LBSE): masks were displaced on targets whose children carry transforms, the mask region given by x, y, width and height was ignored so content reaching past it was not cut off, and the cached mask image was never dropped on layout, leaving a resized viewport masking with an image rasterized for the old size.

Stopped rebuilding objectBoundingBox gradients on every layout size change in the Layer-Based SVG Engine (LBSE), which used to discard the cached gradient and re-collect its attributes (serializing every animated property back to a string, including gradientTransform) on the next paint. That work is wasted for objectBoundingBox units, whose coordinates resolve against the object bounding box with the userspace transform recomputed on every paint anyway, so only the clients are repainted now, while userSpaceOnUse gradients resolve against the viewport and are still invalidated as before.

Cached the SVG fill and stroke paint server directly on the renderer in the Layer-Based SVG Engine (LBSE), instead of in the shared referenced-resources table living in a renderer's rar data, where every fill and every stroke paid the cost of a hash map lookup just to reach the cache, which gives a small win on the MotionMark/Suits performance test. It also fixed a shape referencing a paint server that does not exist yet, which kept painting unfilled once an element finally took that id, because the shape registered itself as a pending resource under the full resolved URL while the lookups used the bare fragment identifier.

Sped up mapping the paint dirty rect through transforms in the Layer-Based SVG Engine (LBSE). Transformed SVG paints inverted the full 4x4 matrix on every paint, and now use the cheaper inverse of the 2x3 affine transform whenever the transform is affine, falling back to the 4x4 inverse only for 3D transforms.

Made SVG clip-path no longer force a RenderLayer in the Layer-Based SVG Engine (LBSE). A bare clip is now applied during painting through a shared ClipPathPaintScope, a scope object that sets up the clip in its constructor and tears it down afterwards, handling CSS basic-shape and box clips as well as SVG clipper resources so both regular CSS boxes and SVG content share one path. This is a further step in removing the intrinsic need for layers on SVG renderers, continuing the effort to close the performance gap between LBSE and the legacy SVG engine.

Fixed dynamic x and y updates on SVG <foreignObject> elements, which stopped taking effect after the viewport geometry started being derived from the resolved style. The x, y, width and height attributes are presentation attributes mapped to the CSS x, y, width and height properties, but only width and height marked the presentational hint style as dirty when they changed, so a style recalc never ran for x and y and layout kept reading stale values. All four geometry attributes now invalidate the presentational hint style, matching how <rect> handles its geometry, so setting x.baseVal.value from script repositions the <foreignObject> as expected.

Added support for external and data: URL references to clip-path, markers and paint servers (gradients and patterns) in the Layer-Based SVG Engine (LBSE). Until now the LBSE resource resolvers only looked for the referenced fragment inside the local document, so markup like url(file.svg#id) silently resolved to nothing, while filters already worked and the legacy SVG engine handled all of these since a few weeks.

Fixed SVG filters vanishing on elements with a very large bounding box in the Layer-Based SVG Engine (LBSE). The filter region was seeded with the element's object bounding box and then united with each referenced <filter> region, but a referenced <filter> brings its own region, and that region alone decides where the filter paints, so the union could grow far past the image buffer limits and get clamped down to scale that made the output disappear. When every function in the chain is a <filter> reference the bounding box is now dropped and only the referenced regions are kept, matching what the legacy SVG engine does, while objectBoundingBox filter units still resolve exactly as before and HTML/CSS filters are untouched.

WPE WebKit 📟

Add the WebView::run-color-chooser API to WPE to allow applications to show color choosers, similar to WebKitGTK's API.

The WPE port can now use libsecret for persistent credential storage, reusing the implementation from the WebKitGTK port. This is disabled by default and can be toggled passing -DUSE_LIBSECRET=ON to CMake when configuring the build.

Add the WebKitClipboardPermissionRequest API to WPE, allowing support for the clipboard permission similar to WebKItGTK.

Community & Events 🤝

The videos of the Web Engines Hackfest 2026 talks have been published, including the sessions from the new WPE WebKit track. This year the following WebKit-related talks have been recorded:

That’s all for this week!

by Igalia WebKit Team at August 10, 2026 06:51 PM

July 28, 2026

Igalia WebKit Team

WebKit Igalia Periodical #71

Update on what happened in WebKit in the week from July 14 to July 27.

This two-week update includes plenty of changes to the Skia compositor, changes to multimedia support, three blog posts, and assorted improvements.

Cross-Port 🐱

The Web Inspector “Layout & Rendering” timeline now shows a Layout Invalidated event for every element that needs relayout, not just the layout root (with the old root-only event renamed to Layout Scheduled). This unveils why some layouts take much longer than others. No more guessing which of dozens of nodes is actually to blame!

The webkit://gpu page has gained a dark style, which will be used when the system settings indicate that dark mode is preferred by the user.

Multimedia 🎥

GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.

The experimental GstWebRTC backend was removed and libwebrtc usage was enabled in the main branch. We hope to enable WebRTC support by default in the 2.56 series, scheduled around March 2027.

MP4 edit lists support was enabled in the MSE backend, improving timestamp accuracy, specially when handling of B-frames.

Graphics 🖼️

Split the compositing walk in the Skia compositor into a damage pass and a paint pass, so the frame damage is known before the first draw. The damage pass walks the layer tree with a SkNoDrawCanvas in place of the real canvas, so every draw is discarded and only the damage is collected. Both passes run from a single paint() that applies animations and computes the transforms once, so the two see the same tree. Knowing the damage up front is what lets the compositor eventually paint only the parts of a frame that actually changed.

Wired up damage-driven compositing on the Skia compositor, so a frame re-composites only the region that actually changed instead of the whole surface, when the UseDamagingInformationForCompositing feature is enabled (not yet on by default). Each frame's damage is combined with what each swap-chain target still needs to redraw since it was last drawn into, and the clear and every draw are clipped to that region, which is a milestone towards no longer repainting untouched pixels every frame.

Made the root layer collect the frame damage itself in the Skia compositor, instead of having each layer report its own changes. Reporting leaves a gap whenever a layer is in no position to report, e.g. a destroyed one took its painted rectangle with it, so what it had drawn stayed on screen. The root now holds one rectangle per layer and compares it against what each frame's walk finds, so a layer that moved is repainted in both places, and a layer the walk never reaches is repainted where it used to be and dropped. Nothing has to notice anything for the pixels it left behind to be repainted, which is what makes it safe to restrict composition to the damaged region by default in future commits.

Limited every content draw to the target's repaint region in the Skia compositor, so a composited frame can redraw only the pixels that actually changed. Each content type restricts itself to the region's rectangles rather than clipping the canvas, since a multi-rectangle clip cannot be a hardware scissor and would make Skia build a mask and break batching. This is the groundwork for damage-driven compositing, which stays off by default behind the damage-tracking feature flag, as the compositor still passes no region and nothing is restricted yet.

Made each swap-chain target track its own damage since it was last current. Repainting only what changed is correct only when drawing into the target that holds the previous frame, but the swap chain hands back whichever target is free, and that one is a frame or more behind. Each frame's damage is now added to every target as it is recorded and cleared from a target when that target is presented, instead of being built as a side effect of reading it.

Taught the tile and image draws in the Skia compositor to split themselves by damage rectangle, so a frame only repaints the parts of a layer that actually changed. A new SkiaDamageRegion holds the frame's damage in device space and is built once per frame, and each draw is restricted to it: skipped when it touches no damage, split into one sub-draw per damage rectangle it overlaps, or drawn under a device-space clip when a rotated or skewed transform rules out working with rectangles. Nothing feeds a damage region in yet, so every draw still paints in full—this prepares for future patches enabling using damage information in the composition

Fixed missing repaints when compositor-applied layer state changes dynamically in the Coordinated Graphics backend. A layer recorded damage when its backing store re-rendered or a new contents buffer arrived, but the compositor also handles filters, masks, clip path changes, the contents rectangle, the contents tiling, the blend mode and contents visibility, and changing any of those alters the pixels it produces without dirtying a tile. Those setters now damage the whole layer, so a compositor that repaints only the damaged rectangles no longer leaves the previous frame's pixels on screen.

Community & Events 🤝

Nikolas Zimmermann has written a two-part blog series about the current the new Layer-Based SVG Engine (LBSE), with the first post covering the effort to reduce layer overhead using layers conditionally, and the second about how compositing is being implemented and the complications introduced due to paint ordering rules.

Loïc Le Page has published a blog post explaining how to use the new WPEPlatform API to implement a custom WPE integration. While presented example uses GLFW and EGL to show Web content on an X11 window, the concepts are useful for anyone looking into embedding WPE.

That’s all for this week!

by Igalia WebKit Team at July 28, 2026 01:04 AM

July 22, 2026

Nikolas Zimmermann

Implementing compositing in LBSE

Keeping paint order correct with paint order segments

July 22, 2026 12:00 AM

July 14, 2026

Nikolas Zimmermann

Reducing layer overhead in LBSE

Conditional layer creation in the layer based SVG engine

July 14, 2026 12:00 AM

July 13, 2026

Igalia WebKit Team

WebKit Igalia Periodical #70

Update on what happened in WebKit in the week from June 30 to July 13.

The summer continues with many updates to the new SVG engine (LBSE), improvements to the new Skia-based compositor, some small API additions, and ever-important stable releases with security fixes.

Cross-Port 🐱

Enabled the CloseWatcher API and dialog's closedby attribute in stable.

New API has been added which allows specifying per-navigation User-Agent string values using webkit_policy_decision_use_with_policies(). Applications now have more granularity to decide which User-Agent websites are presented with, complementing the existing global WebKitSettings:user-agent setting.

Graphics 🖼️

Roughly halved the cost of the Skia based compositor on WPE running on Vivante GPUs with the Etnaviv driver, by turning off Skia's mipmap sharpening option. That option is enabled by default and makes the Skia shader generator append a small negative level-of-detail (LOD) bias to every mipmap-capable texture sample. WPE does not use mipmapping at all, so the bias sharpened nothing, but it still turned each texture fetch into a LOD lookup, which is a slow path on the tiled GPUs found in the i.MX series. Disabling it restores usage of faster, plain fetch operations.

Fixed broken rendering with the Skia compositor on WPE when super-tiled textures are enabled on Vivante GPUs. Those tile buffers are allocated padded up to a multiple of 64 pixels, so the physical texture is larger than the logical tile, but the Skia backing failed to take this difference into account, leading to distorted tile images being rendered.

Stopped the Skia compositor from blending opaque layers on WPE. Every layer was drawn with the default source-over blend mode, which leaves GPU blending switched on even for fully opaque layers that do not need it, so the cost was paid on every composited frame.

Layers that are opaque, drawn at full opacity and using the default blend mode are now composited with a plain source blend mode instead, which lets Skia turn blending off and lowers GPU bandwidth usage, benefiting tiled GPUs the most.

Cached the concatenated SVG transform attribute matrix on graphics elements in the Layer-Based SVG Engine (LBSE).

Reading the transform attribute walked the whole transform list and multiplied every item together again, and that happened around three times per animation frame for each element, even though the result only changes when the transform list itself is mutated.

The concatenated matrix is now stored on the element and invalidated whenever a transform-related attribute changes, so the multiplication runs once per mutation instead of once per read. This cuts repeated matrix work out of the per-frame path for animated SVG content.

Moved the clip out of the SVG child-paint loop in the Layer-Based SVG Engine (LBSE).

Painting a container used to set up a clip rectangle for every child shape in turn, so each shape did its own graphics-context save, clip and restore even though the clip rectangle was identical for all of them. When there is a single region to clip to and no child paints into its own layer, that clip is now established once and shared by every child, transformed or not.

This removes a per-shape save and clip from the hot painting path of SVG documents with many children.

Cached the SVG transform origin on SVG renderers in the Layer-Based SVG Engine (LBSE).

Every transform flush recomputed the origin for each non-layered SVG shape, even though it only depends on the transform-origin style and the transform reference box, and sampling MotionMark's Suits test at fixed complexity showed that computation taking around 1% of the WebProcess main thread.

The origin is now cached and keyed on the reference box, with a style change to transform-origin or transform-box dropping the cache, and the fast path is limited to plain SVG transforms so viewport containers and CSS-transformed renderers keep computing it directly. This removes a repeated per-shape cost from animated SVG content, and the caching scope can be widened later.

Cached the SVG viewport size used to resolve the transform reference box in the Layer-Based SVG Engine (LBSE).

The default transform-box for SVG is view-box, so every transformed shape resolved the viewport from the SVG root's content box again on each query, both when updating its local transform and again during paint. The viewport is constant after layout, so it is now cached on the <svg> element and only recomputed when layout actually changes it, on resize, zoom or a viewBox update. This removes another repeated per-frame computation from the transform path for animated SVG content.

Coalesced the SVG transform flush into one minimal repaint per container in the Layer-Based SVG Engine (LBSE).

Once per rendering update WebKit processes every SVG renderer whose transform changed, whether from script or an animation, and that repaint pass was the dominant per-frame cost on MotionMark's Suits subtest. Instead of walking each moved renderer up to its repaint container, the flush now computes each child's rectangle in its parent's coordinate space, unions the children per parent, maps that single union up the chain once, and issues one repaintUsingContainer() call per repaint container rather than one per shape.

This also stops requesting outline bounds, which for SVG merely duplicated the visual overflow rectangle, and refreshes the bounding-box and visual-overflow caches that a layout would normally update, so getBBox() and paint or hit-test culling never read a stale rectangle. This collapses many backing-store invalidations into one while keeping the repainted region minimal, closing the performance gap to the legacy SVG engine.

Avoided re-resolving the SVG transform from style on every paint in the Layer-Based SVG Engine (LBSE).

Non-layer SVG renderers already cache their transform in m_localTransform, but the painting code path used to recompute it from scratch each time, concatenating the transform list, applying transform-origin and multiplying matrices, only because the cached value uses a different transform origin. The paint transform is now derived directly from the cached one by translating around the nominal origin, which removes that per-paint recomputation and cuts the cost of painting transformed SVG content.

Fixed a repaint bug in the Layer-Based SVG Engine (LBSE) where dynamically changing a marker's markerUnits or orient attribute left stale pixels behind. Such a change resizes every shape that references the marker, but a referencing shape without a layer gets no post-layout position update, so only its new bounds were repainted—a shrinking marker left its former area on screen.

The visual overflow rectangle, markers included, is now cached at the end of shape layout while the geometry is still current, so a marker change can repaint the old bounds before recomputing the new ones. The extra repaint is limited to markers, since gradients and patterns do not affect a client's bounds, and the resulting repaint rects are more accurate than the legacy SVG engine's.

WPE WebKit 📟

Added a new feature flag, BackForwardCacheWithMedia, which may be used to disable storing pages with media content in the back-forward cache. This should solve the problem with hardware decoders kept occupied on low-end devices in case of caching pages with media after navigation.

Releases 📦️

WebKitGTK 2.52.5 and WPE WebKit 2.52.5 have been released, including a number of fixes for security issues, and therefore it is recommended to update. An accompanying security advisory will be published in the coming days. Additionally, these releases include small improvements and Web compatibility improvements.

That’s all for this week!

by Igalia WebKit Team at July 13, 2026 10:59 PM

June 29, 2026

Igalia WebKit Team

WebKit Igalia Periodical #69

Update on what happened in WebKit in the week from June 22 to June 29.

After a small break after the Web Engines Hackgest, we're back with another round of updates, this time with a couple of exciting improvements to the SVG engine, a WebRTC fix, and support for WebP images with the toDataURL() API.

Cross-Port 🐱

Made RenderLayer creation conditional for SVG renderers in the new Layer-Based SVG Engine (LBSE), so a layer is now only created when one is actually needed for intrinsic reasons (3D transforms, opacity, etc.) instead of unconditionally for every renderer. Plain 2D transforms no longer force a layer and are applied directly during painting. This is the groundwork for follow-up patches that remove the intrinsic need for layers when applying clipping, masking and filters to SVG subtrees. It is an important milestone towards reducing the overhead that has been holding back LBSE performance compared to the legacy SVG engine.

Fixed the paint order of non-composited children around composited SVG siblings in the Layer-Based SVG Engine (LBSE). A layered container paints its children from a single flat list in DOM (and SVG paint) order, but some children are composited into their own GraphicsLayer for reasons like will-change, a 3D transform or certain opacity cases. The flat child list is now split into contiguous paint-order segments at those boundaries, with each run of plain children painted by its own overlay layer placed at the correct depth in the compositor's child list. This keeps every child in its DOM order without giving trailing siblings a RenderLayer or backing store of their own, and a container with no composited children produces no segments at all, so the common case costs nothing. This allows us to support composition within LBSE subtrees in a performant way, after dropping the requirement that every renderer creates a layer.

Multimedia 🎥

GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.

Fixed initial decoding issues on LibWebRTC on platforms that do video decoding on the final playback stage (for efficiency and performance), instead of on the LibWebRTC decoder component.

Graphics 🖼️

Added support for producing WebP images with canvas' .toDataURL(). Using 1.0 as the quality setting will produce lossless images, which matches the behaviour of Chromium and Firefox.

That’s all for this week!

by Igalia WebKit Team at June 29, 2026 09:00 PM

June 16, 2026

Igalia WebKit Team

WebKit Igalia Periodical #68

Update on what happened in WebKit in the week from June 9 to June 16.

The major highlight this week is the Web Engines Hackfest! Despite it, there are a variety of updates as well, such as various improvements to input handling in WPE WebKit and WebKitGTK, WPE menu rendering changes, and a plethora of other smaller improvements.

Cross-Port 🐱

Input methods may now know whether a field is intended to be used as search input, in which case the WebKitInputMethodContext:input-purpose property will have the value WEBKIT_INPUT_PURPOSE_SEARCH.

Due to GTK not providing an equivalent value for GtkInputPurpose, the default behaviour is to continue mapping search fields to GTK_INPUT_PURPOSE_FREE_FORM as before; but custom input methods may use the new value to detect search inputs. When using WPEPlatform, the value is mapped to WPE_INPUT_PURPOSE_SEARCH, which has been added as well.

Handle selections as part of moveBefore.

Corrected user activation propagation for close watchers.

Invalidate :lang() and :dir() selectors after moveBefore.

Enable Close Watchers in preview.

WPE WebKit 📟

WPE now renders its own popup menus for elements such as select. It supports all styling options the web provides such as colors and fonts. The internal menu can be overriden with the existing WebView::show-option-menu signal. Cog for example still renders its own (with a recent commit).

A colorful context menu

A context menu with a simpler style

A context menu inside an iFrame

A context menu inside a rotated container

Community & Events 🤝

The Web Engines Hackfest started! We had a fantastic first day of talks, and now are heading to breakout sessions. Make sure to check the schedule for sessions that may interest you!

That’s all for this week!

by Igalia WebKit Team at June 16, 2026 10:14 AM