Crash when using pushContext() together with coroutine.yield() (Simulator)

sdk 1.10.0, mac, Simulator

It seems that using something like this crashes the simulator (not sure about hardware)

function playdate.update()
    gfx.pushContext()
       for i = 1,60 do
         -- additional drawing here
         coroutine.yield()
       end
    gfx.popContext() -- reset any drawing state modifications
    playdate.drawFPS(0,0)
    updateBlinkers()
    updateTimers()
end

The problem can be observed on this branch of my project I prepared:

Crazy-Gravity-Playdate-bug-context-yield.zip (3.0 MB)

To reproduce, fly the plane into the wall. (The explosion animation uses coroutine.yield)
After the animation ends, the simulator will crash.

To fix the bug, either remove the pair of push/PopContext calls in main.lua OR remove the coroutine.yield() call from gameViewModel.lua

I was not able to create a minimal example here though.
Some snippets from the project:

main.lua

local activeScreen = GameScreen()

function playdate.update()
    gfx.pushContext()
        activeScreen:update()
    gfx.popContext() -- reset any drawing state modifications
    playdate.drawFPS(0,0)
    updateBlinkers()
    updateTimers()
end

gameViewModel.lua

if collision and explosion == nil and not Debug then
        if Sounds then thrust_sound:stop() end
        thrust = 0
        explosion = GameExplosion()
        print("KABOOM")
        while explosion:update() do
            calcPlane() -- keep updating plane as a ghost target for camera
            CalcGameCam()
            RenderGame()
            playdate.drawFPS(0,0)
            coroutine.yield() -- let system update the screen
        end
        DecreaseLife()
    end

Removing the push/popContext is actually viable for me, I had it included as a safety measure because perhaps the activeScreen would invert colors or set a clipRect etc, making the fps counter invisible.

The hunch I have is that there is something abbout the pushContext not being matched by a popContext when coroutine.yield is called in between to end the frame, leading to an illegal state in the SDK, which crashes the simulator.

I understand this project might be too big to peruse entirely, but perhaps the problem is in plain sight and there is something for me to learn regarding the workings of these methods.

I reasoned that this would be the order of execution in my code:
main.lua

  • pushContext

gameViewModel.lua
60x:

  • some draw calls
  • coroutine yield
  • C ends the frame, pops context
  • coroutine resume

main.lua

  • popContext -- this fails and crashes, because the Context created in lua has been popped in C

To test this hypothesis, I added
playdate.graphics.pushContext()
above the DecreaseLife() call.
This fixed the problem indeed!

another fix is to only call pushContext and not popContext() in playdate:update()

Can someone reassure me that this will not overwhelm the stack with Contexts that never get popped? @dave ?

Aha! Found the root problem: playdate.debugDraw() is called even when the lua update thread has yielded, and it's clearing the context stack. Then when update() continues and calls popContext() the stack is empty so it throws an error. (There's another deeper bug there causing it to crash instead of show the error.. That one's going to take some more excavating.) Here's a minimal test case:

function playdate.update()
    playdate.graphics.pushContext()
    coroutine.yield()
    playdate.graphics.popContext()
end

function playdate.debugDraw() end

I'll fix that so it skips the debugDraw() during a yield while there's an active context stack.

And to answer your last question: Yes, when playdate.update() ends we automatically clear the context stack. :slight_smile:

1 Like