| @@ -8,7 +8,6 @@ | |||
| #include "src/config.h" | |||
| #include "src/disassembler.h" | |||
| #include "src/emulator.h" | |||
| #include "src/gamegear.h" | |||
| #include "src/logging.h" | |||
| #include "src/rom.h" | |||
| @@ -35,21 +34,16 @@ int main(int argc, char *argv[]) | |||
| retval = disassemble_file(config->src_path, config->dst_path); | |||
| retval = retval ? EXIT_SUCCESS : EXIT_FAILURE; | |||
| } else { | |||
| ROM *rom; | |||
| ROM rom; | |||
| const char* errmsg; | |||
| if ((errmsg = rom_open(&rom, config->rom_path))) { | |||
| ERROR("couldn't load ROM image '%s': %s", config->rom_path, errmsg) | |||
| retval = EXIT_FAILURE; | |||
| } else { | |||
| GameGear *gg = gamegear_create(); | |||
| printf("crater: emulating: %s\n", rom->name); | |||
| gamegear_load(gg, rom); | |||
| emulate(gg); | |||
| gamegear_destroy(gg); | |||
| rom_close(rom); | |||
| printf("crater: emulating: %s\n", rom.name); | |||
| emulate(&rom, config->fullscreen, config->scale); | |||
| rom_close(&rom); | |||
| } | |||
| } | |||
| @@ -38,7 +38,7 @@ static void print_help(const char *arg1) | |||
| " -g, --debug show logging information while running; add twice (-gg)\n" | |||
| " to show more detailed logs, including an emulator trace\n" | |||
| " -s, --scale <n> scale the game screen by an integer factor\n" | |||
| " (applies to windowed mode only)\n" | |||
| " (applies to windowed mode only; defaults to 4)\n" | |||
| " -a, --assemble <in> [<out>] convert z80 assembly source code into a\n" | |||
| " binary file that can be run by crater\n" | |||
| " -d, --disassemble <in> [<out>] convert a binary file into z80 assembly\n" | |||
| @@ -307,7 +307,7 @@ static int parse_args(Config *config, int argc, char *argv[]) | |||
| filename based on the input file, replacing its extension with ".gg" or | |||
| ".asm" (or adding it, if none is present). | |||
| */ | |||
| static void guess_assembler_output_file(Config* config) | |||
| static void guess_assembler_output_file(Config *config) | |||
| { | |||
| char *src = config->src_path, *ptr = src + strlen(src) - 1, | |||
| *ext = config->assemble ? ".gg" : ".asm"; | |||
| @@ -326,28 +326,39 @@ static void guess_assembler_output_file(Config* config) | |||
| /* | |||
| Ensure that the combination of arguments in a config object are valid. | |||
| Some modifications are made in the case of missing arguments, like guessing | |||
| the filenames for assembler output files. | |||
| */ | |||
| static bool sanity_check(Config* config) | |||
| static bool sanity_check(Config *config) | |||
| { | |||
| bool assembler = config->assemble || config->disassemble; | |||
| if (config->fullscreen && config->scale > 1) { | |||
| if (config->fullscreen && config->scale) { | |||
| ERROR("cannot specify a scale in fullscreen mode") | |||
| return false; | |||
| } else if (config->assemble && config->disassemble) { | |||
| ERROR("cannot assemble and disassemble at the same time") | |||
| return false; | |||
| } else if (assembler && (config->fullscreen || config->scale > 1)) { | |||
| } else if (assembler && (config->fullscreen || config->scale)) { | |||
| ERROR("cannot specify emulator options in assembler mode") | |||
| return false; | |||
| } else if (assembler && !config->src_path) { | |||
| ERROR("assembler mode requires an input file") | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| /* | |||
| Set some default values for missing arguments. | |||
| Some additional sanity checking is done. | |||
| */ | |||
| static bool set_defaults(Config *config) | |||
| { | |||
| bool assembler = config->assemble || config->disassemble; | |||
| if (!config->scale) { | |||
| config->scale = 4; | |||
| } | |||
| if (assembler && !config->dst_path) { | |||
| guess_assembler_output_file(config); | |||
| } | |||
| @@ -355,7 +366,6 @@ static bool sanity_check(Config* config) | |||
| ERROR("refusing to overwrite the assembler input file; pass -r to override") | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| @@ -376,14 +386,14 @@ int config_create(Config** config_ptr, int argc, char* argv[]) | |||
| config->assemble = false; | |||
| config->disassemble = false; | |||
| config->fullscreen = false; | |||
| config->scale = 1; | |||
| config->scale = 0; | |||
| config->rom_path = NULL; | |||
| config->src_path = NULL; | |||
| config->dst_path = NULL; | |||
| config->overwrite = false; | |||
| retval = parse_args(config, argc, argv); | |||
| if (retval == CONFIG_OK && !sanity_check(config)) | |||
| if (retval == CONFIG_OK && !(sanity_check(config) && set_defaults(config))) | |||
| retval = CONFIG_EXIT_FAILURE; | |||
| if (retval != CONFIG_OK) { | |||
| config_destroy(config); | |||
| @@ -406,7 +406,7 @@ static bool write_disassembly(const char *path, char **lines) | |||
| */ | |||
| bool disassemble_file(const char *src_path, const char *dst_path) | |||
| { | |||
| ROM *rom; | |||
| ROM rom; | |||
| const char *errmsg; | |||
| char **lines; | |||
| @@ -416,8 +416,8 @@ bool disassemble_file(const char *src_path, const char *dst_path) | |||
| return false; | |||
| } | |||
| lines = disassemble(rom); | |||
| rom_close(rom); | |||
| lines = disassemble(&rom); | |||
| rom_close(&rom); | |||
| DEBUG("Writing output file") | |||
| return write_disassembly(dst_path, lines); | |||
| @@ -2,105 +2,155 @@ | |||
| Released under the terms of the MIT License. See LICENSE for details. */ | |||
| #include <signal.h> | |||
| #include <stdint.h> | |||
| #include <SDL.h> | |||
| #include "emulator.h" | |||
| #include "gamegear.h" | |||
| #include "logging.h" | |||
| #include "util.h" | |||
| #define SCREEN_WIDTH 3 * (160 + 96) // TODO: define elsewhere; use scale | |||
| #define SCREEN_HEIGHT 3 * (144 + 48) | |||
| typedef struct { | |||
| GameGear *gg; | |||
| SDL_Window *window; | |||
| SDL_Renderer *renderer; | |||
| SDL_Texture *texture; | |||
| uint32_t *pixels; | |||
| } Emulator; | |||
| static GameGear *global_gg; | |||
| static SDL_Window *window; | |||
| SDL_Renderer* renderer; | |||
| static Emulator emu; | |||
| /* | |||
| Signal handler for SIGINT. | |||
| Signal handler for SIGINT. Tells the GameGear to power off, if it exists. | |||
| */ | |||
| static void handle_sigint(int sig) | |||
| { | |||
| (void) sig; | |||
| if (global_gg) | |||
| gamegear_power_off(global_gg); | |||
| if (emu.gg) | |||
| gamegear_power_off(emu.gg); // Safe! | |||
| } | |||
| /* | |||
| TODO: ... | |||
| Set up SDL for drawing the game. | |||
| */ | |||
| static void setup_graphics() | |||
| static void setup_graphics(bool fullscreen, unsigned scale) | |||
| { | |||
| if (SDL_Init(SDL_INIT_VIDEO) < 0) | |||
| FATAL("SDL failed to initialize: %s", SDL_GetError()); | |||
| SDL_CreateWindowAndRenderer(SCREEN_WIDTH, SCREEN_HEIGHT, | |||
| SDL_WINDOW_BORDERLESS/* |SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE ? */, | |||
| &window, &renderer); | |||
| uint32_t flags; | |||
| if (fullscreen) | |||
| flags = SDL_WINDOW_FULLSCREEN_DESKTOP; | |||
| else | |||
| flags = SDL_WINDOW_BORDERLESS|SDL_WINDOW_RESIZABLE; | |||
| SDL_CreateWindowAndRenderer( | |||
| scale * GG_SCREEN_WIDTH, scale * GG_SCREEN_HEIGHT, | |||
| flags, &emu.window, &emu.renderer); | |||
| if (!window) | |||
| if (!emu.window) | |||
| FATAL("SDL failed to create a window: %s", SDL_GetError()); | |||
| if (!renderer) | |||
| if (!emu.renderer) | |||
| FATAL("SDL failed to create a renderer: %s", SDL_GetError()); | |||
| SDL_SetRenderDrawColor(renderer, 0x33, 0x33, 0x33, 0xFF); | |||
| SDL_RenderClear(renderer); | |||
| SDL_RenderPresent(renderer); | |||
| emu.texture = SDL_CreateTexture(emu.renderer, SDL_PIXELFORMAT_ARGB8888, | |||
| SDL_TEXTUREACCESS_STREAMING, GG_SCREEN_WIDTH, GG_SCREEN_HEIGHT); | |||
| if (!emu.texture) | |||
| FATAL("SDL failed to create a texture: %s", SDL_GetError()); | |||
| emu.pixels = cr_malloc( | |||
| sizeof(uint32_t) * GG_SCREEN_WIDTH * GG_SCREEN_HEIGHT); | |||
| SDL_RenderSetLogicalSize(emu.renderer, GG_SCREEN_WIDTH, GG_SCREEN_HEIGHT); | |||
| SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); | |||
| SDL_SetWindowTitle(emu.window, "crater"); | |||
| SDL_ShowCursor(SDL_DISABLE); | |||
| SDL_SetRenderDrawColor(emu.renderer, 0x00, 0x00, 0x00, 0xFF); | |||
| SDL_RenderClear(emu.renderer); | |||
| SDL_RenderPresent(emu.renderer); | |||
| } | |||
| /* | |||
| GameGear callback: handle SDL logic at the end of a frame. | |||
| Actually send the pixel data to the screen. | |||
| */ | |||
| static void draw_frame(GameGear *gg) | |||
| static void draw_frame() | |||
| { | |||
| SDL_RenderPresent(renderer); | |||
| SDL_UpdateTexture(emu.texture, NULL, emu.pixels, | |||
| GG_SCREEN_WIDTH * sizeof(uint32_t)); | |||
| SDL_SetRenderDrawColor(emu.renderer, 0x00, 0x00, 0x00, 0xFF); | |||
| SDL_RenderClear(emu.renderer); | |||
| SDL_RenderCopy(emu.renderer, emu.texture, NULL, NULL); | |||
| SDL_RenderPresent(emu.renderer); | |||
| } | |||
| /* | |||
| Handle SDL events, mainly quit events and button presses. | |||
| */ | |||
| static void handle_events(GameGear *gg) | |||
| { | |||
| SDL_Event e; | |||
| while (SDL_PollEvent(&e)) { | |||
| if (e.type == SDL_QUIT) { | |||
| gamegear_power_off(gg); | |||
| return; | |||
| } | |||
| // TODO: buttons | |||
| } | |||
| } | |||
| SDL_SetRenderDrawColor(renderer, 0x33, 0x33, 0x33, 0xFF); | |||
| SDL_RenderClear(renderer); | |||
| /* | |||
| GameGear callback: Draw the current frame and handle SDL event logic. | |||
| */ | |||
| static void frame_callback(GameGear *gg) | |||
| { | |||
| draw_frame(); | |||
| handle_events(gg); | |||
| } | |||
| /* | |||
| TODO: ... | |||
| Clean up SDL stuff allocated in setup_graphics(). | |||
| */ | |||
| static void cleanup_graphics() | |||
| { | |||
| SDL_DestroyRenderer(renderer); | |||
| SDL_DestroyWindow(window); | |||
| window = NULL; | |||
| renderer = NULL; | |||
| free(emu.pixels); | |||
| SDL_DestroyTexture(emu.texture); | |||
| SDL_DestroyRenderer(emu.renderer); | |||
| SDL_DestroyWindow(emu.window); | |||
| SDL_Quit(); | |||
| emu.window = NULL; | |||
| emu.renderer = NULL; | |||
| emu.texture = NULL; | |||
| } | |||
| /* | |||
| Emulate a Game Gear. Handle I/O with the host computer. | |||
| Emulate a ROM in a Game Gear while handling I/O with the host computer. | |||
| Block until emulation is finished. | |||
| */ | |||
| void emulate(GameGear *gg) | |||
| void emulate(ROM *rom, bool fullscreen, unsigned scale) | |||
| { | |||
| global_gg = gg; | |||
| emu.gg = gamegear_create(); | |||
| signal(SIGINT, handle_sigint); | |||
| gamegear_set_callback(gg, draw_frame); | |||
| setup_graphics(); | |||
| setup_graphics(fullscreen, scale); | |||
| gamegear_attach_callback(emu.gg, frame_callback); | |||
| gamegear_attach_display(emu.gg, emu.pixels); | |||
| gamegear_load(emu.gg, rom); | |||
| gamegear_simulate(gg); | |||
| gamegear_simulate(emu.gg); | |||
| if (gamegear_get_exception(gg)) | |||
| ERROR("caught exception: %s", gamegear_get_exception(gg)) | |||
| if (gamegear_get_exception(emu.gg)) | |||
| ERROR("caught exception: %s", gamegear_get_exception(emu.gg)) | |||
| else | |||
| WARN("caught signal, stopping...") | |||
| if (DEBUG_LEVEL) | |||
| gamegear_print_state(gg); | |||
| gamegear_print_state(emu.gg); | |||
| cleanup_graphics(); | |||
| gamegear_clear_callback(gg); | |||
| signal(SIGINT, SIG_DFL); | |||
| global_gg = NULL; | |||
| gamegear_destroy(emu.gg); | |||
| emu.gg = NULL; | |||
| } | |||
| @@ -3,8 +3,10 @@ | |||
| #pragma once | |||
| #include "gamegear.h" | |||
| #include <stdbool.h> | |||
| #include "rom.h" | |||
| /* Functions */ | |||
| void emulate(GameGear*); | |||
| void emulate(ROM*, bool, unsigned); | |||
| @@ -18,6 +18,11 @@ | |||
| /* | |||
| Create and return a pointer to a new GameGear object. | |||
| The GameGear operates in headless mode by default (i.e., without any | |||
| noticeable output). You'll probably want to attach a frame-completion | |||
| callback with gamegear_attach_callback() and a display with | |||
| gamegear_attach_display(). | |||
| */ | |||
| GameGear* gamegear_create() | |||
| { | |||
| @@ -93,18 +98,34 @@ void gamegear_power_off(GameGear *gg) | |||
| /* | |||
| Set a callback to be triggered whenever the GameGear completes a frame. | |||
| The callback is passed a reference to the GameGear object. | |||
| */ | |||
| void gamegear_set_callback(GameGear *gg, GGFrameCallback callback) | |||
| void gamegear_attach_callback(GameGear *gg, GGFrameCallback callback) | |||
| { | |||
| gg->callback = callback; | |||
| } | |||
| /* | |||
| Reset the GameGear's frame callback function. | |||
| Set a display to written to whenever the GameGear draws a pixel. | |||
| The array must be (GG_SCREEN_WIDTH * GG_SCREEN_HEIGHT) pixels large, where | |||
| each pixel is a 32-bit integer in ARGB order (i.e., A is the top 8 bits). | |||
| */ | |||
| void gamegear_attach_display(GameGear *gg, uint32_t *pixels) | |||
| { | |||
| gg->vdp.pixels = pixels; | |||
| } | |||
| /* | |||
| Reset any callbacks or displays attached to the GameGear. | |||
| This returns the GameGear to headless mode. | |||
| */ | |||
| void gamegear_clear_callback(GameGear *gg) | |||
| void gamegear_detach(GameGear *gg) | |||
| { | |||
| gg->callback = NULL; | |||
| gg->vdp.pixels = NULL; | |||
| } | |||
| /* | |||
| @@ -12,6 +12,9 @@ | |||
| #include "rom.h" | |||
| #include "z80.h" | |||
| #define GG_SCREEN_WIDTH (160 + 96) | |||
| #define GG_SCREEN_HEIGHT (144 + 48) | |||
| #define GG_FPS 60 | |||
| #define GG_EXC_BUFF_SIZE 128 | |||
| @@ -37,8 +40,11 @@ GameGear* gamegear_create(); | |||
| void gamegear_destroy(GameGear*); | |||
| void gamegear_load(GameGear*, const ROM*); | |||
| void gamegear_simulate(GameGear*); | |||
| void gamegear_power_off(GameGear*); | |||
| void gamegear_set_callback(GameGear*, GGFrameCallback); | |||
| void gamegear_clear_callback(GameGear*); | |||
| void gamegear_power_off(GameGear*); // TODO: generic "gamegear_input()" with a power-off option | |||
| void gamegear_attach_callback(GameGear*, GGFrameCallback); | |||
| void gamegear_attach_display(GameGear*, uint32_t*); | |||
| void gamegear_detach(GameGear*); | |||
| const char* gamegear_get_exception(GameGear*); | |||
| void gamegear_print_state(const GameGear*); | |||
| @@ -154,15 +154,13 @@ static bool find_and_read_header(ROM *rom) | |||
| } | |||
| /* | |||
| Create and load a ROM image located at the given path. | |||
| Load a ROM image located at the given path. | |||
| rom_ptr will point to the new object if created successfully, and NULL will | |||
| be returned. Otherwise, rom_ptr will not be modified and an error string | |||
| will be returned. The error string should not be freed. | |||
| NULL will be returned if the ROM is opened successfully. Otherwise, and an | |||
| error string will be returned. The error string should not be freed. | |||
| */ | |||
| const char* rom_open(ROM **rom_ptr, const char *path) | |||
| const char* rom_open(ROM *rom, const char *path) | |||
| { | |||
| ROM *rom; | |||
| FILE *fp; | |||
| struct stat st; | |||
| @@ -178,8 +176,6 @@ const char* rom_open(ROM **rom_ptr, const char *path) | |||
| return (st.st_mode & S_IFDIR) ? rom_err_isdir : rom_err_notfile; | |||
| } | |||
| rom = cr_malloc(sizeof(ROM)); | |||
| // Set defaults: | |||
| rom->name = NULL; | |||
| rom->data = NULL; | |||
| @@ -227,18 +223,16 @@ const char* rom_open(ROM **rom_ptr, const char *path) | |||
| return rom_err_sms; | |||
| } | |||
| *rom_ptr = rom; | |||
| return NULL; | |||
| } | |||
| /* | |||
| Free a ROM object previously created with rom_open(). | |||
| Free memory previously allocated by the ROM during rom_open(). | |||
| */ | |||
| void rom_close(ROM *rom) | |||
| { | |||
| free(rom->name); | |||
| free(rom->data); | |||
| free(rom); | |||
| } | |||
| /* | |||
| @@ -42,7 +42,7 @@ typedef struct { | |||
| /* Functions */ | |||
| const char* rom_open(ROM**, const char*); | |||
| const char* rom_open(ROM*, const char*); | |||
| void rom_close(ROM*); | |||
| const char* rom_product(const ROM*); | |||
| const char* rom_region(const ROM*); | |||
| @@ -12,13 +12,16 @@ | |||
| #define CODE_REG_WRITE 2 | |||
| #define CODE_CRAM_WRITE 3 | |||
| extern SDL_Renderer* renderer; | |||
| /* | |||
| Initialize the Video Display Processor (VDP). | |||
| The VDP will write to its pixels array whenever it draws a scanline. It | |||
| defaults to NULL, but you should set it to something if you want to see its | |||
| output. | |||
| */ | |||
| void vdp_init(VDP *vdp) | |||
| { | |||
| vdp->pixels = NULL; | |||
| vdp->vram = cr_malloc(sizeof(uint8_t) * VDP_VRAM_SIZE); | |||
| vdp->cram = cr_malloc(sizeof(uint8_t) * VDP_CRAM_SIZE); | |||
| memset(vdp->vram, 0x00, VDP_VRAM_SIZE); | |||
| @@ -105,19 +108,16 @@ static uint8_t get_backdrop_addr(const VDP *vdp) | |||
| /* | |||
| TODO: ... | |||
| */ | |||
| static void draw_pixel(uint8_t y, uint8_t x, uint16_t color) | |||
| static void draw_pixel(VDP *vdp, uint8_t y, uint8_t x, uint16_t color) | |||
| { | |||
| uint8_t r = 0x11 * (color & 0x000F); | |||
| uint8_t g = 0x11 * ((color & 0x00F0) >> 4); | |||
| uint8_t b = 0x11 * ((color & 0x0F00) >> 8); | |||
| SDL_SetRenderDrawColor(renderer, r, g, b, 0xFF); | |||
| uint32_t argb = (0xFF << 24) + (r << 16) + (g << 8) + b; | |||
| uint8_t i, j; | |||
| for (i = 0; i < 3; i++) { | |||
| for (j = 0; j < 3; j++) | |||
| SDL_RenderDrawPoint(renderer, 3 * x + i, 3 * y + j); | |||
| } | |||
| // TODO | |||
| vdp->pixels[y * (160 + 96) + x] = argb; | |||
| } | |||
| /* | |||
| @@ -154,7 +154,7 @@ static void draw_background(VDP *vdp) | |||
| (((bp3 >> i) & 1) << 3); | |||
| color = vdp->cram[2 * idx] + (vdp->cram[2 * idx + 1] << 8); | |||
| offx = hflip ? (col * 8 + (7 - i)) : (col * 8 + i); | |||
| draw_pixel(vdp->v_counter, offx, color); | |||
| draw_pixel(vdp, vdp->v_counter, offx, color); | |||
| } | |||
| } | |||
| } | |||
| @@ -181,6 +181,9 @@ static void draw_sprites(VDP *vdp) | |||
| */ | |||
| static void draw_scanline(VDP *vdp) | |||
| { | |||
| if (!vdp->pixels) | |||
| return; | |||
| draw_background(vdp); | |||
| draw_sprites(vdp); | |||
| } | |||
| @@ -14,6 +14,8 @@ | |||
| /* Structs */ | |||
| typedef struct { | |||
| uint32_t *pixels; | |||
| uint8_t *vram; | |||
| uint8_t *cram; | |||
| uint8_t regs[VDP_REGS]; | |||