From a746c726eee4de5263c505251d73a4df18e62400 Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sat, 3 Sep 2016 03:39:32 +0300 Subject: [PATCH] Added basic automatic ROM tester --- Core/display.c | 22 ++-- Core/gb.h | 2 + Core/memory.c | 5 +- Core/z80_cpu.c | 2 +- Makefile | 26 ++++- Tester/main.c | 283 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 322 insertions(+), 18 deletions(-) create mode 100755 Tester/main.c diff --git a/Core/display.c b/Core/display.c index 5355ca3..4c9874f 100755 --- a/Core/display.c +++ b/Core/display.c @@ -201,7 +201,7 @@ void display_vblank(GB_gameboy_t *gb) /* Called every Gameboy vblank. Does FPS-capping and calls user's vblank callback if Turbo Mode allows. */ if (gb->turbo) { int64_t nanoseconds = get_nanoseconds(); - if (nanoseconds <= gb->last_vblank + FRAME_LENGTH) { + if (!gb->turbo_dont_skip && nanoseconds <= gb->last_vblank + FRAME_LENGTH) { return; } gb->last_vblank = nanoseconds; @@ -386,16 +386,18 @@ void GB_display_run(GB_gameboy_t *gb) /* Render. This chunk is outside the Mode 3 if, because otherwise we might not render some pixels, since this function only runs between atomic CPU changes, and not every clock. */ - int16_t current_lcdc_x = ((gb->display_cycles % LINE_LENGTH - MODE2_LENGTH) & ~7) - (gb->effective_scx & 0x7); - for (;gb->previous_lcdc_x < current_lcdc_x; gb->previous_lcdc_x++) { - if (gb->previous_lcdc_x >= 160) { - continue; + if (!gb->disable_rendering) { + int16_t current_lcdc_x = ((gb->display_cycles % LINE_LENGTH - MODE2_LENGTH) & ~7) - (gb->effective_scx & 0x7); + for (;gb->previous_lcdc_x < current_lcdc_x; gb->previous_lcdc_x++) { + if (gb->previous_lcdc_x >= 160) { + continue; + } + if (gb->previous_lcdc_x < 0) { + continue; + } + gb->screen[gb->io_registers[GB_IO_LY] * 160 + gb->previous_lcdc_x] = + get_pixel(gb, gb->previous_lcdc_x, gb->io_registers[GB_IO_LY]); } - if (gb->previous_lcdc_x < 0) { - continue; - } - gb->screen[gb->io_registers[GB_IO_LY] * 160 + gb->previous_lcdc_x] = - get_pixel(gb, gb->previous_lcdc_x, gb->io_registers[GB_IO_LY]); } if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH + MODE3_LENGTH) { /* Mode 3 */ diff --git a/Core/gb.h b/Core/gb.h index 18a78ab..7600606 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -410,6 +410,8 @@ typedef struct GB_gameboy_s { /* Misc */ bool turbo; + bool turbo_dont_skip; + bool disable_rendering; uint32_t ram_size; // Different between CGB and DMG uint8_t boot_rom[0x900]; bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank diff --git a/Core/memory.c b/Core/memory.c index d8719c8..9fd2a54 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -547,10 +547,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_apu_write(gb, addr & 0xFF, value); return; } - if (gb->io_registers[addr & 0xFF] != 0x37) { - GB_log(gb, "Wrote %02x to %04x (HW Register)\n", value, addr); - } - gb->io_registers[addr & 0xFF] = 0x37; + GB_log(gb, "Wrote %02x to %04x (HW Register)\n", value, addr); return; } } diff --git a/Core/z80_cpu.c b/Core/z80_cpu.c index 21b0838..07eb153 100644 --- a/Core/z80_cpu.c +++ b/Core/z80_cpu.c @@ -11,7 +11,7 @@ typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode); static void ill(GB_gameboy_t *gb, uint8_t opcode) { - GB_log(gb, "Illegal Opcode. Halting."); + GB_log(gb, "Illegal Opcode. Halting.\n"); gb->interrupt_enable = 0; gb->halted = true; } diff --git a/Makefile b/Makefile index c6a2291..1bfd534 100755 --- a/Makefile +++ b/Makefile @@ -76,18 +76,23 @@ endif ifeq ($(PLATFORM),windows32) SDL_TARGET := $(BIN)/sdl/sameboy.exe $(BIN)/sdl/sameboy_debugger.exe $(BIN)/sdl/SDL.dll +TESTER_TARGET := $(BIN)/tester/sameboy_tester.exe else SDL_TARGET := $(BIN)/sdl/sameboy +TESTER_TARGET := $(BIN)/tester/sameboy_tester endif cocoa: $(BIN)/Sameboy.app sdl: $(SDL_TARGET) $(BIN)/sdl/dmg_boot.bin $(BIN)/sdl/cgb_boot.bin $(BIN)/sdl/LICENSE bootroms: $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/dmg_boot.bin +tester: $(TESTER_TARGET) $(BIN)/tester/dmg_boot.bin $(BIN)/tester/cgb_boot.bin + # Get a list of our source files and their respective object file targets CORE_SOURCES := $(shell ls Core/*.c) SDL_SOURCES := $(shell ls SDL/*.c) +TESTER_SOURCES := $(shell ls Tester/*.c) ifeq ($(PLATFORM),Darwin) COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) @@ -97,8 +102,7 @@ endif CORE_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(CORE_SOURCES)) COCOA_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(COCOA_SOURCES)) SDL_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(SDL_SOURCES)) - -ALL_OBJECTS := $(CORE_OBJECTS) $(COCOA_OBJECTS) $(SDL_OBJECTS) +TESTER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(TESTER_SOURCES)) # Automatic dependency generation @@ -107,6 +111,9 @@ ifneq ($(MAKECMDGOALS),clean) ifneq ($(filter $(MAKECMDGOALS),sdl),) -include $(SDL_OBJECTS:.o=.dep) endif +ifneq ($(filter $(MAKECMDGOALS),tester),) +-include $(TESTER_OBJECTS:.o=.dep) +endif ifneq ($(filter $(MAKECMDGOALS),cocoa),) -include $(COCOA_OBJECTS:.o=.dep) endif @@ -196,7 +203,20 @@ $(BIN)/sdl/SDL.dll: @$(eval MATCH := $(shell ls $(POTENTIAL_MATCHES) 2> NUL | head -n 1)) cp "$(MATCH)" $@ -$(BIN)/sdl/%.bin: $(BOOTROMS_DIR)/%.bin +# Tester + +$(BIN)/tester/sameboy_tester: $(CORE_OBJECTS) $(TESTER_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) +ifeq ($(CONF), release) + strip $@ +endif + +$(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) -Wl,/subsystem:console + +$(BIN)/sdl/%.bin $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ diff --git a/Tester/main.c b/Tester/main.c new file mode 100755 index 0000000..c4a9bca --- /dev/null +++ b/Tester/main.c @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#define snprintf _snprintf +#endif + +#include "gb.h" +#include "debugger.h" + +static bool running = false; +static char *filename; +static char *bmp_filename; +static char *log_filename; +static FILE *log_file; +static void replace_extension(const char *src, size_t length, char *dest, const char *ext); +static bool push_start_a; +static unsigned int test_length = 60 * 40; +GB_gameboy_t gb; + +static unsigned int frames = 0; +const char bmp_header[] = { +0x42, 0x4D, 0x48, 0x68, 0x01, 0x00, 0x00, 0x00, +0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00, +0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x70, 0xFF, +0xFF, 0xFF, 0x01, 0x00, 0x20, 0x00, 0x03, 0x00, +0x00, 0x00, 0x02, 0x68, 0x01, 0x00, 0x12, 0x0B, +0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +uint32_t bitmap[160*144]; + +static void vblank(GB_gameboy_t *gb) +{ + /* Do not press any buttons during the last two seconds, this might cause a + screenshot to be taken while the LCD is off if the press makes the game + load graphics. */ + if (push_start_a && frames < test_length - 120) { + switch (frames % 40) { + case 0: + gb->keys[7] = true; // Start down + break; + case 10: + gb->keys[7] = false; // Start up + break; + case 20: + gb->keys[4] = true; // A down + break; + case 30: + gb->keys[4] = false; // A up + break; + } + } + + if (frames == test_length) { + FILE *f = fopen(bmp_filename, "wb"); + fwrite(&bmp_header, 1, sizeof(bmp_header), f); + fwrite(&bitmap, 1, sizeof(bitmap), f); + fclose(f); + running = false; + } + else if (frames == test_length - 1) { + gb->disable_rendering = false; + } + + frames++; +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + if (!log_file) log_file = fopen(log_filename, "w"); + fprintf(log_file, "%s", string); +} + +#ifdef __APPLE__ +#include +#endif + +static const char *executable_folder(void) +{ + static char path[1024] = {0,}; + if (path[0]) { + return path; + } + /* Ugly unportable code! :( */ +#ifdef __APPLE__ + unsigned int length = sizeof(path) - 1; + _NSGetExecutablePath(&path[0], &length); +#else +#ifdef __linux__ + ssize_t length = readlink("/proc/self/exe", &path[0], sizeof(path) - 1); + assert (length != -1); +#else +#ifdef _WIN32 + HMODULE hModule = GetModuleHandle(NULL); + GetModuleFileName(hModule, path, sizeof(path) - 1); +#else + /* No OS-specific way, assume running from CWD */ + getcwd(&path[0], sizeof(path) - 1); + return path; +#endif +#endif +#endif + size_t pos = strlen(path); + while (pos) { + pos--; +#ifdef _WIN32 + if (path[pos] == '\\') { +#else + if (path[pos] == '/') { +#endif + path[pos] = 0; + break; + } + } + return path; +} + +static char *executable_relative_path(const char *filename) +{ + static char path[1024]; + snprintf(path, sizeof(path), "%s/%s", executable_folder(), filename); + return path; +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return (r << 24) | (g << 16) | (b << 8); +} + +static void replace_extension(const char *src, size_t length, char *dest, const char *ext) +{ + memcpy(dest, src, length); + dest[length] = 0; + + /* Remove extension */ + for (size_t i = length; i--;) { + if (dest[i] == '/') break; + if (dest[i] == '.') { + dest[i] = 0; + break; + } + } + + /* Add new extension */ + strcat(dest, ext); +} + +int main(int argc, char **argv) +{ +#define str(x) #x +#define xstr(x) str(x) + fprintf(stderr, "SameBoy Tester v" xstr(VERSION) "\n"); + + if (argc == 1) { + fprintf(stderr, "Usage: %s [--dmg] [--start] [--length seconds]" +#ifndef _WIN32 + " [--jobs number of tests to run simultaneously]" +#endif + " rom ...\n", argv[0]); + exit(1); + } + +#ifndef _WIN32 + unsigned int max_forks = 1; + unsigned int current_forks = 0; +#endif + + bool dmg = false; + for (unsigned i = 1; i < argc; i++) { + if (strcmp(argv[i], "--dmg") == 0) { + fprintf(stderr, "Using DMG mode\n"); + dmg = true; + continue; + } + + if (strcmp(argv[i], "--start") == 0) { + fprintf(stderr, "Pushing Start and A\n"); + push_start_a = true; + continue; + } + + if (strcmp(argv[i], "--length") == 0 && i != argc - 1) { + test_length = atoi(argv[++i]) * 60; + fprintf(stderr, "Test length is %d seconds\n", test_length / 60); + continue; + } + +#ifndef _WIN32 + if (strcmp(argv[i], "--jobs") == 0 && i != argc - 1) { + max_forks = atoi(argv[++i]); + /* Make sure wrong input doesn't blow anything up. */ + if (max_forks < 1) max_forks = 1; + if (max_forks > 16) max_forks = 16; + fprintf(stderr, "Running up to %d tests simultaneously\n", max_forks); + continue; + } + + if (max_forks > 1) { + while (current_forks >= max_forks) { + int wait_out; + while(wait(&wait_out) == -1); + current_forks--; + } + + current_forks++; + if (fork() != 0) continue; + } +#endif + + if (dmg) { + GB_init(&gb); + if (GB_load_boot_rom(&gb, executable_relative_path("dmg_boot.bin"))) { + perror("Failed to load boot ROM"); + exit(1); + } + } + else { + GB_init_cgb(&gb); + if (GB_load_boot_rom(&gb, executable_relative_path("cgb_boot.bin"))) { + perror("Failed to load boot ROM"); + exit(1); + } + } + + filename = argv[i]; + fprintf(stderr, "Testing ROM %s\n", filename); + + if (GB_load_rom(&gb, filename)) { + perror("Failed to load ROM"); + exit(1); + } + + GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); + GB_set_pixels_output(&gb, &bitmap[0]); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_log_callback(&gb, log_callback); + + size_t path_length = strlen(filename); + + char bitmap_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .bmp + NULL */ + replace_extension(filename, path_length, bitmap_path, ".bmp"); + bmp_filename = &bitmap_path[0]; + + char log_path[path_length + 5]; + replace_extension(filename, path_length, log_path, ".log"); + log_filename = &log_path[0]; + + /* Run emulation */ + running = true; + gb.turbo = gb.turbo_dont_skip = gb.disable_rendering = true; + frames = 0; + while (running) { + GB_run(&gb); + } + + if (log_file) { + fclose(log_file); + log_file = NULL; + } + + GB_free(&gb); +#ifndef _WIN32 + if (max_forks > 1) { + exit(0); + } +#endif + } +#ifndef _WIN32 + int wait_out; + while(wait(&wait_out) != -1); +#endif + return 0; +} +