What if you tally up the amount of delay you should have done, then when it gets to a big enough granularity where a sleep call would actually give you that amount, sleep.
Eg. Don't try to sleep accurately between the simulated cycles. Let things happen "too fast" for longer and do the delays in batches.
Not sure if this would wind up perceivably too bursty. Just an idea based on things I have seen in other domains.
For the record I don't think spinning is too bad of it's a very short duration, shorter than the scheduler will give you when you take your thread off the CPU.
Sleep, at least on Windows, is allowed to have +/- 15 ms (default system heartbeat interval) of accuracy, which I don't think would work for this case.
I could mess around with the Win32 API or the NT API or something to make the heartbeat rate more granular, but according to the docs that kills battery, so I'm guessing it's going to be doing pretty much the same thing as what I'm doing manually.
... that can still work. Measure how much time it actually slept and adjust accordingly. Or, is there some timer that runs at a reasonable rate (100Hz) where you csn yield once you've done 10msec worth of emulation? (I don't know the windows API, so just spitballing from the embedded world here)
You seem like you appreciate good timers. If you ever want to revolt in horror/have a few hearty belly laughs, look into how windows has and does handle various granularities of timing
Raising the timer interrupt to 1ms with timeBeginPeriod should still be less costly (with regards to power consumption) than a spin loop. But yeah it's all tradeoffs..
Yep, we had this exact issue on our live servers deployed to Windows Server 2016 in a VM - essentially I would do:
1) post tasks to process some data
2) while(tasksNotFinished) Sleep(1);
Well, very quickly found out that the Sleep(1) was always a minimum of 16ms(or a multiple of 16ms), which made the whole thing stupidly slow. We fixed it by running timeBeginPeriod(1) which overrides the system timer resolution to 1ms. Yes it would kill the battery on laptops, but on a server this is not an issue.
The way I would solve that is an event and a count of tasks. When the last thread to complete its job sees InterlockedDecrement(&ntasks) == 0, call SetEvent to wake up the waiting thread.
Yes, that was another solution we considered but we needed to deploy a patch quickly and setting the timer resolution was considered less risky and faster to do. But it would have worked as well as it should wake up the waiting thread immediately when used with WaitForMultipleObjectsEx.
AFAIK, most modern game engines have a threshold of headroom for leftover time that must be hit to eliminate needless spin, but also stay responsive.
Back when everyone was targeting 60fps, >~2.25ms of 'leftover time' was needed during the individual 16.6667ms run of that loop to actually trigger sleep instead of spinning.
Note that's specific to the graphical rendering thread, and things like the mouse/kb input have their own sleep durations, with or without thresholds.
You can run period of emulation time at full speed as long as difference in speed is not observable from outside. There's no point to delay regular memory reads and writes by synchronizing every CPU cycle to real time: no one will notice that. If it writes to video memory, result is usually not instantly visible too, and becomes visible only when screen scan position approaches that position in memory. Probably Game Boy has some sprite hardware and changing few registers can cause huge changes on screen, but anyway it's not possible to output changes on screen instantly to host system, so you can draw whole virtual frame at full host speed, considering that you keep track of scan position based on elapsed CPU cycles (T-states or something like that).
Once your emulation time period between two screen frames is completed, you can pause emulation until it catches up to next frame in real time, making process sleeping and not consuming CPU in between frames. Input events and sound add additional complexity, but it's doable.
A nice way to implement this is with a frame budget. You simply keep a variable that keeps track of how many miliseconds of budget you have left. Every time you render to the screen you increase the budget by 1000/fps. Then you simply run the emulation until the budget runs out. if the emulation gets too far ahead of the rendering you can consume extra budget by simply sleeping -- which is now less important to get exactly right, you can simply sleep away 90% of the budget for example, so if the sleep overshoots you've got some play.
Were all those errors observable back in 2000? It could be that compiler and operating systems differences now are what caused/revealed some bugs. For example the speed problem was probably much less back then due to slower computers.
> passing in a member of a record rather than a pointer to that record... No idea why
...sounds like a difference in how the Windows API was exposed. In Delphi and Free Pascal there are two ways to expose a C function that uses a pointer. This:
void Foo(int* ip);
can be exposed like this:
procedure Foo(ip: PInteger); cdecl;
or like this:
procedure Foo(var ip: Integer); cdecl;
The former allows you to pass any pointer to integer, the latter requires an Integer variable reference (which is implemented by the compiler by passing a pointer to the variable). The difference between the two is that you can pass a Nil (Pascal's NULL) to the first, but the second requires a variable.
Older versions of Borland Pascal and Delphi (not sure about newer Delphi versions) used the second declaration style where it made sense, but Free Pascal uses a more direct translation of the Windows headers, opting for the first declaration style.
My guess is that the compiler used by the original author of UGE used the first style.
> I found (apparently) the only Gameboy emulator written in Pascal, ever: UGE.
Hmm, I don't know. If I've learned anything about programming languages, then it's that the progression of programs written in them is usually: #1 Hello World, #2 Factorial, #3 Gameboy Emulator.
Yeah, i think his Google-fu skills need improvement, in another page he mentions that there weren't any IRC clients in Pascal, but i remember at least a couple of IRC libraries for Delphi (and personally i wrote an IRC component in Delphi - you could drop the component on a form and drag-and-drop your way on a simple IRC client, which i used at the time to make dedicated themed clients for the communities i was into :-P).
This is a fantastic example of my favorite programming technique:
When nothing makes sense and you're real emotional about it and want to give up... GIVE UP!*
*But you have to write another one from scratch tomorrow because there's good odds you'll do it right on accident and will then be able to diff and understand exactly where you went wrong. Being able to identify this situation and when doing this is useful as early as possible will make you very valuable. The break between attempts is important, go do literally anything else besides code.
I once spent a week going in literal circles because of some degree/radian conversion errors. The rewrite took about 15 minutes.
Never trust anyone's ability to count, not even your own.
It's particularly useful in this exact situation, where the differences are mere +'s and -'s, off by one here or there, maybe a bad copy paste with similar looking variable names. Typically math related
> going in literal circles because of some degree/radian conversion errors
Hehe, nice.
And thanks for the tip! Retrying tomorrow is nothing new to me, but I never thought to save yesterday's progress and diff it with the next day's rewrite to figure out why I was stuck.
This connects to Tim Harford's Ted Talk "A powerful way to unleash your natural creativity." Quitting when you hit the wall and moving on to another task will allow your subconscious to work on the problem, often hand delivering it to you when you jump back on your original task. I think knowing I would have to start from scratch the next day would throw that system into hyperdrive. Thanks for sharing!
The first thing I noticed is that the sound output is going through this convoluted 2-buffer system where it writes to one, and when it fills up, switch to the other
I've worked with some Windows audio code before and this is the norm --- while it's playing one buffer, you write to the other one, and switch between the two so the sound card gets an uninterrupted stream of data. In earlier versions of Windows (3.x), the DMA on the sound card would read directly from the buffer.
I'd say the best part of this whole thing is that the original author even made his code available. If he hadn't, we'd be stuck with another broken implementation that nobody could fix or improve long after the author abandoned the project.
30 comments
[ 3.1 ms ] story [ 60.8 ms ] threadEg. Don't try to sleep accurately between the simulated cycles. Let things happen "too fast" for longer and do the delays in batches.
Not sure if this would wind up perceivably too bursty. Just an idea based on things I have seen in other domains.
For the record I don't think spinning is too bad of it's a very short duration, shorter than the scheduler will give you when you take your thread off the CPU.
https://docs.microsoft.com/en-us/windows/desktop/api/synchap...
I could mess around with the Win32 API or the NT API or something to make the heartbeat rate more granular, but according to the docs that kills battery, so I'm guessing it's going to be doing pretty much the same thing as what I'm doing manually.
1) post tasks to process some data
2) while(tasksNotFinished) Sleep(1);
Well, very quickly found out that the Sleep(1) was always a minimum of 16ms(or a multiple of 16ms), which made the whole thing stupidly slow. We fixed it by running timeBeginPeriod(1) which overrides the system timer resolution to 1ms. Yes it would kill the battery on laptops, but on a server this is not an issue.
Back when everyone was targeting 60fps, >~2.25ms of 'leftover time' was needed during the individual 16.6667ms run of that loop to actually trigger sleep instead of spinning.
Note that's specific to the graphical rendering thread, and things like the mouse/kb input have their own sleep durations, with or without thresholds.
Once your emulation time period between two screen frames is completed, you can pause emulation until it catches up to next frame in real time, making process sleeping and not consuming CPU in between frames. Input events and sound add additional complexity, but it's doable.
> passing in a member of a record rather than a pointer to that record... No idea why
...sounds like a difference in how the Windows API was exposed. In Delphi and Free Pascal there are two ways to expose a C function that uses a pointer. This:
can be exposed like this: or like this: The former allows you to pass any pointer to integer, the latter requires an Integer variable reference (which is implemented by the compiler by passing a pointer to the variable). The difference between the two is that you can pass a Nil (Pascal's NULL) to the first, but the second requires a variable.Older versions of Borland Pascal and Delphi (not sure about newer Delphi versions) used the second declaration style where it made sense, but Free Pascal uses a more direct translation of the Windows headers, opting for the first declaration style.
My guess is that the compiler used by the original author of UGE used the first style.
Hmm, I don't know. If I've learned anything about programming languages, then it's that the progression of programs written in them is usually: #1 Hello World, #2 Factorial, #3 Gameboy Emulator.
When nothing makes sense and you're real emotional about it and want to give up... GIVE UP!*
*But you have to write another one from scratch tomorrow because there's good odds you'll do it right on accident and will then be able to diff and understand exactly where you went wrong. Being able to identify this situation and when doing this is useful as early as possible will make you very valuable. The break between attempts is important, go do literally anything else besides code.
I once spent a week going in literal circles because of some degree/radian conversion errors. The rewrite took about 15 minutes.
Never trust anyone's ability to count, not even your own.
It's particularly useful in this exact situation, where the differences are mere +'s and -'s, off by one here or there, maybe a bad copy paste with similar looking variable names. Typically math related
Hehe, nice.
And thanks for the tip! Retrying tomorrow is nothing new to me, but I never thought to save yesterday's progress and diff it with the next day's rewrite to figure out why I was stuck.
https://www.ted.com/talks/tim_harford_a_powerful_way_to_unle....
https://en.wikipedia.org/wiki/Multiple_buffering
I've worked with some Windows audio code before and this is the norm --- while it's playing one buffer, you write to the other one, and switch between the two so the sound card gets an uninterrupted stream of data. In earlier versions of Windows (3.x), the DMA on the sound card would read directly from the buffer.