Reviving a 19 Year Old Gameboy Emulator
nickfa.ro
nickfa.ro
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.
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....
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.
> 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.
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.
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.