The Chatterbox > Gaming

GameBoy Emulation

<< < (2/4) > >>

Bobbias:
It's short enough I can toss it in a gist. I didn't include any of the actual programs provided by the advent of code problems, so you'd have to grab those yourself if you actually wanted to test it out. Documentation is somewhat lacking. The basic way it works is that you provide a queue for IO to and from the VM, and pass that into the run function, which runs until some stop condition is reached. By default, the stop condition is an output opcode, but some problems required it wait on input instead of halt on output, because several of the questions involved running several VMs at once with outputs from one VM being passed to the next one in sequence. Originally I only needed to halt when it output something, but once they started requiring multiple VMs to feed into each other I realized I had to modify how that worked. Adding the ability to halt on either input or output was what inspired me to refactor things so you could halt on any opcode. I originally wanted to try my hand at asynchronous operation, but I was struggling to get things to synchronize properly and eventually gave up on that idea because it was overly complicated and unnecessary.

Here's the gist.

Spectere:
Without knowing the exact issue (or exact knowledge of the Advent of Code 2019 problems), I suspect your issue might be with this (on line 134):


--- Code: ---self.break_on & op
--- End code ---

Unless I'm misreading your intention, a bitwise AND isn't really appropriate in this situation since that'll cause any common bit to trigger the breakpoint. Basically, if break_on is set to 0xF0 and op is 0x1F, it'll still trigger the breakpoint.

And yeah, I was kinda thinking of making Plip a bit more asynchronous—most notably splitting off the video and audio threads—but I knew I would be able to get a GameBoy emulator running on a single thread on pretty much any modern system and was very eager to get started on the meat of the project. I think that Plip is just modular enough that I may be able to split those things off into separate threads, but I don't think it'll matter much in the grand scheme of things.

The memory subsystem is by far the biggest sticking point of the project, with fetching values from memory consuming far more CPU time than I like. Being able to control memory access on a class level is fantastic, and with the way the system currently works the CPU should theoretically have the same view of the memory controller that a real GameBoy does without me having to do anything special, but when you multiply a tiny bit of overhead times a million it ends up turning into quite a bit of overhead (and it has a few tricks that allow emulators to detect writes to ROM, as this is what GameBoy memory bank controllers use to modify their registers). As-is, this approach likely won't be viable beyond the third console generation. It'll just take far too many CPU cycles to be viable.

I decided to toss mine up on GitHub, so here ya go (linking directly to the dmg branch, as the master branch only includes a fairly meh CHIP-8 emulator I threw together as a test of the audio/video/input subsystems): https://github.com/Spectere/Plip/tree/dmg. It should theoretically compile with MSVC, but seeing as 90% of the development was done on my Mac (clang) and the remainder was done on my Gentoo box (gcc) I can't guarantee that it'll work without modification.

I sort of like my approach to the CPU in particular. It likely suffers quite a bit of overhead compared to Gambatte's CPU core due to the number of function calls (which, again, pales in comparison to the time spent in PlipMemoryMap) but I aimed to leverage macros in a way to try and expresses what the CPU is doing during each mcycle, as well as stuff some common functionality (i.e. fetching the next byte and advancing the PC) into macros.

The method for decoding the opcode used to be a bit more elegant, such as allowing for both one-off opcodes as well as allowing for a masked opcode (stuff like LD r, r' that take distinct parameters) but I opted for a switch block to effectively force the compiler to create a jump table. While the compiler would have likely created a jump table out of the non-masked if conditions, the lack of this optimization on debug builds made stepping through the op decode process extremely tedious.

Spectere:
I just finished implementing the first part of Plip's console.



Right now it just echoes whatever you type in, but obviously it's going to do a little bit more than that after I'm finished with it. ;) It automatically pauses emulation as well, so the state of the core won't change while the console is open. I'm likely going to add some sort of simple overlay layer as well to indicate the state of emulation (pretty much just a pause icon if the emulation is paused) as well as a shortcut key to single-step the CPU if emulation is paused.

I drew that particular font (8x12) myself, but it should theoretically be able to seamlessly use any 256 character font arranged in a 16x16 grid. It doesn't support Unicode at all (it'll just interpret codepoints as ASCII strings) but that's not really necessary in this case.

The next step is going to be creating a basic command processor for the console. Kinda thinking I'm going to take the simplest approach and keep commands in an alphabetically sorted list, and allowing the nearest unambiguous match (for example, if "getPC" and "getReg" are registered, "get" would throw an error but "getr" would expand to "getReg"). I'm hesitant to spend too much time on this, but at the same time I think I'll regret not making it at least half-decent in the long run.

After that, I'm probably going to provide a more generalized way of reading the register state from the core's CPU. Right now I have a DumpRegisters() function that just spits everything out to a std::string (which is mostly used for core/CPU exception handlers at this time), but that's not always ideal. What I'll probably end up doing is writing a function that formats register info into a std::unordered_map. That would allow me to get the value of individual registers without dealing with the more verbose DumpRegisters() output in a EmuCPU-agnostic way.

Spectere:

--- Quote from: Spectere on August 14, 2020, 10:05:13 PM ---Kinda thinking I'm going to take the simplest approach and keep commands in an alphabetically sorted list, and allowing the nearest unambiguous match (for example, if "getPC" and "getReg" are registered, "get" would throw an error but "getr" would expand to "getReg").

--- End quote ---

This is what I wound up doing, and it ended up being pretty simple.

The console system narrows down the command list using the first token in the user's input. If there are no candidates, it fires off an error. If there is only one candidate (or multiple candidates with an exact/unambiguous match) it'll select that. If there are multiple candidates with no exact matches, it displays each of the possible candidates.

When a command is registered, the console system expects the command's full name, as well as a function pointer (with a pointer to the Console instance and a vector of strings as parameters). When the command is called, the function pointer is called with the console instance and a tokenized parameter list attached.

The only supported commands at this point are "help" and "quit," but it's at a point where I can push ahead and start adding debug commands. I'm not too crazy about how the video subsystem interacts with the console (it's immensely obvious that it's an afterthought), but it's not really harming anything at this point. I might end up refactoring it down the road, but it's fine for now.

Spectere:
I took a break this past week due to having an absolutely horrendous week at work and opted to pick up where I left off tonight. First, a fancy pictographical thing:



Register dumping! Neat. Breakpoints are going to be a bit more "interesting" due to how the frontend only talks to a generic system core object, but it'll still be doable. The onus will basically be on the CPU core to catch the condition and send a message back to halt execution.

I did implement pausing, machine cycle stepping, and frame stepping. I suspect that'll help out pretty significantly in the long run.

In addition to that, I ended up doing a much-needed refactor of the game loop. Before I basically dumped all of that into main.cpp, but I opted to pull that into its own GameLoop class and relegate main.cpp to set up the loop and transfer execution to that. In addition to that, I changed the Console class from using C-style function pointers (which really don't work with non-static member function) to using std::function and binding lambdas (which can access instance members) to that.

Next step: breakpoints. Next next step: making things actually boot.

Edit: Scratch that. Next step: memory fudging.



Being able to get and set arbitrary memory locations is pretty useful. While in this case I just use it to corrupt a few tiles of the Nintendo logo and change the X scroll register (which is actually good, since now I know that my X scrolling implementation works :P), it gives me the ability to peek and poke at the system's various registers, since they're alllllll memory mapped.

Navigation

[0] Message Index

[#] Next page

[*] Previous page

Go to full version