#include #include #include #include #include #include #include #include "action.h" #include "cart.h" #include "nese.h" #include "overlay.h" #include "port.h" #include "save.h" #include "draw.h" #include "cartinfo.h" #include "menu.h" #define NESE_DEBUG "Port" #include "log.h" //#define NESE_TURBO /* * OS-specific file operations * Memory mapping specifically needs to be ported for each OS */ void* nese_map_file(FILE* file, int size, Filemap_Mode map_mode) { int prot = ( Filemap_Mode_Write == map_mode ? PROT_WRITE : PROT_READ); int flags = ( Filemap_Mode_Write == map_mode ? MAP_SHARED : MAP_PRIVATE); void* mem = mmap(NULL, size, prot, flags, fileno(file), 0); if ((void*)-1 == mem) { fprintf(stderr, "Failed to map file: %d\n", errno); mem = NULL; } return mem; } void nese_unmap_file(void* mem, int size) { munmap(mem, size); } int nese_mkdir(const char* dir) { return mkdir(dir, 0777); } int nese_file_size(FILE* file) { int size = -1; if (0 == fseek(file, 0, SEEK_END)) { size = ftell(file); } return size; } /* * Platform-specific allocation * GPU and CPU refer to emulator memory spaces * Note: GPU (and possibly CPU) regions may need * to be placed into internal ram for performance */ void* nese_alloc_gpu(int size) { return calloc(1, size); } void* nese_alloc_cpu(int size) { return calloc(1, size); } void* nese_alloc(int size) { return calloc(1, size); } /* * Platform-specific features and controls */ #define PLAT_FILENAME_SIZE (1024U) typedef enum { Flag_Turbo = 0b1, } Platform_Flags; typedef struct { nes* sys; Cart_Info cart; int64_t t_target; Render_Info renderer; Overlay overlay; int fps_msg_id; Platform_Flags flags; char filename[PLAT_FILENAME_SIZE]; } platform_data; /* Directories */ static int cmp_files(const void* _a, const void* _b) { const char* a = *(const char**)_a; const char* b = *(const char**)_b; int diff = 0; for (char ca = 1, cb = 1; ca && cb && 0 == diff; ++a, ++b) { // Cut extensions; replace underscore with space ca = (*a == '_' ? ' ' : (*a == '.' ? '\0' : *a)); cb = (*b == '_' ? ' ' : (*b == '.' ? '\0' : *b)); diff = (ca - cb); } return diff; } static int count_files(DIR* dir) { int count = 0; struct dirent* de = NULL; while (NULL != (de = readdir(dir))) { if ('.' != de->d_name[0]) ++count; } rewinddir(dir); return count; } static void make_file_list(DIR* dir, File_List* files) { files->count = count_files(dir); files->files = calloc(files->count, sizeof(char*)); struct dirent* de = NULL; int i_file = 0; while (NULL != (de = readdir(dir))) { if ('.' != de->d_name[0]) { files->files[i_file] = strdup(de->d_name); ++i_file; } } qsort(files->files, files->count, sizeof(char*), cmp_files); } static void free_file_list(File_List* files) { for (int i = 0; i < files->count; ++i) { free(files->files[i]); } free(files->files); } static int find_file(File_List* files, const char* filename) { int i = (files->count - 1); for ( ; i >= 0 && (0 != strcmp(files->files[i], filename)); --i); return i; } /* Input */ static const int sdl_action_keycodes[Action_Max] = { [Action_Menu] = SDLK_ESCAPE, [Action_Reset] = SDLK_BACKSPACE, [Action_Load] = SDLK_F2, [Action_Save] = SDLK_F1, [Action_FPS] = SDLK_f, [Action_Turbo] = SDLK_t, }; static const int sdl_button_keycodes[2 * nes_controller_num_buttons] = { SDLK_a, SDLK_s, SDLK_q, SDLK_w, SDLK_UP, SDLK_DOWN, SDLK_LEFT, SDLK_RIGHT, SDLK_LCTRL, SDLK_LALT, SDLK_TAB, SDLK_RETURN, SDLK_KP_8, SDLK_KP_2, SDLK_KP_4, SDLK_KP_6, }; static int keycode_index(int keycode, const int* codes, int n_codes) { int index = n_codes - 1; for ( ; index >= 0 && keycode != codes[index]; --index); return index; } static nese_Action process_events(nes* sys) { nese_Action action = Action_OK; nes_Input* input = &sys->core.memory.input; SDL_Event event = {0}; while (Action_OK == action && 0 != SDL_PollEvent(&event)) { if (SDL_QUIT == event.type) { action = Action_Quit; } else if ( ( SDL_KEYDOWN == event.type || SDL_KEYUP == event.type) && 0 == event.key.repeat ) { int index = keycode_index( event.key.keysym.sym, sdl_button_keycodes, 2 * nes_controller_num_buttons ); if (index >= 0) { index %= nes_controller_num_buttons; uint8_t mask = (1 << index); if (SDL_KEYDOWN == event.type) { input->gamepads[0].buttons |= mask; } else { input->gamepads[0].buttons &= ~mask; } } else if (SDL_KEYDOWN == event.type) { index = keycode_index( event.key.keysym.sym, sdl_action_keycodes, Action_Max ); if (index >= 0) action = index; } } // TODO: Controller inputs } return action; } /* * Time / Video - Should be maximally reusable across platforms */ int nese_frame_start(void* plat_data, uint8_t background) { return render_frame_start( &((platform_data*)plat_data)->renderer, background); } int nese_line_ready(void* plat_data, uint8_t* buffer, int line) { /* platform_data* plat = (platform_data*)plat_data; SDL_Rect rect = { .x = 0, .y = line, .w = nes_ppu_render_w, .h = 1, }; SDL_BlitSurface(plat->scanline, NULL, plat->target, &rect); */ return 0; } #define NS_PER_S (1000L * 1000L * 1000L) #define FRAME_TIME_NS (16639267L) uint64_t time_now(void) { struct timespec ts_now = {0}; clock_gettime(CLOCK_REALTIME, &ts_now); return (ts_now.tv_sec * NS_PER_S) + ts_now.tv_nsec; } int64_t time_sleep_until(int64_t t_target) { int64_t t_now = time_now(); int64_t t_diff = (t_target - t_now) / 1000; if (t_diff > 0) { usleep(t_diff); } return t_diff * 1000; } static nese_Action game_menu(platform_data* plat); int nese_frame_ready(void* plat_data) { platform_data* plat = (platform_data*)plat_data; /* uint8_t* ptr = &plat->sys->core.memory.ppu.vram[0x3C0]; for (int y = 0; y < 16; y += 2) { printf("%04lX", 0x2000 + ptr - plat->sys->core.memory.ppu.vram); for (int x = 0; x < 8; ++x) { printf(" %d %d", (*ptr >> 0) & 3, (*ptr >> 2) & 3); ++ptr; } ptr -= 8; fputs("\n ", stdout); for (int x = 0; x < 8; ++x) { printf(" %d %d", (*ptr >> 4) & 3, (*ptr >> 6) & 3); ++ptr; } putc('\n', stdout); } putc('\n', stdout); */ int status = render_frame(&plat->renderer); if (0 == status) { overlay_render(&plat->overlay, nes_ppu_render_w, nes_ppu_render_h, &plat->renderer.view, plat->renderer.renderer); } if (0 == status) { status = render_frame_end(&plat->renderer); } // TODO: Check status first? nese_Action action = process_events(plat->sys); switch (action) { case Action_Quit: // TODO: Save SRAM status = -1; break; case Action_Save: status = save_state(plat->sys, plat->cart.filename); break; case Action_Load: status = load_state(plat->sys, plat->cart.filename); break; case Action_FPS: if (plat->fps_msg_id <= 0) { plat->fps_msg_id = overlay_add_message(&plat->overlay, "", 0); } else { overlay_clear_message(&plat->overlay, plat->fps_msg_id); plat->fps_msg_id = 0; } break; case Action_Turbo: plat->flags ^= Flag_Turbo; break; case Action_Menu: action = game_menu(plat); if (Action_Reset == action) { nes_reset(plat->sys); } else if (Action_Save == action) { status = save_state(plat->sys, plat->cart.filename); } else if (Action_Load == action) { status = load_state(plat->sys, plat->cart.filename); } else if (Action_Quit == action || NULL == plat->sys->cart_header) { status = -1; } // TODO: Other Actions? break; default: } // TODO: Perform more actions? if (0 == status) { plat->t_target += FRAME_TIME_NS; int64_t slept_ns = 0; if (plat->flags & Flag_Turbo) { int64_t now = time_now(); slept_ns = plat->t_target - now; plat->t_target = now; } else { slept_ns = time_sleep_until(plat->t_target); if (slept_ns <= -FRAME_TIME_NS) { // We're way out of sync. plat->t_target = time_now(); printf("Out of sync: %d\n", (int)slept_ns); } } static int frame = 0; static int64_t slept_total = 0; slept_total += slept_ns; if (60 == ++frame) { if (plat->fps_msg_id > 0) { int64_t game_elapsed = frame * FRAME_TIME_NS; int64_t cpu_elapsed = game_elapsed - slept_total; float fps = (double)(NS_PER_S * frame) / cpu_elapsed; char message[32]; snprintf(message, sizeof(message), "%.1f fps", fps); overlay_update_message(&plat->overlay, plat->fps_msg_id, message); } slept_total = 0; frame = 0; } } return status; } /* Drawing */ void nese_draw_begin(void* plat_data) { platform_data* plat = (platform_data*)plat_data; draw_begin(&plat->renderer, NULL != plat->sys->cart_header); } void nese_draw_text(void* plat_data, const char* str, int x, int y, uint32_t c) { draw_text(&((platform_data*)plat_data)->renderer, &((platform_data*)plat_data)->overlay.font, str, x, y, c); } void nese_text_size(void* plat_data, const char* str, int* w, int* h) { overlay_font_measure(&((platform_data*)plat_data)->overlay.font, str, w, h); } void nese_draw_finish(void* plat_data) { render_frame_end(&((platform_data*)plat_data)->renderer); } /* Audio */ int nese_get_audio_frequency(void*) { return 44100; } // TODO: Audio functions /* Menus */ nese_Action nese_update_input(void* plat_data, nes_Input* input) { time_sleep_until(time_now() + (NS_PER_S / 60)); platform_data* plat = (platform_data*)plat_data; nese_Action action = process_events(plat->sys); if (NULL != input) *input = plat->sys->core.memory.input; return action; } static nese_Action run_rom_menu(platform_data* plat, Menu_State* state, char* filename, int sz_filename) { nese_Action action = Action_Cancel; DIR* dir = opendir("rom"); if (NULL == dir) { modal_popup(plat, "No ROMS found!\nPress any key to exit", color_error); } else { File_List files = {0}; make_file_list(dir, &files); closedir(dir); Menu_State menu = {0}; if (NULL != state) menu = *state; // Add 4 to skip past "rom/" if (strlen(filename) > 4) { int current = find_file(&files, filename + 4); if (current >= 0) menu.cursor = current; } action = run_menu(plat, &menu, &files, 20); if (Action_OK == action) { if ( menu.cursor >= 0 && menu.cursor < files.count) { snprintf(filename, sz_filename - 1, "%s/%s", "rom", files.files[menu.cursor]); } else { action = Action_Cancel; } } free_file_list(&files); if (NULL != state) *state = menu; } return action; } static nese_Action nese_load_cart(platform_data* plat, const char* filename) { nese_Action action = Action_OK; unload_cart(&plat->cart); plat->sys->cart_header = NULL; int status = load_cart(&plat->cart, filename, plat->sys); if (0 != status) { char message[1024]; snprintf(message, sizeof(message), "Failed to load\n%s", filename); action = modal_popup(plat, message, color_error); if (Action_Quit != action) action = Action_Cancel; } return action; } static nese_Action select_rom(platform_data* plat, char* file, int sz_file) { nese_Action action = Action_OK; Menu_State menu = {0}; // TODO: Save SRAM while (Action_OK == action || NULL == plat->sys->cart_header) { action = run_rom_menu(plat, &menu, file, sz_file); if (Action_OK == action) { plat->sys->cart_header = NULL; action = nese_load_cart(plat, file); if (Action_OK == action) break; // Failure - Now no ROM is currently loaded render_clear(&plat->renderer); file[0] = '\0'; if (Action_Quit != action) action = Action_OK; } else if ( ( Action_Cancel == action && NULL == plat->sys->cart_header) || Action_Quit == action) { // If no cart is loaded, or the user tried to quit, just leave break; } } return action; } static nese_Action run_game_menu(platform_data* plat, Menu_State* state) { static char* items[] = { "Resume", "Save", "Restore", "Reset", "Select ROM", "Exit", }; static const nese_Action choices[] = { Action_Cancel, Action_Save, Action_Load, Action_Reset, Action_Menu, Action_Quit, }; static const File_List options = { .files = items, .count = (sizeof(items) / sizeof(*items)), }; Menu_State menu = {0}; if (NULL != state) menu = *state; nese_Action action = run_menu(plat, &menu, &options, 100); if (Action_Menu == action) { action = Action_Cancel; } else if (Action_OK == action) { if ( menu.cursor >= 0 && menu.cursor < (sizeof(choices) / sizeof(*choices))) { action = choices[menu.cursor]; } } if (NULL != state) *state = menu; return action; } static nese_Action game_menu(platform_data* plat) { nese_Action action = Action_OK; Menu_State menu = {0}; while (Action_OK == action) { action = run_game_menu(plat, &menu); if (Action_Menu == action) { // Select ROM action = select_rom(plat, plat->filename, PLAT_FILENAME_SIZE); if (Action_OK == action) { // New ROM selected - Exit loop and reset action = Action_Reset; } else if (Action_Cancel == action) { // No ROM selected - Keep the menu running action = Action_OK; } if (NULL == plat->sys->cart_header) { // A failed ROM load means we shouldn't return to the game menu action = Action_Quit; } } } return action; } /* Platform Data */ static int plat_init(platform_data* plat) { int status = render_info_init(&plat->renderer, plat->sys); if (0 == status) { status = overlay_init(&plat->overlay, plat->renderer.renderer); } if (0 == status) { plat->t_target = time_now(); } return status; } static void plat_done(platform_data* plat) { render_info_done(&plat->renderer); overlay_done(&plat->overlay); SDL_Quit(); } // This is too big for the stack. static nes sys = {0}; int main(int argc, char* argv[]) { int status = 0; // This should be tiny enough to keep on the stack. platform_data plat = { .sys = &sys, }; if (0 == status) { status = plat_init(&plat); } if (0 == status) { if (1 < argc) { strncpy(plat.filename, argv[1], PLAT_FILENAME_SIZE - 1); plat.filename[PLAT_FILENAME_SIZE - 1] = '\0'; nese_Action action = nese_load_cart(&plat, plat.filename); status = (Action_OK == action) ? 0 : -1; } if (NULL == sys.cart_header) { nese_Action action = select_rom( &plat, plat.filename, PLAT_FILENAME_SIZE ); status = (Action_OK == action) ? 0 : -1; } } // This shall invoke menus as needed if (0 == status) { status = nese_start(&sys, &plat); } unload_cart(&plat.cart); plat_done(&plat); return status; }