Handling user input is crucial for any interactive application, especially an emulator. In this chapter, we’ll bridge the gap between your physical keyboard and the Game Boy’s virtual buttons. You’ll learn how to capture keyboard events, map them to the Game Boy’s specific input registers, and integrate this into your emulator’s main loop. By the end, your emulator will respond to your key presses, allowing you to control Game Boy games.

Introduction

So far, our Game Boy emulator can execute CPU instructions, manage memory, and even render basic graphics. But a game isn’t much fun without interaction! This chapter focuses on implementing input handling, specifically translating keyboard presses into the Game Boy’s button states.

We’ll leverage the SDL.NET library, which we’ve already used for graphics, to capture keyboard events. The core challenge is to correctly model the Game Boy’s unique input mechanism and integrate our key mapping logic seamlessly into the existing emulator architecture, ensuring that the Game Boy’s CPU can “read” our button presses.

By the end of this chapter, you’ll be able to press keys on your keyboard and see the Game Boy emulator react, bringing your games to life. This is a significant milestone, as it completes the fundamental interactive loop of the emulator.

Planning & Design

The Game Boy’s input system is relatively simple but requires careful modeling. It uses a single I/O register, 0xFF00 (P1), to detect button presses. This register is somewhat unique because it’s both read and written by the CPU. The CPU writes to P1 to select whether it wants to read the “direction” buttons (Right, Left, Up, Down) or the “action” buttons (A, B, Select, Start). When the CPU reads P1, the bits corresponding to the selected buttons will be 0 if pressed and 1 if not.

Our design needs to address:

  1. Game Boy Button Representation: How do we model the eight Game Boy buttons in F#?
  2. Keyboard Mapping: Which physical keyboard keys correspond to which Game Boy buttons?
  3. Input State Management: How do we track the current pressed/released state of all Game Boy buttons?
  4. SDL Event Integration: How do we capture keyboard events from SDL and update our internal Game Boy button state?
  5. MMU Interaction: How do we expose this button state to the MMU so the CPU can read the P1 register correctly?

Architecture Overview

We’ll introduce a new Input module responsible for managing the Game Boy’s button state and handling SDL keyboard events. This module will maintain the current state of all buttons. The MMU will then query this Input module when the CPU attempts to read the P1 register.

Here’s a conceptual flow:

flowchart TD A[Keyboard Event] --> B[SDL Event Loop] B --> C{Map SDL Key to GB Button} C -->|Yes| D[Update GB Button State] C -->|No| B D --> E[MMU Reads P1 Register] E --> F[CPU Processes Input]

This ensures a clean separation of concerns: the Input module knows about SDL and keyboard keys, while the MMU only knows about Game Boy button states, abstracting away the underlying physical input.

Step-by-Step Implementation

We’ll modify our Emulator.fs, MMU.fs, and potentially create a new Input.fs module.

1. Define Game Boy Buttons and Input State

First, let’s define the Game Boy buttons and a way to store their current state. We’ll add this to a new file, Input.fs.

File: Input.fs

module GbEmulator.Input

open System.Collections.Generic

/// Represents the 8 Game Boy buttons.
type GbButton =
    | Right
    | Left
    | Up
    | Down
    | A
    | B
    | Select
    | Start

/// Represents the current state of all Game Boy buttons.
/// A Dictionary is used for efficient lookup by button type.
type GbInputState =
    {
        ButtonStates: Dictionary<GbButton, bool> // true if pressed, false if released
    }

/// Initializes a new Game Boy input state with all buttons released.
let createInputState () =
    let buttonStates = Dictionary<GbButton, bool>()
    [GbButton.Right; GbButton.Left; GbButton.Up; GbButton.Down;
     GbButton.A; GbButton.B; GbButton.Select; GbButton.Start]
    |> List.iter (fun btn -> buttonStates.Add(btn, false))
    { ButtonStates = buttonStates }

/// Updates the state of a specific Game Boy button.
let updateButtonState (state: GbInputState) (button: GbButton) (isPressed: bool) =
    state.ButtonStates.[button] <- isPressed
    state // Return the modified state (Dictionary is mutable, but we return the record for consistency)

/// Checks if a specific Game Boy button is currently pressed.
let isButtonPressed (state: GbInputState) (button: GbButton) =
    state.ButtonStates.[button]

Explanation:

  • GbButton is a discriminated union representing the 8 distinct Game Boy buttons.
  • GbInputState uses a Dictionary<GbButton, bool> to store whether each button is currently pressed (true) or released (false). Using a Dictionary provides quick access to button states.
  • createInputState initializes this dictionary with all buttons set to false (released).
  • updateButtonState modifies the state of a given button. Note that Dictionary is a mutable type, so we’re performing an in-place update. In F#, it’s common to encapsulate such mutable state within a module or class to maintain functional purity at a higher level.
  • isButtonPressed provides a simple lookup for a button’s state.

2. Define Keyboard Key Mappings

Now, let’s define which physical keyboard keys map to which GbButton. We’ll keep this in Input.fs as well.

File: Input.fs (Append to existing content)

open SDL2

/// Maps SDL_Keycode values to GbButton values.
let private keyMappings =
    Map.ofList [
        (SDL.SDL_Keycode.SDLK_RIGHT, GbButton.Right)
        (SDL.SDL_Keycode.SDLK_LEFT, GbButton.Left)
        (SDL.SDL_Keycode.SDLK_UP, GbButton.Up)
        (SDL.SDL_Keycode.SDLK_DOWN, GbButton.Down)
        (SDL.SDL_Keycode.SDLK_z, GbButton.A) // A button
        (SDL.SDL_Keycode.SDLK_x, GbButton.B) // B button
        (SDL.SDL_Keycode.SDLK_a, GbButton.Select) // Select button
        (SDL.SDL_Keycode.SDLK_s, GbButton.Start) // Start button
    ]

/// Attempts to get the GbButton corresponding to an SDL_Keycode.
let tryGetGbButton (keycode: SDL.SDL_Keycode) : GbButton option =
    Map.tryFind keycode keyMappings

Explanation:

  • keyMappings is an F# Map that stores the association between SDL_Keycode (from SDL2-CS) and our GbButton type.
  • tryGetGbButton is a helper function that safely looks up a GbButton for a given SDL_Keycode, returning an option type (Some GbButton if found, None otherwise). This prevents crashes if an unmapped key is pressed.

3. Integrate Input State into MMU

The MMU (Memory Management Unit) is responsible for handling reads and writes to 0xFF00. We need to modify MMU.fs to allow the MMU to query the current GbInputState.

File: MMU.fs

module GbEmulator.MMU

open GbEmulator.Cpu
open GbEmulator.Input // Add this line to import our Input module

/// Represents the Memory Management Unit's state.
type MmuState =
    {
        Rom: byte array
        Ram: byte array
        Vram: byte array
        Wram: byte array
        Oam: byte array
        IoRegisters: byte array // General purpose I/O registers
        IeRegister: byte // Interrupt Enable register (0xFFFF)
        // Add a field to hold the input state
        InputState: GbInputState
    }

/// Creates a new MMU state.
let createMmu (rom: byte array) (inputState: GbInputState) = // Modify signature
    {
        Rom = rom
        Ram = Array.zeroCreate 0x2000 // 8KB WRAM (0xC000-0xDFFF)
        Vram = Array.zeroCreate 0x2000 // 8KB VRAM (0x8000-0x9FFF)
        Wram = Array.zeroCreate 0x2000 // Work RAM
        Oam = Array.zeroCreate 0xA0 // Object Attribute Memory (OAM)
        IoRegisters = Array.zeroCreate 0x80 // I/O Registers (0xFF00-0xFF7F)
        IeRegister = 0x00uy // Interrupt Enable
        InputState = inputState // Initialize with the passed-in input state
    }

/// Reads a byte from memory at the given address.
let readByte (mmu: MmuState) (addr: uint16) : byte =
    match addr with
    | _ when addr >= 0x0000us && addr <= 0x7FFFus -> mmu.Rom.[int addr] // Cartridge ROM
    | _ when addr >= 0x8000us && addr <= 0x9FFFus -> mmu.Vram.[int (addr - 0x8000us)] // VRAM
    | _ when addr >= 0xC000us && addr <= 0xDFFFus -> mmu.Wram.[int (addr - 0xC000us)] // WRAM
    | _ when addr >= 0xE000us && addr <= 0xFDFFus -> mmu.Wram.[int (addr - 0xE000us)] // Echo RAM (mirror of WRAM)
    | _ when addr >= 0xFE00us && addr <= 0xFE9Fus -> mmu.Oam.[int (addr - 0xFE00us)] // OAM
    | _ when addr >= 0xFF00us && addr <= 0xFF7Fus -> // I/O Registers
        match addr with
        | 0xFF00us -> // P1/JOYP register
            // The Game Boy CPU writes to P1 to select which buttons to read.
            // Bit 5: Select Direction Buttons (P15)
            // Bit 4: Select Action Buttons (P14)
            let p15 = (mmu.IoRegisters.[int (0xFF00us - 0xFF00us)] &&& 0x20uy) = 0x00uy // Is P15 low?
            let p14 = (mmu.IoRegisters.[int (0xFF00us - 0xFF00us)] &&& 0x10uy) = 0x00uy // Is P14 low?

            let mutable result = 0xFFuy // All bits high (buttons not pressed)

            // If P15 is low, read direction buttons
            if p15 then
                if Input.isButtonPressed mmu.InputState Input.GbButton.Right then result <- result &&& (~~~ 0x01uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.Left  then result <- result &&& (~~~ 0x02uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.Up    then result <- result &&& (~~~ 0x04uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.Down  then result <- result &&& (~~~ 0x08uy)

            // If P14 is low, read action buttons
            if p14 then
                if Input.isButtonPressed mmu.InputState Input.GbButton.A      then result <- result &&& (~~~ 0x01uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.B      then result <- result &&& (~~~ 0x02uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.Select then result <- result &&& (~~~ 0x04uy)
                if Input.isButtonPressed mmu.InputState Input.GbButton.Start  then result <- result &&& (~~~ 0x08uy)

            // Combine with the control bits (P15, P14) from the CPU's last write
            result <- result ||| (mmu.IoRegisters.[int (0xFF00us - 0xFF00us)] &&& 0xF0uy)
            result

        | _ -> mmu.IoRegisters.[int (addr - 0xFF00us)] // Other I/O registers
    | 0xFFFFus -> mmu.IeRegister // Interrupt Enable register
    | _ -> 0xFFuy // Unmapped memory returns 0xFF

/// Writes a byte to memory at the given address.
let writeByte (mmu: MmuState) (addr: uint16) (value: byte) : MmuState =
    match addr with
    | _ when addr >= 0x0000us && addr <= 0x7FFFus -> { mmu with Rom = mmu.Rom } // ROM is read-only
    | _ when addr >= 0x8000us && addr <= 0x9FFFus ->
        mmu.Vram.[int (addr - 0x8000us)] <- value; mmu
    | _ when addr >= 0xC000us && addr <= 0xDFFFus ->
        mmu.Wram.[int (addr - 0xC000us)] <- value; mmu
    | _ when addr >= 0xE000us && addr <= 0xFDFFus ->
        mmu.Wram.[int (addr - 0xE000us)] <- value; mmu // Write to WRAM echo
    | _ when addr >= 0xFE00us && addr <= 0xFE9Fus ->
        mmu.Oam.[int (addr - 0xFE00us)] <- value; mmu
    | _ when addr >= 0xFF00us && addr <= 0xFF7Fus ->
        // P1/JOYP register (0xFF00) write: CPU selects which buttons to read
        // Only bits 4 and 5 are writable (P14, P15)
        if addr = 0xFF00us then
            mmu.IoRegisters.[int (addr - 0xFF00us)] <- value &&& 0x30uy // Keep only bits 4 and 5
        else
            mmu.IoRegisters.[int (addr - 0xFF00us)] <- value
        mmu
    | 0xFFFFus -> { mmu with IeRegister = value }
    | _ -> mmu // Do nothing for unmapped writes

Explanation of MMU.fs changes:

  • We added open GbEmulator.Input to use our new input module.
  • MmuState now includes an InputState: GbInputState field.
  • createMmu now takes a GbInputState as an argument, which it stores.
  • Crucially, the readByte function’s 0xFF00us case is updated:
    • It first reads the current value in IoRegisters.[0] (which corresponds to 0xFF00) to determine if the CPU wants to read direction buttons (Bit 5 low) or action buttons (Bit 4 low).
    • p15 and p14 flags check if the respective bits are low (0).
    • result is initialized to 0xFFuy (all bits high, indicating no buttons pressed).
    • Based on p15 and p14, it queries mmu.InputState using Input.isButtonPressed. If a button is pressed, the corresponding bit in result is set to 0. (e.g., ~~~ 0x01uy is 0xFEuy, which effectively clears bit 0).
    • Finally, the previously written P14 and P15 bits from the IoRegisters are re-applied to the result, as these are control bits, not button states.
  • The writeByte for 0xFF00us is also updated to only allow writing to bits 4 and 5, as the lower bits are read-only (reflecting button state).

4. Integrate SDL Event Handling into the Main Loop

Now, we need to modify our main emulator loop (likely in Emulator.fs or Program.fs) to process SDL keyboard events and update the GbInputState.

File: Emulator.fs (or Program.fs if your main loop is there)

First, ensure SDL2-CS is installed.

dotnet add package SDL2-CS --version 2.28.0 # Or latest stable

(Using 2.28.0 as a placeholder for a recent stable version as of 2026-05-05. Verify the latest on NuGet.)

module GbEmulator.Emulator

open SDL2
open System
open System.Diagnostics
open GbEmulator.Cpu
open GbEmulator.MMU
open GbEmulator.Ppu
open GbEmulator.Input // Add this line

/// Represents the overall state of the Game Boy emulator.
type EmulatorState =
    {
        Cpu: CpuState
        Mmu: MmuState
        Ppu: PpuState
        Cycles: int64 // Total CPU cycles elapsed
        Input: GbInputState // Keep a reference to the mutable input state
    }

/// Initializes a new emulator state.
let createEmulator (romData: byte array) =
    let inputState = Input.createInputState() // Create input state
    let mmu = MMU.createMmu romData inputState // Pass input state to MMU
    let cpu = Cpu.createCpu()
    let ppu = Ppu.createPpu()
    { Cpu = cpu; Mmu = mmu; Ppu = ppu; Cycles = 0L; Input = inputState } // Store input state

Now, modify the main loop. This example assumes your main loop is in Emulator.fs and interacts with SDL.

File: Emulator.fs (Modify the main loop function)

// ... (previous code) ...

/// Processes SDL events, updating the emulator's input state.
let handleSdlEvents (emulator: EmulatorState) : EmulatorState =
    let mutable event = SDL.SDL_Event()
    let mutable currentEmulator = emulator // Use mutable local variable for updates

    while SDL.SDL_PollEvent(&event) = 1 do
        match event.type with
        | SDL.SDL_EventType.SDL_QUIT ->
            // Signal to quit the emulator
            currentEmulator <- { currentEmulator with Cpu = { currentEmulator.Cpu with Running = false } }
        | SDL.SDL_EventType.SDL_KEYDOWN ->
            let keycode = event.key.keysym.sym
            match Input.tryGetGbButton keycode with
            | Some gbButton ->
                Input.updateButtonState currentEmulator.Input gbButton true // Update button state
            | None -> () // Ignore unmapped keys
        | SDL.SDL_EventType.SDL_KEYUP ->
            let keycode = event.key.keysym.sym
            match Input.tryGetGbButton keycode with
            | Some gbButton ->
                Input.updateButtonState currentEmulator.Input gbButton false // Update button state
            | None -> () // Ignore unmapped keys
        | _ -> () // Ignore other event types
    currentEmulator

/// The main emulator loop.
let runEmulator (emulator: EmulatorState) : unit =
    let mutable currentEmulator = emulator
    let targetFps = 60.0
    let targetFrameTimeMs = 1000.0 / targetFps
    let mutable lastFrameTime = Stopwatch.GetTimestamp()

    while currentEmulator.Cpu.Running do
        let frameStartTime = Stopwatch.GetTimestamp()

        // 1. Handle SDL events (input, quit)
        currentEmulator <- handleSdlEvents currentEmulator // Call our new event handler

        // 2. Emulate CPU cycles for one frame (roughly 70224 cycles for 60Hz)
        let cyclesThisFrame = 0 // Reset for calculation below
        let maxCycles = 70224 // Game Boy CPU cycles per frame at 60Hz
        let mutable currentCycles = 0

        while currentCycles < maxCycles && currentEmulator.Cpu.Running do
            let (newCpu, newMmu, cyclesExecuted) = Cpu.executeNextInstruction currentEmulator.Cpu currentEmulator.Mmu
            currentEmulator <- { currentEmulator with Cpu = newCpu; Mmu = newMmu; Cycles = currentEmulator.Cycles + int64 cyclesExecuted }
            currentCycles <- currentCycles + cyclesExecuted

            // PPU step (update PPU for the cycles executed)
            let (newPpu, newMmuPpu) = Ppu.step currentEmulator.Ppu newMmu cyclesExecuted
            currentEmulator <- { currentEmulator with Ppu = newPpu; Mmu = newMmuPpu }

        // 3. Render frame if PPU signals (handled by PPU.step)
        // The PPU module will handle rendering to the SDL window itself
        // if a full frame is ready.

        // 4. Frame rate synchronization
        let frameEndTime = Stopwatch.GetTimestamp()
        let elapsedTicks = frameEndTime - frameStartTime
        let elapsedMs = float elapsedTicks / float Stopwatch.Frequency * 1000.0

        let sleepTime = targetFrameTimeMs - elapsedMs
        if sleepTime > 0.0 then
            System.Threading.Thread.Sleep(int sleepTime)

    printfn "Emulator stopped."
    SDL.SDL_Quit()

Explanation of Emulator.fs changes:

  • We added open GbEmulator.Input.
  • EmulatorState now includes Input: GbInputState to hold the reference to our mutable input state.
  • createEmulator now initializes Input.createInputState() and passes it to MMU.createMmu.
  • A new function handleSdlEvents is introduced. This function:
    • Polls for SDL events using SDL.SDL_PollEvent.
    • Handles SDL_QUIT to stop the emulator.
    • For SDL_KEYDOWN and SDL_KEYUP events, it uses Input.tryGetGbButton to find the corresponding GbButton.
    • If a mapped button is found, it calls Input.updateButtonState to set the button’s state (pressed or released).
  • The runEmulator main loop now calls handleSdlEvents at the beginning of each frame.

โšก Quick Note: While Dictionary is mutable, we’re passing it around within a record. In F#, it’s a common pragmatic choice for performance-critical areas like input state, especially when it’s tightly managed within a single logical component (the Input module). The EmulatorState holds a reference to this mutable GbInputState, and functions like handleSdlEvents modify it directly.

Testing & Verification

To verify our input handling, we need a Game Boy ROM that visually indicates button presses.

  1. Build and Run: Compile your project:

    dotnet build
    dotnet run --path/to/your/rom.gb
    

    Replace path/to/your/rom.gb with a Game Boy ROM.

  2. Test with Blargg’s Test ROMs: Blargg’s CPU instruction test ROMs, specifically cpu_instrs/individual/01-special.gb, often include a screen that displays the state of the P1 register, which is perfect for verifying input.

    • Download Blargg’s CPU instruction test ROMs.
    • Run 01-special.gb. It usually shows a sequence of tests. Look for a screen that changes when you press keys.
  3. Manual Playtesting: Load a simple Game Boy game (e.g., Tetris, Dr. Mario).

    • Press the configured keys (Z for A, X for B, etc.).
    • Observe if the game responds correctly to your inputs. Can you move the cursor, make selections, or play the game as expected?
  4. Debugging P1 Register: If things aren’t working, consider adding a temporary logging statement in your MMU.fs within the readByte function for address 0xFF00us. This will let you see the exact value the CPU is reading from the input register.

    // Inside MMU.fs, in the readByte function for 0xFF00us:
    // ...
            result <- result ||| (mmu.IoRegisters.[int (0xFF00us - 0xFF00us)] &&& 0xF0uy)
            // Debugging line:
            // printfn "P1 Read: 0x%02x, P15: %b, P14: %b" result p15 p14
            result
    

    This helps confirm if the result byte reflects your button presses correctly. Remember that 0 means pressed, 1 means released for the button bits.

Production Considerations

Performance

Input handling is generally not a major performance bottleneck for emulators. SDL’s event polling is efficient. The key is to avoid complex logic within the event loop itself. Our current approach of simple dictionary lookups and bitwise operations is very fast.

Maintainability

  • Clear Key Mapping: The keyMappings Map is easy to understand and modify. For a real production emulator, you’d likely want to externalize this configuration (e.g., to a JSON file) to allow user customization.
  • Modular Design: Separating input logic into Input.fs keeps the MMU and Emulator modules cleaner and focused on their core responsibilities.

Customization

A common feature in emulators is configurable key bindings. To achieve this, you would:

  1. Load key mappings from a configuration file (e.g., appsettings.json, or a custom .ini file).
  2. Provide a UI for users to change these mappings and save them. Our current hardcoded keyMappings provides the foundation for this.

Common Issues & Solutions

1. Keys “Sticking” or Not Registering Releases

Issue: Buttons remain pressed even after releasing the key, or key presses are missed. Cause:

  • SDL_KEYUP events are not being processed correctly.
  • The handleSdlEvents loop might not be called frequently enough, or the event queue might be overflowing (unlikely for a simple emulator). Solution:
  • Ensure handleSdlEvents is called every frame in your main loop.
  • Double-check the SDL_KEYUP event handling logic in handleSdlEvents to confirm it correctly calls Input.updateButtonState with false.
  • Verify your keyMappings are consistent for both KEYDOWN and KEYUP events.

2. Incorrect Button Behavior (e.g., A button acts like Right)

Issue: Pressing a key triggers the wrong Game Boy button. Cause:

  • Incorrect keyMappings in Input.fs.
  • Mistakes in the bitwise logic within MMU.readByte for 0xFF00us, where the button states are translated into the P1 register value. Solution:
  • Carefully review Input.keyMappings against your desired keyboard layout.
  • Re-examine the MMU.readByte logic for 0xFF00us. Remember:
    • Bit 0 (0x01) is Right/A
    • Bit 1 (0x02) is Left/B
    • Bit 2 (0x04) is Up/Select
    • Bit 3 (0x08) is Down/Start
    • 0 means pressed, 1 means released. So, result <- result &&& (~~~ 0x01uy) is the correct way to set a bit to 0 if pressed.

3. Emulator Not Responding to Any Keys

Issue: No input is registered at all. Cause:

  • SDL event polling is not happening.
  • The handleSdlEvents function is not being called in the main loop.
  • The MMU isn’t correctly querying the InputState or the InputState isn’t being passed to the MMU. Solution:
  • Verify currentEmulator <- handleSdlEvents currentEmulator is present and correctly placed in runEmulator.
  • Ensure GbEmulator.Input is opened in MMU.fs and Emulator.fs.
  • Confirm that MMU.createMmu receives and stores the GbInputState correctly.
  • Check that the 0xFF00us case in MMU.readByte actually calls Input.isButtonPressed.

Summary & Next Step

In this chapter, you’ve successfully implemented keyboard input for your Game Boy emulator. We defined the Game Boy’s buttons, created a mapping from SDL keyboard events to these buttons, managed the button press state, and integrated this state into the MMU so the CPU can read it. You can now interact with Game Boy ROMs using your keyboard!

Your emulator now has the core components for interactivity: CPU, MMU, PPU, and Input. The next logical step is to add sound, which will bring another dimension of realism to your emulator.

๐Ÿง  Check Your Understanding

  • Why does the Game Boy’s P1 register require both reading and writing by the CPU for input handling?
  • What are the advantages of using a Map for key mappings over a series of if/else statements or a match expression?
  • Explain the purpose of result <- result &&& (~~~ 0x01uy) in the MMU’s readByte function for the P1 register.

โšก Mini Task

  • Modify the Input.fs module to include an alternative set of key mappings (e.g., using WASD for directions and JKL; for action buttons) and a way to switch between them.

๐Ÿš€ Scenario

You’re running a Game Boy test ROM that displays the raw value of the P1 register (0xFF00). When you press the ‘A’ button (mapped to ‘Z’), the P1 register value changes from 0xFF to 0xEF. When you press ‘Right’ (mapped to ‘Right Arrow’), it changes from 0xFF to 0xFE. However, when you press both ‘A’ and ‘Right’ simultaneously, the register value becomes 0xEE. Explain why this happens based on the Game Boy’s P1 register behavior and bitwise operations.

๐Ÿ“Œ TL;DR

  • Game Boy input uses I/O register 0xFF00 (P1), which the CPU writes to select button groups (directions or actions) and reads to get button states.
  • We modeled Game Boy buttons with an F# Discriminated Union and managed their state (pressed/released) using a mutable Dictionary within an Input module.
  • SDL keyboard events (KEYDOWN, KEYUP) are captured and mapped to GbButtons to update the InputState.
  • The MMU’s readByte function for 0xFF00 now queries the InputState based on the CPU’s selection bits (P14, P15) and returns the correct byte, where a 0 indicates a pressed button.

๐Ÿง  Core Flow

  1. SDL event loop captures keyboard KEYDOWN/KEYUP events.
  2. Mapped SDL_Keycode to GbButton using Input.keyMappings.
  3. Input.updateButtonState modifies the GbInputState (a dictionary of button bools).
  4. Game Boy CPU reads 0xFF00 (P1) via MMU.readByte.
  5. MMU.readByte queries the GbInputState based on CPU’s P14/P15 selection bits and constructs the P1 register value, returning 0 for pressed buttons.

๐Ÿš€ Key Takeaway

Effective emulator input handling requires correctly modeling the emulated hardware’s input registers and seamlessly integrating modern input events into that model, ensuring that the emulated CPU “sees” the input as it would on real hardware.


References

  1. F# Language Reference: https://learn.microsoft.com/en-us/dotnet/fsharp/
  2. .NET Documentation: https://learn.microsoft.com/en-us/dotnet/
  3. SDL Documentation: https://wiki.libsdl.org/
  4. Pan Docs (Game Boy Technical Reference): https://gbdev.io/pandocs/
  5. SDL2-CS NuGet Package: https://www.nuget.org/packages/SDL2-CS/

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.