Memory leak in image:drawFaded()?

Seeing a new error in 1.2.1 where I'm consistently crashing on image:drawFaded() which is reporting an out of memory error. I haven't tried to isolate the issue, but I do know that this issue in my game did not show itself before 1.2, so before I do any work I thought I would share this note here first.

I am also drawing a very large image to screen (a portion of it) which benefits from the changes in 1.2 added to speed up drawing the right portion of a large image. This may be the actual issue (perhaps a leak introduced with that change in 1.2?). I'll try to make some time to isolate and figure out where this new error is coming from.

Looking into this a little further, it seems that it may be GC timing related in 1.2.x. It's just crashing due to memory in drawFaded() every time which is a bit strange. Need to dig deeper.

I've been battling this bug for the last week and I think I've got a good demo of it working now.

What appears to be happening is when a program is "leaking" a sufficient amount of memory that would normally be garbage collected without issue, image:drawFaded / fadedImage block the GC from doing so, causing memory to pile up and eventually crash the device. This does not happen on simulator. Disclosure I know basically nothing about how the Lua GC works so take this with a grain of salt :sweat_smile:

import "CoreLibs/graphics"
gfx = playdate.graphics
-- Press A or B to watch the memory tick up until the device crashes

img = gfx.image.new(400, 240, gfx.kColorBlack)
function playdate.update()
    gfx.clear(gfx.kColorWhite)
    for i=1,100 do
        t = { this="table"; 1,2,3 }  -- construct a table and assign it to "t"
        t = nil                      -- set "t" to nil and the table becomes unreachable
        s = "a string"               -- create string variable
        s = "another string"         -- when set to another value the old value becomes unreachable
    end
    if(playdate.buttonIsPressed(playdate.kButtonA)) then -- when A is pressed we drawFaded
        img:drawFaded(0, 0, 0.5, gfx.image.kDitherTypeBayer4x4)
    end
    if(playdate.buttonIsPressed(playdate.kButtonB)) then -- when B is pressed we fadedImage
        img:fadedImage(0.5, gfx.image.kDitherTypeBayer4x4):draw(0, 0)
    end
    -- monitoring memory usage
    playdate.graphics.setColor(playdate.graphics.kColorWhite)
    gfx.fillRect(90, 90, 100, 40)
    gfx.drawText(collectgarbage("count"), 100, 100)
end

memorygraph
Memory-Test.pdx.zip (10.0 KB)

Running this code and pressing A or B results in (at least on my device) a straight line curve of memory usage until it's all gone. Strangely enough, system interrupts sometimes (system menu, lock screen) seem to do something that causes this bug to disappear, but relaunching the app will make it come back. Included the PDX for anyone that wants to confirm and make sure I'm not going crazy.

EDIT: Worth mentioning it doesn't matter what ridiculous minimium GC time you set it to, it runs for 0ms when this is happening. Also doesn't seem to matter what dithering algorithm you use.

I just ran into something similar testing game delivery, where it was using up all the CPU unzipping the file and the GC wasn't ever getting run (bug #1). Then (bug #2) we're just giving up when the allocation fails instead of doing what Lua does, runs emergency GC and tries again. I added that where I was seeing the out of memory error and it works fine. I think I can even do that down at the interface to the memory manager, so it'll force a Lua GC pass anywhere in the system it fails a malloc (on the game task when Lua's available, anyway). Not sure if that might cause surprise hiccups anywhere, but I'll give it a try.

1 Like

That's a good point, I didn't think about the fact there are probably 2 bugs here.

Thankfully the issue with my game is resolved if I manually force 1 step of GC whenever doing any dithering, so I think I'll be doing that until there's a fix.