-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchip8emulator.h
456 lines (416 loc) · 14.6 KB
/
chip8emulator.h
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
#include <fstream>
#include <iomanip>
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include "clock-regulator.h"
#include "screen.h"
class Chip8Emulator {
public:
Chip8Emulator(const std::string& rom_file_path)
// Initialize 4kib of RAM memory and 16 1-byte registers.
: memory_(4096, 0), variable_registers_(16, 0),
screen_("Chip 8 Emulator"),
// Execute at most 1 instruction per 2 milliseconds to emulate the speed
// at which most Chip8 games were made to be run at. Without clock
// regulation the games run way to fast.
cpu_clock_regulator_(/* milliseconds_per_cycle = */ 2),
// Draw the screen once per 17ms (~60hz).
draw_screen_regulator_(/* milliseconds_per_cycle = */ 17) {
// The original Chip-8 interpreter stored the first byte of the program
// at address 200 and so many programs rely on this.
std::ifstream file_stream(rom_file_path);
for (int program_load_location = 0x200; file_stream;
++program_load_location) {
file_stream.read((char*)&memory_[program_load_location], 1);
}
// Initialize the display memory to 32x64 of all zeros.
for (int row = 0; row < 32; ++row) {
std::vector<unsigned char> columns(64, 0);
display_.push_back(std::move(columns));
}
// Chip8 key codes range from 0x0 to 0xF (0-15). This mapping stores the
// corresponding SDL scancode for each Chip8 code.
key_mapping_ = {
SDL_SCANCODE_1,
SDL_SCANCODE_2,
SDL_SCANCODE_3,
SDL_SCANCODE_4,
//
SDL_SCANCODE_Q,
SDL_SCANCODE_W,
SDL_SCANCODE_E,
SDL_SCANCODE_R,
//
SDL_SCANCODE_A,
SDL_SCANCODE_S,
SDL_SCANCODE_D,
SDL_SCANCODE_F,
//
SDL_SCANCODE_Z,
SDL_SCANCODE_X,
SDL_SCANCODE_C,
SDL_SCANCODE_V,
};
// A bitmapped font with characters 0-9 and A-F. Early Chip8 interpreters
// stored this font starting at address 0x050.
int font_load_location = font_address_;
for (const auto font_byte : {
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F
}) {
memory_[font_load_location] = font_byte;
++font_load_location;
}
// Register the "P" key to pause the game.
screen_.OnKeyDown(SDL_SCANCODE_P, [this]() { paused_ = !paused_; });
}
// Chip8 Instructions commonly come either of form:
// - 0xTXYN or
// - 0xTXNN or
// - 0xTNNN or
// - 0xTXYN
// where:
// - T is the type of instruction
// - X and Y are register indicies
// - N[NN] are integer "constants"
//
// The following functions define common bit masks for accessing parts of
// the instructions
int register1(uint16_t instruction) { return (instruction & 0x0F00) >> 8; }
int register2(uint16_t instruction) { return (instruction & 0x00F0) >> 4; }
int constant8(uint16_t instruction) { return (instruction & 0x00FF); }
int constant12(uint16_t instruction) { return (instruction & 0x0FFF); }
// Basic arithmetic functions which properly signal overflow into the 0xF
// register.
unsigned char add(unsigned char a, unsigned char b) {
variable_registers_[0xF] = a + b > 0xFF;
return a + b;
}
unsigned char subtract(unsigned char a, unsigned char b) {
variable_registers_[0xF] = a >= b;
return a - b;
}
// Prints the args to std::cout if DEBUG is defined with some common stream
// manipulations that aid in debugging hex values.
template <typename Arg, typename... Args>
void Debug(Arg&& arg, Args&&... args) {
#ifdef DEBUG
std::cout << std::hex << std::setfill('0') << std::setw(4)
<< std::forward<Arg>(arg);
((std::cout << std::forward<Args>(args)), ...);
std::cout << std::endl;
#endif
}
// Executes the Chip8 program that was loaded from the file in the
// constructor. This call will block until the graphics window is closed.
void BlockingExecute() {
while (screen_.PollEvent()) {
// Refresh the screen and update the delay timer.
if (draw_screen_regulator_.Tick()) {
if (!paused_ && delay_timer_ > 0) {
delay_timer_--;
}
screen_.Clear(Color::Black());
DrawBottomBar();
DrawGameDisplay();
screen_.Update();
}
// Regulate program instruction processing speed to prevent the game from
// running too fast.
if (paused_ || !cpu_clock_regulator_.Tick()) {
continue;
}
// Each Chip8 instruction is two bytes, so we read the next two bytes of
// memory and then mask them into a single value to make handling easier.
uint16_t instruction =
(memory_[program_counter_] << 8) | (memory_[program_counter_ + 1]);
Debug("Instruction 0x", instruction);
program_counter_ += 2;
switch (instruction & 0xF000) {
case (0x0000): {
auto flag = instruction & 0x000F;
// Return from function instruction.
if (flag == 0x000E) {
program_counter_ = stack_.back();
stack_.pop_back();
} else if (flag == 0x0000) {
// Clear screen instruction.
for (auto& row : display_) {
for (auto& cell : row) {
cell = 0;
}
}
}
break;
}
// Unconditional jump.
case (0x1000): {
program_counter_ = constant12(instruction);
break;
}
// Function call instruction.
case (0x2000): {
stack_.push_back(program_counter_);
program_counter_ = constant12(instruction);
break;
}
// Jump equal constant.
case (0x3000): {
if (variable_registers_[register1(instruction)] ==
constant8(instruction)) {
program_counter_ += 2;
}
break;
}
// Jump not equal.
case (0x4000): {
if (variable_registers_[register1(instruction)] !=
constant8(instruction)) {
program_counter_ += 2;
}
break;
}
// Jump equal register.
case (0x5000): {
if (variable_registers_[register1(instruction)] ==
variable_registers_[register2(instruction)]) {
program_counter_ += 2;
}
break;
}
// Set register.
case (0x6000): {
variable_registers_[register1(instruction)] = constant8(instruction);
break;
}
// Add constant to register.
case (0x7000): {
variable_registers_[register1(instruction)] += constant8(instruction);
break;
}
// Two register arithmetic.
case (0x8000): {
auto& vx = variable_registers_[register1(instruction)];
auto& vy = variable_registers_[register2(instruction)];
auto flag = instruction & 0x000F;
if (flag == 0x0000) {
vx = vy;
} else if (flag == 0x0001) {
vx |= vy;
} else if (flag == 0x0002) {
vx &= vy;
} else if (flag == 0x0003) {
vx ^= vy;
} else if (flag == 0x0004) {
vx = add(vx, vy);
} else if (flag == 0x0005) {
vx = subtract(vx, vy);
} else if (flag == 0x0006) {
vx >>= 1;
} else if (flag == 0x0007) {
vx = subtract(vy, vx);
} else if (flag == 0x000E) {
vx <<= 1;
}
break;
}
// Jump not equal register.
case (0x9000): {
if (variable_registers_[register1(instruction)] !=
variable_registers_[register2(instruction)]) {
program_counter_ += 2;
}
break;
}
// Index register set.
case (0xA000): {
index_register_ = constant12(instruction);
break;
}
// Jump by offset.
case (0xB000): {
program_counter_ = constant12(instruction) + variable_registers_[0];
break;
}
// Generate random.
case (0xC000): {
variable_registers_[register1(instruction)] =
(rand() % 255) & constant8(instruction);
break;
}
// Draws the bitmap sprite pointed to by the index register to the
// screen.
case (0xD000): {
auto row_start = variable_registers_[register2(instruction)] % 32;
auto col_start = variable_registers_[register1(instruction)] % 64;
auto height = instruction & 0x000F;
variable_registers_[0xF] = 0;
for (int sprite_row_offset = 0; sprite_row_offset < height;
++sprite_row_offset) {
auto row = row_start + sprite_row_offset;
if (row >= display_.size()) {
break;
}
auto sprite_row = memory_[index_register_ + sprite_row_offset];
for (int sprite_col_offset = 0; sprite_col_offset < 8;
++sprite_col_offset) {
auto col = col_start + sprite_col_offset;
if (col >= display_[row].size()) {
break;
}
// Check if the `sprite_col_offset`th bit from the left is set.
auto set = sprite_row & (1 << (7 - sprite_col_offset));
auto before = display_[row][col];
if (set) {
display_[row][col] ^= 1;
if (before) {
variable_registers_[0xF] = 1;
}
}
}
}
break;
}
// Skip instructions based on key state.
case (0xE000): {
auto skip_state = (instruction & 0x00FF) == 0X009E;
auto key_code =
key_mapping_[variable_registers_[register1(instruction)]];
// Keep track of which keys the game has polled to give a hint of what
// the controls for the game are in the bottom bar.
keys_polled_.insert(SDL_GetScancodeName(key_code));
if (screen_.IsPressed(key_code) == skip_state) {
program_counter_ += 2;
}
break;
}
// The F* instructions are a bit of grab bag...
case (0xF000): {
auto flag = instruction & 0x00FF;
if (flag == 0x0007) {
variable_registers_[register1(instruction)] = delay_timer_;
} else if (flag == 0x0015) {
delay_timer_ = variable_registers_[register1(instruction)];
} else if (flag == 0x0018) {
std::cout << "\a" << std::endl;
} else if (flag == 0x000A) {
if (!screen_.IsPressed(key_mapping_[0])) {
program_counter_ -= 2;
} else {
variable_registers_[register1(instruction)] = 0;
}
} else if (flag == 0x001E) {
index_register_ += variable_registers_[register1(instruction)];
} else if (flag == 0x0029) {
constexpr int kFontCharacterHeight = 5;
index_register_ =
font_address_ +
(variable_registers_[register1(instruction)] & 0x000F) *
kFontCharacterHeight;
} else if (flag == 0x0033) {
auto vx = variable_registers_[register1(instruction)];
memory_[index_register_] = vx / 100;
vx %= 100;
memory_[index_register_ + 1] = vx / 10;
memory_[index_register_ + 2] = vx % 10;
} else if (flag == 0x0055) {
for (int i = 0; i <= register1(instruction); ++i) {
memory_[index_register_ + i] = variable_registers_[i];
}
} else if (flag == 0x0065) {
for (int i = 0; i <= register1(instruction); ++i) {
variable_registers_[i] = memory_[index_register_ + i];
}
}
break;
}
default: {
std::cout << "Unknown instruction" << std::endl;
}
}
}
}
private:
// Draw the actual game video memory to the screen.
void DrawGameDisplay() {
// Determine the scaling factors required to fit the chip8 display
// memory fully to the screen.
auto scale =
std::min(screen_.width() / display_.front().size(),
// Leave some space at the bottom of the screen to
// draw some status info.
(screen_.height() - kBottomBarHeight) / display_.size());
// Generate a vector of all the filled rectangles that need to be drawn.
std::vector<SDL_Rect> rects_to_draw;
for (int row = 0; row < display_.size(); ++row) {
for (int col = 0; col < display_[row].size(); ++col) {
if (display_[row][col]) {
SDL_Rect r;
r.x = col * scale;
r.y = row * scale;
r.w = scale;
r.h = scale;
rects_to_draw.push_back(r);
}
}
}
// Update the screen.
screen_.DrawRects(rects_to_draw, Color::White());
}
// Draws the bottom status bar to the screen.
void DrawBottomBar() {
constexpr int kPadding = 10;
auto start_y = screen_.height() - kBottomBarHeight;
SDL_Rect divider{.x = 0, .y = start_y, .w = screen_.width(), .h = 3};
screen_.DrawRects({divider}, Color::White());
std::string keys = "Controls: ";
for (auto key_it = keys_polled_.begin(); key_it != keys_polled_.end();
++key_it) {
keys += *key_it;
if (std::next(key_it) != keys_polled_.end()) {
keys += ", ";
}
}
auto controls_rect =
screen_.DrawText(keys, 50, start_y + kPadding, Color::White());
auto timer_rect =
screen_.DrawText("Timer: " + std::to_string(delay_timer_),
controls_rect.x + controls_rect.w + kPadding * 2,
start_y + kPadding, Color::White());
if (paused_) {
screen_.DrawText("PAUSED", timer_rect.x + timer_rect.w + kPadding * 2,
start_y + kPadding, Color::Red());
}
}
std::vector<unsigned char> memory_;
std::vector<uint16_t> stack_;
uint16_t program_counter_ = 0x200;
std::vector<unsigned char> variable_registers_;
int index_register_ = 0;
std::vector<std::vector<unsigned char>> display_;
Screen screen_;
std::vector<SDL_Scancode> key_mapping_;
int delay_timer_ = 0;
ClockRegulator cpu_clock_regulator_;
ClockRegulator draw_screen_regulator_;
int font_address_ = 0x050;
std::set<std::string> keys_polled_;
static constexpr int kBottomBarHeight = 100;
bool paused_ = false;
};