-
Notifications
You must be signed in to change notification settings - Fork 2
Initial program structure (idea)
First, we need some trait to abstract over the peripherals of the Game Boy. This includes the display, the sound-device and the buttons. This might also include the debugger, but might not. I think this trait should be defined in its own module -- which would also define all of the helper types (PixelPos
, Color
, ...) plus everything related to the debugger. Potential name of module: env
(for environment
).
// Alternative names: `Io`, `Env`, `Environment`, ...
trait Peripherals {
/// Set's a pixels value on the display. This is used by the PPU whenever
/// a pixel is emitted.
fn set_pixel(&mut self, pos: PixelPos, color: Color);
// ... there are obviously many methods missing here. But we can add
// those later.
}
Alternatively, the trait could be design like this:
trait Peripherals {
type Display: Display;
type Sound: Sound;
type Input: Input;
fn display(&mut self) -> &mut Self::Display;
fn sound(&mut self) -> &mut Self::Sound;
fn input(&mut self) -> &mut Self::Input;
}
trait Display {
fn set_pixel(...);
}
trait Sound { ... }
trait Input { ... }
This might be a bit nicer, since we don't throw random methods for the display, sound and inputs into the same trait.
First of all: the debugger should be flexible enough to allow for two completely different kinds of control: via terminal commands (gdb style) for mahboi-desktop
and via nice HTML UI for mahboi-web
. However, we might want to build TUI for mahboi-desktop
. That would be kind of cool.
What do we want from the debugger?
- Show some statistic, like:
- Real time to execute one frame & Framerate...
- An event log with events of different "importance". The user should be able to hide events that happen often and only view seldom events. For example:
- Rather seldom:
- Switch between double-speed mode and normal mode
- Go into energy saving mode
- ...
- More often:
- Enable/disable certain interrupts
- Switch cartridge banks
- ...
- Probably very often:
- a bunch of stuff...
- Rather seldom:
- Being able to pause execution at any point (manually or via break points)
- When execution is paused, we can:
- See the whole state:
- See the disassembled version of the code around the current program counter
- See all CPU registers
- Inspect all the address space (special IO registers should be displayed in a nicer way)
- See the VRAM in a proper way: render sprites, background, pallettes, ...
- Control execution:
- Execute one instruction
- Continue execution (until the next breakpoint)
- Execute until we leave the current function (
ret
, ...) or call another function (call
, ...)
- Modify data? Not sure how useful that is.
- See the whole state:
Most of the debugger will be implemented in the -web
and -desktop
crate. However, the core
crate needs some access to the debugger, too. That's what the Debugger
trait is for:
trait Debugger {
// This will be used by various parts of the emulator to basically "log"
// events.
fn post_event(&mut self, level: EventLevel, msg: String);
// This needs to be used to pause the emulator (e.g. when a breakpoint is
// hit). We need to pass certain information to this method, like the PC
// to check for breakpoints, but possibly some additional things.
fn should_pause(&self, /* some info about instruction & pc */) -> bool;
}
/// These names are stolen from the standard logging levels. But we can
/// change all of this.
enum EventLevel {
Info,
Debug,
/// For things that occur extremely often
Trace,
}
That's basically everything that needs to happen from the inside of the emulator. Everything else will be controlled by the debugging code outside of core
. Data from the emulator can be accessed via getters.
I would put all of this in a debug
module.
One master struct:
// Or we call the generics `PeripheralsT` and `DebugT`
struct Emulator<'a, P: 'a + Peripherals, D: 'a + Debugger> {
machine: Machine,
debug: &'a mut D,
peripherals: &'a mut P,
// ...
}
This has to be created by mahboi-web
and mahboi-desktop
, providing a debugger and the peripherals: Emulator::new(cartridge, &mut debugger, &mut peripherals)
or something like that.
There should probably be a method execute_frame()
that runs the emulator for exactly 17,556 cycles which is one frame duration (inclusive V-blank). This is probably the main method being called repeatedly by -web
and -desktop
. After executing this once, the emulator has written a new frame via the display (defined as peripherals) and the display buffer can be written to the actual display (minifb
for -desktop
or the canvas for -web
).
But there also be a method executing exactly one cycle of the gameboy (execute_cycle()
?). This can be used by the emulator to step through instruction by instruction.
I'd suggest creating a module primitives
which contains types as:
struct Byte(u8)
struct Addr(u16)
Those implement Add
, Sub
, ... (should use wrapping_*
methods I'd think). Display
and Debug
should be implemented as well (probably doing the same).