April 15, 2023

Apologies

Just wanted to say sorry. Some paradoxes.

(less is more)

For more details, read on.

Edit: rephrasing; publish and augment the text that was initially written in an HTML comment.

Some notes

GTK and GNOME in general

GtkObject/GObject and language bindings (back then, before C++98 stdlib ISO spec and good GCC support) was a pragmatic solution to welcome and unite several developers' communities. With the C language being more bindable (simpler ABI calling conventions).

GTK 4 with (some) API breaks compared to GTK 3 was a good solution for time-to-market for long-time asked features.

Design is actually important, especially to compete with the competition. I find GNOME 44 quite good!

Paradoxes

I'm against proprietary software, the MAGMA (GAFAM), etc. But in practice it's very difficult to avoid them all.

The difference between theory and practice tends to be very small in theory, but in practice it is very large indeed.
Anonymous

Quote copied from Open Sources (Michael Tiemann's chapter).

Acknowledgments

The GNOME Brussels Events at (almost) each new GNOME stable release that I came to (from ~2011 onward).

Various people I talked to at the two GUADECs I came to (Strasbourg and Almería). Traveling a long distance is something that scares me a lot.
Especially for Almería where I had to go to a funeral the day just before taking a "plain" plane at 6am, sleeping (maybe) 1 hour at most during the night in-between. Very sad moment. But not every tears are bad things.

Loads of FOSDEMs I came to.

The local LUG (LouviLUG) that I have the luck to have nearby and to organize now since a long time :-) (Although it's not me who created it, but several-ones passed me the baton).

Overall, I meet and I've met really great minds, and I'm thankful for that.

What's next

I don't really know (or, I prefer not telling you everything). But what I'm sure about is that I need to change one or more things in my life.

Putting away the dark side of my past, keeping what's positive. And probably reverse the roles between job and hobbies (so, keeping CS as a hobby only, and do a retraining and career change). I now approach this with more serenity.

Wish me good luck :-) !

Even though it's not possible to write comments on this blog, don't hesitate to drop me an email. I do read them, and like to have feedbacks and to exchange ideas.

Addictions

Linking several related writings together:

Notes

If we don't say NO to all harmful things that surround us now, we're going to enter what I call a New Middle-Age (a dystopia). Take care, don't be afraid to be different (If you want to be someone, be yourself).

If you want people to do something the healthy way, you must make the healthy way the easy way.
Me - Corollary to a Traditional saying (replace "healthy way" by "right way").

Even though it's not possible to write comments on this blog, don't hesitate to drop me an email. I do read them, and like to have feedbacks and to exchange ideas.

April 14, 2023

2023-04-14 Friday

  • Mail chew, partner call, catch up with Ash & Jaume. Lunch. Partner call.
  • Pleased to see a nice recap of COOL days, and some wrap-up ramblings too.

#91 Inverted Titles

Update on what happened across the GNOME project in the week from April 07 to April 14.

GNOME Core Apps and Libraries

Libadwaita

Building blocks for modern GNOME apps using GTK4.

Alex (they/them) reports

libadwaita has 2 new types of boxed list rows now:

  • Spin rows contain an embedded GtkSpinButton
  • Property rows invert their title and subtitle style same way as Nautilus and Loupe properties have been doing

Software

Lets you install and update applications and system extensions.

Philip Withnall says

Milan Crha has been re-triaging old GNOME Software issues, fixing problems in PackageKit, and smoothing off a lot of sharp edges ready for GNOME Software 44.1.

GNOME Circle Apps and Libraries

Hugo Olabera announces

This week I released version 2 of Wike, which brings a huge revamp to the user interface and also introduces some new features. These are the main changes:

  • Migration to GTK4 + Libadwaita.
  • The search input is now always visible. It also includes an indication of the selected language and a button that gives access to the search settings.
  • Type to search. Just start typing to find articles.
  • New side panel that provides access to the article table of contents, languages, bookmarks and history. It can be used in floating or pinned mode.
  • Bookmarks with multiple lists.
  • New view menu that allows you to change the theme, zoom, typography and more.
  • Content view now defaults to system font.
  • Responsive design. The user interface now adapts to small screens, like phones.
  • Improved status pages (new tab, article not found…).
  • Use of flag icons to help identify languages. This can be turned off.
  • New printing option. This also allows export articles using the “Print to PDF” option.
  • New app icon.
  • Updated translations. Thanks to all translators!

Video Trimmer

Trim videos quickly.

Ivan Molodetskikh announces

I published Video Trimmer v0.8.1. This is a small release that adds keyboard shortcuts, fixes a crash with some videos and updates the platform to GNOME 44.

Pika Backup

Keep your data safe.

Sophie 🏳️‍🌈 🏳️‍⚧️ ✊ reports

A new version of Pika Backup is out. Part of the more than 20 additions and changes in version 0.6 are:

  • Add status information to the list of background apps.
  • Automatically reconnect and retry backups in more situations than before.
  • Fix issues with not freeing up space when deleting older archives.
  • Correctly handle some rare situations like moved backup repositories.
  • Document that restoring from backup via Files does not preserve access rights.

One of our many goals for the next version is a dedicated restore feature. You can support the development with a donation on Open Collective or GitHub sponsors.

gtk-rs

Safe bindings to the Rust language for fundamental libraries from the GNOME stack.

Julian 🍃 says

The properties macro of gtk-rs-core has been published two months ago. Now that the rough edges have been polished, I’ve updated the gtk-rs book too. In the case of the book, this let me remove more than 300 lines. If you want to adapt your own app, you can also have a look at our examples. custom_editable, custom_layout_manager, custom_orientable, expressions, list_box_model, rotation_bin and squeezer_bin all take advantage of this macro. Thanks goes to ranfdev for doing most of the initial work and Bilal Elmoussaoui for getting it over the finish line.

Third Party Projects

gregorni says

This week, I released version 1.2.0 of ASCII Images on Flathub! In this release, French, Russian, Occitan and Italian translations were added, the app now remembers window size and state when closed, and the file manager can now open files with ASCII Images.

Tube Converter

An easy-to-use video downloader (yt-dlp frontend).

Nick reports

Tube Converter V2023.4.1 is here!

This week’s release includes many many fixes for various crashes across both the GNOME and WinUI platforms, the application should be a lot more stable now.

We also added the ability to open a download file directly, as well as the save directory :)

Phosh

A pure wayland shell for mobile devices.

Guido says

We’ve merged support for a menu that appears when long pressing the power button on your phone when using phosh. The emergency call support didn’t land yet, hence that button is dimmed. But the merge request for that is already pending and the gnome-calls side is in place too.

Login Manager Settings

Customize your login screen.

Mazhar Hussain reports

Login Manager Settings has moved from my user profile to a dedicated GitHub organization. The new website is https://gdm-settings.github.io and the new repo is https://github.com/gdm-settings/gdm-settings.git. It also has a collective on OpenCollective now.

Login Manager Settings v3.0 was released. It adds

  • An “Always Show Accessibility Menu” option
  • Option to change cursor size
  • “What’s new” section in ‘About’ window
  • A one time donation request dialog
  • A ‘Donate’ option in app menu

Also, proper names of themes are presented instead of their directory names.

Bugs that were fixed include

  • One could choose a cursor theme as an icon theme.
  • The app failed to run on PureOS.

Login Manager Settings v3.1 was also released. Some translations were accidentally not included in v3.0. So, this is a very small point release, released only to include those translations.

Denaro

A personal finance manager.

Nick announces

Your favorite pizza man is here, announcing Denaro V2023.4.0 🥳

This version includes many many new features and improvements for managing your accounts. First, there is a new Dashboard page that allows users to see information about all accounts at a quick glance. We also added the ability to assign colors to groups and customize decimal and group separators used per account. Many issues were also fixed, including issues importing information OFX and QIF files and random GTK crashes users were experiencing.

I’d like to thank @fsobolev , @DaPigGuy , and all of the translators and contributors who continue to make Denaro better every day ❤️

That’s all for this week!

See you next week, and be sure to stop by #thisweek:gnome.org with updates on your own projects!

April 13, 2023

2023-04-13 Thursday

  • Mail chew, sync with Miklos, COOL community call, catch up with Andras,Pedro & Anna. Sync with Cor.
  • Quick lunch, partner call, customer call with partner. Plugged away at BinaryDataContainer re-factoring to make it swap-able.

April 07, 2023

#90 Enabling Feedback

Update on what happened across the GNOME project in the week from March 31 to April 07.

GNOME Incubating Apps

Loupe

A simple and modern image viewer.

Sophie 🏳️‍🌈 🏳️‍⚧️ ✊ says

Loupe is now available as a preview on Flathub. One of the ideas behind the Incubation process is to enable feedback from the broader community before an app lands in GNOME Core and all features are set in stone. So don’t expect everything to work yet.

Since our last update, we have continued to get some of the more important features done:

  • Loupe now uses mipmaps in rendering, avoiding rendering artifacts.
  • We have merged HighDPI support.
  • I have added an algorithm that detects whether a zoom or a rotate gesture is intended. Quite a tricky task, but necessary for touch gestures to work correctly. However, more work on the rotate gestures is still necessary.
  • Loupe now uses an algorithm that detects if a transparent image will be too dark to be visible on the default background. In that case, Loupe will choose a lighter background (screenshot below).
  • And as usual, we have fixed a bunch of issues.

Thanks for everyone contributing feedback and issue reports. Special thanks go to Matthias Clasen and Benjamin Otte for keeping up with all my attempts to break GTK/GDK and Ivan Molodetskikh for the fruitful cooperation in our joint goal to get images on computer screens.

GNOME Circle Apps and Libraries

Dave Patrick reports

Introducing Mousai v0.7!

If you have been following Mousai on TWIG, nothing should come as a surprise. After almost two years of development, this version comes packed with exciting new features, including a new sleek and intuitive UI, fuzzy search on the history, recognition cancellation, and MPRIS support. You can now also easily copy the title and artist of any song from the UI, remove individual songs from your history, and seek through the player. And for those times when you don’t have an internet connection, we’ve got you covered. Plus, we’ve fixed a ton of bugs and improved stability across the board, along with a full Rust rewrite.

hugoposnic says

I spent the last two weeks improving Curtail. Mainly porting it to Gtk 4 and Libadwaita. I made three new versions to improve the app incrementally with the indispensable help of Maximiliano and Tobias. It was also the opportunity to add some new features and fixed some bugs.

Here is a small tour of the main changes:

  • SVG support
  • Port to GTK 4 and Libadwaita
  • More modern results page
  • No more UI freezes during compression
  • Configurable compression timeout
  • Dependencies updates (OptiPNG -> Oxipng)

The latest update has even been featured on “OMG! Linux”: Curtail Image Compressor Can Now Crush SVGs

Really happy with the current state of Curtail. Thanks for your great feedback!

Plum Nutty (she/they) reports

Chess Clock v0.5 was released! This new release brings a major overhaul to the time control selection screen, simplifying the available presets and emphasizing the manual time entry controls. There’s now only a single button to start the game, and the presets just set the time on the manual time entry.

Gaphor

A simple UML and SysML modeling tool.

Arjan reports

I’m happy to announce a new release of Gaphor.

  • Gaphor is now 100% GTK4 on all supported platforms: Linux (Flatpak + AppImage), Windows, and macOS
  • Gaphor now has a graphical merge conflict resolver
  • Diagrams can be added to diagrams
  • Enable middle-click mouse scrolling of diagrams
  • The language used in the model can be changed independent from your system language
  • And many, many bug fixed and UI improvements

Third Party Projects

Bilal Elmoussaoui announces

Took a bit too long but finally did a new release of flatpak-vscode, it includes various improvements inspired by GNOME Builder as usual:

  • Mount fonts directories
  • Support running inside a container like toolbox
  • Expose session accessibility bus
  • Fix remote development support

Bilal Elmoussaoui reports

A new release of flatpak-github-actions is out with various new configuration options. Details can be found at https://github.com/flatpak/flatpak-github-actions/releases/tag/v6

nxyz announces

This week I released Resonance, an intuitive music player application written in Rust & Python with a clean user interface. Resonance lets you effortlessly manage and play your music collection.

Features:

  • UI updates to reflect currently playing track’s cover art colors
  • Playlist creation & modification
  • Control the player through MPRIS
  • Discord Rich Presence integration
  • Last.fm scrobbling

GH repo: https://github.com/nate-xyz/resonance

Flathub page: https://beta.flathub.org/apps/io.github.nate_xyz.Resonance

Fyodor Sobolev announces

This week Time Switch, a small app to run a task after a timer, got an update:

  • Added ability to create presets to allow saving and restoring settings
  • New shortcuts were added to make the app easier to use with keyboard
  • When running in the background, the app shows timer information in GNOME 44
  • The app will now remember window size and notification text
  • Small UI improvements

Download from Flathub

Tube Converter

An easy-to-use video downloader (yt-dlp frontend).

Nick reports

Tube Converter V2023.4.0 is here! This week’s release features the ability to run downloads in the background (taking advantage of GNOME 44’s new background apps API!) as well as many bugs fixed!

Here’s the full changelog:

  • Added the ability to run downloads in the background (off by default)
  • Fixed an issue where extra escape characters were added in video titles
  • Improved the UX of adding a download
  • Updated translations (Thanks everyone on Weblate!)

Denaro

A personal finance manager.

Nick reports

Denaro V2023.4.0-beta1 got released this week! We have been hard at work the past month and a half improving Denaro’s stability and adding many many new features to the application. This beta is a continuation of the V2023.3.0-beta series, but since we are in a new month, the beta number was reset.

This beta includes a fix for the random crashing many users were experiencing when working with transactions, finally! It also includes a new preference option to automatically backup account files as CSV files to a specific folder. We have a few more features left to polish up and are expecting a stable release next week :)

Here’s the full changelog:

  • Added the ability to customize the decimal and group separators used per account
  • Added the ability to password protect a PDF file
  • Added a preference option to automatically backup account files to a specific folder
  • Fixed an issue where OFX files with security could not be imported
  • Fixed an issue where QIF files could not be imported on non-English systems
  • Fixed an issue where editing a transaction with a receipt would crash the application
  • Updated to GNOME 44 platform and fixed many GTK crashes users were experiencing
  • The UI is now built using blueprint
  • Updated translations (Thanks to everyone on Weblate)!

Crosswords

A crossword puzzle game and creator.

jrb announces

GNOME Crosswords 0.3.8 has been released.

This release adds the following improvements:

  • “New Puzzle” greeter for the editor. • Fully adaptive sizing. Crosswords will shrink to fit available space • Fix end-of-game bugs where you could still edit the puzzle • Use tags instead of labels for puzzle-set metainfo • Enumeration rendering fixes • Miscelaneous bugfixes

This release marks a change in focus. For the next few months, we are going to work more on the Editor than the game. Stay tuned for more changes!

Cartridges

Launch all your games

kramo says

Last week I released Cartridges, a simple Libadwaita game launcher for all your games. This week it received a huge update:

  • You can now download cover art from SteamGridDB automatically!
  • It gained two new import sources: Lutris and itch.
  • There is better feedback for launching and hiding games.

Check it out on Flathub!

That’s all for this week!

See you next week, and be sure to stop by #thisweek:gnome.org with updates on your own projects!

April 05, 2023

Identity v0.5 and Synchronized Scrolled Windows

My university studies and work revolve around image- and video-processing algorithms. I frequently need to compare similar but subtly different output videos: to see how various algorithms solving the same problem behave, or to see the progress as I’m tweaking my own algorithm.

In 2020, I made a GNOME app, Identity, to assist me. It plays multiple videos at once in sync, and lets you switch between them like tabs in a browser. This way you can easily examine the differences at any point.

Identity has seen a number of releases since then and grown a number of helpful features, like zooming or viewing media properties. And now, in v0.5, I have implemented a side-by-side comparison mode. All files are arranged in a row or a column, and their zoom and pan positions are synchronized. You can explore different parts of an image or a video and see how they look across all versions that you opened. This is a quite useful comparison mode, and also more obvious for first-time users.

Identity comparing an image with three upscaling methods in a column

Under the hood, every image sits inside a GtkScrolledWindow, the standard GTK 4 widget that provides scrolling/panning gestures for its child widget, and draws the scroll bars and the overshoot effect.1 It’s easy to synchronize two or more of these scrolled windows together, but avoiding weird gesture interactions can be tricky. Let’s see how to get them to play along.

Synchronizing Positions #

Scrolled windows use GtkAdjustments to monitor the full size of their child widget and to control its current scroll position. Adjustments are objects with properties for the lower and upper bounds (in our case set to 0 and the full child size), the current value (which is the scroll position), and the step and page increments (which set how far arrow keys and PgUp/PgDown scroll the widget). There are two adjustments in every scrolled window: one for horizontal scrolling, and one for vertical scrolling, called hadjustment and vadjustment.

To synchronize multiple scrolled windows which show widgets of matching size, simply use the same two adjustments for all of them. Scrolling one widget will update the adjustments, causing all other widgets to also update their scroll position.

const shared_hadj = new Gtk.Adjustment();
const shared_vadj = new Gtk.Adjustment();

const scroll1 = new Gtk.ScrolledWindow({
    child: pictures[0],
    hadjustment: shared_hadj,
    vadjustment: shared_vadj,
});
const scroll2 = new Gtk.ScrolledWindow({
    // This pictures[1] widget has the same size as pictures[0].
    child: pictures[1],
    // Same adjustments as above!
    hadjustment: shared_hadj,
    vadjustment: shared_vadj,
});

You can run the full example with gjs -m simple.js:

GTK window with two synchronized scrolled windows.

Despite being a relatively simple and supported use-case, adjustment sharing actually makes conditions more favorable for an allocation loss bug that had plagued some of the more complex GTK 4 apps like Sysprof or GNOME Builder. When I implemented the initial version of side-by-side comparison in Identity, I started hitting the bug as well, very easily (panning a video while it was finishing and seeking back to the start was usually enough). So, I decided to investigate, and a few hours of rr and intense discussion in #gtk later, I managed to fix it! Of course, allocation machinery being very complex, this broke some things, but after a few follow-up fixes by the GTK maintainers, the bug seems to have been at last completely conquered. The fixes are included in GTK 4.10 and should make their way into GTK 4.8.4.

Anyhow, Identity can show and synchronize images of different size. Reusing the same adjustments would cause the upper boundaries to mismatch, and things to break. Instead, I keep track of my own, normalized adjustments, which always range from 0 to 1.2 They are bound back and forth with the scrolled window adjustments, so that scrolling will cause an update to the normalized adjustments, and vice versa. In turn, the value of the normalized adjustments are bound together between all open images. This way, zooming into the center of one image will set the values to 0.5, which will scroll all other images into their centers, regardless of their current size.3

Finally, watch out for widgets which can change their size depending on the scroll position, like GtkListView with variably-sized items. Scrolling to a particular point may cause such a widget to update the upper boundary of the adjustment and recompute the scroll position relative to what it now believes to be its size. This may cause a cascading reaction with the synchronized widgets, and potentially an infinite loop.

Fixing Kinetic Scrolling #

Scrolled window implements kinetic deceleration for two-finger panning on a touchpad and one-finger panning on a touchscreen—if you swipe your fingers with some speed, the widget will keep scrolling for a bit, until it comes to a halt. At first it may seem that it works fine—you can try it in the simple example above—until you try to pan one widget, and then quickly pan the other widget, while the first one is still decelerating:

For this demonstration, I used the “Simulate Touchscreen” toggle in the Inspector

Something weird is happening: it’s like the widget doesn’t let you pan until the deceleration is over. The reason for this issue is that the pan gesture and the kinetic deceleration live in each scrolled window separately. So when you pan one scrolled window, it starts updating the (shared) adjustment value every frame, and if you try to pan another scrolled window in the meantime, the movement gets continuously overwritten by the first scrolled window.

The workaround is to stop kinetic deceleration on all other scrolled windows when starting the pan. It’s further complicated by the fact that the pan gestures themselves live inside the scrolled window, and you can’t mess with them. Thankfully, you can catch the two-finger touchpad gesture with a GtkEventControllerScroll and the one-finger touchscreen gesture with a GtkGestureDrag:

// Our scrolled windows, for stopping their kinetic scrolling.
const scrolledWindows = [];

function stopKineticScrollingExcluding(source) {
    for (const widget of scrolledWindows) {
        if (widget === source)
            continue;

        // There's no special function to stop kinetic scrolling,
        // but disabling and enabling it works fine.
        widget.set_kinetic_scrolling(false);
        widget.set_kinetic_scrolling(true);

        // Fix horizontal touchpad panning after resetting
        // kinetic scrolling.
        widget.queue_allocate();
    }
}

const shared_hadj = new Gtk.Adjustment();
const shared_vadj = new Gtk.Adjustment();

function createScrolledWindow() {
    // The scrollable widget.
    const picture = new Gtk.Picture({
        file: image,
        can_shrink: false,
    });

    const scrolledWindow = new Gtk.ScrolledWindow({
        child: picture,
        hadjustment: shared_hadj,
        vadjustment: shared_vadj,
    });
    scrolledWindows.push(scrolledWindow);

    // The scroll controller will catch touchpad pans.
    const scrollController = Gtk.EventControllerScroll.new(
        Gtk.EventControllerScrollFlags.BOTH_AXES,
    );
    scrollController.connect('scroll', (scrollController, _dx, _dy) => {
        const device = scrollController.get_current_event_device();
        if (device?.source === Gdk.InputSource.TOUCHPAD) {
            // A touchpad pan is about to start!
            // Let's stop the kinetic scrolling on other widgets.
            stopKineticScrollingExcluding(scrolledWindow);
        }

        // Let the default scrolling work.
        return false;
    });
    picture.add_controller(scrollController);

    // The drag gesture will catch touchscreen pans.
    const dragGesture = new Gtk.GestureDrag();
    dragGesture.connect('drag-begin', (dragGesture, _x, _y) => {
        const device = dragGesture.get_current_event_device();
        if (device?.source === Gdk.InputSource.TOUCHSCREEN) {
            // A touchscreen pan is about to start!
            // Let's stop the kinetic scrolling on other widgets.
            stopKineticScrollingExcluding(scrolledWindow);
        }

        // We don't want to handle the drag.
        dragGesture.set_state(Gtk.EventSequenceState.DENIED);
    });
    picture.add_controller(dragGesture);

    return scrolledWindow;
}

This gives us panning across all widgets with nice kinetic deceleration which doesn’t break. Try the full example with gjs -m reset-kinetic.js:4

Touchpad panning works as expected across all scrolled windows

There are two extra complications about this code, both related to touchscreen panning. First, we stop the kinetic scrolling on all scrolled windows excluding the one handling the new event. This is because for some reason resetting the kinetic scrolling like this in the middle of a touchscreen pan prevents it from working (touchpad pans keep working fine).

Second, we queue an allocation on the scrolled windows right after resetting the kinetic scrolling. For whatever reason, resetting the kinetic scrolling causes the scrolled window to stop handling horizontal touchscreen pans altogether (vertical and mixed pans keep working fine). I suspect it’s caused by some logic error related to check_attach_pan_gesture(). This function is called when toggling the kinetic scrolling, breaking the horizontal touchscreen pans. Thankfully, it’s also called at the end of allocation, where it fixes back the touchscreen pans. I haven’t investigated this bug further, but it would be nice to get it fixed.

And that’s it! The code we’ve added also comes in useful for implementing custom gestures like zoom or mouse pan. Just remember that when writing custom gestures, you might need to stop the kinetic scrolling on the current scrolled window too, not only on the linked ones.

Closing Thoughts #

When synchronizing scrolled windows, and just dealing with GTK gesture code in general, make sure to test with different input devices, as each has its own quirks. Be careful when scrollable widgets have different sizes, or can change their size depending on the scroll position.

At a higher level, I think it would be better if the kinetic deceleration lived somewhere around the GtkAdjustments themselves. This way it would also be shared between all synchronized scrolled windows, and the workarounds, along with their oddities, wouldn’t be necessary. Something to keep in mind for GTK 5 perhaps.

When discussing a draft of this post with GTK developers and contributors, another potential GTK 5 idea came up. Different scrollable widgets (GtkViewport, GtkListView, GtkTextView, WebKitGTK’s web view, libshumate’s map) have slightly different needs, and GtkScrollable with GtkScrolledWindow can’t offer them all a unified interface that would work without compromises or big technical hurdles. (The last two examples don’t implement the scrollable interface for these reasons.) So, maybe, instead of GtkScrolledWindow, there should be a collection of helpers, and scrollable widgets should show scrollbars and handle scrolling themselves.

With all that said, if you think Identity might be useful to you, download it from Flathub and give it a try! I’d love to hear your thoughts, ways to contact me are linked at the bottom of this page.

Comparing three videos side-by-side in Identity


  1. The subtle glow that shows up when you try to scroll past the end of a scrollable widget. ↩︎

  2. These normalized adjustments are also responsible for the behavior when resizing the Identity window with zoomed-in images: instead of always expanding to the bottom-left, the images expand around their current scroll position. This is because the normalized adjustments don’t change during resizing. So, for example, a value of 0.25 before and after resizing will keep the image scrolled to 25% of its size. ↩︎

  3. This is not the only way to share position between differently sized scrollable widgets, just one that makes sense for Identity’s comparison use-case. You could imagine some other use-case where it makes more sense to share the pixel position, rather than the normalized position. It can be implemented using the same idea of two extra adjustments. It’ll work fine as long as different widgets don’t try to overwrite the upper bound on the same adjustment with different values. ↩︎

  4. Unfortunately, “Simulate Touchscreen” won’t help you see this fix; you’ll need a real touchpad or touchscreen. At the moment, the toggle does not change the device types that the gesture code receives, so it doesn’t run our workaround code. To test Identity, I’ve been using the work-in-progress Mutter SDK branch which has a compositor-level touchscreen emulation. ↩︎

GTK 4.11.1

Here is the first GTK snapshot of the new development cycle. A lot of things fell into place recently, so it is worth taking some time to go through the details of what is new, and what you can expect to see in 4.12.

List View Improvements

The family of GtkListView, GtkColumnView and GtkGridView widgets was one of the big additions in GTK 4. They are meant to replace GtkTreeView, but up until now, this was clearly still a bit aspirational.

In GTK 4.10, we’ve finally taken the big step to port GtkFileChooser away from tree views—a sign that list views are ready for prime time. And the next GTK 4 release will bring a number of missing features:

  • Finally, a fix for the longstanding scrolling bug
  • Better keyboard navigation, with customizable tab behavior
  • Focus control
  • Programmatic scrolling
  • Sections, maybe

Some of these are already available in 4.11.1. We even managed to backport the scrolling fix to 4.10.1.

Better Textures

Textures are used frequently in GTKs GL renderer—for icons and images, for glyphs, and for intermediate offscreen rendering. Most of the time, we don’t have to think about them, they just work. But if the texture is the main content of your app, such as in an image viewer, you need a bit more control over it, and it is important that the corner cases work correctly.

In GTK 4.10, we introduced a GskTextureScale node, which gives applications control over the filtering that is applied when scaling a texture up or down. This lets apps request the use of mipmaps with GSK_SCALING_FILTER_TRILINEAR. GTK 4.12 will automatically use mipmaps when it is beneficial.

One corner case that we’ve recently explored is texture slicing. Whenever a texture is bigger than the GL stack supports, GSK will break it into smaller slices and use separate GL textures for each. Modern GPUs support enormous textures (on my system, the max. texture size is 16384), which means that the slicing support is rarely tested in practice and not well covered by our unit tests either.

We added support for artificially limiting the texture size (with the GSK_MAX_TEXTURE_SIZE environment variable), and promptly discovered that our texture slicing support needed some love. It will work much better in 4.12.

Fractional Scaling

It landed on April 1st, but it is not a joke.

We’ve added support for the experimental wp_fractional_scale_manager_v1 protocol to the Wayland backend, and use the wp_viewporter protocol to tell the compositor  about the scaling that the buffer is using.  It is nice that this was easy to fit into our rendering stack, but don’t expect miracles. It works well with the cairo renderer (as you can see in the video), but we still consider it experimental with the GL and Vulkan renderers.

To try fractional scaling with the GL renderer, set

GDK_DEBUG=gl-fractional

in the environment.

Summary

There’s lots of new things to explore in GTK 4.11. Please try them and let us know what you think, in gitlab or on Discourse.

April 03, 2023

Some details about creating print-quality PDFs

At its core, PDF is an image file format. In theory it is not at all different from the file formats of Gimp, Krita, Photoshop and the like. It consists of a bunch of raster and vector objects on top of each other. In practice there are several differences, the biggest of which is the following:

In PDF you can have images that have different color spaces and resolutions (that is, PPI values). This is by design as it is necessary to achieve high quality printing output.

As a typical example, comic books that are printed in color consist of two different images. The "bottom" one contains only the colors and is typically 300 PPI. On top of that you have the black linework, which is a 1 bit image at 600 or even 1200 PPI. Putting both the linework and colors in the same image file would not work. In the printout the lines would be fuzzy, even if the combined image did contain 1200 PPI.

A deeper explanation can be found in the usual places but the short version is that these two different image types need to be handled in completely opposite ways to make them look good when printed. When converting colors images to printing plates the processing software prioritizes smoothness.  On the other hand for monochrome images the system prioritizes sharpness. Doing this wrong means either getting color images that are blocky or linework that is fuzzy.

When working on A4PDF it was clear from the start that it needs to be able to create PDF files that can be used for commercial quality print jobs. To test this I wrote a Python script that recreates the cover of my recently published book originally typeset with Scribus. The end result was about 100 lines of code in total.

The background image

The main image is a single file without any adornments. It was provided by the illustrator as a single 8031 by 5953 image file. A fully color managed workflow demands the image to be in CMYK format and have a corresponding ICC color profile. There is basically only one file format that supports this use case: TIFF. Interestingly the specification for this file format was finalized in 1992. It is left as an exercise to the reader to determine how many image file formats have been introduced since that time.

A4PDF extracts the embedded ICC profile and stores it in the PDF file. It could also convert the image from the image's ICC colorspace to the specified output color space if they are different, but currently does not.

Text objects

All text color is white and is specified in CMYK colorspace, though it could also be specified in DeviceGray. Defining any object in RGB (even if the actual color was full white) could make the printing house reject the file as invalid and thus unsuitable for printing.

The author name in the front cover uses PDF's character spacing to "spread out" the text. The default character spacing in this font is too tight for use in covers.

PDF can only produce horizontal text. Creating vertical text, as in the spine, requires you to modify the drawing state's transformation matrix stack. In practice this is almost identical to OpenGL, though the PostScript drawing model that PDF uses predates OpenGL by 8 years or so. In this case the text needs a rotate + translate. 

The bar code

Ideally this should be defined with PDF's vector drawing operations. Unfortunately that would require me to implement reading SVG files somehow. It turned out to be a lot less effort to export the SVG from Inkscape at 600 PPI and then convert that to a 1 bit image with the Gimp. The end result is pretty much the same.

This approach works in Scribus as well, but not in LibreOffice. It converts all 1 bit images to 8 bit grayscale meaning that it might be fuzzy when printed. LO used to do this correctly but the behaviour was intentionally changed at some point.

The logo

This is the logo used in the publisher's sci-fi books. You can probably guess that the first book in the series was Philip K. Dick's Do Androids Dream of Electric Sheep?

Like the bar code, this should optimally be defined with vector operations, but again for simplicity a raster image is used instead. However it is different from the bar code image in that it has an alpha channel so that the background starfield shows through. When you export a 1 bit image that has an alpha channel to a PNG, Gimp writes it out as an indexed image with 4 colors (black opaque, white opaque, black transparent, white transparent). A4PDF detects files of this type and stores them in the PDF as 1 bit monochrome images with a 1 bit alpha channel.

This is something even Scribus does not seem to handle correctly. In my testing it seemed to convert these kinds images to 8 bit grayscale instead.

WebKitGTK accelerated compositing rendering

Initial accelerated compositing support

When accelerated compositing support was added to WebKitGTK, there was only X11. Our first approach was quite simple, we sent the web view widget Xwindow ID to the web process to be used as rendering target using GLX. This was very efficient, but soon we realized it broke the GTK rendering model so it was not possible to use a web view inside a GtkOverlay, for example, to show status messages on top. The solution was to use a redirected Xcomposite window in the web process, and use its ID as the render target using GLX. The pixmap ID of the redirected Xcomposite window was sent to the UI process to be painted in the web view widget using a Cairo Xlib surface. Since the rendering happens in the web process, this approach required to use Xdamage to monitor when the redirected Xcomposite window was updated to schedule a web view redraw.

Wayland support

To support accelerated compositing under Wayland we initially added a nested Wayland compositor running in the UI process. The web process connected to the nested Wayland compositor and created a surface to be used as the rendering target using EGL. The good thing about this approach compared to the X11 one, is that we can create an EGLImage from Wayland buffers and use a GDK GL context to paint the contents in the web view. This is more efficient than X11 because we can use OpenGL both in web and UI processes.
WPE, when using the fdo backend, uses the same approach of running a nested Wayland compositor, but in a more efficient way, using DMABUF instead of Wayland buffers when available. So, we decided to use libwpe in the GTK port only for rendering under Wayland, and eventually remove our Wayland compositor implementation.
Before the removal of the custom Wayland compositor we had all these possible combinations:

  • UI Process
    • X11: Cairo Xlib surface
    • Wayland: EGL
  • Web Process
    • X11: GLX using redirected Xwindow
    • Wayland (nested Wayland compositor): EGL using Wayland surface
    • Wayland (libwpe): EGL using libwpe to get the Wayland surface

To reduce a bit the differences, and to make it easier to support WebGL with ANGLE we decided to change X11 to prefer EGL if possible, falling back to GLX only if EGL failed.

GTK4

GTK4 was released and we added support for it. The fact that GTK4 uses GL by default should make the rendering more efficient in accelerated compositing mode. This is definitely true under Wayland, because we are using a GL context already, so we just keep passing a texture to GTK to paint the contents in the web view. However, in the case of X11 we still have a Cairo Xlib surface that GTK paints into a Cairo image surface to be uploaded to the GPU. With GTK4 now we have two more combinations in the UI process side X11 + GTK3, X11 + GTK4, Wayland + GTK3 and Wayland + GTK4.

Reducing all the combinations to (almost) one: DMABUF

All these combinations to support the different platforms made it quite difficult to maintain, every time we get a bug report about something not working in accelerated compositing mode we have to figure out the combination actually used by the reporter, GTK3 or GTK4? X11 or Wayland? using EGL or GLX? custom Wayland compositor or libwpe? driver? version? etc.

We are already using DMABUF in WebKit for different things like WebGL and media rendering, so we thought that we could also use it for sharing the rendered buffer between the web and UI processes. That would be a more efficient solution but it would also drastically reduce the amount of combinations to maintain. The web process always uses the surfaceless platform, so it doesn’t matter if it’s under Wayland or X11. Then we create a surfaceless context as the render target and use EGL and GBM APIs to export the contents as a DMABUF buffer. The UI process imports the DMABUF buffer using EGL and GBM too, to be passed to GTK as a texture that is painted in the web view.

This theoretically recudes all the previous combinations to just one (note that we removed GLX support entirely, making EGL a requirement for accelerated compositing), but there’s a problem under X11: GTK3 doesn’t support EGL on X11 and GTK4 defaults to EGL but falls back to GLX if it doesn’t find an EGL config that perfectly matches the screen visual. In my system it never finds that EGL config because mesa doesn’t expose any 32 bit depth config. So, in the case of GTK3 we have to manually download the buffer to CPU and paint normally using Cairo, but in the case of GTK4 + GLX, GTK uploads the buffer again to be painted using GLX. I don’t think it’s possible to force GTK to use EGL from the API, but at least you can use GDK_DEBUG=gl-egl.

WebKitGTK 2.41.1

WebKitGTK 2.41.1 is the first unstable release of this cycle and already includes the DMABUF support that is used by default. We encourage everybody to try it out and provide feedback or report any issue. Please, export the contents of webkit://gpu and attach it to the bug report when reporting any problem related to graphics. To check if the issue is a regression of the DMABUF implementation you can use WEBKIT_DISABLE_DMABUF_RENDERER=1 to use the WPE renderer or X11 instead. This environment variable and the WPE render/X11 code will be eventually removed if DMABUF works fine.

WPE

If this approach works fine we plan to use something similar for the WPE port and get rid of the nested Wayland compositor there too.

April 02, 2023

Crosswords 0.3.8: Change Management

It’s time for another Crosswords release. This is a somewhat quieter release on the surface as it doesn’t have as many user-visible changes. But like the last release, a lot happened under the hood in preparation for the next phase.

This release marks a change in focus. I’ve shifted my work to the editor instead of the game. I hadn’t given the editor much attention over the past year and it’s overdue for updates. I have a lot of features planned for it; it’s time to make progress on them.

Crosswords Editor

The first change I made was to give a revamp to the workflow for creating a new puzzle. The old editor would let you change the puzzle type while editing it — something that was technically neat but not actually useful to setters. I have different editing sections planned based on the puzzle type, which means that restricting a window to one type. For example, editing an acrostic grid is totally different from editing a crossword grid.

The new greeter also cleans up a  weird flow where the first tab would lock once you’d picked your initial values.

New greeter dialog for the Crossword Editor
New puzzle greeter

To do this, I added a greeter that lets you select the type of puzzle right from the outset. We also took advantage of the fact that we added a separate GType for each puzzle type. It’s very loosely inspired by the new project greeter from GNOME Builder.

The second thing I spent time on wasn’t actually a code change, but a design plan. An implementation challenge I’ve had is balancing letting people use all the crazy features that the ipuz spec allows, and adding guardrails to let people write standard puzzles without thinking about those extra features. The problem with those features is that you can easily end up in a legal-but-weird puzzle file. As an example, imagine a crossword where the numbering of all the clues are out-of-order. That’s a legal .ipuz file and possibly valid in fringe circumstances, but rarely what any puzzle designer actually wants.

There is a new design doc for how to handle intermediate states. The details are complicated, but the overall approach involves adding lint() and fixup() functions to the puzzle types. This will let us make the changes we want, but then let the editor get the puzzle back to a reasonable state.

Many thanks to Federico, who very kindly let me call him on the weekends to talk through the issues and iterate to a proposal. I’ve started updating libipuz to implement this new design.

Crosswords: Adaptive Layout

This is the third release that I will have blogged about the adaptive layout, and it’s the first time I feel good about the results. It has been incredibly challenging to get Crosswords to work well at a variety of sizes. This cycle, I introduced the concept of a both a natural size and a minimum size to the game. This results in a mixture of user control and screen-driven sizing. Here is an example of a crossword scaling to the very small and to the very large.

I hope I can put this feature down for now!

Misc fixes and thanks

There were a number of other fixes this release. Most excitingly, we have a number of new contributors too! Crosswords is a potential GNOME GSOC program and some perspective students spent time trying to learn the code base. Here are the improvements and credits.

Puzzle Set tagging dialog

  • First, we now render the puzzle set description labels to look more like tags. Thanks to Pratham for working on this.
  • Thanks to Tanmay for fixing a bug where horizontal clue enumerations weren’t rendering correctly. He also contributed fixes to keyboard layout dialog, and started a promising thumbnailer (targeting next release)
  • Thanks to Philip for layout fixes, a new icon, and for having a preternatural ability to find bugs in my code as soon as I declare them “done.”
  • I fixed an issue where we didn’t lock down the puzzle correctly after winning the game. I’ve been trying to track this down for six months.
  • Thanks as always to the  translators for translations.

Until next time!

April 01, 2023

Got the Star Trek - The Motion Picture Director's Edition box set? You might wan to check your discs

TL/DR

Star Trek The Motion Picture The Complete Adventure box set claims to contain a special, longer cut of the film. However it seems that this is not the case for some editions. The British edition does contain the longer cut, but the Scandinavian one seems not to. The back of the box still claims that the box set does contain the longer cut.

The claim and evidence

This is the box set in question.

At the back of the box we find the following text:

I bought this at the end of last year but I could not find the longer edition anywhere in the menus. I mentioned this to a friend of mine who has the same box set and he had found it immediately. After a lot of debugging we discovered that he has the British edition of the box set whereas I have the Scandinavian one.

The British box set, which has the longer edition, consists of the following discs:

  • EU151495BLB Blu-ray The Director's Edition Bonus Disc
  • EU151496ULB 4K UltraHD The Director's Edition Feature Film, Special Features
  • EU151460ULB 4K UltraHD Feature Film, Special Features (Special Longer Version, Theatrical Version)
  • EU151496BLB Blu-ray The Director's Edition Feature Film, Special Features
  • EC150885BLB Blu-ray Feature Film, Special Features

My Scandinavian box set has the following discs:

  • eu151495blb bonus disc
  • eu151496ulb director's edition 4k ultrahd, Feature film
  • eu150884ulb 4k, regular version, feature film
  • eu151496blb blu-ray, director's edition
  • eu150885blb blu-ray, regular version, feature film

The only differences here are that one disc has a serial number with letters EC instead of EU and that the British edition has this disc:

Note how it says "special longer version" in microscopic letters under the Paramount logo. This text does not appear on any disc in the Scandinavian edition.

The Scandinavian edition does not have this disc. Instead it has the disc with the product id EU150884ULB whereas the corresponding British disc has the product id EU151460ULB .The missing content can not be on any of the other discs, because they are the same in both editions (the EU/EC issue notwithstanding). 

I reported this to the store I bought the box set from. After a lot of convincing they agreed to order a replacement box set. When it arrived we inspected the contained discs and they were the same as in the box set I already had. This would imply that the entire print run is defective. After more convincing we managed to get the Finnish importer to report the issue to the distributor in Sweden.

They eventually replied that the longer edition is in "the extra disc in the set that is in a cardboard sleeve". This is not the case, as that disc contains only extras, is the same in the British box set and further is a regular Blu-Ray, not a  4k UHD one as it should be and as it is on the British edition.

We reported all this back up the chain and have not heard anything back for several weeks now.

What to do if you have a non-British (and presumably non-US) edition of this box set?

Check whether your discs contain the special longer edition. The British disc has this this fairly unambiguous selection screen for it when you insert the disc:


If your box set does not contain this version of the film, report the issue to where you bought the box set from. If this really is a larger issue (as it would seem), the more bug reports they get the faster things will get fixed.

The film store said that they have sold tens of these box sets and that I was the first to notice the issue so don't assume that other people have already reported it. This is not an issue of the physical copy that I have because the replacement box set was defective in the same way.

Speculation: why is it broken?

The British disc that has the special longer edition does not contain Finnish subtitles (or possibly any non-English languages). When the box was being assembled someone found this out, decided that they can't ship a disc without localisation and replaced the disc with a regular version of the film that does not have the longer cut. But they did not change the back of the box, which states that the box set contains the longer edition which it does not seem to have.

Niepce March 2023 updates

This is the March 2023 update for Niepce. This is not an April's fool, and this is not the year I can announce a release on April's fool day. Sorry about that.

Continuing with the renderer / previewer cache.

I had to move ncr1 to Rust. I didn't port it all, but the widget and the main API are now in a Rust crate npc-craw2, the fourth one in the workspace. The goal of this crate is to provide the interface to the rendering pipeline. Some of the work included moving away from Cairo surface and using GdkTextures instead. The main pipeline is still the original C++ code using GEGL, it's easier for me to bind the C++ code than to write wrappers for GEGL.

In the same way, I also ported most of the darkroom module to Rust. This module is the one that will allow the image editing and currently only handle displaying the images.

All of this was necessary to finish the render / previewer integration and making it work asynchronously: the image rendering happen in the background without freezing the UI. There are still some issues but it on overall, it works well.

Alternative rendering

The initial rendering code for camera raw files was written a long time ago using GEGL, and with GEGL built in camera raw converter. It works, but the result are not satisfying. It's not GEGL's fault it is just that camera raw processing is a complex matter, requires a lot of work, and GEGL here is just a set of operations to build image processing pipelines. It's also more complex now as some cameras require lens correction. There are long term plans for it including using libopenraw, adding lensfun, etc. but this will come later.

I also have a few plans down the road, including compatibility with existing (open source) raw processing, namely with the two most populars, RawTherapee and Darktable. The easiest way to have compatible rendering is to use their code.

RawTherapee

Here we go, I took RawTherapee, added it as a submodule and built a Rust crate, rtengine, around its camera raw engine. I have written up more details on how I did this.

Its integration is light for now, the longer term is to treat the .pp3 on import as sidecars and use them, eventually as the default processor in that case. This is the compatibility I'm talking about. In the long run editing parameters is in the card, it's part of what I consider necessary for the initial release.

But what about Darktable? One thing at a team, but it's in the card.

UI changes

Now as a prelude to change the the rendering parameters (i.e. the adjustement you'll do to the pictures), I have added a simple UI to select ncr or rtengine and this is saved. This is also useful to manually test the rendering cache. The information is displayed in the new "Process" section of the metadata pane.

Rendering cache

I now have a rendering cache that will cache previews on disk, by storing a PNG. This lead to an interesting about space vs time for a cache storage, and I mean the image format. PNG seems like a good compromise for speed but I get 20-30MB files, while JPEG would be more space efficient, but lossy3. This can be investigated later.

This has been more work than anticipated, but not taking some shortcuts lead to some more infrastructure built to provide the core features.

Importer

I moved on to the importer. That's the part that will be used to add images into the catalog. Currently it support importing in place, and a very rough importing from a camera or a flash card. This is very light in functionality.

One of the key feature I want with the importer is being able to copy images from one place to another, and sort they automatically. This is particularly useful when importing from a camera or memory card where the images are all together.

I wrote a shell script not too long ago to do the same from a download of my phone pictures. The file IMG_0123.JPG with the Exif property DateTimeOriginal that is set to 1 April 2023 is copied to the directory 2023/20230401. This is the way I like it, but the importer will be more flexible. The longer term will possibly see a renaming feature.

To implement this image copying workflow, and to help testing, I wrote a command line tool to run the copy. This involves recursively (as an option) walking through the directories, grouping files together (this is called bundle), extracting the date from the metadata, and copying the files. One of the shortcomings of the Rust std::fs::copy is that it doesn't copy the timestamps, so I had to implement this. Also I learned that the file creation date isn't a thing you can change.

Ever felt that you have two pieces of code you need to put together and they don't fit? That's where I'm at now. The copying code needs to be integrated in the UI, and it doesn't fit at all, nor with the existing importer. The whole thing is taking longer than I wished, but it'll get there as I will redesign the import code.

Other

I also updated the Flatpak manifest so I can build a flatpak with the current code.

And last but not least, I have submitted a few PR for RawTherapee, mostly issues triggered by the adresss sanitizer: Issue #6708 - fix overlapping buffer strcpy, Issue #6721 - Fix memory leak in Crop.

Thank you reading.

1

NCR stands for Niepce Camera Raw, i.e. the camera raw processing code. When I started it eons ago. I had great plans, that didn't materialize, like build a processing pipeline based on GEGL, libopenraw, exempi, etc.

2

There is an unrelated ncr crate on crates.io, so I decided to not use that crate name, and didn't want to use npc-ncr, even though the crate is private to the application and not intended to be published separately.

3

As I write this now it hits me that I could do an experiment with lossless JPEG compression. This is what is used by file formats like CR2 or DNG. And given that the PNG of a rendered CR2 is about the same size, it's possible that it is a viable idea.

March 30, 2023

Ensuring steady frame rates with GPU-intensive clients

On Wayland, a surface is the basic primitive used to build what users refer to as a “window”. Wayland clients define their contents by attaching buffers to surfaces. This turns the contents of the buffer into the current surface contents. Wayland clients are free to attach a new buffer to a surface anytime. When a Wayland compositor like Mutter starts working on a new output frame, it picks the latest available buffer for each visible surface. This is called “mailbox semantics” (the buffers are metaphorical letters falling into a mailbox, the visible “letter” is the last one on top).

Problem

With hardware accelerated drawing, a client normally attaches a new buffer to a surface right after it finished calling OpenGL/Vulkan/<insert your favourite drawing API> APIs to define the contents of the buffer. When the compositor processes the protocol requests attaching the buffer to the surface, the GPU generally hasn’t finished drawing to the buffer yet.

Since the contents of the compositor’s output frame depend on the contents of each visible surface, the former cannot complete before the GPU finishes drawing to each of the picked surface buffers (and subsequently to the compositor’s own output buffer, in the general case).

If the GPU does not finish drawing in time for the next display refresh cycle, the compositor’s output frame misses that cycle and is delayed by at least the duration of one refresh cycle. This can be noticeable as judder/stutter, because the compositor’s frame rate is reduced, and the contents of some frames are not consistent with the timing when they become visible.

The likelihood of that happening depends largely on the clients, mainly on how long it takes the GPU to draw their buffer contents and how much time lies between when a client starts drawing to its buffer and when the compositor starts working on its resulting output frame.

In summary, a Wayland compositor can miss a display refresh cycle because the GPU failed to finish drawing to a client buffer in time.

This diagram visualizes a normal and problematic case:

Left side: normal case, right side: problematic case
Left side: normal case, right side: problematic case

Solution

Basic idea

The basic idea is simple: the compositor considers a client buffer “available” per the mailbox semantics only once the GPU finishes drawing to it. Until then, it picks the previously available buffer.

Complications

Now if it was as simple as that might sound, there would be no need to write a >1000-word article about it. 🙂

The main thing which makes things more complicated is that, together with attaching a new buffer, various other surface states can be modified in the same commit. All state changes in the same commit must be applied atomically, i.e. the user must either see all or none of them (per Wayland’s “every frame is perfect” motto). For an example, there are various states which affect how a Wayland surface is scaled for display. Attaching a new buffer and changing the scaling state in the same commit ensures that the surface always appears consistently. If the buffer size and scaling state were to change independently, the surface might intermittently appear in the wrong size.

As if that wasn’t complicated enough, Wayland has so-called synchronized sub-surfaces. State changes for a synchronized sub-surface are not applied immediately, but only the next time any state changes are applied for its parent surface. Conceptually, one can think of the committed sub-surface state becoming part of the parent surface’s state commit. Again, all state combined like this between sub-surfaces (which can be nested, i.e. a sub-surface can be the parent of another sub-surface) and their parents must be applied atomically, all or nothing, to ensure that sub-surfaces and their parents always appear consistently as a whole.

This means that the compositor cannot simply wait for the GPU to finish drawing to client buffers, while applying other corresponding surface state immediately. It needs to stage the committed state changes somehow, and actually apply them only once the GPU has finished drawing to all new buffers attached in the same combined state commit.

Enter transactions

The idea for “stage somehow” is to introduce the concept of a transaction, which combines a set of state changes for one or multiple (sub-)surfaces. When a client commits a set of state changes for a surface, they are inserted into an appropriate transaction; either a new one or an existing one, depending on circumstances.

When the committed state changes should get applied per Wayland protocol semantics, the transaction is committed and inserted into a queue of committed transactions. The queue is ordered such that for any given surface,  state commits are applied in the same order as they were committed by the client. This ensures that the contents of a surface never appear to “move backwards” because one transaction affecting the surface managed to “overtake” another one.

A transaction is considered ready to be applied only once both of these conditions are true:

  1. It’s the oldest (closest to the queue head) transaction in the queue for all surfaces it carries state for.
  2. The GPU has finished drawing to all client buffers attached in the transaction.

Once both of these conditions are true, the transaction is applied atomically. From that point on, the compositor uses the state in the transaction for its output frames.

Results

I implemented the solution described above in Mutter merge request !1880, which was merged for the GNOME 44 release. While it went under the radar of news outlets, I hope that many of you will notice the benefits!

One situation where the benefits of transactions can be noticed is interactive OpenGL applications such as games, with “vsync” disabled (e.g. for better input → output latency), you should be less likely to see stuttering due to Mutter missing a display refresh cycle, in particular in fullscreen and if Mutter can use direct scanout of client buffers.

If the GPU & drivers support true high priority EGL contexts which can preempt lower priority ones (as of this writing, this is true e.g. with “not too old” Intel GPUs), Mutter can now sustain full frame rate even if clients are GPU-bound to lower frame rates, as demonstrated in this video:

Even if the GPU & drivers do not support this, Mutter should now get bogged down less by such heavy clients, in particular the mouse cursor.

It’s effective for X clients running via Xwayland as well, not only for native Wayland clients.

Long term, all major Wayland compositors will want to do something like this. gamescope already does.

Thanks

It took almost two years (on and off, not full-time) from having the initial idea, deciding to try implementing it myself, until finally getting it ready to be merged. I wasn’t very familiar with the Mutter code or Wayland protocol semantics when I started, so I couldn’t have done it without a lot of help from many Mutter and Wayland developers. I am deeply grateful to all of you.

Thanks to Jakub Steiner for the featured image and to Niels De Graef for the diagram of this post.

I would also like to thank Red Hat for giving me the opportunity to work on this, even though “Mutter developer” isn’t really a central part of my job description.

March 28, 2023

New gitlab.freedesktop.org spamfighting abilities

As of today, gitlab.freedesktop.org allows anyone with a GitLab Developer role or above to remove spam issues. If you are reading this article a while after it's published, it's best to refer to the damspam README for up-to-date details. I'm going to start with the TLDR first.

For Maintainers

Create a personal access token with API access and save the token value as $XDG_CONFIG_HOME/damspam/user.token Then run the following commands with your project's full path (e.g. mesa/mesa, pipewire/wireplumber, xorg/lib/libX11):

$ pip install git+https://gitlab.freedesktop.org/freedesktop/damspam
$ damspam request-webhook foo/bar
# clean up, no longer needed.
$ pip uninstall damspam
$ rm $XDG_CONFIG_HOME/damspam/user.token
The damspam command will file an issue in the freedesktop/fdo-bots repository. This issue will be automatically processed by a bot and should be done by the time you finish the above commands, see this issue for an example. Note: the issue processing requires a git push to an internal repo - if you script this for multiple repos please put a sleep(30) in to avoid conflicts.

Once the request has been processed (and again, this should be instant), any issue in your project that gets assigned the label Spam will be processed automatically by damspam. See the next section for details.

For Developers

Once the maintainer for your project has requested the webhook, simply assign the Spam label to any issue that is spam. The issue creator will be blocked (i.e. cannot login), this issue and any other issue filed by the same user will be closed and made confidential (i.e. they are no longer visible to the public). In the future, one of the GitLab admins can remove that user completely but meanwhile, they and their spam are gone from the public eye and they're blocked from producing more. This should happen within seconds of assigning the Spam label.

For GitLab Admins

Create a personal access token with API access for the @spambot user and save the token value as $XDG_CONFIG_HOME/damspam/spambot.token. This is so you can operate as spambot instead of your own user. Then run the following command to remove all tagged spammers:

$ pip install git+https://gitlab.freedesktop.org/freedesktop/damspam
$ damspam purge-spammers
The last command will list any users that are spammers (together with an issue that should make it simple to check whether it is indeed spam) and after interactive confirmation purge them as requested. At the time of writing, the output looks like this:
$ damspam purge-spammers
0: naughtyuser              : https://gitlab.freedesktop.org/somenamespace/project/-/issues/1234: [STREAMING@TV]!* LOOK AT ME
1: abcuseless               : https://gitlab.freedesktop.org/somenamespace/project/-/issues/4567: ((@))THIS STREAM IS IMPORTANT
2: anothergit               : https://gitlab.freedesktop.org/somenamespace/project/-/issues/8778: Buy something, really
3: whatawasteofalife        : https://gitlab.freedesktop.org/somenamespace/project/-/issues/9889: What a waste of oxygen I am
Purging a user means a full delete including all issues, MRs, etc. This is nonrecoverable!
Please select the users to purge:
[q]uit, purge [a]ll, or the index: 
     
Purging the spammers will hard-delete them and remove anything they ever did on gitlab. This is irreversible.

How it works

There are two components at play here: hookiedookie, a generic webhook dispatcher, and damspam which handles the actual spam issues. Hookiedookie provides an HTTP server and "does things" with JSON data on request. What it does is relatively generic (see the Settings.yaml example file) but it's set up to be triggered by a GitLab webhook and thus receives this payload. For damspam the rules we have for hookiedookie come down to something like this: if the URL is "webhooks/namespace/project" and damspam is set up for this project and the payload is an issue event and it has the "Spam" label in the issue labels, call out to damspam and pass the payload on. Other rules we currently use are automatic reload on push events or the rule to trigger the webhook request processing bot as above.

This is also the reason a maintainer has to request the webhook. When the request is processed, the spambot installs a webhook with a secret token (a uuid) in the project. That token will be sent as header (a standard GitLab feature). The project/token pair is also added to hookiedookie and any webhook data must contain the project name and matching token, otherwise it is discarded. Since the token is write-only, no-one (not even the maintainers of the project) can see it.

damspam gets the payload forwarded but is otherwise unaware of how it is invoked. It checks the issue, fetches the data needed, does some safety check and if it determines that yes, this is spam, then it closes the issue, makes it confidential, blocks the user and then recurses into every issue this user ever filed. Not necessarily in that order. There are some safety checks, so you don't have to worry about it suddenly blocking every project member.

Why?

For a while now, we've suffered from a deluge of spam (and worse) that makes it through the spam filters. GitLab has a Report Abuse feature for this but it's... woefully incomplete. The UI guides users to do the right thing - as reporter you can tick "the user is sending spam" and it automatically adds a link to the reported issue. But: none of this useful data is visible to admins. Seriously, look at the official screenshots. There is no link to the issue, all you get is a username, the user that reported it and the content of a textbox that almost never has any useful information. The link to the issue? Not there. The selection that the user is a spammer? Not there.

For an admin, this is frustrating at best. To verify that the user is indeed sending spam, you have to find the issue first. Which, at best, requires several clicks and digging through the profile activities. At worst you know that the user is a spammer because you trust the reporter but you just can't find the issue for whatever reason.

But even worse: reporting spam does nothing immediately. The spam stays up until an admin wakes up, reviews the abuse reports and removes that user. Meanwhile, the spammer can happily keep filing issues against the project. Overall, it is not a particularly great situation.

With hookiedookie and damspam, we're now better equipped to stand against the tide of spam. Anyone who can assign labels can help fight spam and the effect is immediate. And it's - for our use-cases - safe enough: if you trust someone to be a developer on your project, we can trust them to not willy-nilly remove issues pretending they're spam. In fact, they probably could've deleted issues beforehand already anyway if they wanted to make them disappear.

Other instances

While we're definitely aiming at gitlab.freedesktop.org, there's nothing in particular that requires this instance. If you're the admin for a public gitlab instance feel free to talk to Benjamin Tissoires or me to check whether this could be useful for you too, and what changes would be necessary.

March 26, 2023

No-Added-Sugar Granola Recipe

Granola is tasty, crunchy, it can take many shapes and forms, and it’s great both for breakfast and as a snack. This recipe will help you make one that’s not too sweet and which requires few ingredients.

You can easily make your own variants, e.g. replacing almonds with hazzlenuts, or replacing raisins with dried cranberries. You can also replace some of the seeds by 50 g of chocolate chips or chocolate shavings. Add the chocolate when the granola is cool and it will remain as-is, but mix it right after taking the granola out of the oven and it will completely melt and disappear, giving the granola an even golden color and a light chocolate flavor. Add some fresh banana slices or some banana chips, pour some coconut milk, and you have a great chocolate, banana and coconut breakfast granola. If you make your own variant, let me know about it!

For approximately 500 g.

Ingredients

  • 300 g of oatmeal
  • 50 g of almonds
  • 50 g of squash seeds
  • 50 g of sunflower seeds
  • 100 g of raisins
  • 200 ml of water

All you need to make some no-added-sugar granola

Instructions

  • Soak the raisins in the water for 1 h.

The soaked raisins

  • Mix the oatmeal and the seeds in a large bowl.

The oatmeal and seeds mix

  • Just before the end of the soaking time, pre-heat the oven at 150 °C.
  • Mix the raisins with the oatmeal and the seeds, one tablespoon at a time to ensure the water is well spread.

All the ingredients are now mixed

  • Spread the unbaked granola on a baking tray, make the layer uniform.

The granola in the baking tray, ready to be baked

  • Bake the granola 1 hour, mixing it every 15 minutes to ensure it’s baked evenly.

The granola after 1 hour in the oven

  • Let the granola cool down out of the oven.
  • Store it in jars.

Add some fresh fruits, pour some plant milk in, get some tea or coffee, and you made some great breakfast

March 24, 2023

We need better support for SSH host certificates

Github accidentally committed their SSH RSA private key to a repository, and now a bunch of people's infrastructure is broken because it needs to be updated to trust the new key. This is obviously bad, but what's frustrating is that there's no inherent need for it to be - almost all the technological components needed to both reduce the initial risk and to make the transition seamless already exist.

But first, let's talk about what actually happened here. You're probably used to the idea of TLS certificates from using browsers. Every website that supports TLS has an asymmetric pair of keys divided into a public key and a private key. When you contact the website, it gives you a certificate that contains the public key, and your browser then performs a series of cryptographic operations against it to (a) verify that the remote site possesses the private key (which prevents someone just copying the certificate to another system and pretending to be the legitimate site), and (b) generate an ephemeral encryption key that's used to actually encrypt the traffic between your browser and the site. But what stops an attacker from simply giving you a fake certificate that contains their public key? The certificate is itself signed by a certificate authority (CA), and your browser is configured to trust a preconfigured set of CAs. CAs will not give someone a signed certificate unless they prove they have legitimate ownership of the site in question, so (in theory) an attacker will never be able to obtain a fake certificate for a legitimate site.

This infrastructure is used for pretty much every protocol that can use TLS, including things like SMTP and IMAP. But SSH doesn't use TLS, and doesn't participate in any of this infrastructure. Instead, SSH tends to take a "Trust on First Use" (TOFU) model - the first time you ssh into a server, you receive a prompt asking you whether you trust its public key, and then you probably hit the "Yes" button and get on with your life. This works fine up until the point where the key changes, and SSH suddenly starts complaining that there's a mismatch and something awful could be happening (like someone intercepting your traffic and directing it to their own server with their own keys). Users are then supposed to verify whether this change is legitimate, and if so remove the old keys and add the new ones. This is tedious and risks users just saying "Yes" again, and if it happens too often an attacker can simply redirect target users to their own server and through sheer fatigue at dealing with this crap the user will probably trust the malicious server.

Why not certificates? OpenSSH actually does support certificates, but not in the way you might expect. There's a custom format that's significantly less complicated than the X509 certificate format used in TLS. Basically, an SSH certificate just contains a public key, a list of hostnames it's good for, and a signature from a CA. There's no pre-existing set of trusted CAs, so anyone could generate a certificate that claims it's valid for, say, github.com. This isn't really a problem, though, because right now nothing pays attention to SSH host certificates unless there's some manual configuration.

(It's actually possible to glue the general PKI infrastructure into SSH certificates. Please do not do this)

So let's look at what happened in the Github case. The first question is "How could the private key have been somewhere that could be committed to a repository in the first place?". I have no unique insight into what happened at Github, so this is conjecture, but I'm reasonably confident in it. Github deals with a large number of transactions per second. Github.com is not a single computer - it's a large number of machines. All of those need to have access to the same private key, because otherwise git would complain that the private key had changed whenever it connected to a machine with a different private key (the alternative would be to use a different IP address for every frontend server, but that would instead force users to repeatedly accept additional keys every time they connect to a new IP address). Something needs to be responsible for deploying that private key to new systems as they're brought up, which means there's ample opportunity for it to accidentally end up in the wrong place.

Now, best practices suggest that this should be avoided by simply placing the private key in a hardware module that performs the cryptographic operations, ensuring that nobody can ever get at the private key. The problem faced here is that HSMs typically aren't going to be fast enough to handle the number of requests per second that Github deals with. This can be avoided by using something like a Nitro Enclave, but you're still going to need a bunch of these in different geographic locales because otherwise your front ends are still going to be limited by the need to talk to an enclave on the other side of the planet, and now you're still having to deal with distributing the private key to a bunch of systems.

What if we could have the best of both worlds - the performance of private keys that just happily live on the servers, and the security of private keys that live in HSMs? Unsurprisingly, we can! The SSH private key could be deployed to every front end server, but every minute it could call out to an HSM-backed service and request a new SSH host certificate signed by a private key in the HSM. If clients are configured to trust the key that's signing the certificates, then it doesn't matter what the private key on the servers is - the client will see that there's a valid certificate and will trust the key, even if it changes. Restricting the validity of the certificate to a small window of time means that if a key is compromised an attacker can't do much with it - the moment you become aware of that you stop signing new certificates, and once all the existing ones expire the old private key becomes useless. You roll out a new private key with new certificates signed by the same CA and clients just carry on trusting it without any manual involvement.

Why don't we have this already? The main problem is that client tooling just doesn't handle this well. OpenSSH has no way to do TOFU for CAs, just the keys themselves. This means there's no way to do a git clone ssh://git@github.com/whatever and get a prompt asking you to trust Github's CA. Instead, you need to add a @cert-authority github.com (key) line to your known_hosts file by hand, and since approximately nobody's going to do that there's only marginal benefit in going to the effort to implement this infrastructure. The most important thing we can do to improve the security of the SSH ecosystem is to make it easier to use certificates, and that means improving the behaviour of the clients.

It should be noted that certificates aren't the only approach to handling key migration. OpenSSH supports a protocol for key rotation, basically by allowing the server to provide a set of multiple trusted keys that the client can cache, and then invalidating old ones. Unfortunately this still requires that the "new" private keys be deployed in the same way as the old ones, so any screwup that results in one private key being leaked may well also result in the additional keys being leaked. I prefer the certificate approach.

Finally, I've seen a couple of people imply that the blame here should be attached to whoever or whatever caused the private key to be committed to a repository in the first place. This is a terrible take. Humans will make mistakes, and your systems should be resilient against that. There's no individual at fault here - there's a series of design decisions that made it possible for a bad outcome to occur, and in a better universe they wouldn't have been necessary. Let's work on building that better universe.

comment count unavailable comments

How to Propose Features to GNOME

Introduction

Recently, GNOME added an option into GNOME Settings to adjust pointer acceleration, which was a feature that the developers and designers were originally against. One person managed to convince them, by giving one reason. Thanks to them, pointer acceleration options are now available in GNOME Settings!

Firstly, I’m going to summarize the relevant parts of the proposal and discussion behind the addition, and explain how it was accepted. Then, to build on top of that, GNOME’s philosophy and the importance of taking it into consideration. And lastly, how to propose features to GNOME and what to avoid.

However, this article is not about whether GNOME is successful with their philosophy, and the tone of developers and designers. Additionally, this isn’t about where to propose features, rather how to formulate the proposal and what to consider.

Disclaimer: I am not speaking on the behalf of GNOME.

Summary of the Proposal and Discussion

Felipe Borges, a maintainer of GNOME Settings, submitted a merge request to make pointer acceleration options discoverable in GNOME Settings. However, Georges Stavracas, a maintainer, and Allan Day, a designer, were strongly against this addition and expressed that it does not benefit the majority of users and can lead to confusion. At that time, this feature was leaning towards rejection.

I responded that this feature can hugely benefit gamers, as many of them are sensitive to pointer acceleration during gameplay. I asked to reconsider this position, as there is a considerably large amount of people who would appreciate it (albeit still a minority).

Georges responded that they were still against it. They argued that it isn’t relevant for most people, and the target audience will be left confused. In my opinion, ignoring the unjust rant, their reasoning was completely justified and valid.1 In the end, they were unconvinced.

Later, John Smith commented that having the flat profile (a nondefault option on GNOME) as opposed to the default is desirable for people who suffer from hand tremors (shaking/fidgety hands) – John often needs to tweak pointer acceleration within GNOME Tweaks for their relative, who suffers from Parkinson. They also pointed out that they use GNOME Tweaks more often than GNOME Settings because of that feature.

Afterwards, the developers and designers were convinced, and then discussed with John Smith on adding it and wording it appropriately. The merge request was then accepted and merged, and finally added to GNOME Settings in GNOME 44.

Importance of Taking GNOME’s Philosophy Into Consideration

So what happened? How come I couldn’t convince the developers and designers even though my statement was correct, while John Smith convinced them even though their statement was just as correct?

That is because John Smith took GNOME’s philosophy (especially target audience) into consideration, whereas I didn’t. While both of our points were correct, mine was, to put it in the nicest way possible, irrelevant. This sole difference between our approaches lead to opposite directions of this merge request, as only John Smith’s approach contributed to GNOME’s philosophy, and thus literally made the developers and designers reconsider.

Understanding GNOME’s philosophy and taking it into consideration is, in my opinion, the most important factor when proposing anything to GNOME. GNOME takes the philosophy very seriously, so it is really important that proposals satisfy it. As shown above, this is a matter of acceptance and rejection.

What is GNOME’s Philosophy?

GNOME’s philosophy is sophisticated and there is a lot of room for misunderstanding. Keep in mind that I wrote an article about GNOME’s philosophy in “What is GNOME’s Philosophy?”, where it explains in depth.

To summarize the article, productivity is a key focus for GNOME. However, GNOME approaches it in a very specific manner: The target audiences are people with disabilities, the average computer and mobile user, as well as developers interested in GNOME. GNOME aims to create a coherent environment and experience, courtesy of the Human Interface Guidelines, in which the guidelines are heavily based on cognitive ergonomics, such as perception, memory, reasoning, and motor response. GNOME also encourages peacefulness and organization by discouraging some (sometimes conventional) features.

This means, GNOME is fully open with feature proposals, as long as the feature complies with the philosophy.

How Should I Propose Features?

Now that we (hopefully) have a better understanding of GNOME’s philosophy and why it’s important to take it into consideration, let’s look at how I would recommend proposing features.

There are many important notes and recommendations I’d like to make whenever you propose anything (including features) to GNOME:

  • Correctness and relevance are different from one another. Being correct doesn’t necessarily mean that the proposal will contribute to GNOME’s philosophy. Relevancy, however, ensures that the information satisfies with GNOME’s philosophy, so it’s important that proposals are relevant, as shown with the merge request.
  • Using GNOME primarily doesn’t automatically make you its target audience. Please take the time to ask yourself if your proposal contributes to GNOME’s philosophy. If it does, explain how it benefits the target audience, not how it benefits you. Same goes to commenting/replying to someone.
  • Follow the GNOME Code of Conduct. Being respectful to others is a really important step for a healthy relationship between you and GNOME. Of course, it doesn’t mean that every party will behave respectfully, including GNOME members.
  • This isn’t 100% guaranteed, and may be really exhausting. Don’t rush yourself to reply as soon as possible. If you are tired or aren’t in the mood for replying, then comment another time. The last thing you’d want to do is offend a maintainer and have your proposal rejected, whether it is done intentionally or not.
  • Do not set any expectations for being treated well by members. Unfortunately, some members may behave poorly (like in that merge request) and even violate the Code of Conduct without dire consequences. Dealing with unjust situations can be difficult, even if you were respectful. If a member is behaving inappropriately, you can try to follow Procedure For Reporting Code of Conduct Incidents. In any case, make sure not to seem offensive or demanding.

Whenever you propose a feature to GNOME, ask yourself this question: “Does it comply with GNOME’s Philosophy?

If it does, then GNOME developers and designers will likely be interested in your idea. Of course, like explained above, proper communication is crucial, as well as wording it appropriately. Nevertheless, if the proposal fundamentally goes against the philosophy, then it will very likely be rejected.

Conclusion

One thing I’ve learned from experience is that GNOME mainly cares about proposals that serve the philosophy, as they take it very seriously.

Providing good user experience is difficult, as preferences have a cost. It can also be difficult to know what features the target audience would want, especially when they are nontechnical users, as many of them may not know how and who to contact. Having people propose features is a wonderful privilege for all of us, so it’s really important that we do it with care, and put the time and effort to word it in a way that we consider the philosophy, and explain thoroughly how it contributes to it.

Hopefully, this helps you understand GNOME’s goals with the desktop and its philosophy. With a better understanding, you should be able to carefully propose and formulate the feature you want.


Edit 1: Add bullet point for members behaving inappropriately (credit to Maksym Hazevych)


Footnotes

  1. “Acceleration” is a term I always had a hard time understanding, so I can fully agree that the term is really confusing. I don’t remember learning it in school, and I suffer from comprehension difficulties as well, so reading definitions and watching videos haven’t really helped so far. The only hint I’ve gotten from is in Mario Kart Wii, where I associate acceleration with “the vehicle goes from 0 to brrr”. 

March 23, 2023

libpeas-2

Now that GNOME 44 is out the door, I took some time to do a bunch of the refactoring I’ve wanted in libpeas for quite some time. For those not in the know, libpeas is the plugin engine behind applications like Gedit and Builder.

This does include an ABI break but libpeas-1.0 and libpeas-2 can be installed side-by-side.

In particular, I wanted to remove a bunch of deprecated API that is well over a decade old. It wasn’t used for very long and causes libpeas to unnecessarily link against gobject-introspection-1.0.

Additionally, there is no need for the libpeas-gtk library anymore. With GTK 4 came much more powerful list widgets. Combine that with radically different plugin UI designs, the “one stop plugin configuration widget” in libpeas-gtk just isn’t cutting it.

Now that there is just the single library, using subdirectories in includes does not make sense. Just #include <libpeas.h> now.

Therefore, PeasEngine is now a GListModel containing PeasPluginInfo.

I also made PeasExtensionSet a GListModel which can be convenient when you want to filter which extensions use care about using something like GtkFilterListModel.

And that is one of the significant reasons for the ABI break. Previously, PeasPluginInfo was a boxed-type, incompatible with GListModel. It is now derived from GObject and thusly provides properties for all the important bits, including PeasPluginInfo:loaded to denote if the plugin is loaded.

A vestige of the old-days is PeasExtension which was really just an alias to GObject. This just isn’t needed anymore and we use GObject directly in function prototypes.

PeasActivatable is also removed because creating interfaces is so easy these days with language bindings and/or G_DECLARE_INTERFACE() that it doesn’t make sense to have such an interface in-tree. Just create the interface you want rather than shoehorning this one in.

I’ve taken this opportunity to rename our development branch to main and you can get the old libpeas-1.0 ABI from the very fresh 1.36 branch.

March 22, 2023

Endless contributions to GNOME 44

The GNOME 44 release is rushing towards us like an irate pangolin! Here is a quick roundup of some of the Endless OS Foundation team’s contributions over its six-month development cycle.

Software

As in the previous cycle, our team has been a key contributor to GNOME Software 44. Based on a very unscientific analysis of the Git commit log, about 30% of non-merge commits and 75% of merge commits to GNOME Software during this cycle came from our team. Co-maintainer Philip Withnall has continued his work to refactor Software’s internal threading model to improve its reliability. He’s also contributed a number of fixes in both GNOME Software and Flatpak to fix issues related to app updates, such as not leaking large temporary directories when an update fails. Dan Nicholson fixed an issue in Flatpak which would cause Software to remove an app rather than updating it when its ID changes.

Georges Stavracas added some sysprof instrumentation which allowed him to quickly pinpoint the main cause of slow loading of category pages. To our collective surprise, the culprit was… loading remote icons for apps! Georges fixed this issue by downloading icons asynchronously. A side-by-side comparison is really quite striking:

As we came closer to the release of Endless OS 5, we realised we needed some improvements to the handling of OS updates in GNOME Software, such as showing a Learn More link for major upgrades, distinguishing between major upgrades and minor updates, and using the distro’s icon when showing a minor update. Like Endless OS, GNOME OS uses eos-updater, although these improvements will not kick in fully there right now, since it currently does not set any OS version metadata on its updates, or a logo in os-release.

The GNOME Software updates page, showing a minor update for Endless OS with the Endless logo beside it.

Of course, we’ve also contributed to the ongoing maintenance of Software, and other functional improvements such as displaying release urgency levels for firmware updates.

Looking ahead, Joana Filizola has spearheaded a series of user studies on topics like how navigation within Software works, discoverability of search, and the name ‘Software’ itself: we hope these will bear fruit in future GNOME cycles.

Shell

As well as ongoing maintenance of Shell and Mutter, Georges Stavracas contributed improvements to the quick settings pills, adding subtitles to improve the information density. This went hand-in-hand with work to improve GNOME’s handling of Flatpak apps that are running in the background (i.e. without a visible window). Previously this was rather crude: if a Flatpak app ran without a window for some period of time, you would get a decontextualized dialog box asking if you want to allow the app to keep running. Choosing the “wrong” option would kill the app and forbid it from running in the background in future – breaking core functionality for certain apps. In GNOME 44, background apps are instead listed within the quick settings popover, and those apps that do use the background portal API to ask nicely to run in the background are allowed to do so without user interaction.

We also supported the design team’s experiments around how window focus is communicated.

GLib

Philip Withnall has, as in many previous cycles, contributed tens of hours of ongoing maintenance to this library that underpins the entire desktop. This has included a number of GVariant security fixes (like this one), GApplication security fixes, GDBus refcounting fixes, and more. Philip also added g_free_sized() and g_aligned_free_sized(), mirroring similar functions in C23, so that applications can start using these without needing to check for (or wait for) C23 support in the toolchain.

Initial Setup

I spent somewhat fewer hours—but not zero!—on general maintenance of Initial Setup. Georges fixed a regression that meant that privacy policies could not be viewed from within Initial Setup; I fixed the display of a shortlist of keyboard layouts, and of non-ASCII characters in location names after switching locale; and Cassidy James Blaede refreshed the design of the password page to use Adwaita widgets & styling.

Password page of GNOME Initial Setup. The password fields have inline icons to edit the text and reveal the password.

…and more

Every quarter, the engineering teams at Endless OS Foundation have an “intermission week”, where the team sets aside our normal priorities to focus on addressing tech debt, wishlist items, innovative or experimental ideas, and learning. Some of the items above came out of the last couple of intermission weeks! On top of that, Philip has spent some time experimenting with APIs to allow apps’ state to be saved and restored; and João Paulo Rechi Vita explored making the GNOME Online Accounts daemon quit when idle, saving a small but non-zero amount of RAM. Neither of these are quite in a production-ready state, but as they say: there’s always another release!

Meanwhile, we’ve been working on extending the set of web apps offered in GNOME Software on Endless OS, using more expansive criteria than the list shipped by GNOME Software by default, and a different delivery mechanism for the catalogue. More on this in a future post!

March 21, 2023

WebKitGTK API for GTK 4 Is Now Stable

With the release of WebKitGTK 2.40.0, WebKitGTK now finally provides a stable API and ABI for GTK 4 applications. The following API versions are provided:

  • webkit2gtk-4.0: this API version uses GTK 3 and libsoup 2. It is obsolete and users should immediately port to webkit2gtk-4.1. To get this with WebKitGTK 2.40, build with -DPORT=GTK -DUSE_SOUP2=ON.
  • webkit2gtk-4.1: this API version uses GTK 3 and libsoup 3. It contains no other changes from webkit2gtk-4.0 besides the libsoup version. With WebKitGTK 2.40, this is the default API version that you get when you build with -DPORT=GTK. (In 2.42, this might require a different flag, e.g. -DUSE_GTK3=ON, which does not exist yet.)
  • webkitgtk-6.0: this API version uses GTK 4 and libsoup 3. To get this with WebKitGTK 2.40, build with -DPORT=GTK -DUSE_GTK4=ON. (In 2.42, this might become the default API version.)

WebKitGTK 2.38 had a different GTK 4 API version, webkit2gtk-5.0. This was an unstable/development API version and it is gone in 2.40, so applications using it will break. Fortunately, that should be very few applications. If your operating system ships GNOME 42, or any older version, or the new GNOME 44, then no applications use webkit2gtk-5.0 and you have no extra work to do. But for operating systems that ship GNOME 43, webkit2gtk-5.0 is used by gnome-builder, gnome-initial-setup, and evolution-data-server:

  • For evolution-data-server 3.46, use this patch which applies on evolution-data-server 3.46.4.
  • For gnome-initial-setup 43, use this patch which applies on gnome-initial-setup 43.2. (Update: for your convenience, this patch will be included in gnome-initial-setup 43.3.)
  • For gnome-builder 43, all required changes are present in version 43.7.

Remember, patching is only needed for GNOME 43. Other versions of GNOME will have no problems with WebKitGTK 2.40.

There is no proper online documentation yet, but in the meantime you can view the markdown source for the migration guide to help you with porting your applications. Although the API is now stable and it is close to feature parity with the GTK 3 version, there are some problems to be aware of:

Big thanks to everyone who helped make this possible.

March 20, 2023

a world to win: webassembly for the rest of us

Good day, comrades!

Today I'd like to share the good news that WebAssembly is finally coming for the rest of us weirdos.

A world to win

WebAssembly for the rest of us

17 Mar 2023 – BOB 2023

Andy Wingo

Igalia, S.L.

This is a transcript-alike of a talk that I gave last week at BOB 2023, a gathering in Berlin of people that are using "technologies beyond the mainstream" to get things done: Haskell, Clojure, Elixir, and so on. PDF slides here, and I'll link the video too when it becomes available.

WebAssembly, the story

WebAssembly is an exciting new universal compute platform

WebAssembly: what even is it? Not a programming language that you would write software in, but rather a compilation target: a sort of assembly language, if you will.

WebAssembly, the pitch

Predictable portable performance

  • Low-level
  • Within 10% of native

Reliable composition via isolation

  • Modules share nothing by default
  • No nasal demons
  • Memory sandboxing

Compile your code to WebAssembly for easier distribution and composition

If you look at what the characteristics of WebAssembly are as an abstract machine, to me there are two main areas in which it is an advance over the alternatives.

Firstly it's "close to the metal" -- if you compile for example an image-processing library to WebAssembly and run it, you'll get similar performance when compared to compiling it to x86-64 or ARMv8 or what have you. (For image processing in particular, native still generally wins because the SIMD primitives in WebAssembly are more narrow and because getting the image into and out of WebAssembly may imply a copy, but the general point remains.) WebAssembly's instruction set covers a broad range of low-level operations that allows compilers to produce efficient code.

The novelty here is that WebAssembly is both portable while also being successful. We language weirdos know that it's not enough to do something technically better: you have to also succeed in getting traction for your alternative.

The second interesting characteristic is that WebAssembly is (generally speaking) a principle-of-least-authority architecture: a WebAssembly module starts with access to nothing but itself. Any capabilities that an instance of a module has must be explicitly shared with it by the host at instantiation-time. This is unlike DLLs which have access to all of main memory, or JavaScript libraries which can mutate global objects. This characteristic allows WebAssembly modules to be reliably composed into larger systems.

WebAssembly, the hype

It’s in all browsers! Serve your code to anyone in the world!

It’s on the edge! Run code from your web site close to your users!

Compose a library (eg: Expat) into your program (eg: Firefox), without risk!

It’s the new lightweight virtualization: Wasm is what containers were to VMs! Give me that Kubernetes cash!!!

Again, the remarkable thing about WebAssembly is that it is succeeding! It's on all of your phones, all your desktop web browsers, all of the content distribution networks, and in some cases it seems set to replace containers in the cloud. Launch the rocket emojis!

WebAssembly, the reality

WebAssembly is a weird backend for a C compiler

Only some source languages are having success on WebAssembly

What about Haskell, Ocaml, Scheme, F#, and so on – what about us?

Are we just lazy? (Well...)

So why aren't we there? Where is Clojure-on-WebAssembly? Where are the F#, the Elixir, the Haskell compilers? Some early efforts exist, but they aren't really succeeding. Why is that? Are we just not putting in the effort? Why is it that Rust gets to ride on the rocket ship but Scheme does not?

WebAssembly, the reality (2)

WebAssembly (1.0, 2.0) is not well-suited to garbage-collected languages

Let’s look into why

As it turns out, there is a reason that there is no good Scheme implementation on WebAssembly: the initial version of WebAssembly is a terrible target if your language relies on the presence of a garbage collector. There have been some advances but this observation still applies to the current standardized and deployed versions of WebAssembly. To better understand this issue, let's dig into the guts of the system to see what the limitations are.

GC and WebAssembly 1.0

Where do garbage-collected values live?

For WebAssembly 1.0, only possible answer: linear memory

(module
  (global $hp (mut i32) (i32.const 0))
  (memory $mem 10)) ;; 640 kB

The primitive that WebAssembly 1.0 gives you to represent your data is what is called linear memory: just a buffer of bytes to which you can read and write. It's pretty much like what you get when compiling natively, except that the memory layout is more simple. You can obtain this memory in units of 64-kilobyte pages. In the example above we're going to request 10 pages, for 640 kB. Should be enough, right? We'll just use it all for the garbage collector, with a bump-pointer allocator. The heap pointer / allocation pointer is kept in the mutable global variable $hp.

(func $alloc (param $size i32) (result i32)
  (local $ret i32)
  (loop $retry
    (local.set $ret (global.get $hp))
    (global.set $hp
      (i32.add (local.get $size) (local.get $ret)))

    (br_if 1
      (i32.lt_u (i32.shr_u (global.get $hp) 16)
                (memory.size))
      (local.get $ret))

    (call $gc)
    (br $retry)))

Here's what an allocation function might look like. The allocation function $alloc is like malloc: it takes a number of bytes and returns a pointer. In WebAssembly, a pointer to memory is just an offset, which is a 32-bit integer (i32). (Having the option of a 64-bit address space is planned but not yet standard.)

If this is your first time seeing the text representation of a WebAssembly function, you're in for a treat, but that's not the point of the presentation :) What I'd like to focus on is the (call $gc) -- what happens when the allocation pointer reaches the end of the region?

GC and WebAssembly 1.0 (2)

What hides behind (call $gc) ?

Ship a GC over linear memory

Stop-the-world, not parallel, not concurrent

But... roots.

The first thing to note is that you have to provide the $gc yourself. Of course, this is doable -- this is what we do when compiling to a native target.

Unfortunately though the multithreading support in WebAssembly is somewhat underpowered; it lets you share memory and use atomic operations but you have to create the threads outside WebAssembly. In practice probably the GC that you ship will not take advantage of threads and so it will be rather primitive, deferring all collection work to a stop-the-world phase.

GC and WebAssembly 1.0 (3)

Live objects are

  • the roots
  • any object referenced by a live object

Roots are globals and locals in active stack frames

No way to visit active stack frames

What's worse though is that you have no access to roots on the stack. A GC has to keep live objects, as defined circularly as any object referenced by a root, or any object referenced by a live object. It starts with the roots: global variables and any GC-managed object referenced by an active stack frame.

But there we run into problems, because in WebAssembly (any version, not just 1.0) you can't iterate over the stack, so you can't find active stack frames, so you can't find the stack roots. (Sometimes people want to support this as a low-level capability but generally speaking the consensus would appear to be that overall performance will be better if the engine is the one that is responsible for implementing the GC; but that is foreshadowing!)

GC and WebAssembly 1.0 (3)

Workarounds

  • handle stack for precise roots
  • spill all possibly-pointer values to linear memory and collect conservatively

Handle book-keeping a drag for compiled code

Given the noniterability of the stack, there are basically two work-arounds. One is to have the compiler and run-time maintain an explicit stack of object roots, which the garbage collector can know for sure are pointers. This is nice because it lets you move objects. But, maintaining the stack is overhead; the state of the art solution is rather to create a side table (a "stack map") associating each potential point at which GC can be called with instructions on how to find the roots.

The other workaround is to spill the whole stack to memory. Or, possibly just pointer-like values; anyway, you conservatively scan all words for things that might be roots. But instead of having access to the memory to which the WebAssembly implementation would spill your stack, you have to do it yourself. This can be OK but it's sub-optimal; see my recent post on the Whippet garbage collector for a deeper discussion of the implications of conservative root-finding.

GC and WebAssembly 1.0 (4)

Cycles with external objects (e.g. JavaScript) uncollectable

A pointer to a GC-managed object is an offset to linear memory, need capability over linear memory to read/write object from outside world

No way to give back memory to the OS

Gut check: gut says no

If that were all, it would already be not so great, but it gets worse! Another problem with linear-memory GC is that it limits the potential for composing a number of modules and the host together, because the garbage collector that manages JavaScript objects in a web browser knows nothing about your garbage collector over your linear memory. You can easily create memory leaks in a system like that.

Also, it's pretty gross that a reference to an object in linear memory requires arbitrary read-write access over all of linear memory in order to read or write object fields. How do you build a reliable system without invariants?

Finally, once you collect garbage, and maybe you manage to compact memory, you can't give anything back to the OS. There are proposals in the works but they are not there yet.

If the BOB audience had to choose between Worse is Better and The Right Thing, I think the BOB audience is much closer to the Right Thing. People like that feel instinctual revulsion to ugly systems and I think GC over linear memory describes an ugly system.

GC and WebAssembly 1.0 (5)

There is already a high-performance concurrent parallel compacting GC in the browser

Halftime: C++ N – Altlangs 0

The kicker is that WebAssembly 1.0 requires you to write and deliver a terrible GC when there is already probably a great GC just sitting there in the host, one that has hundreds of person-years of effort invested in it, one that will surely do a better job than you could ever do. WebAssembly as hosted in a web browser should have access to the browser's garbage collector!

I have the feeling that while those of us with a soft spot for languages with garbage collection have been standing on the sidelines, Rust and C++ people have been busy on the playing field scoring goals. Tripping over the ball, yes, but eventually they do manage to make within striking distance.

Change is coming!

Support for built-in GC set to ship in Q4 2023

With GC, the material conditions are now in place

Let’s compile our languages to WebAssembly

But to continue the sportsball metaphor, I think in the second half our players will finally be able to get out on the pitch and give it the proverbial 110%. Support for garbage collection is coming to WebAssembly users, and I think even by the end of the year it will be shipping in major browsers. This is going to be big! We have a chance and we need to sieze it.

Scheme to Wasm

Spritely + Igalia working on Scheme to WebAssembly

Avoid truncating language to platform; bring whole self

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

Even with GC, though, WebAssembly is still a weird machine. It would help to see the concrete approaches that some languages of interest manage to take when compiling to WebAssembly.

In that spirit, the rest of this article/presentation is a walkthough of the approach that I am taking as I work on a WebAssembly compiler for Scheme. (Thanks to Spritely for supporting this work!)

Before diving in, a meta-note: when you go to compile a language to, say, JavaScript, you are mightily tempted to cut corners. For example you might implement numbers as JavaScript numbers, or you might omit implementing continuations. In this work I am trying to not cut corners, and instead to implement the language faithfully. Sometimes this means I have to work around weirdness in WebAssembly, and that's OK.

When thinking about Scheme, I'd like to highlight a few specific areas that have interesting translations. We'll start with value representation, which stays in the GC theme from the introduction.

Scheme to Wasm: Values

;;       any  extern  func
;;        |
;;        eq
;;     /  |   \
;; i31 struct  array

The unitype: (ref eq)

Immediate values in (ref i31)

  • fixnums with 30-bit range
  • chars, bools, etc

Explicit nullability: (ref null eq) vs (ref eq)

The GC extensions for WebAssembly are phrased in terms of a type system. Oddly, there are three top types; as far as I understand it, this is the result of a compromise about how WebAssembly engines might want to represent these different kinds of values. For example, an opaque JavaScript value flowing into a WebAssembly program would have type (ref extern). On a system with NaN boxing, you would need 64 bits to represent a JS value. On the other hand a native WebAssembly object would be a subtype of (ref any), and might be representable in 32 bits, either because it's a 32-bit system or because of pointer compression.

Anyway, three top types. The user can define subtypes of struct and array, instantiate values of those types, and access their fields. The life cycle of reference-typed objects is automatically managed by the run-time, which is just another way of saying they are garbage-collected.

For Scheme, we need a common supertype for all values: the unitype, in Bob Harper's memorable formulation. We can use (ref any), but actually we'll use (ref eq) -- this is the supertype of values that can be compared by (pointer) identity. So now we can code up eq?:

(func $eq? (param (ref eq) (ref eq))
           (result i32)
  (ref.eq (local.get a) (local.get b)))

Generally speaking in a Scheme implementation there are immediates and heap objects. Immediates can be encoded in the bits of a value, whereas for heap object the bits of a value encode a reference (pointer) to an object on the garbage-collected heap. We usually represent small integers as immediates, as well as booleans and other oddball values.

Happily, WebAssembly gives us an immediate value type, i31. We'll encode our immediates there, and otherwise represent heap objects as instances of struct subtypes.

Scheme to Wasm: Values (2)

Heap objects subtypes of struct; concretely:

(struct $heap-object
  (struct (field $tag-and-hash i32)))
(struct $pair
  (sub $heap-object
    (struct i32 (ref eq) (ref eq))))

GC proposal allows subtyping on structs, functions, arrays

Structural type equivalance: explicit tag useful

We actually need to have a common struct supertype as well, for two reasons. One is that we need to be able to hash Scheme values by identity, but for this we need an embedded lazily-initialized hash code. It's a bit annoying to take the per-object memory hit but it's a reality, and the JVM does it this way, so it must not be so terrible.

The other reason is more subtle: WebAssembly's type system is built in such a way that types that are "structurally" equivalent are indistinguishable. So a pair has two fields, besides the hash, but there might be a number of other fundamental object types that have the same shape; you can't fully rely on WebAssembly's dynamic type checks (ref.test et al) to be able to query the type of a value. Instead we re-use the low bits of the hash word to include a type tag, which might be 1 for pairs, 2 for vectors, 3 for closures, and so on.

Scheme to Wasm: Values (3)

(func $cons (param (ref eq)
                   (ref eq))
            (result (ref $pair))
  (struct.new_canon $pair
    ;; Assume heap tag for pairs is 1.
    (i32.const 1)
    ;; Car and cdr.
    (local.get 0)
    (local.get 1)))

(func $%car (param (ref $pair))
            (result (ref eq))
  (struct.get $pair 1 (local.get 0)))

With this knowledge we can define cons, as a simple call to struct.new_canon pair.

I didn't have time for this in the talk, but there is a ghost haunting this code: the ghost of nominal typing. See, in a web browser at least, every heap object will have its first word point to its "hidden class" / "structure" / "map" word. If the engine ever needs to check that a value is of a specific shape, it can do a quick check on the map word's value; if it needs to do deeper introspection, it can dereference that word to get more details.

Under the hood, testing whether a (ref eq) is a pair or not should be a simple check that it's a (ref struct) (and not a fixnum), and then a comparison of its map word to the run-time type corresponding to $pair. If subtyping of $pair is allowed, we start to want inline caches to handle polymorphism, but the checking the map word is still the basic mechanism.

However, as I mentioned, we only have structural equality of types; two (struct (ref eq)) type definitions will define the same type and have the same map word (run-time type / RTT). Hence the _canon in the name of struct.new_canon $pair: we create an instance of $pair, with the canonical run-time-type for objects having $pair-shape.

In earlier drafts of the WebAssembly GC extensions, users could define their own RTTs, which effectively amounts to nominal typing: not only does this object have the right structure, but was it created with respect to this particular RTT. But, this facility was cut from the first release, and it left ghosts in the form of these _canon suffixes on type constructor instructions.

For the Scheme-to-WebAssembly effort, we effectively add back in a degree of nominal typing via type tags. For better or for worse this results in a so-called "open-world" system: you can instantiate a separately-compiled WebAssembly module that happens to define the same types and use the same type tags and it will be able to happily access the contents of Scheme values from another module. If you were to use nominal types, you would't be able to do so, unless there were some common base module that defined and exported the types of interests, and which any extension module would need to import.

(func $car (param (ref eq)) (result (ref eq))
  (local (ref $pair))
  (block $not-pair
    (br_if $not-pair
      (i32.eqz (ref.test $pair (local.get 0))))
    (local.set 1 (ref.cast $pair) (local.get 0))
    (br_if $not-pair
      (i32.ne
        (i32.const 1)
        (i32.and
          (i32.const 0xff)
          (struct.get $heap-object 0 (local.get 1)))))
    (return_call $%car (local.get 1)))

  (call $type-error)
  (unreachable))

In the previous example we had $%car, with a funny % in the name, taking a (ref $pair) as an argument. But in the general case (barring compiler heroics) car will take an instance of the unitype (ref eq). To know that it's actually a pair we have to make two checks: one, that it is a struct and has the $pair shape, and two, that it has the right tag. Oh well!

Scheme to Wasm

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

But with all of that I think we have a solid story on how to represent values. I went through all of the basic value types in Guile and checked that they could all be represented using GC types, and it seems that all is good. Now on to the next point: varargs.

Scheme to Wasm: Varargs

(list 'hey)      ;; => (hey)
(list 'hey 'bob) ;; => (hey bob)

Problem: Wasm functions strongly typed

(func $list (param ???) (result (ref eq))
  ???)

Solution: Virtualize calling convention

In WebAssembly, you define functions with a type, and it is impossible to call them in an unsound way. You must call $car exactly 2 arguments or it will not compile, and those arguments have to be of specific types, and so on. But Scheme doesn't enforce these restrictions on the language level, bless its little miscreant heart. You can call car with 5 arguments, and you'll get a run-time error. There are some functions that can take a variable number of arguments, doing different things depending on incoming argument count.

How do we square these two approaches to function types?

;; "Registers" for args 0 to 3
(global $arg0 (mut (ref eq)) (i31.new (i32.const 0)))
(global $arg1 (mut (ref eq)) (i31.new (i32.const 0)))
(global $arg2 (mut (ref eq)) (i31.new (i32.const 0)))
(global $arg3 (mut (ref eq)) (i31.new (i32.const 0)))

;; "Memory" for the rest
(type $argv (array (ref eq)))
(global $argN (ref $argv)
        (array.new_canon_default
          $argv (i31.const 42) (i31.new (i32.const 0))))

Uniform function type: argument count as sole parameter

Callee moves args to locals, possibly clearing roots

The approach we are taking is to virtualize the calling convention. In the same way that when calling an x86-64 function, you pass the first argument in $rdi, then $rsi, and eventually if you run out of registers you put arguments in memory, in the same way we'll pass the first argument in the $arg0 global, then $arg1, and eventually in memory if needed. The function will receive the number of incoming arguments as its sole parameter; in fact, all functions will be of type (func (param i32)).

The expectation is that after checking argument count, the callee will load its arguments from globals / memory to locals, which the compiler can do a better job on than globals. We might not even emit code to null out the argument globals; might leak a little memory but probably would be a win.

You can imagine a world in which $arg0 actually gets globally allocated to $rdi, because it is only live during the call sequence; but I don't think that world is this one :)

Scheme to Wasm

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

Great, two points out of the way! Next up, tail calls.

Scheme to Wasm: Tail calls

;; Call known function
(return_call $f arg ...)

;; Call function by value
(return_call_ref $type callee arg ...)

Friends -- I almost cried making this slide. We Schemers are used to working around the lack of tail calls, and I could have done so here, but it's just such a relief that these functions are just going to be there and I don't have to think much more about them. Technically speaking the proposal isn't merged yet; checking the phases document it's at the last station before headed to the great depot in the sky. But, soon soon it will be present and enabled in all WebAssembly implementations, and we should build systems now that rely on it.

Scheme to Wasm

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

Next up, my favorite favorite topic: delimited continuations.

Scheme to Wasm: Prompts (1)

Problem: Lightweight threads/fibers, exceptions

Possible solutions

  • Eventually, built-in coroutines
  • binaryen’s asyncify (not yet ready for GC); see Julia
  • Delimited continuations

“Bring your whole self”

Before diving in though, one might wonder why bother. Delimited continuations are a building-block that one can use to build other, more useful things, notably exceptions and light-weight threading / fibers. Could there be another way of achieving these end goals without having to implement this relatively uncommon primitive?

For fibers, it is possible to implement them in terms of a built-in coroutine facility. The standards body seems willing to include a coroutine primitive, but it seems far off to me; not within the next 3-4 years I would say. So let's put that to one side.

There is a more near-term solution, to use asyncify to implement coroutines somehow; but my understanding is that asyncify is not ready for GC yet.

For the Guile flavor of Scheme at least, delimited continuations are table stakes of their own right, so given that we will have them on WebAssembly, we might as well use them to implement fibers and exceptions in the same way as we do on native targets. Why compromise if you don't have to?

Scheme to Wasm: Prompts (2)

Prompts delimit continuations

(define k
  (call-with-prompt ’foo
    ; body
    (lambda ()
      (+ 34 (abort-to-prompt 'foo)))
    ; handler
    (lambda (continuation)
      continuation)))

(k 10)       ;; ⇒ 44
(- (k 10) 2) ;; ⇒ 42

k is the _ in (lambda () (+ 34 _))

There are a few ways to implement delimited continuations, but my usual way of thinking about them is that a delimited continuation is a slice of the stack. One end of the slice is the prompt established by call-with-prompt, and the other by the continuation of the call to abort-to-prompt. Capturing a slice pops it off the stack, copying it out to the heap as a callable function. Calling that function splats the captured slice back on the stack and resumes it where it left off.

Scheme to Wasm: Prompts (3)

Delimited continuations are stack slices

Make stack explicit via minimal continuation-passing-style conversion

  • Turn all calls into tail calls
  • Allocate return continuations on explicit stack
  • Breaks functions into pieces at non-tail calls

This low-level intuition of what a delimited continuation is leads naturally to an implementation; the only problem is that we can't slice the WebAssembly call stack. The workaround here is similar to the varargs case: we virtualize the stack.

The mechanism to do so is a continuation-passing-style (CPS) transformation of each function. Functions that make no calls, such as leaf functions, don't need to change at all. The same goes for functions that make only tail calls. For functions that make non-tail calls, we split them into pieces that preserve the only-tail-calls property.

Scheme to Wasm: Prompts (4)

Before a non-tail-call:

  • Push live-out vars on stacks (one stack per top type)
  • Push continuation as funcref
  • Tail-call callee

Return from call via pop and tail call:

(return_call_ref (call $pop-return)
                 (i32.const 0))

After return, continuation pops state from stacks

Consider a simple function:

(define (f x y)
  (+ x (g y))

Before making a non-tail call, a "tailified" function will instead push all live data onto an explicitly-managed stack and tail-call the callee. It also pushes on the return continuation. Returning from the callee pops the return continuation and tail-calls it. The return continuation pops the previously-saved live data and continues.

In this concrete case, tailification would split f into two pieces:

(define (f x y)
  (push! x)
  (push-return! f-return-continuation-0)
  (g y))

(define (f-return-continuation-0 g-of-y)
  (define k (pop-return!))
  (define x (pop! x))
  (k (+ x g-of-y)))

Now there are no non-tail calls, besides calls to run-time routines like push! and + and so on. This transformation is implemented by tailify.scm.

Scheme to Wasm: Prompts (5)

abort-to-prompt:

  • Pop stack slice to reified continuation object
  • Tail-call new top of stack: prompt handler

Calling a reified continuation:

  • Push stack slice
  • Tail-call new top of stack

No need to wait for effect handlers proposal; you can have it all now!

The salient point is that the stack on which push! operates (in reality, probably four or five stacks: one in linear memory or an array for types like i32 or f64, three for each of the managed top types any, extern, and func, and one for the stack of return continuations) are managed by us, so we can slice them.

Someone asked in the talk about whether the explicit memory traffic and avoiding the return-address-buffer branch prediction is a source of inefficiency in the transformation and I have to say, yes, but I don't know by how much. I guess we'll find out soon.

Scheme to Wasm

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

Okeydokes, last point!

Scheme to Wasm: Numbers

Numbers can be immediate: fixnums

Or on the heap: bignums, fractions, flonums, complex

Supertype is still ref eq

Consider imports to implement bignums

  • On web: BigInt
  • On edge: Wasm support module (mini-gmp?)

Dynamic dispatch for polymorphic ops, as usual

First, I would note that sometimes the compiler can unbox numeric operations. For example if it infers that a result will be an inexact real, it can use unboxed f64 instead of library routines working on heap flonums ((struct i32 f64); the initial i32 is for the hash and tag). But we still need a story for the general case that involves dynamic type checks.

The basic idea is that we get to have fixnums and heap numbers. Fixnums will handle most of the integer arithmetic that we need, and will avoid allocation. We'll inline most fixnum operations as a fast path and call out to library routines otherwise. Of course fixnum inputs may produce a bignum output as well, so the fast path sometimes includes another slow-path callout.

We want to minimize binary module size. In an ideal compile-to-WebAssembly situation, a small program will have a small module size, down to a minimum of a kilobyte or so; larger programs can be megabytes, if the user experience allows for the download delay. Binary module size will be dominated by code, so that means we need to plan for aggressive dead-code elimination, minimize the size of fast paths, and also minimize the size of the standard library.

For numbers, we try to keep module size down by leaning on the platform. In the case of bignums, we can punt some of this work to the host; on a JavaScript host, we would use BigInt, and on a WASI host we'd compile an external bignum library. So that's the general story: inlined fixnum fast paths with dynamic checks, and otherwise library routine callouts, combined with aggressive whole-program dead-code elimination.

Scheme to Wasm

  • Value representation
  • Varargs
  • Tail calls
  • Delimited continuations
  • Numeric tower

Hey I think we did it! Always before when I thought about compiling Scheme or Guile to the web, I got stuck on some point or another, was tempted down the corner-cutting alleys, and eventually gave up before starting. But finally it would seem that the stars are aligned: we get to have our Scheme and run it too.

Miscellenea

Debugging: The wild west of DWARF; prompts

Strings: stringref host strings spark joy

JS interop: Export accessors; Wasm objects opaque to JS. externref.

JIT: A whole ’nother talk!

AOT: wasm2c

Of course, like I said, WebAssembly is still a weird machine: as a compilation target but also at run-time. Debugging is a right proper mess; perhaps some other article on that some time.

How to represent strings is a surprisingly gnarly question; there is tension within the WebAssembly standards community between those that think that it's possible for JavaScript and WebAssembly to share an underlying string representation, and those that think that it's a fool's errand and that copying is the only way to go. I don't know which side will prevail; perhaps more on that as well later on.

Similarly the whole interoperation with JavaScript question is very much in its early stages, with the current situation choosing to err on the side of nothing rather than the wrong thing. You can pass a WebAssembly (ref eq) to JavaScript, but JavaScript can't do anything with it: it has no prototype. The state of the art is to also ship a JS run-time that wraps each wasm object, proxying exported functions from the wasm module as object methods.

Finally, some language implementations really need JIT support, like PyPy. There, that's a whole 'nother talk!

WebAssembly for the rest of us

With GC, WebAssembly is now ready for us

Getting our languages on WebAssembly now a S.M.O.P.

Let’s score some goals in the second half!

(visit-links
 "gitlab.com/spritely/guile-hoot-updates"
 "wingolog.org"
 "wingo@igalia.com"
 "igalia.com"
 "mastodon.social/@wingo")

WebAssembly has proven to have some great wins for C, C++, Rust, and so on -- but now it's our turn to get in the game. GC is coming and we as a community need to be getting our compilers and language run-times ready. Let's put on the coffee and bang some bytes together; it's still early days and there's a world to win out there for the language community with the best WebAssembly experience. The game is afoot: happy consing!

Writing elsewhere; the pain of moving platforms

I’ve been doing a lot of writing elsewhere of late. Some links:

  • I’ve written a fair amount in the past year for the Tidelift blog, most recently on the EU’s Cyber Resiliency Act and what it might mean for open source.
  • I wrote last week at opensource.com; the latest in a now multi-year series on board candidates in elections for the Open Source Initiative.
  • I have a newsletter on the intersection of open and machine learning at openml.fyi. It is fun!
  • I’ve moved to the fediverse for most of my social media—I’m social.coop/@luis_in_brief (and you can subscribe to this blog via the fediverse at @lu.is/@admin).

I don’t love (mostly) leaving Twitter; as I’ve said a few times, the exposure to different people there helped make me a better person. But one of my primary political concerns is the rise of fascism in the US, and that absolutely includes Elon and the people who enable him. I can’t quit cold-turkey; unfortunately, too many things I care about (or need to follow for work) haven’t left. But I can at least sleep well.

March 18, 2023

Integrating the RawTherapee engine

RawTherapee is one of the two major open source RAW photo processing applications, the other is Darktable.

Can I leverage RawTherapee RAW processing code for use in Niepce? Yes I can.

So let's review of I did it.

Preamble

License-wise GPL-3.0 is a match.

In term of tech stack, there are a few complexities.

  1. RawTherapee is written in C++, while Niepce is being converted to Rust. Fortunately it's not really an issue, it require just a bit of work, even at the expense of writing a bit of C++.
  2. It is not designed to be used as a library: it's an application. Fortunately there is a separation between the engine (rtengine) and the UI (rtgui) which will make our life easier. There are a couple of places where this separation blurs, but nothing that can't be fixed.
  3. The UI toolkit is gtkmm 3.0. This is a little point of friction here as Niepce uses GTK4 with some leftovers C++ code using gtkmm 4.0. We are porting the engine, so it shouldn't matter except that the versions of neither glibmm nor cairomm match (ie they are incompatible) and the engine relies on them heavily.
  4. Build system: it uses CMake. Given that RawTherapee is not meant to be built as a library, changes will be required. I will take a different approach though.

Organization

The code will not be imported in the repository and instead will be used as a git submodule. I already have cxx that way for the code generator. Given that some code needs to be changed, it will reference my own fork of RawTherapee, based on 5.9, with as much as I can upstreamed.

The Rust wrappers will live in their own crate: Niepce application code is setup as a workspace with 4 crates: npc-fwk, npc-engine, npc-craw and niepce. This would be the fifth: rtengine.

The rtengine crate will provide the API for the Rust code. No C++ will be exposed.

npc-craw (Niepce Camera Raw1), as it is meant to implement the whole image processing pipeline, will use this crate. We'll create a trait for the pipeline and implement it for both the ncr pipeline and rtengine.

Integrating

Build system

Niepce wrap everything into a meson build. So to build rtengine we will build a static library and install the supporting file. We have to bring in a lot of explicit dependencies, which bring a certain amount bloat, but we can see later if there is a way to reduce this. It's tedious to assemble everything.

The first build didn't include everything needed. I had to fix this as I was writing the wrappers.

Dependencies

glibmm and cairomm: the version used for gtkmm-3.0 and gtkmm-4.0 differs. glibmm changed a few things like some enum are now C++ enum class (better namespacing), and Glib::RefPtr<> is now a std::shared_ptr<>. The biggest hurdle is the dependency on the concurrency features of glibmm (Glib::Mutex) that got completely removed in glibmm-2.68 (gtkmm-3.0 uses glibmm-2.4). I did a rough port to use the C++ library, and upstream has a languishing work in progress pull request. Other changes include adding explicit includes. I also need to remove gtkmm dependencies leaking into the engine.

Rust wrapper

I recommend heavily to make sure you can build your code with the address sanitizer. In the case of Niepce, I have had it for a long time, and made sure it still worked when I inverted the build order to link the main binary with Rust instead of C++.

Using cxx I created a minimum interface to the C++ code. The problem was to understand how it works. Fortunately the command line interface for RawTherapee does exactly that. This is the logic we'll follow in the Rust code.

Lets create the bridge module. We need to bridge the following types:

  • InitialImage which represents the image to process.
  • ProcParams which represents the parameters for processing the the image.
  • PartialProfile which is used to populate the ProcParams from a processing profile.
  • ProcessingJob which represents the job of processing the image.
  • ImageIO which is one of the classes the processed image data inherit from, the one that implement getting the scanlines.

Ownership is a bit complicated you should pay attention how these types get cleaned up. For example a ProcessingJob ownership get transfered to the processImage() function, unless there is an error, in which case there is a destroy() function (it's a static method) to call. While PartialProfile needs deleteInstance() to be called before being destroyed, or it will leak.

Example:

let mut proc_params = ffi::proc_params_new();
let mut raw_params = unsafe {
    ffi::profile_store_load_dynamic_profile(image.pin_mut().get_meta_data())
};
ffi::partial_profile_apply_to(&raw_params, proc_params.pin_mut(), false);

We have created proc_params as a UniquePtr<ProcParams>. We obtain a raw_params as a UniquePtr<PartialProfile>. UniquePtr<> is like a Box<> but for use when coming from a C++ std::unique_ptr<>.

raw_params.pin_mut().delete_instance();

raw_params will be freed when getting out of scope, but if you don't call delete_instance() (the function is renamed in the bridge to follow Rust conventions), memory will leak. The pin_mut() is necessary to obtain a Pin<> of the pointer for a mutable pointer required as the instance.

let job = ffi::processing_job_create(
    image.pin_mut(),
    proc_params.as_ref().unwrap(),
    false,
);
let mut error = 0_i32;
// Warning: unless there is an error, process_image will consume it.
let job = job.into_raw();
let imagefloat = unsafe { ffi::process_image(job, &mut error, false) };
if imagefloat.is_null() {
    // Only in case of error.
    unsafe { ffi::processing_job_destroy(job) };
    return Err(Error::from(error));
}

This last bit, we create the job as a UniquePtr<ProcessingJob> but then we have to obtain the raw pointer to sink either with process_image(), or in case of error, sink with processing_job_destroy(). into_raw() do consume the UniquePtr<>.

image is also is a UniquePtr<InitialImage> and InitialImage has a decreaseRef() to unref the object that must be called to destroy the object. It would be called like this:

unsafe { ffi::decrease_ref(image.into_raw()) };

Most issues got detected with libasan, either as memory errors or as memory leaks. There is a lot of pointer manipulations, but let's limit this to the bridge and not expose it ; at least unlike in C++, cxx::UniquePtr<> consume the smart pointer when turning it into a raw pointer, there is no risk to use it again, at least in the Rust code.

Also, some glue code needed to be written as some function take Glib::ustring instead of std::string, constructors needs to be wrapped to return UniquePtr<>. Multiple inheritence make some direct method call not possible, and static methods are still work in progress with cxx.

One good way to test this was to write a simple command line program. As the code shown above, it's tricky to use correctly, so I wrote a safe API to use the engine, one that is more in line with Niepce "architecture".

At that point rendering an image is the following code:

use rtengine::RtEngine;

let engine = RtEngine::new();
if engine.set_file(filename, true /* is_raw */).is_err() {
    std::process::exit(3);
}

match engine.process() {
    Err(error) => {
        println!("Error, couldn't render image: {error}");
        std::process::exit(2);
    }
    Ok(image) => {
        image.save_png("image.png").expect("Couldn't save image");
    }
}

Results

I have integrated it in the app. For now switching rendering engine needs a code change, there is a bit more work to integrate rendering parameters to the app logic.

Here is how a picture from my Canon G7X MkII looked with the basic pipeline from ncr:

ncr rendering

Here is how it looks with the RawTherapee engine:

RawTherapee engine rendering

As you can notice, lens correction is applied.

1

there is an unrelated ncr crate on crates.io, so I decided to not use that crate name, and didn't want to use npc-ncr, even though the crate is private to the application and not intended to be published separately.

March 17, 2023

Mushroom Gardiane Recipe

Gardiane is a traditional recipe from Camargue, this vegan variant replaces streaked bull meat by mushrooms. I tried it with seitan too but it ends up a bit too soggy and mushy to my liking, especially after some time in the fridge. This vegan variant also has the advantage of requiring virtually no set up — in the original recipe you have to marinate the meat for 24–48 hours — and of taking less time to cook, as you only need 1–2 hours instead of 2–4 hours.

Regarding olives, the most traditional would be black Picholines, though I prefer to use black Lucques. Please please please do not use greek style olives as they contain way too much salt, they would spoil the gardiane. I strongly recommend using olives with stones as it helps keeping them in good shape while everything cooks, and removing the stones with your mouth is part of the experience, like cherry stones in clafoutis.

For 6–8 servings.

Ingredients

  • 2 kg of button mushrooms
  • 200 g of firm tofu
  • 200 g of smoked firm tofu
  • 500 g of black olives, preferably stoned Picholines or Lucques
  • 2 large yellow onions
  • 75 cl of red wine, preferably Côte du Rhône or Languedoc
  • 6 garlic cloves
  • ⅓ tsp of ground cloves
  • 2 tsp of thyme
  • 2 tsp of rosemary
  • 2 large bay leaves
  • a bit of ground black pepper
  • 1 tsp of salt
  • 3 tbsp of starch or flower
  • 500 g long grain white rice, preferably Camargue
  • some olive oil
  • some water

This day I couldn’t find Picholines or Lucques, and I forgot to invite the tofu to the familly photo

Instructions

  • Remove the stems from the mushrooms. Remove dirty bits from the stems.
  • Peel the mushroom caps, this will help the wine soak in.
  • Cut the caps into 3–4 cm wide pieces, in pratice that means you don’t cut the small ones, you cut the medium-sized ones in two, and you cut the larger ones in three or four.
  • Wash the caps and stems in a salad spinner.

The mushrooms, peeled, cut and washed

  • Cut the tofu in 2 cm long and 5 mm thick batonnets.

The cut smoked tofu

  • Peel the onions. Cut the onions in 8–10 vertically.
  • Peel the garlic cloves. Mince the garlic cloves.
  • If the olives come in water, remove the water.
  • Put a large pot on medium heat.
  • In the pot, put a bit of olive oil and lightly brown the onions with the garlic, the thyme and the rosemary. Reserve.

The browned onions

  • In the pot, put a bit of olive oil and lightly brown the tofu. Reserve.

The browned tofu

  • In the pot, put a bit of olive oil and lightly brown the mushrooms. Do it in several batches so they don’t cook too much in their own water, the goal is to get somw brown bits.

A batch of browned mushrooms, these cooked in their own water a bit too much, but it’s fine as they still have some brown bits

  • Remove the pot from the fire, and pour everything in it but the starch or flour, the black pepper and the rice. This means you add the wine, the mushrooms, the onions, the tofu, the olives, the bay leaves, the ground cloves, and the salt.
  • Add water in the pot until everything is covered.

The pot is filled with all the ingredients, the gardiane is ready to be cooked

  • Cover the pot, put it on medium-low heat and let it cook for 2 hours.
  • While the gardiane cooks, prepare and cook the rice.
  • Remove the pot from the stove.
  • Put some sauce from the gardiane in a glass with the starch or flour. Mix them with a spoon and put the liquid back in the pot.
  • Add the black pepper in the pot and stir, the starch or flour will very lightly thicken the sauce.

A served plate

  • Serve the gardiane either aside the rice or on top of it, as you prefer. Serve with another bottle of a similar wine.

This recipe is approved by Tobias Bernard

Libadwaita 1.3

Another cycle, another release. Let’s take a look at what’s new.

Banners

Screenshot of AdwBanner

AdwBanner is a brand new widget that replaces GtkInfoBar.

Jamie started implementing it before 1.2 was released, but we were already in the API freeze so it was delayed to this cycle instead.

While it looks more or less the same as GtkInfoBar, it’s not a direct replacement. AdwBanner has a title and optionally one button. That’s it. It does not have a close button, it cannot have multiple buttons or arbitrary children. In exchange, it’s easier to use, behaves consistently and has an adaptive layout:

Wide screenshot of AdwBanner. The button is on the right, the title is centered.

Medium-width screenshot of AdwBanner. The button is on the right, the title is left-aligned as it wouldn't fit centered.

Narrow screenshot of AdwBanner. The title is center-aligned, the button is centered below it.

Like GtkInfoBar, AdwBanner has a built-in revealer and can be shown and hidden with an animation.

There are situations where it cannot be used, but in most of those cases GtkInfobar was already the wrong choice and they should be redesigned. For example, Epiphany was using them for the “save password” prompt, and is using a popover for that now.

Tab Overview

A work-in-progress grid-based tab overview A work-in-progress carousel-based tab overview

AdwTabOverview is a new tab overview widget for AdwTabView that finally makes it possible to use tabs on mobile devices without implementing a mobile switcher manually.

Back when I wrote HdyTabView, I mentioned a tab overview widget in the works, and even had demo screenshots. Of course, basically everything from that demo got rewritten since then, and the carousel for narrow mode got scrapped completely, but now we’re getting into The Ship of Theseus territory.

This required a pretty big rework of AdwTabView to allow tabs to have thumbnails when they are not visible, and in particular it does not use a GtkStack internally anymore.

By default the selected tab has a live thumbnail and other thumbnails are static, but apps can opt into using live thumbnails for specific pages. They can also control the thumbnail alignment in case the thumbnail gets clipped. Thumbnails themselves are currently not public, but it might be interesting to use them for e.g. tooltips at some point.

Overview is not currently used very widely – it’s available in Console, and there is a merge request adding it to Epiphany, but I didn’t have the energy to finish the latter this cycle.

Tab Button

Screenshot of AdwTabButton. It's showing two tab buttons. One displays 3 open tabs, the other one 15 and has an attention indicator

AdwTabButton is much less interesting, it’s just a button that shows the number of open tabs in a given AdwTabView. It’s intended to be used as the button that opens the tab overview on mobile devices.

Unlike tab overview, this widget is more or less a direct port of what Epiphany has been using since 3.34. It does have one new feature though – it can display an indicator if a tab needs attention.

Accessibility

You might have noticed that widgets like AdwViewStack, AdwTabView or AdwEntryRow were not accessible in libadwaita 1.2.x. The reason for that is that GTK didn’t provide public API to make that possible. While GtkAccessible existed, it wasn’t possible to implement it outside GTK itself.

This cycle, Lukáš Tyrychtr has implemented the missing pieces, as well as fixed a few other issues. And so libadwaita widgets are now properly accessible.

Animation Additions

One of AdwAnimation features is that it automatically follows the system setting for disabling animations. While this is the expected behavior in most cases, there are a few where it gets in the way instead.

One of these cases is in apps where the animations are the app’s primary content, such as Elastic, and so I added a property that allows a specific animation to ignore the setting.

The animation in the demo is now using it as well, for the same reason as Elastic, and in the future it will allow us to have working spinners while animations are disabled system-wide as well.

A screenshot of an animation graph from Elastic

Elastic also draws an animation graph before running the actual animation, and while for timed animations it’s relatively easy since easing functions are available through the public API, it wasn’t possible for spring animations. It is now, via calculate_value() and calculate_velocity().

All of this uncovered a few bugs with spring animations – for example, the velocity was completely wrong for overdamped springs, and Manuel Genovés fixed them.

Unrelated to the above, a common complaint about AdwPropertyAnimationTarget was that it prints a critical if the object containing the property it’s animating is finalized before the target. While it was easy to avoid in C, it was nearly impossible from bindings. And so we don’t print that critical anymore.

Other Changes

  • Christopher Davis added a way to make AdwActionRow subtitle selectable.
  • Matt Jakeman added title-lines and subtitle-lines properties to AdwExpanderRow, matching AdwActionRow.
  • AdwEntryRow now has a grab_focus_without_selecting() method, matching GtkEntry.
  • The API to set an icon on AdwActionRow and AdwExpanderRow are now deprecated, since they were mostly unused. Apps that need icons can add a GtkImage as a prefix widget instead.
  • AdwMessageDialog now has the async choose() method, matching the new GTK dialogs like GtkAlertDialog. The response signal is still there and is not deprecated, but in some cases the new method may be more convenient, particularly from bindings:

    [GtkCallback]
    private async void clicked_cb () {
        var dialog = new Adw.MessageDialog (
            this,
            "Replace File?",
            "A file named “example.png” already exists. Do you want to replace it?"
        );
    
        dialog.add_response ("cancel", "_Cancel");
        dialog.add_response ("replace", "_Replace");
    
        dialog.set_response_appearance (
            "replace",
            DESTRUCTIVE
        );
    
        var response = yield dialog.choose (null);
    
        if (response == "replace") {
            // handle replacing
        }
    }
    
  • Corey Berla added missing drag-n-drop related API to AdwTabBar to make it work properly in Nautilus.
  • Since GTK now allows to change texture filtering, AdwAvatar properly scales custom images, so they don’t appear pixelated when downscaled or blurry when upscaled. This only works if the custom image is a GdkTexture – if your app is using a different GdkPaintable, you will need to do the equivalent change yourself.
  • Jason Francis implemented dark style and high contrast support when running on Windows.
  • Selected items in lists and grids are now using accent color instead of grey, same as Nautilus in 43. Sidebars and menus still look the same as before.

New Dependencies

The accessibility additions, scaled texture render nodes in AdwAvatar, as well as mask render nodes (that I didn’t mention because it’s an internal change) and deprecation fixes mean that libadwaita 1.3 requires GTK 4.10 instead of 4.6.


As always, thanks to all the contributors, and thanks to my employer, Purism, for letting me work on libadwaita and GTK to make this release happen.

Status update 17/03/2023

Hello from my parents place, sitting on the border of Wales & England, listening to this excellent Victor Rice album, thinking of this time last year when I actually got to watch him play at Freedom Sounds Festival, which was one of my first adventures of the post-lockdown 2020s.

I have many distractions at the moment, many being work/life admin but here are some of the more interesting ones:

  • Playing in a new band in Santiago – Killo Karallo – recording some initial music which is to come out next week
  • Preparing new Vladimir Chicken music, also cooked and ready for release in April
  • Figuring out how we can grow the GNOME OpenQA tests while keeping them fun to work with. Here’s an experimental commandline tool which might help with that.
  • Learning about marketing, analytics, and search engine optimization.
  • Trying out the new LLaMA language model and generally trying to keep up with the ongoing revolution in content generation technology.

Also I got to see real snow for the first time in a few years! Thanks Buxton!



March 16, 2023

Register for Linux App Summit 2023!

LAS 2023 is happening next month and registrations are open!

You can  check the schedule in https://conf.linuxappsummit.org/event/5/timetable/#20230422

We are excited to have you visiting us in Brno, Czech Republic. The conference starts on Friday, April 21st, with a pre-registration social event. Saturday and Sunday are full of interesting talks, panels, workshops, and more!

March 15, 2023

Portfolio 0.9.15

After a long hiatus, a new release of Portfolio is out 📱🤓. This new release comes with important bug fixes, small-detail additions and a few visual improvements.

In terms of visuals, by popular demand, the most notable change is the use of regular icons for the files browser view. It should be easier now to quickly catch what each file is about. Thanks to @AngelTomkins for the initial implementation, @Exalm for helping with the reviews, and to the GNOME design team for such lovely new icons.

Another addition is support for system-wide style management. This is specially useful now that desktops like GNOME provide quick toggle buttons to switch between dark and light modes. Thanks to @pabloyoyoista for the initial implementation.

One small-detail change to the properties view is the addition of file permissions.  Plus, the properties view was broken down into three different sections to reduce the visual load, and labels can now be selected which is useful for copying locations or ellipsized values.

Moving on to bug fixes, two important changes landed. The first one solves an issue which prevented opening files with special characters 🤦. Thanks to @jwaataja for detecting and fixing this issue. The second one solves an issue with Portfolio not properly detecting mount points under some specific conditions. Thanks to mo2mo for reaching out and sharing his system details, so I could figure this out.

Last but never least, many thanks to @carlosgonz0, @Vistaus, @rffontenelle, @AsciiWolf, and @eson57 for keeping translations up to date, and  to @rene-coty for the new French translation.

March 10, 2023

pre-initialization of garbage-collected webassembly heaps

Hey comrades, I just had an idea that I won't be able to work on in the next couple months and wanted to release it into the wild. They say if you love your ideas, you should let them go and see if they come back to you, right? In that spirit I abandon this idea to the woods.

Basically the idea is Wizer-like pre-initialization of WebAssembly modules, but for modules that store their data on the GC-managed heap instead of just in linear memory.

Say you have a WebAssembly module with GC types. It might look like this:

(module
  (type $t0 (struct (ref eq)))
  (type $t1 (struct (ref $t0) i32))
  (type $t2 (array (mut (ref $t1))))
  ...
  (global $g0 (ref null eq)
    (ref.null eq))
  (global $g1 (ref $t1)
    (array.new_canon $t0 (i31.new (i32.const 42))))
  ...
  (function $f0 ...)
  ...)

You define some struct and array types, there are some global variables, and some functions to actually do the work. (There are probably also tables and other things but I am simplifying.)

If you consider the object graph of an instantiated module, you will have some set of roots R that point to GC-managed objects. The live objects in the heap are the roots and any object referenced by a live object.

Let us assume a standalone WebAssembly module. In that case the set of types T of all objects in the heap is closed: it can only be one of the types $t0, $t1, and so on that are defined in the module. These types have a partial order and can thus be sorted from most to least specific. Let's assume that this sort order is just the reverse of the definition order, for now. Therefore we can write a general type introspection function for any object in the graph:

(func $introspect (param $obj anyref)
  (block $L2 (ref $t2)
    (block $L1 (ref $t1)
      (block $L0 (ref $t0)
        (br_on_cast $L2 $t2 (local.get $obj))
        (br_on_cast $L1 $t1 (local.get $obj))
        (br_on_cast $L0 $t0 (local.get $obj))
        (unreachable))
      ;; Do $t0 things...
      (return))
    ;; Do $t1 things...
    (return))
  ;; Do $t2 things...
  (return))

In particular, given a WebAssembly module, we can generate a function to trace edges in an object graph of its types. Using this, we can identify all live objects, and what's more, we can take a snapshot of those objects:

(func $snapshot (result (ref (array (mut anyref))))
  ;; Start from roots, use introspect to find concrete types
  ;; and trace edges, use a worklist, return an array of
  ;; all live objects in topological sort order
  )

Having a heap snapshot is interesting for introspection purposes, but my interest is in having fast start-up. Many programs have a kind of "initialization" phase where they get the system up and running, and only then proceed to actually work on the problem at hand. For example, when you run python3 foo.py, Python will first spend some time parsing and byte-compiling foo.py, importing the modules it uses and so on, and then will actually run foo.py's code. Wizer lets you snapshot the state of a module after initialization but before the real work begins, which can save on startup time.

For a GC heap, we actually have similar possibilities, but the mechanism is different. Instead of generating an array of all live objects, we could generate a serialized state of the heap as bytecode, and another function to read the bytecode and reload the heap:

(func $pickle (result (ref (array (mut i8))))
  ;; Return an array of bytecode which, when interpreted,
  ;; can reconstruct the object graph and set the roots
  )
(func $unpickle (param (ref (array (mut i8))))
  ;; Interpret the bytecode, building object graph in
  ;; topological order
  )

The unpickler is module-dependent: it will need one case to construct each concrete type $tN in the module. Therefore the bytecode grammar would be module-dependent too.

What you would get with a bytecode-based $pickle/$unpickle pair would be the ability to serialize and reload heap state many times. But for the pre-initialization case, probably that's not precisely what you want: you want to residualize a new WebAssembly module that, when loaded, will rehydrate the heap. In that case you want a function like:

(func $make-init (result (ref (array (mut i8))))
  ;; Return an array of WebAssembly code which, when
  ;; added to the module as a function and invoked, 
  ;; can reconstruct the object graph and set the roots.
  )

Then you would use binary tools to add that newly generated function to the module.

In short, there is a space open for a tool which takes a WebAssembly+GC module M and produces M', a module which contains a $make-init function. Then you use a WebAssembly+GC host to load the module and call the $make-init function, resulting in a WebAssembly function $init which you then patch in to the original M to make M'', which is M pre-initialized for a given task.

Optimizations

Some of the object graph is constant; for example, an instance of a struct type that has no mutable fields. These objects don't have to be created in the init function; they can be declared as new constant global variables, which an engine may be able to initialize more efficiently.

The pre-initialized module will still have an initialization phase in which it builds the heap. This is a constant function and it would be nice to avoid it. Some WebAssembly hosts will be able to run pre-initialization and then snapshot the GC heap using lower-level facilities (copy-on-write mappings, pointer compression and relocatable cages, pre-initialization on an internal level...). This would potentially decrease latency and may allow for cross-instance memory sharing.

Limitations

There are five preconditions to be able to pickle and unpickle the GC heap:

  1. The set of concrete types in a module must be closed.

  2. The roots of the GC graph must be enumerable.

  3. The object-graph edges from each live object must be enumerable.

  4. To prevent cycles, we have to know when an object has been visited: objects must have identity.

  5. We must be able to create each type in a module.

I think there are three limitations to this pre-initialization idea in practice.

One is externref; these values come from the host and are by definition not introspectable by WebAssembly. Let's keep the closed-world assumption and consider the case where the set of external reference types is closed also. In that case if a module allows for external references, we can perhaps make its pickling routines call out to the host to (2) provide any external roots (3) identify edges on externref values (4) compare externref values for identity and (5) indicate some imported functions which can be called to re-create exernal objects.

Another limitation is funcref. In practice in the current state of WebAssembly and GC, you will only have a funcref which is created by ref.func, and which (3) therefore has no edges and (5) can be re-created by ref.func. However neither WebAssembly nor the JS API has no way of knowing which function index corresponds to a given funcref. Including function references in the graph would therefore require some sort of host-specific API. Relatedly, function references are not comparable for equality (func is not a subtype of eq), which is a little annoying but not so bad considering that function references can't participate in a cycle. Perhaps a solution though would be to assume (!) that the host representation of a funcref is constant: the JavaScript (e.g.) representations of (ref.func 0) and (ref.func 0) are the same value (in terms of ===). Then you could compare a given function reference against a set of known values to determine its index. Note, when function references are expanded to include closures, we will have more problems in this area.

Finally, there is the question of roots. Given a module, we can generate a function to read the values of all reference-typed globals and of all entries in all tables. What we can't get at are any references from the stack, so our object graph may be incomplete. Perhaps this is not a problem though, because when we unpickle the graph we won't be able to re-create the stack anyway.

OK, that's my idea. Have at it, hackers!

Maps and GNOME 44

 So it's that time that occurs twice a year when we're approaching a new GNOME release, this time 44.



In Maps there's been some polish touching up some of the rough edges in 43.x (from the GTK 4 port).

For example keyboard navigation of the search results is back.


Also, the focus handling of the entry is fixed so that the quirky flickering (re-popping up the the results popover when typing a space into the search entry) is now gone.

Also thanks to Christopher Davis for fixing up the styling of the results!

We also had contributions from an anonymous user improving the style of some labels, among others the tooltips of the headerbar buttons.


Adrien Plazas has worked on making the „Export as Image“ dialog fit on phones.


Unfortunately an issue has come to our attention with the way the „go to“ animations works, resulting some times when covering large distances (for example when selecting a search result far away) that results in throttling from the tile server (HTTP 429, „too many requests”). This results in empty tiles showing up at the destination (and one needs to sometimes wait a while before Maps gets the view refreshed with new tiles (after restarting to force re-trying).

 As this results in a really bad user experience as a work-around we have disabled the animations for now (the animations when zooming is still in place, and of course panning the map view still works as usual).

I also cherry-picked this for the 43 branch, and will probably also cut a 43.4 with this change (versions prior to 43.0 is not affected, as the animations in libchamplain uses another algorithm, first zooming out „high“ (a low zoom level) before moving, resulting in less intermediate tiles compared to the “fly to“-like animations now used in libshumate.

 We will try to come up with a better solution in libshumate soon. Either reverting to an approach like that in libchamplain, or try to implement our own rate limiting of some sort during animations to avoid firing off too many requests.

 Meanwhile at the libshumate side James Westman has been busy working on the vector tile support implementing label support among other things.

He is also working on a vector tile style using the GNOME color palette in light and dark variants.


This can be trying out on https://maps.jwestman.net/

 There will also be some other things coming for the next development cycle for GNOME 45, but let's save that for next time!

March 07, 2023

Flathub in 2023

It’s been quite a few months since the most recent updates about Flathub last year. We’ve been busy behind the scenes, so I’d like to share what we’ve been up to at Flathub and why—and what’s coming up from us this year. I want to focus on:

  • Where Flathub is today as a strong ecosystem with 2,000 apps
  • Our progress on evolving Flathub from a build service to an app store
  • The economic barrier to growing the ecosystem, and its consequences
  • What’s next to overcome our challenges with focused initiatives

Today

Flathub is going strong: we offer 2,000 apps from over 1,500 collaborators on GitHub. We’re averaging 700,000 app downloads a day, with 898 million HTTP requests totalling 88.3 TB served by our CDN each day (thank you Fastly!). Flatpak has, in my opinion, solved the largest technical issue which has held back the mainstream growth and acceptance of Linux on the desktop (or other personal computing devices) for the past 25 years: namely, the difficulty for app developers to publish their work in a way that makes it easy for people to discover, download (or sideload, for people in challenging connectivity environments), install and use. Flathub builds on that to help users discover the work of app developers and helps that work reach users in a timely manner.

Initial results of this disintermediation are promising: even with its modest size so far, Flathub has hundreds of apps that I have never, ever heard of before—and that’s even considering I’ve been working in the Linux desktop space for nearly 20 years and spent many of those staring at the contents of dselect (showing my age a little) or GNOME Software, attending conferences, and reading blog posts, news articles, and forums. I am also heartened to see that many of our OS distributor partners have recognised that this model is hugely complementary and additive to the indispensable work they are doing to bring the Linux desktop to end users, and that “having more apps available to your users” is a value-add allowing you to focus on your core offering and not a zero-sum game that should motivate infighting.

Ongoing Progress

Getting Flathub into its current state has been a long ongoing process. Here’s what we’ve been up to behind the scenes:

Development

Last year, we concluded our first engagement with Codethink to build features into the Flathub web app to move from a build service to an app store. That includes accounts for users and developers, payment processing via Stripe, and the ability for developers to manage upload tokens for the apps they control. In parallel, James Westman has been working on app verification and the corresponding features in flat-manager to ensure app metadata accurately reflects verification and pricing, and to provide authentication for paying users for app downloads when the developer enables it. Only verified developers will be able to make direct uploads or access payment settings for their apps.

Legal

So far, the GNOME Foundation has acted as an incubator and legal host for Flathub even though it’s not purely a GNOME product or initiative. Distributing software to end users along with processing and forwarding payments and donations also has a different legal profile in terms of risk exposure and nonprofit compliance than the current activities of the GNOME Foundation. Consequently, we plan to establish an independent legal entity to own and operate Flathub which reduces risk for the GNOME Foundation, better reflects the independent and cross-desktop interests of Flathub, and provides flexibility in the future should we need to change the structure.

We’re currently in the process of reviewing legal advice to ensure we have the right structure in place before moving forward.

Governance

As Flathub is something we want to set outside of the existing Linux desktop and distribution space—and ensure we represent and serve the widest community of Linux users and developers—we’ve been working on a governance model that ensures that there is transparency and trust in who is making decisions, and why. We have set up a working group with myself and Martín Abente Lahaye from GNOME, Aleix Pol Gonzalez, Neofytos Kolokotronis, and Timothée Ravier from KDE, and Jorge Castro flying the flag for the Flathub community. Thanks also to Neil McGovern and Nick Richards who were also more involved in the process earlier on.

We don’t want to get held up here creating something complex with memberships and elections, so at first we’re going to come up with a simple/balanced way to appoint people into a board that makes key decisions about Flathub and iterate from there.

Funding

We have received one grant for 2023 of $100K from Endless Network which will go towards the infrastructure, legal, and operations costs of running Flathub and setting up the structure described above. (Full disclosure: Endless Network is the umbrella organisation which also funds my employer, Endless OS Foundation.) I am hoping to grow the available funding to $250K for this year in order to cover the next round of development on the software, prepare for higher operations costs (e.g., accounting gets more complex), and bring in a second full-time staff member in addition to Bartłomiej Piotrowski to handle enquiries, reviews, documentation, and partner outreach.

We’re currently in discussions with NLnet about funding further software development, but have been unfortunately turned down for a grant from the Plaintext Group for this year; this Schmidt Futures project around OSS sustainability is not currently issuing grants in 2023. However, we continue to work on other funding opportunities.

Remaining Barriers

My personal hypothesis is that our largest remaining barrier to Linux desktop scale and impact is economic. On competing platforms—mobile or desktop—a developer can offer their work for sale via an app store or direct download with payment or subscription within hours of making a release. While we have taken the “time to first download” time down from months to days with Flathub, as a community we continue to have a challenging relationship with money. Some creators are lucky enough to have a full-time job within the FLOSS space, while a few “superstar” developers are able to nurture some level of financial support by investing time in building a following through streaming, Patreon, Kickstarter, or similar. However, a large proportion of us have to make do with the main payback from our labours being a stream of bug reports on GitHub interspersed with occasional conciliatory beers at FOSDEM (other beverages and events are available).

The first and most obvious consequence is that if there is no financial payback for participating in developing apps for the free and open source desktop, we will lose many people in the process—despite the amazing achievements of those who have brought us to where we are today. As a result, we’ll have far fewer developers and apps. If we can’t offer access to a growing base of users or the opportunity to offer something of monetary value to them, the reward in terms of adoption and possible payment will be very small. Developers would be forgiven for taking their time and attention elsewhere. With fewer apps, our platform has less to entice and retain prospective users.

The second consequence is that this also represents a significant hurdle for diverse and inclusive participation. We essentially require that somebody is in a position of privilege and comfort that they have internet, power, time, and income—not to mention childcare, etc.—to spare so that they can take part. If that’s not the case for somebody, we are leaving them shut out from our community before they even have a chance to start. My belief is that free and open source software represents a better way for people to access computing, and there are billions of people in the world we should hope to reach with our work. But if the mechanism for participation ensures their voices and needs are never represented in our community of creators, we are significantly less likely to understand and meet those needs.

While these are my thoughts, you’ll notice a strong theme to this year will be leading a consultation process to ensure that we are including, understanding and reflecting the needs of our different communities—app creators, OS distributors and Linux users—as I don’t believe that our initiative will be successful without ensuring mutual benefit and shared success. Ultimately, no matter how beautiful, performant, or featureful the latest versions of the Plasma or GNOME desktops are, or how slick the newly rewritten installer is from your favourite distribution, all of the projects making up the Linux desktop ecosystem are subdividing between ourselves an absolutely tiny market share of the global market of personal computers. To make a bigger mark on the world, as a community, we need to get out more.

What’s Next?

After identifying our major barriers to overcome, we’ve planned a number of focused initiatives and restructuring this year:

Phased Deployment

We’re working on deploying the work we have been doing over the past year, starting first with launching the new Flathub web experience as well as the rebrand that Jakub has been talking about on his blog. This also will finally launch the verification features so we can distinguish those apps which are uploaded by their developers.

In parallel, we’ll also be able to turn on the Flatpak repo subsets that enable users to select only verified and/or FLOSS apps in the Flatpak CLI or their desktop’s app center UI.

Consultation

We would like to make sure that the voices of app creators, OS distributors, and Linux users are reflected in our plans for 2023 and beyond. We will be launching this in the form of Flathub Focus Groups at the Linux App Summit in Brno in May 2023, followed up with surveys and other opportunities for online participation. We see our role as interconnecting communities and want to be sure that we remain transparent and accountable to those we are seeking to empower with our work.

Whilst we are being bold and ambitious with what we are trying to create for the Linux desktop community, we also want to make sure we provide the right forums to listen to the FLOSS community and prioritise our work accordingly.

Advisory Board

As we build the Flathub organisation up in 2023, we’re also planning to expand its governance by creating an Advisory Board. We will establish an ongoing forum with different stakeholders around Flathub: OS vendors, hardware integrators, app developers and user representatives to help us create the Flathub that supports and promotes our mutually shared interests in a strong and healthy Linux desktop community.

Direct Uploads

Direct app uploads are close to ready, and they enable exciting stuff like allowing Electron apps to be built outside of flatpak-builder, or driving automatic Flathub uploads from GitHub actions or GitLab CI flows; however, we need to think a little about how we encourage these to be used. Even with its frustrations, our current Buildbot ensures that the build logs and source versions of each app on Flathub are captured, and that the apps are built on all supported architectures. (Is 2023 when we add RISC-V? Reach out if you’d like to help!). If we hand upload tokens out to any developer, even if the majority of apps are open source, we will go from this relatively structured situation to something a lot more unstructured—and we fear many apps will be available on only 64-bit Intel/AMD machines.

My sketch here is that we need to establish some best practices around how to integrate Flathub uploads into popular CI systems, encouraging best practices so that we promote the properties of transparency and reproducibility that we don’t want to lose. If anyone is a CI wizard and would like to work with us as a thought partner about how we can achieve this—make it more flexible where and how build tasks can be hosted, but not lose these cross-platform and inspectability properties—we’d love to hear from you.

Donations and Payments

Once the work around legal and governance reaches a decent point, we will be in the position to move ahead with our Stripe setup and switch on the third big new feature in the Flathub web app. At present, we have already implemented support for one-off payments either as donations or a required purchase. We would like to go further than that, in line with what we were describing earlier about helping developers sustainably work on apps for our ecosystem: we would also like to enable developers to offer subscriptions. This will allow us to create a relationship between users and creators that funds ongoing work rather than what we already have.

Security

For Flathub to succeed, we need to make sure that as we grow, we continue to be a platform that can give users confidence in the quality and security of the apps we offer. To that end, we are planning to set up infrastructure to help ensure developers are shipping the best products they possibly can to users. For example, we’d like to set up automated linting and security scanning on the Flathub back-end to help developers avoid bad practices, unnecessary sandbox permissions, outdated dependencies, etc. and to keep users informed and as secure as possible.

Sponsorship

Fundraising is a forever task—as is running such a big and growing service. We hope that one day, we can cover our costs through some modest fees built into our payments—but until we reach that point, we’re going to be seeking a combination of grant funding and sponsorship to keep our roadmap moving. Our hope is very much that we can encourage different organisations that buy into our vision and will benefit from Flathub to help us support it and ensure we can deliver on our goals. If you have any suggestions of who might like to support Flathub, we would be very appreciative if you could reach out and get us in touch.

Finally, Thank You!

Thanks to you all for reading this far and supporting the work of Flathub, and also to our major sponsors and donors without whom Flathub could not exist: GNOME Foundation, KDE e.V., Mythic Beasts, Endless Network, Fastly, and Equinix Metal via the CNCF Community Cluster. Thanks also to the tireless work of the Freedesktop SDK community to give us the runtime platform most Flatpaks depend on, particularly Seppo Yli-Olli, Codethink and others.

I wanted to also give my personal thanks to a handful of dedicated people who keep Flathub working as a service and as a community: Bartłomiej Piotrowski is keeping the infrastructure working essentially single-handedly (in his spare time from keeping everything running at GNOME); Kolja Lampe and Bart built the new web app and backend API for Flathub which all of the new functionality has been built on, and Filippe LeMarchand maintains the checker bot which helps keeps all of the Flatpaks up to date.

And finally, all of the submissions to Flathub are reviewed to ensure quality, consistency and security by a small dedicated team of reviewers, with a huge amount of work from Hubert Figuière and Bart to keep the submissions flowing. Thanks to everyone­—named or unnamed—for building this vision of the future of the Linux desktop together with us.

(originally posted to Flathub Discourse, head there if you have any questions or comments)

3D Printing on Endless OS

Screenshot

I recently got a 3D printer for my birthday, and I have been absolutely loving tinkering, printing, and learning with it. There is such a wealth of openly-shared and -licensed models out there, I’ve been having fun finding things to print for myself, my friends, and especially my toddler. The best part for me, though? The fact that I can dive deep into this whole new world of 3D modeling, slicing, and printing using the computer I already have with freely-available software right in the app store I already use on Endless OS: App Center with Flathub.

I got a Creality Ender 3 v2 printer, which is to say: a popular and well-reviewed printer that didn’t break the bank. For a super quick (and over-simplified) look at the 3D printing process, there are a few steps to get from nothing to a physical 3D object in your hands:

  1. Design a thing; this can be on a whiteboard, in a 2D design app, or whatever. The point is, you have to start with some sort of specification/measurements.

  2. Model that thing; you have to take the idea you have and turn it into a 3D shape on the computer. You can use a modeling program like Blender if you know what you’re doing or want to use your model in different contexts, or you can use a computer-aided drawing (CAD) tool to define the shapes for your object.

  3. Slice the model; this is the process of taking a defined 3D object and turning it into actual instructions for your specific printer, filament, etc. 3D printer companies usually offer free slicing programs designed with their own printers in mind, but it’s mostly all inter-compatible and based on the same few open source projects.

  4. Print the model; slicing outputs a GCODE file, which is very literal instructions for your printer to follow. You have to get that GCODE file onto your printer, either with an SD card, a USB connection, or a network connection. Then it can be printed!

With all the open source software that powers 3D printing combined with the open sharing culture, you can accomplish each of these steps on Endless OS or any other Linux-based OS that has access to Flathub. Let’s look at each step and the options you have:

1. Design a thing

I love whiteboards, so I usually start there if I’m designing a new thing from scratch. I’ll sketch a few angles out until I’m confident the measurements make sense, then I’ll move on to modeling.

However, if you’re trying to convert a 2D object into 3D, or you just want to play around in a 2D space first, I recommend Inkscape! It’s a great vector image editor, and it’s available on Flathub. You can lay things out and make sure they make sense in 2D space before moving onto the more complex 3D modeling.

If you already live in Blender, you can of course use it to design your object from the start, as well. It’s also available on Flathub.

2. Model that thing

Modeling is my weakest point personally, as I haven’t taken the time to really learn Blender or the open source CAD tools. So for me, I only tackle simple 3D models—and I use the web-based Autodesk Tinkercad to do it.

Screenshot of Tinkercad in Chromium

Luckily on Endless OS you can trivially install web apps like Tinkercad from the included Chromium web browser; just hit the little “Install” button that pops up on the address bar when you’re on the site. Then Tinkercad’s icon will be added to the app grid on your home screen and you can add it to your favorites on the dash.

GNOME Web also supports web apps, Tinkercad works there as well. If you use Google Chrome or any other browser based on Chromium, it should also have an easy to use install button. If you use Firefox, you can always use Tinkercad from in the browser itself.

If you’re more experienced (or adventurous than I am now), you can also try something like FreeCAD or OpenSCAD from Flathub—available in App Center right out of the box on Endless OS.

3. Slice the model

Slicing is the most exciting step of the process to me, if only because people keep innovating in 3D printing purely on the software side to get more speed, performance, etc. out of existing hardware—and that all happens in the slicers.

Popular 3D printer companies like UltiMaker/MakerBot, Prusa Labs, and FlashForge have their slicers available on Flathub. While others might offer downloads for their software on their website, my experience has been it’s less integrated over all; I’d stick to the ones on Flathub.

If whoever makes your printer doesn’t have a slicer on Flathub, fret not! They’re basically all inter-compatible, as the slicers are all based on the same handful of open source projects. Personally, I prefer UltiMaker Cura, and I could even add my Creality-brand Ender 3 v2 from in Cura’s UI with all the right preset values. Some people prefer PrusaSlicer, and it’s a similar story there.

4. Print the model

The simplest/most old-school way of printing is to save your sliced GCODE file out to a microSD card, eject it, then stick it in your printer, and use your printer’s physical screen and wheel to select the print file. And that will work just fine! You can also connect your printer over USB and print directly from your slicing software.

Screenshot of Prusa's GCODE viewer inspecting a model

If you want to learn more about how your model will print before actually printing, you can use your slicer or a separate program to preview the GCODE file. While Cura has a preview built in, sometimes I like using PrusaSlicer’s included GCODE viewer app (installed alongside PrusaSlicer) to inspect a specific GCODE file, too.

Screenshot of the Raspberry Pi imager with OctoPi selected

If you really want to go all out and have a spare Raspberry Pi, you can install OctoPrint with the OctoPi OS, and print to, monitor, and control your printer remotely. I won’t get into all the details, but you can get started with the official Raspberry Pi Imager by navigating to install an “other specific-purpose OS,” “3D printing,” and then “OctoPi.”


Hopefully these apps and tips are useful to you! I’ve been super happy with the experience of designing, modeling, slicing, and printing all entirely from Endless OS—and if you have access to Flathub on your OS of choice, you can do it, too!

March 06, 2023

Building a big-endian Arm system in Yocto

For reasons I won't bore anyone with I needed to build a 32-bit big-endian system with the Yocto Project to test a package, and I thought I'd write the steps down in case I ever need to do it again (or, even more unlikely, someone else needs to do it).

For unsurprising reasons I thought I'd do a big-endian Arm build. So we start by picking the qemuarm machine, which is a Armv7-A processor (Cortex-A15, specifically) in little-endian mode by default.

MACHINE = "qemuarm"

qemumarm.conf requires tune-cortexa15.inc which then requires arch-armv7ve.inc, and this file defines the base tunes. The default tune is armv7ve, we can make it big-endian by simply adding a b:

DEFAULTTUNE:qemuarm = "armv7veb"

And now we just build an image:

$ MACHINE=qemuarm bitbake core-image-minimal
...
Summary: 4 tasks failed:
  .../poky/meta/recipes-kernel/linux/linux-yocto_6.1.bb:do_package_qa
  .../poky/meta/recipes-graphics/xorg-proto/xorgproto_2022.2.bb:do_configure
  .../poky/meta/recipes-core/glib-2.0/glib-2.0_2.74.5.bb:do_configure
  .../poky/meta/recipes-graphics/wayland/wayland_1.21.0.bb:do_configure

Or not.

There are two failure cases here. First, the kernel:

ERROR: linux-yocto-6.1.9+gitAUTOINC+d7393c5752_ccd3b20fb5-r0 do_package_qa: QA Issue: Endiannes did not match (1, expected 0) in /lib/modules/6.1.9-yocto-standard/kernel/net/ipv4/ah4.ko [arch]

It turns out the kernel needs to be configured specifically to be big or little endian, and the default configuration is, predictably, little endian. There is a bug open to make this automatic, but big-endian really is dead because it has been open since 2016. The solution is a quick kernel configuration fragment added to the kernel's SRC_URI:

CONFIG_CPU_BIG_ENDIAN=y
CONFIG_CPU_LITTLE_ENDIAN=n

With this, the kernel builds as expected. The second set of failures are all from Meson, failing to execute a target binary:

../xorgproto-2022.2/meson.build:22:0: ERROR: Executables created by c compiler armeb-poky-linux-gnueabi-gcc [...] are not runnable.

Meson is trying to run the target binaries in a qemu-user that we set up, but the problem here is to save build time we only build the qemu targets that are typically used. This doesn't include usermode big-endian 32-bit Arm, so this target needs enabling:

QEMU_TARGETS:append = " armeb"

Now the image builds successfully, and we discover that indeed gdbm refuses to open a database which was generated on a system with a different endian.

February 27, 2023

GSoC 2023: GNOME Foundation has been accepted as a mentoring org!

We are glad to announce that once again the GNOME Foundation will be part of Google Summer of Code. We are interested in onboarding new contributors that are passionate about GNOME and motivated to become long term GNOME developers!

@Contributors interested in participating in GSoC with GNOME should visit https://gsoc.gnome.org for more information.

@Mentors interested in mentoring projects this year should file a gitlab issue in Teams / Engagement / Internship Project Ideas · GitLab 2

Project ideas

Our ideas list is available in GNOME + GSoC | 2023 Project Ideas 2 and is the result of the discussions in Teams / Engagement / Internship Project Ideas · GitLab 2.

You can still submit project ideas until March 19th, when GSoC applicants are expected to submit their final proposals.

Important upcoming dates:

  • Now – March 19: Proactive GSoC contributors will reach out asking questions about your ideas list and receive feedback from us so they can start crafting their project proposals.

    @Contributors make sure you research enough about the project and work towards making a small contribution. You should consider the proposals available in GNOME + GSoC | 2023 Project Ideas or propose your own project ideas as soon as possible in Teams / Engagement / Internship Project Ideas · GitLab 2
    Make sure you approach potential mentors to move your idea towards an internship.

    @Mentors, point contributors to gsoc.gnome.org for more information and be patient with their questions. Contributors are open to suggest new project proposals and you should indicate whether you’d be interested in mentoring those proposals and help them compose a project proposal that is realistic and benefits the project.

  • March 20 – April 4 18:00 UTC: GSoC contributors will submit their proposals through the program website.
  • April 4 – 26: We (admins and mentors) will review all submitted GSoC Contributor proposals and consider how many we want to select (based on how many committed mentors we have available). Admins and mentors will rank the contribution proposals.
  • April 27 18:00 UTC: Deadline to submit ranked slot requests (Org Admins enter requests)
  • April 27 – May 2: Google Program Admins review and assign org slots
  • May 3: Organizations receive notification of their accepted GSoC 2023 Contributors
  • May 4: Accepted GSoC 2023 GSoC Contributor projects are announced
  • May 4 – 28: Community Bonding Period
  • May 27: Deadline to notify Google Admins of an inactive GSoC Contributor that you wish to remove from the program
  • May 29: Coding begins

For more information on the timeline, visit Google Summer of Code 2023 Timeline  |  Google Developers

If you have any doubts or questions, please reply to this message on Discourse.

February 25, 2023

Reducing code size in librsvg by removing an unnecessary generic struct

Someone mentioned cargo-bloat the other day and it reminded me that I have been wanting to measure the code size for generic functions in librsvg, and see if there are improvements to be made.

Cargo-bloat can give you a rough estimate of the code size for each Rust crate in a compiled binary, and also a more detailed view of the amount of code generated for individual functions. It needs a [bin] target to work on; if you have just a [lib], it will not do anything. So, for librsvg's purposes, I ran cargo-bloat on the rsvg-convert binary.

$ cargo bloat --release --crates
    Finished release [optimized] target(s) in 0.23s
    Analyzing target/release/rsvg-bench

 File  .text     Size Crate
10.0%  38.7%   1.0MiB librsvg
 4.8%  18.8% 505.5KiB std
 2.5%   9.8% 262.8KiB clap
 1.8%   7.1% 191.3KiB regex
 ... lines omitted ...
25.8% 100.0%   2.6MiB .text section size, the file size is 10.2MiB

Note: numbers above are a result of guesswork. They are not 100% correct and never will be.

The output above is for cargo bloat --release --crates. The --release option is to generate an optimized binary, and --crates tells cargo-bloat to just print a summary of crate sizes. The numbers are not completely accurate since, for example, inlined functions may affect callers of a particular crate. Still, this is good enough to start getting an idea of the sizes of things.

In this case, the librsvg crate's code is about 1.0 MB.

Now, let's find what generic functions we may be able to condense. When cargo-bloat is run without --crates, it prints the size of individual functions. After some experimentation, I ended up with cargo bloat --release -n 0 --filter librsvg. The -n 0 option tells cargo-bloat to print all functions, not just the top N biggest ones, and --filter librsvg is to make it print functions only in that crate, not for example in std or regex.

$ cargo bloat --release -n 0 --filter librsvg

File .text    Size   Crate Name
0.0%  0.0%  1.2KiB librsvg librsvg::element::ElementInner<T>::new
0.0%  0.0%  1.2KiB librsvg librsvg::element::ElementInner<T>::new
0.0%  0.0%  1.2KiB librsvg librsvg::element::ElementInner<T>::new
... output omitted ...
0.0%  0.0%    825B librsvg librsvg::element::ElementInner<T>::set_style_attribute
0.0%  0.0%    825B librsvg librsvg::element::ElementInner<T>::set_style_attribute
0.0%  0.0%    825B librsvg librsvg::element::ElementInner<T>::set_style_attribute
... output omitted ...
0.0%  0.0%    358B librsvg librsvg::element::ElementInner<T>::get_cond
0.0%  0.0%    358B librsvg librsvg::element::ElementInner<T>::get_cond
0.0%  0.0%    358B librsvg librsvg::element::ElementInner<T>::get_cond
... etc ...

After looking a bit at the output, I found the "duplicated" functions I wanted to find. What is happening here is that ElementInner<T> is a type with generics, and rustc is generating one copy of each of its methods for every type instance. So, there is one copy of each method for ElementInner<Circle>, one for ElementInner<Rect>, and so on for all the SVG element types.

The code around that is a bit convoluted; it's in a part of the library that hasn't gotten much cleanup after the C to Rust port and initial refactoring. Let's see what it is like.

The initial code

Librsvg parses the XML in an SVG document and builds something that resembles a DOM tree. The tree itself uses the rctree crate; it has reference-counted nodes and functions like first_child or next_sibling. Nodes can represent XML elements, or character content inside XML tags. Here we are interested in elements only.

Consider an element like this:

<path d="M0,0 L10,10 L0,10 Z" fill="black"/>

Let's look at how librsvg represents that. Inside each reference-counted node in an rctree, librsvg keeps a NodeData enum that can differentiate between elements and character content:

enum NodeData {
    Element(Element),
    Text(Chars),
}

Then, Element is an enum that can distinguish between all the elements in the svg namespace that librsvg supports:

enum Element {
    Circle(Box<ElementInner<Circle>>),
    Ellipse(Box<ElementInner<Ellipse>>),
    Path(Box<ElementInner<Path>>),
    // ... about 50 others omitted ...
}

Inside each of those enum's variants there is an ElementInner<T>, a struct with a generic type parameter. ElementInner holds the data for the DOM-like element:

struct ElementInner<T: ElementTrait> {
    element_name: QualName,
    attributes: Attributes,
    // ... other fields omitted
    element_impl: T,
}

For the <path> element above, this struct would contain the following:

  • element_name: a qualified name path with an svg namespace.
  • attributes: an array of (name, value) pairs, in this case (d, "M0,0 L10,10 L0,10 Z"), (fill, "black").
  • element_impl: A concrete type, Path in this case.

The specifics of the Path type are not terribly interesting here; it's just the internal representation for Bézier paths.

struct Path {
    path: Rc<SvgPath>,
}

Let's look at the details of the memory layout for all of this.

Initial memory layout

Here is how the enums and structs above are laid out in memory, in terms of allocations, without taking into account the rctree:Node that wraps a NodeData.

NodeData enum, and ElementInner<T> (description in text)

There is one allocated block for the NodeData enum, and that block holds the enum's discriminant and the embedded Element enum. In turn, the Element enum has its own discriminant and space for a Box (i.e. a pointer), since each of its variants just holds a single box.

That box points to an allocation for an ElementInner<T>, which itself contains a Path struct.

It is awkward that the fields to hold XML-isms like an element's name and its attributes are in ElementInner<T>, not in Element. But more importantly, ElementInner<T> has a little bunch of methods:

impl<T: ElementTrait> ElementInner<T> {
    fn new(...) -> ElementInner<T> {
        // lots of construction
    }

    fn element_name(&self) -> &QualName {
        ...
    }

    fn get_attributes(&self) -> &Attributes {
        ...
    }

    // A bunch of other methods
}

However, none but one of these methods actually use the element_impl: T field! That is, all of them do things that are common to all element types. The only method that really deals with the element_impl field is the ::draw() method, and the only thing it does is to delegate down to the concrete type's implementation of ::draw().

Removing that generic type

So, let's shuffle things around. I did this:

  • Turn enum Element into a struct Element, with the fields common to all element types.

  • Have an Element.element_data field...

  • ... that is of type ElementData, an enum that actually knows about all supported element types.

There are no types with generics in here:

struct Element {
    element_name: QualName,
    attributes: Attributes,
    // ... other fields omitted
    element_data: ElementData,
}

enum ElementData {
    Circle(Box<Circle>),
    Ellipse(Box<Ellipse>),
    Path(Box<Path>),
    // ...
}

Now the memory layout looks like this:

NodeData enum with boxes, Element, and ElementData (description in text)

One extra allocation, but let's see if this changes the code size.

Code size

We want to know the size of the .text section in the ELF file.

# old
$ objdump --section-headers ./target/release/rsvg-bench
Idx Name          Size      VMA               LMA               File off  Algn
 15 .text         0029fa17  000000000008a060  000000000008a060  0008a060  2**4
(2750999 bytes)

# new
Idx Name          Size      VMA               LMA               File off  Algn
 15 .text         00271ff7  000000000008b060  000000000008b060  0008b060  2**4
(2564087 bytes)

The new code is is 186912 bytes smaller. Not earth-shattering, but cargo-bloat no longer shows duplicated functions which have no reason to be monomorphized, since they don't touch the varying data.

old:

$ cargo bloat --release --crates
 File  .text     Size Crate
10.0%  38.7%   1.0MiB librsvg
# lines omitted
25.8% 100.0%   2.6MiB .text section size, the file size is 10.2MiB

new:

$ cargo bloat --release --crates
 File  .text     Size Crate
 9.2%  37.5% 939.5KiB librsvg
24.6% 100.0%   2.4MiB .text section size, the file size is 10.0MiB

Less code should help a bit with cache locality, but the functions involved are not in hot loops. Practically all of librsvg's time is spent in Cairo for rasterization, and Pixman for compositing.

Dynamic dispatch

All the concrete types (Circle, ClipPath, etc.) implement ElementTrait, which has things like a draw() method, although that is not visible in the types above. This is what is most convenient for librsvg; using Box<ElementTrait> for type erasure would be a little awkward there — we used it a long time ago, but not anymore.

Eventually the code needs to find the ElementTrait vtable that corresponds to each of ElementData's variants:

let data: &dyn ElementTrait = match self {
    ElementData::Circle(d) =>   &**d,
    ElementData::ClipPath(d) => &**d,
    ElementData::Ellipse(d) =>  &**d,
    // ...
};

data.some_method_in_the_trait(...);

The ugly &**d is to arrive at the &dyn ElementTrait that each variant implements. It will get less ugly when pattern matching for boxes gets stabilized in the Rust compiler.

This is not the only way of doing things. For librsvg it is convenient to actually know the type of an element, that is, to keep an enum of the known element types. Other kinds of code may be perfectly happy with the type erasure that happens when you have a Box<SomeTrait>. If that code needs to go back to the concrete type, an alternative is to use something like the downcast-rs crate, which lets you recover the concrete type inside the box.

Heap usage actually changed

You may notice in the diagrams below that the original NodeData didn't box its variants, but now it does.

Old:

enum NodeData {
    Element(Element),
    Text(Chars),
}

New:

enum NodeData {
    Element(Box<Element>),
    Text(Box<Chars>),
}

One thing I didn't notice during the first round of memory reduction is that the NodeData::Text(Chars) variant is not boxed. That is, the size of NodeData enum is the size of the biggest of Element and Chars, plus space for the enum's discriminant. I wanted to make both variants the same size, and by boxing them they occupy only a pointer each.

I measured heap usage for a reasonably large SVG:

India Roadway Map, from Wikimedia Commons

I used Valgrind's Massif to measure peak memory consumption during loading:

valgrind --tool=massif --massif-out-file=massif.out ./target/release/rsvg-bench --num-load 1 --num-render 0 India_roadway_map.svg
ms_print massif.out

The first thing that ms_print shows is an overview of the program's memory usage over time, and the list of snapshots it created. The following is an extract of its output for the new version of the code, where snapshot 36 is the one with peak memory usage:

MB
14.22^                                                                      : 
     |                                                @#::::::::::::::::::::: 
     |                                              @@@#:      :::: :: ::: :: 
     |                                            @@@@@#:      :::: :: ::: :: 
     |                                          @@@ @@@#:      :::: :: ::: :: 
     |                                        @@@ @ @@@#:      :::: :: ::: :: 
     |                                       @@@@ @ @@@#:      :::: :: ::: :: 
     |                                    @@@@@@@ @ @@@#:      :::: :: ::: :: 
     |                                  @@@@ @@@@ @ @@@#:      :::: :: ::: :: 
     |                                 @@ @@ @@@@ @ @@@#:      :::: :: ::: :::
     |                               @@@@ @@ @@@@ @ @@@#:      :::: :: ::: :::
     |                              @@ @@ @@ @@@@ @ @@@#:      :::: :: ::: :::
     |                             @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: :::
     |                          @@@@@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: :::
     |                        :@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
     |                     @@@:@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
     |                 @@@@@ @:@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
     |              :::@ @ @ @:@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
     |            :@:: @ @ @ @:@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
     |     @@@@::::@:: @ @ @ @:@@@ @@@ @@ @@ @@@@ @ @@@#:      :::: :: ::: ::@
   0 +----------------------------------------------------------------------->Mi
     0                                                                   380.9

Number of snapshots: 51
 Detailed snapshots: [3, 4, 5, 9, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 (peak), 50]

Since we are just measuring memory consumption during loading, the chart shows that memory usage climbs steadily until it peaks when the complete SVG is loaded, and then it stays more or less constant while librsvg does the initial CSS cascade.

The version of librsvg without changes shows this (note how the massif snapshot with peak usage is number 39 in this one):

--------------------------------------------------------------------------------
  n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
 39    277,635,004       15,090,640       14,174,848       915,792            0

That is, 15,090,640 bytes.

And after making the changes in memory layout, we get this:

--------------------------------------------------------------------------------
  n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
 36    276,041,810       14,845,456       13,935,702       909,754            0

I.e. after the changes, the peak usage of heap memory when the whole file is loaded is 14,845,456 bytes. So the changes above not only reduced the code size, but also slightly lowered memory consumption at runtime. Nice!

Wall-clock performance

This file is not huge — say, 15 MB when loaded — so whatever we gained in memory consumption is a negligible win. It's nice to know that code size can be reduced, but it is not a problem for librsvg either way.

I did several measurements of the time used by the old and new versions to render the same file, and there was no significant difference. This is because although we may get better cache locality and everything, the time spent executing the element-related code is much smaller than the rendering code. That is, Cairo takes up most of the runtime of rsvg-convert, and librsvg itself takes relatively little of it.

Conclusion

At least for this case, it was feasible to reduce the amount of code emitted for generics, since this is a case where we definitely didn't need generics! The code size in the ELF file's .text section shrank by 186912 bytes, out of 2.6 MB.

For code that does need generics, one can take different approaches. For example, a function that take arguments of type AsRef<Path> can first actually obtain the &Path, and then pass that to a function that does the real work. For example, from the standard library:

impl PathBuf {
    pub fn push<P: AsRef<Path>>(&mut self, path: P) {
        self._push(path.as_ref())
    }

    fn _push(&mut self, path: &Path) {
        // lots of code here
    }
}

The push function will be monomorphized into very tiny functions that call _push after converting what you passed to a &Path reference, but the big _push function is only emitted once.

There is also the momo crate, which helps doing similar things automatically. I have not used it yet, so I can't comment further on it.

You can see the patches for librsvg in the merge request.

February 24, 2023

Introducing Elastic

Elastic is a new spring animation editor app.

Screenshot of Elastic

Ever since 1.0, libadwaita has had spring animations. These animations aren’t controlled with a duration and an easing function, but instead with physical properties: damping ratio (or optionally damping), mass, stiffness and initial velocity, as well as epsilon. While this allows for a lot of control over the animation, it can be pretty hard to understand if you’re not familiar with physics involved, and to be truly useful it needs an editor.

So, Elastic is that editor. It provides a way to tweak each parameter, explains what they do, allows to preview the animation in various ways, and generates the code to create that animation.

Screenshot of the animation graph in Elastic

Backstory

This app has a pretty convoluted history.

At one point of time, before 1.0, me and Tobias wanted to rework libadwaita demo and Tobias made new mockups for it. At about the same time, I was experimenting to see how feasible spring animations would be and needed some playground. Since it was understood that this will eventually be in libadwaita, Tobias made this design a part of the new demo:

Spring animation demo mockups

So I implemented it about as closely as I could at the time, but since then a few things happened:

  • We decided that the new demo was too design-focused to work well as, well, a demo (one of the purposes of which is using it as a test case for libadwaita itself). So, instead, we decided to make the new design a separate app called Patterns. That app is stalled for other reasons as it needs features that libadwaita doesn’t provide at the moment. Hopefully, next cycle we should have everything needed to finish it.
  • We decided that the spring animation page was too involved for a demo and was better off separately. So I split that code out and ported it to Vala instead of C.
  • As part of his GSoC project, Manuel Genovés implemented proper spring animations instead of a hacky prototype, and I ported the editor to that.

And then… everyone kinda forgot about it – Patterns was still blocked on other things, Manuel implemented a simpler animation demo for libadwaita and this one was left out.

Fast forward two years, Tobias was reviewing an app in Circle and needed a way to play with spring animations. We realized we never did anything with the editor, so I turned it into a proper app, fixed the remaining bugs and made it generate code instead of just previewing animations. We named it Spring Editor (imaginative, I know) and my impression was that design tools were covered by the new app inclusion process, so I submitted it to incubator.

It turned out more complicated than expected, so I decided to ignore it and try and get it into GNOME Circle instead. So now the Spring Editor name is too generic, so the app got renamed again, this time to Elastic (thanks Christopher Davis for that name) and it’s on Flathub now.


Anyway, the app is finally out, it’s on Flathub, so have fun. :)

Download on Flathub

UPDATE: It’s in Circle now.

February 23, 2023

Tip for debugging refcounting issues: change ref calls to copy

Over the last couple of days I’ve been looking at a refcounting issue in GLib’s D-Bus implementation.

As with many things in GLib, the allocations in this code use refcounting, rather than new/free, to manage object lifecycles. This makes it quite hard to debug lifecycle issues, because the location of a bug (a ref or unref call which isn’t correct) can be quite far removed (in time and code) from where the effects of that bug become visible. This is because the effects of refcounting problems only become visible when an object’s refcount reaches zero, or when the program ends and its refcount still hasn’t reached zero.

While debugging this code, I tried an approach I haven’t before: changing some of the ref calls on the buggy object to be copy calls instead. (Specifically, changing g_object_ref() to g_dbus_message_copy().) That split up the lifecycle of the object into smaller pieces, narrowing down the sets of ref/unref calls which could be buggy. Ultimately, this allowed me to find some bugs in the code, and hopefully those are the bugs causing the refcounting issue. Since the issue is intermittent, it’s a bit hard to be sure.

This debugging approach was possible in this case because the object I was debugging is immutable, so passing around copies of it doesn’t affect the behaviour of other bits of code vs passing around the original. Hence this approach is only applicable in some situations. But it’s another good reason why using immutable objects is quite helpful when writing code, and it’s certainly an approach I’m going to be using again when I can.

February 22, 2023

Flathub Brand Refresh

While Flatpak itself is an architecturally decentralized way to distribute apps, Flathub embraces the convenience for people to come to one place to install apps. There’s some fairly big changes coming to Flathub that I’m extremely excited about, that will make sure the new wave of Linux apps doesn’t fizzle out and we’ll maintain a sustainable application ecosystem.

Thus there is no better time to refresh what has become a little dated, Flathub’s visual identity. It centers around the core of what Flathub is — the apps themselves. The identity isn’t very loud and shouty. The flashy colors and emphasis remains on the apps themselves, Flathub is the neutral platform for the apps to shine.

Brand Guidelines

Take a peek at the brand guidelines to learn more about the new Flathub brand or download the logos in scalable format. The download buttons for app websites are also available.

Flathub Light WIP Flathub Dark WIP

Building a brand on neutral greys isn’t quite an easy sell, but precisely targets the main point of the Flathub brand. It creates the stage for apps to shine. Flathub isn’t flashy by itself. It allows the apps to be at the center of your attention.

If you’ve read until this point, you deserve a treat! Here’s some eye candy from the early stages of the process. Wallpapers derived from the elemental shapes of the logomark. Clearly off limits now, but can’t just throw them away, can I?

Flathub Patterns Flathub Candy

Big shoutout to razze for his ongoing work on the website and implementing the brand so quickly. Many thank yous to Tobias Bernard for significant involvment in this during the Berlin Mini GUADEC and Václav Vančura for sensible feedback.

Previously

February 20, 2023

Writing Bindable API, 2023 Edition

First of all, you should go on the gobject-introspection website and read the page on how to write bindable API. What I’m going to write here is going to build upon what’s already documented, or will update the best practices, so if you maintain a GObject/C library, or you’re writing one, you must be familiar with the basics of gobject-introspection. It’s 2023: it’s already too bad we’re still writing C libraries, we should at the very least be responsible about it.

A specific note for people maintaining an existing GObject/C library with an API designed before the mainstream establishment of gobject-introspection (basically, anything written prior to 2011): you should really consider writing all new types and entry points with gobject-introspection in mind, and you should also consider phasing out older API and replacing it piecemeal with a bindable one. You should have done this 10 years ago, and I can already hear the objections, but: too bad. Just because you made an effort 10 years ago it doesn’t mean things are frozen in time, and you don’t get to fix things. Maintenance means constantly tending to your code, and that doubly applies if you’re exposing an API to other people.


Let’s take the “how to write bindable API” recommendations, and elaborate them a bit.

Structures with custom memory management

The recommendation is to use GBoxed as a way to specify a copy and a free function, in order to clearly define the memory management semantics of a type.

The important caveat is that boxed types are necessary for:

  • opaque types that can only be heap allocated
  • using a type as a GObject property
  • using a type as an argument or return value for a GObject signal

You don’t need a boxed type for the following cases:

  • your type is an argument or return value for a method, function, or virtual function
  • your type can be placed on the stack, or can be allocated with malloc()/free()

Additionally, starting with gobject-introspection 1.76, you can specify the copy and free function of a type without necessarily registering a boxed type, which leaves boxed types for the thing they were created: signals and properties.

Addendum: object types

Boxed types should only ever be used for plain old data types; if you need inheritance, then the strong recommendation is to use GObject. You can use GTypeInstance, but only if you know what you’re doing; for more information on that, see my old blog post about typed instances.

Functionality only accessible through a C macro

This ought to be fairly uncontroversial. C pre-processor symbols don’t exist at the ABI level, and gobject-introspection is a mechanism to describe a C ABI. Never, ever expose API only through C macros; those are for C developers. C macros can be used to create convenience wrappers, but remember that anything they call must be public API, and that other people will need to re-implement the convenience wrappers themselves, so don’t overdo it. C developers deserve some convenience, but not at the expense of everyone else.

Addendum: inline functions

Static inline functions are also not part of the introspectable ABI of a library, because they cannot be used with dlsym(); you can provide inlined functions for performance reasons, but remember to always provide their non-inlined equivalent.

Direct C structure access for objects

Again, another fairly uncontroversial rule. You shouldn’t be putting anything into an instance structure, as it makes your API harder to future-proof, and direct access cannot do things like change notification, or memoization.

Always provide accessor functions.

va_list

Variadic argument functions are mainly C convenience. Yes, some languages can support them, but it’s a bad idea to have this kind of API exposed as the only way to do things.

Any variadic argument function should have two additional variants:

  • a vector based version, using C arrays (zero terminated, or with an explicit length)
  • a va_list version, to be used when creating wrappers with variadic arguments themselves

The va_list variant is kind of optional, since not many people go around writing variadic argument C wrappers, these days, but at the end of the day you might be going to write an internal function that takes a va_list anyway, so it’s not particularly strange to expose it as part of your public API.

The vector-based variant, on the other hand, is fundamental.

Incidentally, if you’re using variadic arguments as a way to collect similarly typed values, e.g.:

// void
// some_object_method (SomeObject *self,
//                     ...) G_GNUC_NULL_TERMINATED

some_object_method (obj, "foo", "bar", "baz", NULL);

there’s very little difference to using a vector and C99’s compound literals:

// void
// some_object_method (SomeObject *self,
//                     const char *args[])

some_object_method (obj, (const char *[]) {
                      "foo",
                      "bar",
                      "baz",
                      NULL,
                    });

Except that now the compiler will be able to do some basic type check and scream at you if you’re doing something egregiously bad.

Compound literals and designated initialisers also help when dealing with key/value pairs:

typedef struct {
  int column;
  union {
    const char *v_str;
    int v_int;
  } value;
} ColumnValue;

enum {
  COLUMN_NAME,
  COLUMN_AGE,
  N_COLUMNS
};

// void
// some_object_method (SomeObject *self,
//                     size_t n_columns,
//                     const ColumnValue values[])

some_object_method (obj, 2,
  (ColumnValue []) {
    { .column = COLUMN_NAME, .data = { .v_str = "Emmanuele" } },
    { .column = COLUMN_AGE, .data = { .v_int = 42 } },
  });

So you should seriously reconsider the amount of variadic arguments convenience functions you expose.

Multiple out parameters

Using a structured type with a out direction is a good recommendation as a way to both limit the amount of out arguments and provide some future-proofing for your API. It’s easy to expand an opaque pointer type with accessors, whereas adding more out arguments requires an ABI break.

Addendum: inout arguments

Don’t use in-out arguments. Just don’t.

Pass an in argument to the callable for its input, and take an out argument or a return value for the output.

Memory management and ownership of inout arguments is incredibly hard to capture with static annotations; it mainly works for scalar values, so:

void
some_object_update_matrix (SomeObject *self,
                           double *xx,
                           double *yy,
                           double *xy,
                           double *yx)

can work with xx, yy, xy, yx as inout arguments, because there’s no ownership transfer; but as soon as you start throwing things in like pointers to structures, or vectors of string, you open yourself to questions like:

  • who allocates the argument when it goes in?
  • who is responsible for freeing the argument when it comes out?
  • what happens if the function frees the argument in the in direction and then re-allocates the out?
  • what happens if the function uses a different allocator than the one used by the caller?
  • what happens if the function has to allocate more memory?
  • what happens if the function modifies the argument and frees memory?

Even if gobject-introspection nailed down the rules, they could not be enforced, or validated, and could lead to leaks or, worse, crashes.

So, once again: don’t use inout arguments. If your API already exposes inout arguments, especially for non-scalar types, consider deprecations and adding new entry points.

Addendum: GValue

Sadly, GValue is one of the most notable cases of inout abuse. The oldest parts of the GNOME stack use GValue in a way that requires inout annotations because they expect the caller to:

  • initialise a GValue with the desired type
  • pass the address of the value
  • let the function fill the value

The caller is then left with calling g_value_unset() in order to free the resources associated with a GValue. This means that you’re passing an initialised value to a callable, the callable will do something to it (which may or may not even entail re-allocating the value) and then you’re going to get it back at the same address.

It would be a lot easier if the API left the job of initialising the GValue to the callee; then functions could annotate the GValue argument with out and caller-allocates=1. This would leave the ownership to the caller, and remove a whole lot of uncertainty.

Various new (comparatively speaking) API allow the caller to pass an unitialised GValue, and will leave initialisation to the callee, which is how it should be, but this kind of change isn’t always possible in a backward compatible way.

Arrays

You can use three types of C arrays in your API:

  • zero-terminated arrays, which are the easiest to use, especially for pointers and strings
  • fixed-size arrays
  • arrays with length arguments

Addendum: strings and byte arrays

A const char* argument for C strings with a length argument is not an array:

/**
 * some_object_load_data:
 * @self: ...
 * @str: the data to load
 * @len: length of @str in bytes, or -1
 *
 * ...
 */
void
some_object_load_data (SomeObject *self,
                       const char *str,
                       ssize_t len)

Never annotate the str argument with array length=len. Ideally, this kind of function should not exist in the first place. You should always use const char* for NUL-terminated strings, possibly UTF-8 encoded; if you allow embedded NUL characters then use a bytes array:

/**
 * some_object_load_data:
 * @self: ...
 * @data: (array length=len) (element-type uint8): the data to load
 * @len: the length of the data in bytes
 *
 * ...
 */
void
some_object_load_data (SomeObject *self,
                       const unsigned char *data,
                       size_t len)

Instead of unsigned char you can also use uint8_t, just to drive the point home.

Yes, it’s slightly nicer to have a single entry point for strings and byte arrays, but that’s just a C convenience: decent languages will have a proper string type, which always comes with a length; and string types are not binary data.

Addendum: GArray, GPtrArray, GByteArray

Whatever you do, however low you feel on the day, whatever particular tragedy befell your family at some point, please: never use GLib array types in your API. Nothing good will ever come of it, and you’ll just spend your days regretting this choice.

Yes: gobject-introspection transparently converts between GLib array types and C types, to the point of allowing you to annotate the contents of the array. The problem is that that information is static, and only exists at the introspection level. There’s nothing that prevents you from putting other random data into a GPtrArray, as long as it’s pointer-sized. There’s nothing that prevents a version of a library from saying that you own the data inside a GArray, and have the next version assign a clear function to the array to avoid leaking it all over the place on error conditions, or when using g_autoptr.

Adding support for GLib array types in the introspection was a well-intentioned mistake that worked in very specific cases—for instance, in a library that is private to an application. Any well-behaved, well-designed general purpose library should not expose this kind of API to its consumers.

You should use GArray, GPtrArray, and GByteArray internally; they are good types, and remove a lot of the pain of dealing with C arrays. Those types should never be exposed at the API boundary: always convert them to C arrays, or wrap them into your own data types, with proper argument validation and ownership rules.

Addendum: GHashTable

What’s worse than a type that contains data with unclear ownership rules decided at run time? A type that contains twice the amount of data with unclear ownership rules decided at run time.

Just like the GLib array types, hash tables should be used but never directly exposed to consumers of an API.

Addendum: GList, GSList, GQueue

See above, re: pain and misery. On top of that, linked lists are a terrible data type that people should rarely, if ever, use in the first place.

Callbacks

Your callbacks should always be in the form of a simple callable with a data argument:

typedef void (* SomeCallback) (SomeObject *obj,
                               gpointer data);

Any function that takes a callback should also take a “user data” argument that will be passed as is to the callback:

// scope: call; the callback data is valid until the
// function returns
void
some_object_do_stuff_immediately (SomeObject *self,
                                  SomeCallback callback,
                                  gpointer data);

// scope: notify; the callback data is valid until the
// notify function gets called
void
some_object_do_stuff_with_a_delay (SomeObject *self,
                                   SomeCallback callback,
                                   gpointer data,
                                   GDestroyNotify notify);

// scope: async; the callback data is valid until the async
// callback is called
void
some_object_do_stuff_but_async (SomeObject *self,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer data);

// not pictured here: scope forever; the data is valid fori
// the entirety of the process lifetime

If your function takes more than one callback argument, you should make sure that it also takes a different user data for each callback, and that the lifetime of the callbacks are well defined. The alternative is to use GClosure instead of a simple C function pointer—but that comes at a cost of GValue marshalling, so the recommendation is to stick with one callback per function.

Addendum: the closure annotation

It seems that many people are unclear about the closure annotation.

Whenever you’re describing a function that takes a callback, you should always annotate the callback argument with the argument that contains the user data using the (closure argument) annotation, e.g.

/**
 * some_object_do_stuff_immediately:
 * @self: ...
 * @callback: (scope call) (closure data): the callback
 * @data: the data to be passed to the @callback
 *
 * ...
 */

You should not annotate the data argument with a unary (closure).

The unary (closure) is meant to be used when annotating the callback type:

/**
 * SomeCallback:
 * @self: ...
 * @data: (closure): ...
 *
 * ...
 */
typedef void (* SomeCallback) (SomeObject *self,
                               gpointer data);

Yes, it’s confusing, I know.

Sadly, the introspection parser isn’t very clear about this, but in the future it will emit a warning if it finds a unary closure on anything that isn’t a callback type.

Ideally, you don’t really need to annotate anything when you call your argument user_data, but it does not hurt to be explicit.


A cleaned up version of this blog post will go up on the gobject-introspection website, and we should really have a proper set of best API design practices on the Developer Documentation website by now; nevertheless, I do hope people will actually follow these recommendations at some point, and that they will be prepared for new recommendations in the future. Only dead and unmaintained projects don’t change, after all, and I expect the GNOME stack to last a bit longer than the 25 years it already spans today.