frame limiter - need suggestions

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
frame limiter - need suggestions
by on (#57395)
I've come to the point where my emulator runs most (all?) mapper-0 games pretty well, but the frame rate is so high that it renders the games unplayable. my PPU implementation is currently scanline-based, and the whole thing is currently wildly inaccurate, but that's beside the point.

So, what's the best way of implementing a frame limiter?

I was thinking about doing it like I would do if I was programming a video game, that is, calculate a delta time and match it against my desired frame rate (60 Hz)
something like this:
Code:
starttime = getticks();

emulate_one_frame();

delta = getticks()-starttime;

if(delta < some_value)
delay(some_value - delta);


I do realize that this might not be completely kosher though, and it might waste a lot of CPU cycles for nothing.

here is some pseudocode of my emulators main loop:
Code:

while(cpu_cycles() < 113)
run_cpu();

nextscanline();
clear_cpu_cycles();

check_for_irqs()
if(nmi_occured)
{
do_nmi_stuff();
draw_to_screen();
}



any suggestions or good advice?

by on (#57397)
It's far from the best method, but if you don't care about being exact, you can update a counter of how many frames have gone by in the current second. When the counter hits 60, just loop until the value returned by time(NULL) has changed.

That should slow things down plenty syscall and all.

by on (#57398)
Frame limiters can get complex when you start working in fastforward and frame skipping.

Here's the frame limiter from my emu (well the one that's based on ticks, anyway). It's pretty damn good if I say so myself. I've used emulators that had some pretty terrible frame limiters (older versions of VBA come to mind right away):

Code:
void Emulator::RegulateFrameRate_Clock(int fps)
{
   // change 'fps' to number of milliseconds between frames
   if(fps <= 0)
   {
      if(bPALMode)      fps = 20;
      else            fps = 17;
   }
   else
      fps = 1000 / fps;

   // get time between next frame and now
   s32 dif = (signed)(::GlbGetTick() - nNextFrame);

   if(dif < 0)   // not yet time for next frame
   {
      ::GlbSleep(1);
      return;
   }

   nNextFrame += fps;

   if(dif >= (fps*nSkipFramesBehind))
   {
      if(nFramesSkipped >= nMaxFrameSkip)
         ResyncFrameTime(0);
      else
      {
         ++nFramesSkipped;
         DoFrame(0,1);
         return;
      }
   }

   nFramesSkipped = 0;
   DoFrame(1,0);
}


Notes:
- fps (passed to the function) is 0 normally. But it could be something much higher if the player is holding the fastforward button to speed up the game.

- ::GlbGetTick() is just a function that returns the current tick in milliseconds. On Windows, you can use GetTickCount()

- ::GlbSleep() is just a function that sleeps the given number of milliseconds. On Windows, you can use Sleep()

- nNextFrame is the tick on which you'd do the next frame

- nSkipFramesBehind is the number of frames at which point the emu will start skipping frames in order to start catching up. IE: if it's 5, it'll start skipping frames once you fall 5 frames behind. Don't set it too low or it'll get very choppy. I have it user defined in my emu.

- nMaxFrameSkip is the maximum number of frames the emu will skip before "giving up" trying to catch up. Lower values = emu will opt to run "slower". Higher values = emu will opt to run "choppier" (assuming it can't run full speed)

- ResyncFrameTime just "catches up" without actually doing any emulation. IE:
Code:
nNextFrame = ::GlbGetTick();
nFramesSkipped = 0;


It's important that ResyncFrameTime be called whenever emulation resumes from a paused state to prevent the emu from trying to "catch up" to make up for time where it wasn't spent emulating. Like for instance, when the user goes in a menu or in a config screeen or the like.

- DoFrame(0,1) skips a frame (emulates a frame without drawing it -- faster)

- DoFrame(1,0) emulates and draws a frame (normal, slower than skipping a frame)

- Intended use is as follows:

Code:
while( emulator_running )
{
   ProcessMessagesAndUserInput();

   if( user_is_pressing_fastforward)
      RegulateFrameRate_Clock( fastforward_fps );
   else
      RegulateFrameRate_Clock(0);
}


- It doesn't do exactly 60 FPS for NTSC (it does 17 ms per frame which is a little slower .. 60 FPS would be 16.6667 ms per frame). I didn't care about getting it exact because this was my secondary limiter (my primary one works on audio samples to keep things synced to sound)

by on (#57432)
thanks for the help, disch! I'm going to see if I can implement something similar right off... Much appreciated!


really smart idea to keep the emulator synchronized with the audio, btw! I'm definitely going to do that once I've implemented a working APU... which right now seems somewhat difficult, given that I've never done any audio signal programming whatsoever.

edit:

Got it to work! Except that I use SDL for delaying and getting ticks, and as it turns out, SDL_Delay (equivalent to sleep()) is not playing ball with the OS scheduler, and thus delays are unpredictable.

ohwell, thanks a lot!

by on (#57464)
I never had problems with SDL's time functions.

What OS are you on?

by on (#57466)
Make sure you don't use a really bad frame limiter that makes the CPU go to 100% usage. Specifically, avoid calls do the DirectX vblank wait function. I've seen tons of emulators do this, and it makes me mad.

by on (#57468)
Dwedit wrote:
Specifically, avoid calls do the DirectX vblank wait function.

Then what do you recommend to wait for vblank so that a blit doesn't tear? The GBA and DS versions of one of my games call the SWI for VBlankIntrWait(), but the PC version calls Allegro vsync(), which sends the CPU to 100% usage.

by on (#57469)
I think you use MMTimers, combined with polling which scanline the display is on, and maybe also check if too much time has passed since the last timer event.
Then go bug the Allegro people to replace their vblank code with that.

Edit: Use TimerQueue instead of MultimediaTimer on Windows 2000 and higher.

by on (#57533)
disch:

I'm going completely cross-platform, it currently compiles and runs on linux, win32 and mac os. I've got two versions, one with front-end and one without. It seems that when I use SDL_Delay from within the front-end version (which utilizes Qt), the delay is somewhat arbitrary, at least on win32. It might be due that I update the emulator (& SDL) with a timer set to 0 ms which, in theory at least, means that it's constantly firing. Qt doesn't provide any sleep/delay as long as I'm not using threads, which I am not.


However, it doesn't misbehave as such when I'm using the version without a frontend, so I think I've worked it out for myself. I need to find a different approach in the GUI version.

by on (#57539)
sahib wrote:
disch:

I'm going completely cross-platform, it currently compiles and runs on linux, win32 and mac os. I've got two versions, one with front-end and one without. It seems that when I use SDL_Delay from within the front-end version (which utilizes Qt), the delay is somewhat arbitrary, at least on win32. It might be due that I update the emulator (& SDL) with a timer set to 0 ms which, in theory at least, means that it's constantly firing. Qt doesn't provide any sleep/delay as long as I'm not using threads, which I am not.


However, it doesn't misbehave as such when I'm using the version without a frontend, so I think I've worked it out for myself. I need to find a different approach in the GUI version.


I am also using Qt/SDL and notice some audio anomalies. I use a QSemaphore to kick off emulation in my SDL callback. The callback takes the previous frame's audio data and mixes it into the stream. I notice audible clicks at about 1Hz. I haven't been able to diagnose the source of those clicks yet. I suppose it is due to the callback being delayed past the end of the playing audio stream. I have read that the callback doesn't actually post the data to the audio stream until after it returns. This all makes sense but I am currently at a loss for how to make the callback more responsive!

I don't currently use timers. In implementing frame limiting I don't think I would use timers, either. I would just grab some "tick" from some cross-platformy method and do math on it.

by on (#57556)
My emu is crossplatform as well (wxWidgets + SDL), although I think I used wxWidgets' timer functions instead of SDL's.

Quote:
Qt doesn't provide any sleep/delay as long as I'm not using threads, which I am not.


That's kind of lame. I guess you could always #ifdef your own in if SDL's aren't cutting it. It's kind of a pain, but it's only 2 functions we're talking about, and they're both really small.

Quote:
I notice audible clicks at about 1Hz. I haven't been able to diagnose the source of those clicks yet. I suppose it is due to the callback being delayed past the end of the playing audio stream. I have read that the callback doesn't actually post the data to the audio stream until after it returns. This all makes sense but I am currently at a loss for how to make the callback more responsive!


Ah. Yeah this is a challenge.

Running the emulator from the callback is not a good idea. The callback should just fill the audio buffer asap and return. Running a whole frame of emulation will take considerably longer.

I had to double buffer audio in my system. Basically the idea is I have a ring buffer which my emu fills with audio, and the callback empties audio from.

You can keep the latency on SDL fairly low, and just have a larger ring buffer.

by on (#57609)
NESICIDE wrote:

I don't currently use timers. In implementing frame limiting I don't think I would use timers, either. I would just grab some "tick" from some cross-platformy method and do math on it.


I don't use timers for my frame-limiter, I just use a QTimer to set the update interval for the emulators' update-loop. The frame-timing currently uses a SDL_Delay-call, which on win32 equals a call to Sleep(), which, gets called from within the update-loop.


I reckon I'll just get around to implement the APU next, and hopefully that'll provide a better platform for frame timing...

thanks for the input, guys :)