Log In  


Cart #wutewuwiho-1 | 2024-06-02 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
5

hi @zep! This cart should not error, but it does (pico8 0.2.6b / linux)

-- wrap everything in func _init to fix it
--function _init()

--uncomment this to fix it
--poke(0,0)

--make this local to fix it
coro = cocreate(function()
  -- uncomment this to fix it
-- local x=1
  for i=0,32000 do
    -- flr() is arbitary here; it's
    -- just a func that should
    -- return only one value
    local rets = pack(flr(i))
    if #rets~=1 then
      local str="flr rvals:"
      for i,v in ipairs(rets) do
        str ..= "\n\t"..i..": "..tostr(v)
      end
      str ..="\nstat(1): "..stat(1)
      print(str.."\n")
      assert(false)
    end
    -- uncomment this to fix it
--    if flr==flr then end
  end
end)

cls()

--remove second param to fix it
assert(coresume(coro, "uh-oh"))

print"no errors"

It seems like the string "uh oh" is being popped from the stack in a completely wrong place.

See also https://www.lexaloffle.com/bbs/?tid=141267 , a picotron coroutine bug. This cart is a modified version of @jesstelford's code there. I'm unsure if the root cause is related, but the result is the same.

Notably, this cart consistently crashes every single time (unlike the picotron version), and seems directly related to stat(1) going over 1.0

I marked a bunch of things you can uncomment to make this test case pass. (there are some very weird things you can do to make it suddenly work)


I'm fuzzy on the details, but it seems like something like this is happening:

  1. coresume(coro, "uh-oh") pushes "uh-oh" to the internal lua/C/lua_State stack, where it stays for a while (because there isn't a yield() that pops that value)
  2. the coroutine takes a long time (stat(1)>1), and pico8 pauses my cart to do its own thing (flip, etc)
  3. pico8 prepares to hand control back to my cart, by restoring the state my cart was in when it got forcibly paused. it restores the stack with "uh-oh" on it, but it does it wrong somehow
  4. my code resumes running, maybe right at the point where flr() returns? (that would be quite a coincidence, but it might explain why irrelevant changes like local x=1 would squash the bug)
  5. flr returns the correct value, but also the rest of the stack, which is "uh oh".
5


2

I was having a similar problem in some code of mine. I think I've figured out what's going on and that you've related it to stat(1) going above 1 sort of backs up my suspicions.

This is pure speculation but here's what I think it happening:

Typically we'll have an _update and _draw function in our program and if our performance is bad we start dropping to a lower frame rate. That's important because it means the program isn't grinding to a halt completely, it's just slowing down. Which sounds a whole lot like coroutines to me. So behind the scenes @zep must be taking _update and _draw and turning them into coroutines when the game runs. Nothing too shocking there.

But most Pico-8 games don't use coroutines explicitly. So if performance in _update is tanking there needs to be some way to kick us out of _update so we can do a _draw_ cycle even though most games will never include a yield anywhere. There's all sorts of ways you could do that but one way would be add a bunch of "sneaky" yields into API functions. Functions which everyone will use. Then every API function can check the current value of stat(1) and if it's taking too long it can cut the frame rate, yield and, after the draw cycle, pick up right where it left off. (This isn't quite right as you'll see but that was my thinking.)

If you're not using coroutines in your code then that all works seamlessly and you never notice. But if you are using corountines the trouble can start to occur. Since coroutines can call other coroutines—or functions which call yield at some point—we're not necessarily calling coresume on the coroutine we think we are.

I've modified your example a bit to show what I mean.

  db = {}
  my_co = cocreate(
     function (a)
        local count = 0
        while true do
           count = (count + 1) % 15000
           x = pack(split(a))  -- for instance.
           if #x > 1 then
              add(db, 'extra arg: '..x[2])
              add(db, 'caught after '..count..' iterations')
           end
           if count == 0 then
              yield()
           end
        end
     end
  )

  count_draw = 0
  function _draw()
     count_draw += 1
     cls()
     print(count_draw)
     for d in all(db) do
        print(d)
     end
  end

  function _update()
     assert(coresume(my_co, '1,2,3'))
  end

There is an explicit yield there but it doesn't happen for a long time. Now I'm missing something in my explanation because if you remove the yield then you never get kicked out of _update and never hit _draw, which is what I thought would happen, but pack is getting the extra argument so this seems to be where the coroutine machinery is getting mixed up.

My guess:

  1. my_co gets called with the string '1,2,3'
  2. the loop takes too long and split (or maybe pack?) does a
    "sneaky" yield which is then bound to my_co
  3. which is then resumed also with the string '1,2,3' causing it to
    be appended to the output of split and packed up by pack.

The thing I'm missing might have to do with how Pico-8 (and presumably Picotron if to a lesser extent) throttles commands. Unless I'm remembering wrong, Pico-8 code runs slower than it technically needs to because the API commands are artificially throttled to take a consistent amount of time on the virtual CPU. Sneaky yields could be a mechanism to force commands to waste time and could be why they don't actually kick us out of _update like I thought they would because zep is handling them slightly differently behind the scenes. But when one of those yields happens into our own coroutine code we catch it instead of Pico-8 catching it and if we give it extra arguments it happily takes them. (Having now read the last part of your post, this would also explain the "coincidence" of just happening to pick up right before the function returns. It's not a coincidence if that's where the sneaky yield is happening.)

Like I said that's all pure speculation.

The fix that's worked for me in my situation though is pretty simple. A function make_co is basically a wrapper around a wrapper around cocreate. It takes a function and returns a new function. That function when called (with arguments) wraps the orignal function in yet another function which does not take arguments and it's that function (with no arguments) from which we create the coroutine.

  db = {}
  co_q = {}

  function make_co(f)
     return function(...)
        local args = {...}
        local co = cocreate(
           function()
              f(args)
           end
        )
        add(co_q, co)
     end
  end

  -- In this version, my_co is a function that you call with whatever
  -- arguments you need. It creates a coroutine that takes no arguments
  -- and adds it to a queue.
  my_co = make_co(
     function (a)
        local count = 0
        while true do
           count = (count + 1) % 15000
           x = pack(split(a))  -- for instance.
           if #x > 1 then
              add(db, 'extra arg: '..x[2])
              add(db, 'caught after '..count..' iterations')
           end
           if count == 0 then
              yield()
           end
        end
     end
  )

  function _init()
     my_co('1,2,3')
  end

  count_draw = 0
  function _draw()
     count_draw += 1
     cls()
     print(count_draw)
     for d in all(db) do
        print(d)
     end
  end

  function _update()
     -- process all the coroutines in the queue.
     for co in all(co_q) do
        assert(coresume(co))
     end
  end

No more errors!



[Please log in to post a comment]