Skip to content

Initial program structure (idea)

Lukas Kalbertodt edited this page Aug 15, 2018 · 2 revisions

Peripherals

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.

Debugging

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...
  • 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.

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.

Main entry point

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.

Primitives

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).

Clone this wiki locally