From 1ec05edfb93a1ab71bd1a48c5c6ef0f7f7724b7c Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Tue, 31 Dec 2024 23:49:57 -0600 Subject: [PATCH] Implement Joypad --- devices/host_interface.go | 1 + devices/interrupts.go | 4 ++ devices/joypad.go | 132 ++++++++++++++++++++++++++++++++++++++ hardware/dmg.go | 9 +++ host/cli.go | 7 ++ host/ui.go | 39 +++++++++++ 6 files changed, 192 insertions(+) create mode 100644 devices/joypad.go diff --git a/devices/host_interface.go b/devices/host_interface.go index 2dbc019..6e39315 100644 --- a/devices/host_interface.go +++ b/devices/host_interface.go @@ -4,6 +4,7 @@ import "image" type HostInterface interface { Framebuffer() chan<- image.Image + JoypadInput() <-chan JoypadInputs Log(msg string, args ...any) LogErr(msg string, args ...any) LogWarn(msg string, args ...any) diff --git a/devices/interrupts.go b/devices/interrupts.go index 57a85c4..b757dd9 100644 --- a/devices/interrupts.go +++ b/devices/interrupts.go @@ -150,6 +150,10 @@ func (ic *InterruptController) RequestLCD() { ic.requested.lcd = true } +func (ic *InterruptController) RequestJoypad() { + ic.requested.joypad = true +} + func (ic *InterruptController) RequestSerial() { ic.requested.serial = true } diff --git a/devices/joypad.go b/devices/joypad.go new file mode 100644 index 0000000..c6ca846 --- /dev/null +++ b/devices/joypad.go @@ -0,0 +1,132 @@ +package devices + +import ( + "sync" + + "github.com/maxfierke/gogo-gb/mem" +) + +const ( + REG_JOYP = 0xFF00 +) + +const ( + REG_JOYP_BIT_A_RIGHT = iota + REG_JOYP_BIT_B_LEFT + REG_JOYP_BIT_SELECT_UP + REG_JOYP_BIT_START_DOWN + REG_JOYP_BIT_DPAD_SEL + REG_JOYP_BIT_BUTTONS_SEL +) + +type JoypadInputs struct { + A bool + B bool + Up bool + Down bool + Left bool + Right bool + Start bool + Select bool +} + +func (ji JoypadInputs) AnyPressed() bool { + return ji.A || ji.B || ji.Up || ji.Down || ji.Left || ji.Right || ji.Start || ji.Select +} + +type Joypad struct { + readButtons bool + readDPad bool + inputState JoypadInputs + inputStateMu sync.Mutex + + ic *InterruptController +} + +func NewJoypad(ic *InterruptController) *Joypad { + return &Joypad{ + ic: ic, + } +} + +func (j *Joypad) ReceiveInputs(inputs JoypadInputs) { + j.inputStateMu.Lock() + defer j.inputStateMu.Unlock() + + if inputs.AnyPressed() { + j.ic.RequestJoypad() + } + + j.inputState = inputs +} + +func (j *Joypad) OnRead(mmu *mem.MMU, addr uint16) mem.MemRead { + if addr == REG_JOYP { + j.inputStateMu.Lock() + defer j.inputStateMu.Unlock() + + var ( + readButtons uint8 + readDPad uint8 + startDown uint8 + selectUp uint8 + bLeft uint8 + aRight uint8 + ) + + if j.readButtons { + readButtons = 1 << REG_JOYP_BIT_BUTTONS_SEL + } + + if j.readDPad { + readDPad = 1 << REG_JOYP_BIT_DPAD_SEL + } + + if j.inputState.Start && j.readButtons { + startDown = 1 << REG_JOYP_BIT_START_DOWN + } + + if j.inputState.Down && j.readDPad { + startDown |= 1 << REG_JOYP_BIT_START_DOWN + } + + if j.inputState.Select && j.readButtons { + selectUp = 1 << REG_JOYP_BIT_SELECT_UP + } + + if j.inputState.Up && j.readDPad { + selectUp |= 1 << REG_JOYP_BIT_SELECT_UP + } + + if j.inputState.B && j.readButtons { + bLeft = 1 << REG_JOYP_BIT_B_LEFT + } + + if j.inputState.Left && j.readDPad { + bLeft |= 1 << REG_JOYP_BIT_B_LEFT + } + + if j.inputState.A && j.readButtons { + aRight = 1 << REG_JOYP_BIT_A_RIGHT + } + + if j.inputState.Right && j.readDPad { + aRight |= 1 << REG_JOYP_BIT_A_RIGHT + } + + readByte := (readButtons | readDPad | startDown | selectUp | bLeft | aRight) ^ 0xFF + + return mem.ReadReplace(readByte) + } + + return mem.ReadPassthrough() +} + +func (j *Joypad) OnWrite(mmu *mem.MMU, addr uint16, value byte) mem.MemWrite { + if addr == REG_JOYP { + j.readButtons = readBit(value, REG_JOYP_BIT_BUTTONS_SEL) == 0 + j.readDPad = readBit(value, REG_JOYP_BIT_DPAD_SEL) == 0 + } + + return mem.WriteBlock() +} diff --git a/hardware/dmg.go b/hardware/dmg.go index 11a3537..66fd0f4 100644 --- a/hardware/dmg.go +++ b/hardware/dmg.go @@ -58,6 +58,7 @@ type DMG struct { mmu *mem.MMU cartridge *cart.Cartridge ic *devices.InterruptController + joypad *devices.Joypad ppu *devices.PPU serial *devices.SerialPort timer *devices.Timer @@ -89,6 +90,7 @@ func NewDMG(opts ...DMGOption) (*DMG, error) { cartridge: cart.NewCartridge(), debugger: debug.NewNullDebugger(), ic: ic, + joypad: devices.NewJoypad(ic), ppu: devices.NewPPU(ic), serial: devices.NewSerialPort(), timer: devices.NewTimer(), @@ -107,6 +109,7 @@ func NewDMG(opts ...DMGOption) (*DMG, error) { mmu.AddHandler(mem.MemRegion{Start: 0xE000, End: 0xFDFF}, echo) // Echo RAM (mirrors WRAM) mmu.AddHandler(mem.MemRegion{Start: 0xFEA0, End: 0xFEFF}, unmapped) // Nop writes, zero reads + mmu.AddHandler(mem.MemRegion{Start: 0xFF00, End: 0xFF00}, dmg.joypad) // Joypad mmu.AddHandler(mem.MemRegion{Start: 0xFF01, End: 0xFF02}, dmg.serial) // Serial Port (Control & Data) mmu.AddHandler(mem.MemRegion{Start: 0xFF04, End: 0xFF07}, dmg.timer) // Timer (not RTC) mmu.AddHandler(mem.MemRegion{Start: 0xFF40, End: 0xFF41}, dmg.ppu) // LCD status, control registers @@ -174,6 +177,12 @@ func (dmg *DMG) Run(host devices.HostInterface) error { fakeVBlank := time.NewTicker(time.Second / 60) defer fakeVBlank.Stop() + go func() { + for inputs := range host.JoypadInput() { + dmg.joypad.ReceiveInputs(inputs) + } + }() + for { if err := dmg.Step(); err != nil { return err diff --git a/host/cli.go b/host/cli.go index ee802c5..38ebf83 100644 --- a/host/cli.go +++ b/host/cli.go @@ -10,6 +10,7 @@ import ( type CLIHost struct { fbChan chan image.Image + inputChan chan devices.JoypadInputs logger *log.Logger exitedChan chan bool serialCable devices.SerialCable @@ -20,6 +21,7 @@ var _ Host = (*CLIHost)(nil) func NewCLIHost() *CLIHost { return &CLIHost{ fbChan: make(chan image.Image, 3), + inputChan: make(chan devices.JoypadInputs), exitedChan: make(chan bool), logger: log.Default(), serialCable: &devices.NullSerialCable{}, @@ -30,6 +32,10 @@ func (h *CLIHost) Framebuffer() chan<- image.Image { return h.fbChan } +func (h *CLIHost) JoypadInput() <-chan devices.JoypadInputs { + return h.inputChan +} + func (h *CLIHost) Log(msg string, args ...any) { h.logger.Printf(msg+"\n", args...) } @@ -61,6 +67,7 @@ func (h *CLIHost) AttachSerialCable(serialCable devices.SerialCable) { func (h *CLIHost) Run(console hardware.Console) error { done := make(chan error) defer close(h.exitedChan) + defer close(h.inputChan) // "Renderer" go func() { diff --git a/host/ui.go b/host/ui.go index a4ce9f9..3e3ec78 100644 --- a/host/ui.go +++ b/host/ui.go @@ -12,6 +12,7 @@ import ( type UI struct { fbChan chan image.Image + inputChan chan devices.JoypadInputs logger *log.Logger exitedChan chan bool serialCable devices.SerialCable @@ -24,6 +25,7 @@ var _ Host = (*UI)(nil) func NewUIHost() *UI { return &UI{ fbChan: make(chan image.Image, 3), + inputChan: make(chan devices.JoypadInputs), exitedChan: make(chan bool), logger: log.Default(), serialCable: &devices.NullSerialCable{}, @@ -34,6 +36,10 @@ func (ui *UI) Framebuffer() chan<- image.Image { return ui.fbChan } +func (ui *UI) JoypadInput() <-chan devices.JoypadInputs { + return ui.inputChan +} + func (ui *UI) Log(msg string, args ...any) { ui.logger.Printf(msg+"\n", args...) } @@ -63,6 +69,38 @@ func (ui *UI) AttachSerialCable(serialCable devices.SerialCable) { } func (ui *UI) Update() error { + var inputs devices.JoypadInputs + + if ebiten.IsKeyPressed(ebiten.KeyX) { + inputs.A = true + } + + if ebiten.IsKeyPressed(ebiten.KeyZ) { + inputs.B = true + } + + if ebiten.IsKeyPressed(ebiten.KeyEnter) { + inputs.Start = true + } + + if ebiten.IsKeyPressed(ebiten.KeyShiftRight) { + inputs.Select = true + } + + if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { + inputs.Up = true + } else if ebiten.IsKeyPressed(ebiten.KeyArrowDown) { + inputs.Down = true + } + + if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { + inputs.Left = true + } else if ebiten.IsKeyPressed(ebiten.KeyArrowRight) { + inputs.Right = true + } + + ui.inputChan <- inputs + return nil } @@ -99,6 +137,7 @@ func (ui *UI) Run(console hardware.Console) error { } }() + defer close(ui.inputChan) defer close(ui.exitedChan) ui.Log("Handing over to ebiten")