git.y1.nz

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.