Scaling images with Cairo is much slower in GTK4

I have been trying to make a simple image viewer and noticed that scaling images with Cairo in a DrawingArea is a lot slower in GTK4 in comparison to GTK3. Not only that, but I also face a “Attempt to create texture of size 20250x13182 but max size is 16384. Clipping will occur.” warning pretty quickly, something that doesn’t seem to happen in GTK3. Here is some sample code:

GTK3:

import cairo
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk


def draw(da, ctx):
    global surface
    
    ctx.scale(scale, scale)
    ctx.set_source_surface(surface)
    ctx.get_source().set_filter(cairo.FILTER_NEAREST)
    ctx.paint()
    
    da.set_size_request(surface.get_width() * scale, surface.get_height() * scale)
    
def clicked(btn):
    global scale
    scale *= 1.5
    da = btn.get_parent().get_children()[1].get_child().get_child()
    da.queue_draw()


def main():
    global scale
    scale = 1
    
    global surface
    surface = cairo.ImageSurface.create_from_png("image.png")
    
    win = Gtk.Window()
    win.connect('destroy', lambda w: Gtk.main_quit())
    win.set_default_size(500, 500)
    
    box = Gtk.Box()
    win.add(box)
    
    btn = Gtk.Button()
    btn.set_label("Scale")
    box.add(btn)
    btn.connect('clicked', clicked)
    
    scrolled_win = Gtk.ScrolledWindow()
    box.add(scrolled_win)

    drawingarea = Gtk.DrawingArea()
    drawingarea.set_vexpand(True)
    drawingarea.set_hexpand(True)
    scrolled_win.add(drawingarea)
    drawingarea.connect('draw', draw)
    

    win.show_all()
    Gtk.main()


if __name__ == '__main__':
    main()

GTK4:

#!/usr/bin/env python
import cairo
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk


def draw(da, ctx, x, y):
    global surface
    
    ctx.scale(scale, scale)
    ctx.set_source_surface(surface)
    ctx.get_source().set_filter(cairo.FILTER_NEAREST)
    ctx.paint()
    
    da.set_size_request(surface.get_width() * scale, surface.get_height() * scale)
    
def clicked(btn):
    global scale
    scale *= 1.5
    da = btn.get_parent().get_last_child().get_child().get_child()
    da.queue_draw()
    
def build_ui(app):
    global scale
    scale = 1
    
    global surface
    surface = cairo.ImageSurface.create_from_png("image.png")
    
    win = Gtk.Window()
    win.connect('destroy', lambda w: Gtk.main_quit())
    win.set_default_size(500, 500)
    win.set_application(app)
    
    box = Gtk.Box()
    win.set_child(box)
    
    btn = Gtk.Button()
    btn.set_label("Scale")
    box.append(btn)
    btn.connect('clicked', clicked)
    
    scrolled_win = Gtk.ScrolledWindow()
    box.append(scrolled_win)

    drawingarea = Gtk.DrawingArea()
    drawingarea.set_vexpand(True)
    drawingarea.set_hexpand(True)
    scrolled_win.set_child(drawingarea)
    drawingarea.set_draw_func(draw)
    

    win.present()


def main():
    global scale
    scale = 1
    
    global surface
    surface = cairo.ImageSurface.create_from_png("upscale.png")
    
    app = Gtk.Application()
    app.connect("activate", build_ui)

    app.run()



if __name__ == '__main__':
    main()

So what exactly is going on here? And more importantly, how can I achieve a better performance with GTK4 for a simple image viewer? (scaling PixBuf is unfortunately also on the slower side, and it seems GEGL isn’t easily usable in GTK4 either)

Edit: The slowness seems to be caused by the renderer, as setting GSK_RENDERER to cairo gives pretty much the same speed in GTK4.

Cairo is re-uploading the image texture from the CPU every frame which will be quite slow. If you want faster rendering in GTK4 then you will want to use snapshots, and avoid Cairo + Gtk.DrawingArea unless it’s absolutely necessary. That will ensure the texture is cached on the GPU. Here is a quick example of how to do this with Gdk.Texture.new_for_pixbuf and Gdk.Paintable.snapshot.

#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
gi.require_version("GdkPixbuf", "2.0")
from gi.repository import Gtk, Gdk, GdkPixbuf

def clicked(btn):
    btn.img.scale *= 1.5
    btn.img.queue_resize()
 
def build_ui(app):
    win = Gtk.Window()
    win.set_default_size(500, 500)
    win.set_application(app)

    box = Gtk.Box()
    win.set_child(box)

    btn = Gtk.Button()
    btn.set_label("Scale")
    box.append(btn)
    btn.connect('clicked', clicked)

    scrolled_win = Gtk.ScrolledWindow()
    box.append(scrolled_win)

    img = MyWidget("image.png")
    img.set_vexpand(True)
    img.set_hexpand(True)
    scrolled_win.set_child(img)

    btn.img = img

    win.present()

class MyWidget(Gtk.Widget):
    def __init__(self, path, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
        self.texture = Gdk.Texture.new_for_pixbuf(self.pixbuf)
        self.scale = 1
    def do_snapshot(self, snapshot):
        width = self.texture.get_intrinsic_width() * self.scale
        height = self.texture.get_intrinsic_height() * self.scale
        self.texture.snapshot(snapshot, width, height)
    def do_get_request_mode(self):
        return Gtk.SizeRequestMode.CONSTANT_SIZE
    def do_measure(self, orientation, for_size):
        if orientation == Gtk.Orientation.HORIZONTAL:
            width = self.texture.get_intrinsic_width() * self.scale
            return (width, width, -1, -1)
        else:
            height = self.texture.get_intrinsic_height() * self.scale
            return (height, height, -1, -1)

def main():
    app = Gtk.Application()
    app.connect("activate", build_ui)

    app.run()

if __name__ == '__main__':
    main()

I don’t know the specifics of why, but this should also solve the issue with hitting the maximum texture size.

For extra speed you’ll also want to offload the Pixbuf loading to another thread, either manually, or using GdkPixbuf.Pixbuf.new_from_stream_async.

1 Like

Look at the menu demo in gtk4-demo for an example of image scaling with GTK4

1 Like