It is currently Mon Dec 10, 2018 6:56 pm

All times are UTC - 7 hours





Post new topic Reply to topic  [ 4 posts ] 
Author Message
PostPosted: Thu Jul 12, 2018 10:17 am 
Offline
User avatar

Joined: Fri Nov 24, 2017 2:40 pm
Posts: 95
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.


Top
 Profile  
 
PostPosted: Fri Jul 13, 2018 5:48 am 
Offline
Formerly ~J-@D!~
User avatar

Joined: Sun Mar 12, 2006 12:36 am
Posts: 474
Location: Rive nord de Montréal
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.

_________________
((λ (x) (x x)) (λ (x) (x x)))


Top
 Profile  
 
PostPosted: Tue Jul 17, 2018 12:58 pm 
Offline
User avatar

Joined: Fri Nov 24, 2017 2:40 pm
Posts: 95
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


Top
 Profile  
 
PostPosted: Tue Jul 24, 2018 11:18 am 
Offline
User avatar

Joined: Mon Jan 03, 2005 10:36 am
Posts: 3141
Location: Tampere, Finland
Cool!

_________________
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 4 posts ] 

All times are UTC - 7 hours


Who is online

Users browsing this forum: No registered users and 3 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group