This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Little coroutine lib for cc65. (In progress)

Little coroutine lib for cc65. (In progress)
by on (#220933)
I recently started working a a little coroutine library for cc65 very similar to the simple API for Lua coroutines. It currently comes in at ~200 bytes of code, and I think the yield/resume cost is reasonable from eyeballing the asm, but I haven't actually done any cycle counting.
https://gist.github.com/slembcke/ef7ae2 ... e9b2cbb948

For anyone that's not familiar, they are basically really lightweight threads that you explicitly switch between. They are really useful for things like animations or state machines where you want to a function that runs over several frames.

Basically, the game loop code for the block falling game I'm working on was getting a little confusing. A lot of code was spread out over several frames for animation or cost amortization reasons, and it was getting hard to see what the actual flow was. Something gross like this:

Code:
void update(){
   if(timer == 0){
      // Make blocks fall
   } else if(timer < grid_height){
      // Blit updated tiles to the screen one row of blocks at a time
   } else if(timer < wait_time){
      // Apply matching logic, etc
      // Possibly reset timer to grid_height to reset the wait, but not trigger logic above.
   } else if(){
      // ... more of the same
   }
   
   ++timer;
}


Gross and unwieldy, so I started refactoring it to use function pointers to separate the flow better than an else-if chain, but the code to switch between state functions was kinda dumb too. Either it had to be inlined into the previous state's function, where it didn't really seem to belong, or separated out into yet another function. Coroutines seemed like they would be fun to implement, so I did. Now the code looks more like this:

Code:
// ... Somewhere in the game init code.
// Sets the function to use as a coroutine.
coro_start(update_coro);

void update(){
   // Resume executing the coroutine from where it last yielded.
   coro_resume();
}

void update_coro(){
   while(true){
      // Make blocks fall.
      
      // Yield jumps back to the main thread as if coro_resume() was returning.
      coro_yield();
      
      for(...){
         // Blit row of blocks to the screen
         coro_yield();
      }
      
      timer = 0
      while(timer < wait_time){
         // Apply matching logic and such.
         // Possibly reset timer back to 0.
         coro_yield();
      }
      
      // ... More events in the loop.
   }
}


Using coroutines this way, basically every time coro_yield() is called, it waits for a frame and goes back to executing the "main thread" until it's resumed again. This makes it really easy to write code that happens over time, but is nicely contained in a single function without obscuring the control flow.

On my TODO list yet:
  • Allow the stack buffer to be placed anywhere in RAM and not hard coded in the .s file.
  • Allow switching coroutines (push their state onto their stack buffers).
  • Maybe remove the values passed in and out of yield/resume. Not as useful as in Lua without dynamic typing.
Re: Little coroutine lib for cc65. (In progress)
by on (#220985)
I'm pretty sure there's a demo on this board that uses cooperative multi-threading. If my memory serves right, it's blargg that did it, and moved 128? 256? objects on the screen. Might find it this evening if someone didn't already since then.
Re: Little coroutine lib for cc65. (In progress)
by on (#221347)
I made good progress on this recently. Coroutines are initialized/called by passing a pointer to their stack buffer, and I managed to refactor it a bit so the code size is still barely over 200 bytes. It's reasonably efficient, at about 600 cycles to resume + yield. I certainly wouldn't make one for every sprite, but it's cheap enough that using 2-3 of them to manage player or game state won't eat up more than a few percent of the total cycles each frame. Other than the two issues below, it's pretty complete I think. I decided to call it "Naco".

https://gist.github.com/slembcke/0438c6 ... ca581c495a

Known issues:
  • Coroutines can only be resumed from the main thread, not other coroutines.
  • Register variables are not saved when resuming, so a coroutine cannot be in a function that uses register vars when it yields.

Hopefully somebody else finds it useful! :D
Re: Little coroutine lib for cc65. (In progress)
by on (#221937)
Cool!