SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
Tester/main.c
1 // The tester requires low-level access to the GB struct to detect failures
2 #define GB_INTERNAL
3
4 #include <stdio.h>
5 #include <stdbool.h>
6 #include <unistd.h>
7 #include <time.h>
8 #include <assert.h>
9 #include <signal.h>
10 #ifdef _WIN32
11 #include <direct.h>
12 #include <windows.h>
13 #define snprintf _snprintf
14 #else
15 #include <sys/wait.h>
16 #endif
17
18 #include <Core/gb.h>
19 #include <Core/random.h>
20
21 static bool running = false;
22 static char *filename;
23 static char *bmp_filename;
24 static char *log_filename;
25 static char *sav_filename;
26 static FILE *log_file;
27 static void replace_extension(const char *src, size_t length, char *dest, const char *ext);
28 static bool push_start_a, start_is_not_first, a_is_bad, b_is_confirm, push_faster, push_slower,
29 do_not_stop, push_a_twice, start_is_bad, allow_weird_sp_values, large_stack, push_right,
30 semi_random, limit_start, pointer_control, unsafe_speed_switch;
31 static unsigned int test_length = 60 * 40;
32 GB_gameboy_t gb;
33
34 static unsigned int frames = 0;
35 static bool use_tga = false;
36 static uint8_t bmp_header[] = {
37 0x42, 0x4D, 0x48, 0x68, 0x01, 0x00, 0x00, 0x00,
38 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00,
39 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x70, 0xFF,
40 0xFF, 0xFF, 0x01, 0x00, 0x20, 0x00, 0x03, 0x00,
41 0x00, 0x00, 0x02, 0x68, 0x01, 0x00, 0x12, 0x0B,
42 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00,
43 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
44 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
45 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
46 };
47
48 static uint8_t tga_header[] = {
49 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
50 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x90, 0x00,
51 0x20, 0x28,
52 };
53
54 uint32_t bitmap[256*224];
55
56 static char *async_input_callback(GB_gameboy_t *gb)
57 {
58 return NULL;
59 }
60
61 static void handle_buttons(GB_gameboy_t *gb)
62 {
63 if (!gb->cgb_double_speed && unsafe_speed_switch) {
64 return;
65 }
66 /* Do not press any buttons during the last two seconds, this might cause a
67 screenshot to be taken while the LCD is off if the press makes the game
68 load graphics. */
69 if (push_start_a && (frames < test_length - 120 || do_not_stop)) {
70 unsigned combo_length = 40;
71 if (start_is_not_first || push_a_twice) combo_length = 60; /* The start item in the menu is not the first, so also push down */
72 else if (a_is_bad || start_is_bad) combo_length = 20; /* Pressing A has a negative effect (when trying to start the game). */
73
74 if (semi_random) {
75 if (frames % 10 == 0) {
76 unsigned key = (((frames / 20) * 0x1337cafe) >> 29) & 7;
77 gb->keys[0][key] = (frames % 20) == 0;
78 }
79 }
80 else {
81 switch ((push_faster ? frames * 2 :
82 push_slower ? frames / 2 :
83 push_a_twice? frames / 4:
84 frames) % combo_length + (start_is_bad? 20 : 0) ) {
85 case 0:
86 if (!limit_start || frames < 20 * 60) {
87 GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, true);
88 }
89 if (pointer_control) {
90 GB_set_key_state(gb, GB_KEY_LEFT, true);
91 GB_set_key_state(gb, GB_KEY_UP, true);
92 }
93
94 break;
95 case 10:
96 GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, false);
97 if (pointer_control) {
98 GB_set_key_state(gb, GB_KEY_LEFT, false);
99 GB_set_key_state(gb, GB_KEY_UP, false);
100 }
101 break;
102 case 20:
103 GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true);
104 break;
105 case 30:
106 GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false);
107 break;
108 case 40:
109 if (push_a_twice) {
110 GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true);
111 }
112 else if (gb->boot_rom_finished) {
113 GB_set_key_state(gb, GB_KEY_DOWN, true);
114 }
115 break;
116 case 50:
117 GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false);
118 GB_set_key_state(gb, GB_KEY_DOWN, false);
119 break;
120 }
121 }
122 }
123
124 }
125
126 static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
127 {
128 /* Detect common crashes and stop the test early */
129 if (frames < test_length - 1) {
130 if (gb->backtrace_size >= 0x200 + (large_stack? 0x80: 0) || (!allow_weird_sp_values && (gb->registers[GB_REGISTER_SP] >= 0xfe00 && gb->registers[GB_REGISTER_SP] < 0xff80))) {
131 GB_log(gb, "A stack overflow has probably occurred. (SP = $%04x; backtrace size = %d) \n",
132 gb->registers[GB_REGISTER_SP], gb->backtrace_size);
133 frames = test_length - 1;
134 }
135 if (gb->halted && !gb->interrupt_enable && gb->speed_switch_halt_countdown == 0) {
136 GB_log(gb, "The game is deadlocked.\n");
137 frames = test_length - 1;
138 }
139 }
140
141 if (frames >= test_length && !gb->disable_rendering) {
142 bool is_screen_blank = true;
143 if (!gb->sgb) {
144 for (unsigned i = 160 * 144; i--;) {
145 if (bitmap[i] != bitmap[0]) {
146 is_screen_blank = false;
147 break;
148 }
149 }
150 }
151 else {
152 if (gb->sgb->mask_mode == 0) {
153 for (unsigned i = 160 * 144; i--;) {
154 if (gb->sgb->screen_buffer[i] != gb->sgb->screen_buffer[0]) {
155 is_screen_blank = false;
156 break;
157 }
158 }
159 }
160 }
161
162 /* Let the test run for extra four seconds if the screen is off/disabled */
163 if (!is_screen_blank || frames >= test_length + 60 * 4) {
164 FILE *f = fopen(bmp_filename, "wb");
165 if (use_tga) {
166 tga_header[0xC] = GB_get_screen_width(gb);
167 tga_header[0xD] = GB_get_screen_width(gb) >> 8;
168 tga_header[0xE] = GB_get_screen_height(gb);
169 tga_header[0xF] = GB_get_screen_height(gb) >> 8;
170 fwrite(&tga_header, 1, sizeof(tga_header), f);
171 }
172 else {
173 (*(uint32_t *)&bmp_header[0x2]) = sizeof(bmp_header) + sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb) + 2;
174 (*(uint32_t *)&bmp_header[0x12]) = GB_get_screen_width(gb);
175 (*(int32_t *)&bmp_header[0x16]) = -GB_get_screen_height(gb);
176 (*(uint32_t *)&bmp_header[0x22]) = sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb) + 2;
177 fwrite(&bmp_header, 1, sizeof(bmp_header), f);
178 }
179 fwrite(&bitmap, 1, sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb), f);
180 fclose(f);
181 if (!gb->boot_rom_finished) {
182 GB_log(gb, "Boot ROM did not finish.\n");
183 }
184 if (is_screen_blank) {
185 GB_log(gb, "Game probably stuck with blank screen. \n");
186 }
187 if (sav_filename) {
188 GB_save_battery(gb, sav_filename);
189 }
190 running = false;
191 }
192 }
193 else if (frames >= test_length - 1) {
194 gb->disable_rendering = false;
195 }
196 }
197
198 static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
199 {
200 if (!log_file) log_file = fopen(log_filename, "w");
201 fprintf(log_file, "%s", string);
202 }
203
204 #ifdef __APPLE__
205 #include <mach-o/dyld.h>
206 #endif
207
208 static const char *executable_folder(void)
209 {
210 static char path[1024] = {0,};
211 if (path[0]) {
212 return path;
213 }
214 /* Ugly unportable code! :( */
215 #ifdef __APPLE__
216 uint32_t length = sizeof(path) - 1;
217 _NSGetExecutablePath(&path[0], &length);
218 #else
219 #ifdef __linux__
220 size_t __attribute__((unused)) length = readlink("/proc/self/exe", &path[0], sizeof(path) - 1);
221 assert(length != -1);
222 #else
223 #ifdef _WIN32
224 HMODULE hModule = GetModuleHandle(NULL);
225 GetModuleFileName(hModule, path, sizeof(path) - 1);
226 #else
227 /* No OS-specific way, assume running from CWD */
228 getcwd(&path[0], sizeof(path) - 1);
229 return path;
230 #endif
231 #endif
232 #endif
233 size_t pos = strlen(path);
234 while (pos) {
235 pos--;
236 #ifdef _WIN32
237 if (path[pos] == '\\') {
238 #else
239 if (path[pos] == '/') {
240 #endif
241 path[pos] = 0;
242 break;
243 }
244 }
245 return path;
246 }
247
248 static char *executable_relative_path(const char *filename)
249 {
250 static char path[1024];
251 snprintf(path, sizeof(path), "%s/%s", executable_folder(), filename);
252 return path;
253 }
254
255 static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
256 {
257 #ifdef GB_BIG_ENDIAN
258 if (use_tga) {
259 return (r << 8) | (g << 16) | (b << 24);
260 }
261 return (r << 0) | (g << 8) | (b << 16);
262 #else
263 if (use_tga) {
264 return (r << 16) | (g << 8) | (b);
265 }
266 return (r << 24) | (g << 16) | (b << 8);
267 #endif
268 }
269
270 static void replace_extension(const char *src, size_t length, char *dest, const char *ext)
271 {
272 memcpy(dest, src, length);
273 dest[length] = 0;
274
275 /* Remove extension */
276 for (size_t i = length; i--;) {
277 if (dest[i] == '/') break;
278 if (dest[i] == '.') {
279 dest[i] = 0;
280 break;
281 }
282 }
283
284 /* Add new extension */
285 strcat(dest, ext);
286 }
287
288
289 int main(int argc, char **argv)
290 {
291 fprintf(stderr, "SameBoy Tester v" GB_VERSION "\n");
292
293 if (argc == 1) {
294 fprintf(stderr, "Usage: %s [--dmg] [--sgb] [--cgb] [--start] [--length seconds] [--sav] [--boot path to boot ROM]"
295 #ifndef _WIN32
296 " [--jobs number of tests to run simultaneously]"
297 #endif
298 " rom ...\n", argv[0]);
299 exit(1);
300 }
301
302 #ifndef _WIN32
303 unsigned int max_forks = 1;
304 unsigned int current_forks = 0;
305 #endif
306
307 bool dmg = false;
308 bool sgb = false;
309 bool sav = false;
310 const char *boot_rom_path = NULL;
311
312 GB_random_set_enabled(false);
313
314 for (unsigned i = 1; i < argc; i++) {
315 if (strcmp(argv[i], "--dmg") == 0) {
316 fprintf(stderr, "Using DMG mode\n");
317 dmg = true;
318 sgb = false;
319 continue;
320 }
321
322 if (strcmp(argv[i], "--sgb") == 0) {
323 fprintf(stderr, "Using SGB mode\n");
324 sgb = true;
325 dmg = false;
326 continue;
327 }
328
329 if (strcmp(argv[i], "--cgb") == 0) {
330 fprintf(stderr, "Using CGB mode\n");
331 dmg = false;
332 sgb = false;
333 continue;
334 }
335
336 if (strcmp(argv[i], "--tga") == 0) {
337 fprintf(stderr, "Using TGA output\n");
338 use_tga = true;
339 continue;
340 }
341
342 if (strcmp(argv[i], "--start") == 0) {
343 fprintf(stderr, "Pushing Start and A\n");
344 push_start_a = true;
345 continue;
346 }
347
348 if (strcmp(argv[i], "--length") == 0 && i != argc - 1) {
349 test_length = atoi(argv[++i]) * 60;
350 fprintf(stderr, "Test length is %d seconds\n", test_length / 60);
351 continue;
352 }
353
354 if (strcmp(argv[i], "--boot") == 0 && i != argc - 1) {
355 fprintf(stderr, "Using boot ROM %s\n", argv[i + 1]);
356 boot_rom_path = argv[++i];
357 continue;
358 }
359
360 if (strcmp(argv[i], "--sav") == 0) {
361 fprintf(stderr, "Saving a battery save\n");
362 sav = true;
363 continue;
364 }
365
366 #ifndef _WIN32
367 if (strcmp(argv[i], "--jobs") == 0 && i != argc - 1) {
368 max_forks = atoi(argv[++i]);
369 /* Make sure wrong input doesn't blow anything up. */
370 if (max_forks < 1) max_forks = 1;
371 if (max_forks > 16) max_forks = 16;
372 fprintf(stderr, "Running up to %d tests simultaneously\n", max_forks);
373 continue;
374 }
375
376 if (max_forks > 1) {
377 while (current_forks >= max_forks) {
378 int wait_out;
379 while (wait(&wait_out) == -1);
380 current_forks--;
381 }
382
383 current_forks++;
384 if (fork() != 0) continue;
385 }
386 #endif
387 filename = argv[i];
388 size_t path_length = strlen(filename);
389
390 char bitmap_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .bmp + NULL */
391 replace_extension(filename, path_length, bitmap_path, use_tga? ".tga" : ".bmp");
392 bmp_filename = &bitmap_path[0];
393
394 char log_path[path_length + 5];
395 replace_extension(filename, path_length, log_path, ".log");
396 log_filename = &log_path[0];
397
398 char sav_path[path_length + 5];
399 if (sav) {
400 replace_extension(filename, path_length, sav_path, ".sav");
401 sav_filename = &sav_path[0];
402 }
403
404 fprintf(stderr, "Testing ROM %s\n", filename);
405
406 if (dmg) {
407 GB_init(&gb, GB_MODEL_DMG_B);
408 if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("dmg_boot.bin"))) {
409 fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("dmg_boot.bin"));
410 exit(1);
411 }
412 }
413 else if (sgb) {
414 GB_init(&gb, GB_MODEL_SGB2);
415 if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("sgb2_boot.bin"))) {
416 fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("sgb2_boot.bin"));
417 exit(1);
418 }
419 }
420 else {
421 GB_init(&gb, GB_MODEL_CGB_E);
422 if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("cgb_boot.bin"))) {
423 fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("cgb_boot.bin"));
424 exit(1);
425 }
426 }
427
428 GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank);
429 GB_set_pixels_output(&gb, &bitmap[0]);
430 GB_set_rgb_encode_callback(&gb, rgb_encode);
431 GB_set_log_callback(&gb, log_callback);
432 GB_set_async_input_callback(&gb, async_input_callback);
433 GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE);
434 GB_set_rtc_mode(&gb, GB_RTC_MODE_ACCURATE);
435 GB_set_emulate_joypad_bouncing(&gb, false); // Adds too much noise
436
437 if (GB_load_rom(&gb, filename)) {
438 perror("Failed to load ROM");
439 exit(1);
440 }
441
442 /* Game specific hacks for start attempt automations */
443 /* It's OK. No overflow is possible here. */
444 start_is_not_first = strcmp((const char *)(gb.rom + 0x134), "NEKOJARA") == 0 ||
445 strcmp((const char *)(gb.rom + 0x134), "GINGA") == 0;
446 a_is_bad = strcmp((const char *)(gb.rom + 0x134), "DESERT STRIKE") == 0 ||
447 /* Restarting in Puzzle Boy/Kwirk (Start followed by A) leaks stack. */
448 strcmp((const char *)(gb.rom + 0x134), "KWIRK") == 0 ||
449 strcmp((const char *)(gb.rom + 0x134), "PUZZLE BOY") == 0;
450 start_is_bad = strcmp((const char *)(gb.rom + 0x134), "BLUESALPHA") == 0 ||
451 strcmp((const char *)(gb.rom + 0x134), "ONI 5") == 0;
452 b_is_confirm = strcmp((const char *)(gb.rom + 0x134), "ELITE SOCCER") == 0 ||
453 strcmp((const char *)(gb.rom + 0x134), "SOCCER") == 0 ||
454 strcmp((const char *)(gb.rom + 0x134), "GEX GECKO") == 0 ||
455 strcmp((const char *)(gb.rom + 0x134), "BABE") == 0;
456 push_faster = strcmp((const char *)(gb.rom + 0x134), "MOGURA DE PON!") == 0 ||
457 strcmp((const char *)(gb.rom + 0x134), "HUGO2 1/2") == 0 ||
458 strcmp((const char *)(gb.rom + 0x134), "HUGO") == 0;
459 push_slower = strcmp((const char *)(gb.rom + 0x134), "BAKENOU") == 0;
460 do_not_stop = strcmp((const char *)(gb.rom + 0x134), "SPACE INVADERS") == 0;
461 push_right = memcmp((const char *)(gb.rom + 0x134), "BOB ET BOB", strlen("BOB ET BOB")) == 0 ||
462 strcmp((const char *)(gb.rom + 0x134), "LITTLE MASTER") == 0 ||
463 /* M&M's Minis Madness Demo (which has no menu but the same title as the full game) */
464 (memcmp((const char *)(gb.rom + 0x134), "MINIMADNESSBMIE", strlen("MINIMADNESSBMIE")) == 0 &&
465 gb.rom[0x14e] == 0x6c);
466 /* This game has some terrible menus. */
467 semi_random = strcmp((const char *)(gb.rom + 0x134), "KUKU GAME") == 0;
468
469
470
471 /* This game temporarily sets SP to OAM RAM */
472 allow_weird_sp_values = strcmp((const char *)(gb.rom + 0x134), "WDL:TT") == 0 ||
473 /* Some mooneye-gb tests abuse the stack */
474 strcmp((const char *)(gb.rom + 0x134), "mooneye-gb test") == 0;
475
476 /* This game uses some recursive algorithms and therefore requires quite a large call stack */
477 large_stack = memcmp((const char *)(gb.rom + 0x134), "MICRO EPAK1BM", strlen("MICRO EPAK1BM")) == 0 ||
478 strcmp((const char *)(gb.rom + 0x134), "TECMO BOWL") == 0;
479 /* High quality game that leaks stack whenever you open the menu (with start),
480 but requires pressing start to play it. */
481 limit_start = strcmp((const char *)(gb.rom + 0x134), "DIVA STARS") == 0;
482 large_stack |= limit_start;
483
484 /* Pressing start while in the map in Tsuri Sensei will leak an internal screen-stack which
485 will eventually overflow, override an array of jump-table indexes, jump to a random
486 address, execute an invalid opcode, and crash. Pressing A twice while slowing down
487 will prevent this scenario. */
488 push_a_twice = strcmp((const char *)(gb.rom + 0x134), "TURI SENSEI V1") == 0;
489
490 /* Yes, you should totally use a cursor point & click interface for the language select menu. */
491 pointer_control = memcmp((const char *)(gb.rom + 0x134), "LEGO ATEAM BLPP", strlen("LEGO ATEAM BLPP")) == 0;
492 push_faster |= pointer_control;
493
494 /* Games that perform an unsafe speed switch, don't input until in double speed */
495 unsafe_speed_switch = strcmp((const char *)(gb.rom + 0x134), "GBVideo") == 0 || // lulz this is my fault
496 strcmp((const char *)(gb.rom + 0x134), "POKEMONGOLD 2") == 0; // Pokemon Adventure
497
498
499 /* Run emulation */
500 running = true;
501 gb.turbo = gb.turbo_dont_skip = gb.disable_rendering = true;
502 frames = 0;
503 unsigned cycles = 0;
504 while (running) {
505 cycles += GB_run(&gb);
506 if (cycles >= 139810) { /* Approximately 1/60 a second. Intentionally not the actual length of a frame. */
507 handle_buttons(&gb);
508 cycles -= 139810;
509 frames++;
510 }
511 /* This early crash test must not run in vblank because PC might not point to the next instruction. */
512 if (gb.pc == 0x38 && frames < test_length - 1 && GB_read_memory(&gb, 0x38) == 0xFF) {
513 GB_log(&gb, "The game is probably stuck in an FF loop.\n");
514 frames = test_length - 1;
515 }
516 }
517
518
519 if (log_file) {
520 fclose(log_file);
521 log_file = NULL;
522 }
523
524 GB_free(&gb);
525 #ifndef _WIN32
526 if (max_forks > 1) {
527 exit(0);
528 }
529 #endif
530 }
531 #ifndef _WIN32
532 int wait_out;
533 while (wait(&wait_out) != -1);
534 #endif
535 return 0;
536 }
537
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.