Skip to content

Commit

Permalink
Implement Joypad
Browse files Browse the repository at this point in the history
  • Loading branch information
maxfierke committed Jan 1, 2025
1 parent 5f1fbbf commit 1ec05ed
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 0 deletions.
1 change: 1 addition & 0 deletions devices/host_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions devices/interrupts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
132 changes: 132 additions & 0 deletions devices/joypad.go
Original file line number Diff line number Diff line change
@@ -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()
}
9 changes: 9 additions & 0 deletions hardware/dmg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions host/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{},
Expand All @@ -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...)
}
Expand Down Expand Up @@ -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() {
Expand Down
39 changes: 39 additions & 0 deletions host/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{},
Expand All @@ -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...)
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 1ec05ed

Please sign in to comment.