A story about GIFs and GameMaker
(and on where 60 seconds of the 62s loading time may come from)
Once upon a time (sometime in 2020), I wrote an extension that dynamically loads GIFs in GameMaker.
The premise is simple enough - if you're making something that accepts user-created animations, you might want GIFs, but GM can only load still textures at the moment.
The way this works is that the extension will extract frame information and pixels from a GIF file, compose them as instructed, and add them as frames to a GM sprite (in other words, an animated texture).
The code is an adaptation of Yanrishatum's GIF reader for Haxe Format library, tweaked and cross-compiled to GML (GameMaker's scripting language). Doing decompression in a dynamic language isn't the fastest, but much easier than compiling a bunch of native libraries.
There was also an edge case: if you gave it high-resolution, long GIFs, it could take multiple seconds to load them. For example, this 640x400, 93-frame GIF would take some 4-5s to load, depending on the system:
This wasn't the primary use case so I briefly looked at the profiler, thought "guess adding a hundred large-ish textures is kind of slow, huh?" and left it be.
Recently someone agreed to pay me a little money to add an option to load GIFs in portions so that the user can tell how's the loading coming along. So I did that - a reader-loop was now a little state machine reading sections and compiling textures for GM.
While doing that I made the test project display how long each operation takes. Most of it was as expected (e.g. frames that change more pixels of the image take longer), but there was also something unusual:
This 1024x600, 250-frame GIF that I recorded for a window_shape update was taking a whole minute to load, and most of that time was spent on drawing the later frames of the GIF.
At first I thought that I managed to make a catastrophic mistake somewhere, but the culprit turned out to be sprite_add_from_surface
, a function that takes a "surface" (transient texture / render target) and adds it to a sprite.
The first calls to the function are always quick enough (5..8ms on my desktop for a 1024x600 texture), but each subsequent frame takes a little longer than the last, with frame 250 taking 270..280ms.
And if you add each surface as a separate sprite, this doesn't happen! My first guess was that GM tries to fit the frames on a texture atlas, but that is not the case (and apparently a thing that some people are annoyed about).
I then decided to check what GM versions are affected and found that it this reproduces in the stable release, and LTS, and long-sunset GameMaker: Studio, and even in a GameMaker 8.1 version from 2011. Perhaps it's always been this way and no one really noticed..?
Anyway, after adding an option to load GIF frames as separate sprites, the "problem GIF" now loads in just over 2s, which is still longer than I'd like, but much better than before - especially now that you can distribute this time over multiple frames.
And that's about it!
The extension (it's free!) can be found on itch;
The source code for it can be found on GitHub;
And if you'd like to know what'll happen to the underlying issue, I filed a report (#6165) for it.
This reminds be of a bug I reported in GM:Studio 1.4. Each call to draw_getpixel() or surface_getpixel() was slower than the last. After a hundred or so iterations it became a big problem. The suggested fix was "don't do that". It did not appear to affect HTML5.
Cool story! Next up, .webp