-
Notifications
You must be signed in to change notification settings - Fork 2
/
gameboy.go
executable file
·186 lines (160 loc) · 4.52 KB
/
gameboy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package gameboy
import (
"context"
"fmt"
"io"
"io/ioutil"
"github.com/scottyw/tetromino/gameboy/audio"
"github.com/scottyw/tetromino/gameboy/controller"
"github.com/scottyw/tetromino/gameboy/cpu"
"github.com/scottyw/tetromino/gameboy/display"
"github.com/scottyw/tetromino/gameboy/interrupts"
"github.com/scottyw/tetromino/gameboy/memory"
"github.com/scottyw/tetromino/gameboy/oam"
"github.com/scottyw/tetromino/gameboy/ppu"
"github.com/scottyw/tetromino/gameboy/serial"
"github.com/scottyw/tetromino/gameboy/speakers"
"github.com/scottyw/tetromino/gameboy/timer"
)
// Config control emulator behaviour
type Config struct {
RomFilename string
DisableVideoOutput bool
DisableAudioOutput bool
DebugCPU bool
DebugLCD bool
SerialWriter io.Writer
}
// Gameboy represents the Gameboy itself
type Gameboy struct {
audio *audio.Audio
config Config
controller *controller.Controller
cpu *cpu.CPU
display *display.Display
interrupts *interrupts.Interrupts
ppu *ppu.PPU
mapper *memory.Mapper
speakers *speakers.Speakers
timer *timer.Timer
}
// NewGameboy returns a new Gameboy
func New(config Config) *Gameboy {
// Create interrrupts subsystem
i := interrupts.New()
// Create OAM memory
oam := oam.New()
// Create speakers
var a *audio.Audio
var s *speakers.Speakers
if !config.DisableAudioOutput {
s = speakers.New()
a = audio.New(s.Left(), s.Right())
} else {
a = audio.New(nil, nil)
}
// Create the PPU
ppu := ppu.New(i, oam, config.DebugLCD)
// Create the serial bus subsystem
serial := serial.New(config.SerialWriter)
// Create the timer subsystem
timer := timer.New()
// Load the ROM file
rom := readRomFile(config.RomFilename)
// Create controller
controller := controller.New()
mapper := memory.New(rom, i, oam, ppu, controller, serial, timer, a)
// Create CPU
c := cpu.New(i, oam, config.DebugCPU, mapper)
// Initialize internal data structures
c.Initialize()
// Create a display
var d *display.Display
if !config.DisableVideoOutput {
d = display.New(controller, c.OnInput, config.DebugLCD)
}
return &Gameboy{
audio: a,
config: config,
controller: controller,
cpu: c,
display: d,
interrupts: i,
ppu: ppu,
mapper: mapper,
speakers: s,
timer: timer,
}
}
func (gb *Gameboy) Cleanup() {
if gb.speakers != nil {
gb.speakers.Cleanup()
}
if gb.display != nil {
gb.display.Cleanup()
}
}
func readRomFile(romFilename string) []byte {
var rom []byte
if romFilename == "" {
panic("No ROM file specified")
}
rom, err := ioutil.ReadFile(romFilename)
if err != nil {
panic(fmt.Sprintf("Failed to read the ROM file at \"%s\" (%v)", romFilename, err))
}
return rom
}
func (gb *Gameboy) runFrame(ctx context.Context) bool {
// The Game Boy clock runs at 4.194304MHz
// One machine cycle is 4 clock cycles
// Each loop iteration below represents one machine cycle
// Each LCD frame is 17556 machine cycles
for mtick := 0; mtick < 17556; mtick++ {
gb.cpu.ExecuteMachineCycle()
gb.ppu.EndMachineCycle()
gb.mapper.EndMachineCycle()
gb.audio.EndMachineCycle()
timerInterruptRequested := gb.timer.EndMachineCycle()
if timerInterruptRequested {
gb.interrupts.RequestTimer()
}
}
frame := gb.ppu.Frame()
if gb.display != nil {
return gb.display.RenderFrame(frame)
}
return false
// The emulator can run a frame much faster than a real Gameboy when running on a modern computer.
// There is no need to sleep now between frames however, because the audio subsystem consumes
// samples at a fixed rate from a blocking channel. The emulator can only push audio samples into
// the channel at the same rate that the "speakers" consume them. Since the "speakers" are
// consuming the data at the rate of a real Gameboy (in order to make sound play correctly), the
// rest of the emulator is slowed to the same correct rate. In "fast" mode, the emulator disables
// the "speakers" meaning there is no constraint on how fast samples are consumed or on how fast
// the emulator runs.
}
// Run the Gameboy
func (gb *Gameboy) Run(ctx context.Context) {
defer gb.Cleanup()
// var frames uint8
// t0 := time.Now().UnixMicro()
for {
select {
case <-ctx.Done():
return
default:
if gb.runFrame(ctx) {
return
}
}
// Show FPS
// if frames == 0 {
// t1 := time.Now().UnixMicro()
// microseconds := (t1 - t0) / 256
// fmt.Printf(">>>> %0d fps ( %0d μs/frame )\n", 1000000/microseconds, microseconds)
// t0 = time.Now().UnixMicro()
// }
// frames++
}
}