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:
- Game Boy Button Representation: How do we model the eight Game Boy buttons in F#?
- Keyboard Mapping: Which physical keyboard keys correspond to which Game Boy buttons?
- Input State Management: How do we track the current pressed/released state of all Game Boy buttons?
- SDL Event Integration: How do we capture keyboard events from SDL and update our internal Game Boy button state?
- MMU Interaction: How do we expose this button state to the MMU so the CPU can read the
P1register 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:
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:
GbButtonis a discriminated union representing the 8 distinct Game Boy buttons.GbInputStateuses aDictionary<GbButton, bool>to store whether each button is currently pressed (true) or released (false). Using aDictionaryprovides quick access to button states.createInputStateinitializes this dictionary with all buttons set tofalse(released).updateButtonStatemodifies the state of a given button. Note thatDictionaryis 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.isButtonPressedprovides 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:
keyMappingsis an F#Mapthat stores the association betweenSDL_Keycode(fromSDL2-CS) and ourGbButtontype.tryGetGbButtonis a helper function that safely looks up aGbButtonfor a givenSDL_Keycode, returning anoptiontype (Some GbButtonif found,Noneotherwise). 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.Inputto use our new input module. MmuStatenow includes anInputState: GbInputStatefield.createMmunow takes aGbInputStateas an argument, which it stores.- Crucially, the
readBytefunction’s0xFF00uscase is updated:- It first reads the current value in
IoRegisters.[0](which corresponds to0xFF00) to determine if the CPU wants to read direction buttons (Bit 5 low) or action buttons (Bit 4 low). p15andp14flags check if the respective bits are low (0).resultis initialized to0xFFuy(all bits high, indicating no buttons pressed).- Based on
p15andp14, it queriesmmu.InputStateusingInput.isButtonPressed. If a button is pressed, the corresponding bit inresultis set to0. (e.g.,~~~ 0x01uyis0xFEuy, which effectively clears bit 0). - Finally, the previously written P14 and P15 bits from the
IoRegistersare re-applied to the result, as these are control bits, not button states.
- It first reads the current value in
- The
writeBytefor0xFF00usis 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. EmulatorStatenow includesInput: GbInputStateto hold the reference to our mutable input state.createEmulatornow initializesInput.createInputState()and passes it toMMU.createMmu.- A new function
handleSdlEventsis introduced. This function:- Polls for SDL events using
SDL.SDL_PollEvent. - Handles
SDL_QUITto stop the emulator. - For
SDL_KEYDOWNandSDL_KEYUPevents, it usesInput.tryGetGbButtonto find the correspondingGbButton. - If a mapped button is found, it calls
Input.updateButtonStateto set the button’s state (pressed or released).
- Polls for SDL events using
- The
runEmulatormain loop now callshandleSdlEventsat 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.
Build and Run: Compile your project:
dotnet build dotnet run --path/to/your/rom.gbReplace
path/to/your/rom.gbwith a Game Boy ROM.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.
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?
Debugging
P1Register: If things aren’t working, consider adding a temporary logging statement in yourMMU.fswithin thereadBytefunction for address0xFF00us. 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 resultThis helps confirm if the
resultbyte reflects your button presses correctly. Remember that0means pressed,1means 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
keyMappingsMapis 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.fskeeps theMMUandEmulatormodules cleaner and focused on their core responsibilities.
Customization
A common feature in emulators is configurable key bindings. To achieve this, you would:
- Load key mappings from a configuration file (e.g.,
appsettings.json, or a custom.inifile). - Provide a UI for users to change these mappings and save them.
Our current hardcoded
keyMappingsprovides 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_KEYUPevents are not being processed correctly.- The
handleSdlEventsloop might not be called frequently enough, or the event queue might be overflowing (unlikely for a simple emulator). Solution: - Ensure
handleSdlEventsis called every frame in your main loop. - Double-check the
SDL_KEYUPevent handling logic inhandleSdlEventsto confirm it correctly callsInput.updateButtonStatewithfalse. - Verify your
keyMappingsare consistent for bothKEYDOWNandKEYUPevents.
2. Incorrect Button Behavior (e.g., A button acts like Right)
Issue: Pressing a key triggers the wrong Game Boy button. Cause:
- Incorrect
keyMappingsinInput.fs. - Mistakes in the bitwise logic within
MMU.readBytefor0xFF00us, where the button states are translated into the P1 register value. Solution: - Carefully review
Input.keyMappingsagainst your desired keyboard layout. - Re-examine the
MMU.readBytelogic for0xFF00us. 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
0means pressed,1means released. So,result <- result &&& (~~~ 0x01uy)is the correct way to set a bit to0if pressed.
3. Emulator Not Responding to Any Keys
Issue: No input is registered at all. Cause:
- SDL event polling is not happening.
- The
handleSdlEventsfunction is not being called in the main loop. - The
MMUisn’t correctly querying theInputStateor theInputStateisn’t being passed to theMMU. Solution: - Verify
currentEmulator <- handleSdlEvents currentEmulatoris present and correctly placed inrunEmulator. - Ensure
GbEmulator.Inputisopened inMMU.fsandEmulator.fs. - Confirm that
MMU.createMmureceives and stores theGbInputStatecorrectly. - Check that the
0xFF00uscase inMMU.readByteactually callsInput.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
Mapfor key mappings over a series ofif/elsestatements or amatchexpression? - Explain the purpose of
result <- result &&& (~~~ 0x01uy)in the MMU’sreadBytefunction for the P1 register.
โก Mini Task
- Modify the
Input.fsmodule 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
Dictionarywithin anInputmodule. - SDL keyboard events (
KEYDOWN,KEYUP) are captured and mapped toGbButtons to update theInputState. - The
MMU’sreadBytefunction for0xFF00now queries theInputStatebased on the CPU’s selection bits (P14, P15) and returns the correct byte, where a0indicates a pressed button.
๐ง Core Flow
- SDL event loop captures keyboard
KEYDOWN/KEYUPevents. - Mapped
SDL_KeycodetoGbButtonusingInput.keyMappings. Input.updateButtonStatemodifies theGbInputState(a dictionary of buttonbools).- Game Boy CPU reads
0xFF00(P1) viaMMU.readByte. MMU.readBytequeries theGbInputStatebased on CPU’s P14/P15 selection bits and constructs the P1 register value, returning0for 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
- F# Language Reference: https://learn.microsoft.com/en-us/dotnet/fsharp/
- .NET Documentation: https://learn.microsoft.com/en-us/dotnet/
- SDL Documentation: https://wiki.libsdl.org/
- Pan Docs (Game Boy Technical Reference): https://gbdev.io/pandocs/
- 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.