/* * retro.c -- OreBolt OS Module: Retro Game Launcher * * SPDX-License-Identifier: GPL-2.0-or-later * * Scans /data/roms/ for ROM files, auto-detects the target system by * file extension, displays an LVGL picker menu, and launches the * matching emulator binary via fork()+execv(). Standalone .mod * binary with its own main() and LVGL init. * * Supported systems (by file extension): * .nes - Nintendo Entertainment System * .fds - Famicom Disk System * .smc,.sfc,.fig - Super Nintendo (SNES) * .gb - Game Boy * .gbc - Game Boy Color * .gba - Game Boy Advance * .smd,.md,.bin,.gen - Sega Genesis / Mega Drive * .sms - Sega Master System * .gg - Sega Game Gear * .pce - PC Engine / TurboGrafx-16 * .a26 - Atari 2600 * .ngp,.ngc - Neo Geo Pocket / Color * * Emulator binaries must be installed at the paths listed in * rom_types[] below. * * INPUT MAPPING: * The companion retro_input_mapper daemon (started by S98retro-input * at boot) creates a virtual gamepad via uinput. Emulators should * read from this virtual gamepad rather than /dev/input/event0. * * D-pad: Rotary CW/CCW with axis toggle (rotary press switches * between vertical U/D and horizontal L/R modes, auto- * reverts to V-mode after 3s idle). * A: PLAY short press (confirm/jump/shoot) * B: BACK short press (cancel/run) * START: PLAY long press (1.5s) * SELECT: BACK long press (1.5s) * L/R: Side buttons PREV(165) / NEXT(163) * X/Y: Combo buttons (PLAY+rotate / BACK+rotate) * * N64 is NOT supported -- the X1000E MIPS32r2 @ ~1 GHz with 64 MB RAM * cannot run N64 emulation at playable framerates. * * Toolchain: mipsel-linux-musl-gcc (MIPS32r2, musl libc) */ #include #include #include #include #include #include #include #include #include #include #include #include "h2_ui.h" #include "log_manager.h" /* ------------------------------------------------------------------ */ /* ROM type database */ /* ------------------------------------------------------------------ */ #define MAX_ROM_TYPES 32 #define MAX_ROMS 256 #define ROM_DIR "/data/roms" #define PATH_BUF 512 typedef struct { const char *ext; /* file extension, lowercase, with dot */ const char *system; /* human-readable system name */ const char *emulator; /* absolute path to emulator binary */ const char *core; /* emulator-specific core/flag, or NULL */ } rom_type_t; static const rom_type_t rom_db[MAX_ROM_TYPES] = { /* --- Nintendo --- */ { ".nes", "NES", "/usr/bin/fceux", NULL }, { ".fds", "FDS", "/usr/bin/fceux", NULL }, { ".smc", "SNES", "/usr/bin/snes9x", NULL }, { ".sfc", "SNES", "/usr/bin/snes9x", NULL }, { ".fig", "SNES", "/usr/bin/snes9x", NULL }, { ".gb", "GameBoy", "/usr/bin/gambatte", NULL }, { ".gbc", "GBC", "/usr/bin/gambatte", NULL }, { ".gba", "GBA", "/usr/bin/gpsp", NULL }, /* --- Sega --- */ { ".smd", "Genesis", "/usr/bin/dgen", NULL }, { ".md", "Genesis", "/usr/bin/dgen", NULL }, { ".gen", "Genesis", "/usr/bin/dgen", NULL }, { ".bin", "Genesis", "/usr/bin/dgen", "-g" }, { ".sms", "SMS", "/usr/bin/mednafen", "-ss" }, { ".gg", "GameGear", "/usr/bin/mednafen", "-gg" }, /* --- NEC / Hudson --- */ { ".pce", "PCE", "/usr/bin/mednafen", "-pce" }, /* --- Atari --- */ { ".a26", "Atari2600", "/usr/bin/stella", NULL }, /* --- SNK --- */ { ".ngp", "NeoGeoP", "/usr/bin/mednafen", "-ngp" }, { ".ngc", "NeoGeoP", "/usr/bin/mednafen", "-ngp" }, /* sentinel */ { NULL, NULL, NULL, NULL } }; /* ------------------------------------------------------------------ */ /* ROM entry (populated at scan time) */ /* ------------------------------------------------------------------ */ typedef struct { char path[PATH_BUF]; /* full path to ROM file */ char name[64]; /* display name (filename only) */ const rom_type_t *type; /* pointer into rom_db */ } rom_entry_t; static rom_entry_t roms[MAX_ROMS]; static int rom_count = 0; /* ------------------------------------------------------------------ */ /* Utility: lowercase the last N chars of a string in-place */ /* ------------------------------------------------------------------ */ static void str_lower_tail(char *s, int n) { int len = (int)strlen(s); int start = len - n; if (start < 0) start = 0; for (int i = start; s[i]; i++) s[i] = (char)tolower((unsigned char)s[i]); } /* ------------------------------------------------------------------ */ /* Utility: extract filename from full path */ /* ------------------------------------------------------------------ */ static void extract_filename(const char *path, char *out, int outlen) { const char *slash = strrchr(path, '/'); if (slash) slash++; else slash = path; /* strip the extension for display */ const char *dot = strrchr(slash, '.'); int namelen = dot ? (int)(dot - slash) : (int)strlen(slash); if (namelen > outlen - 1) namelen = outlen - 1; memcpy(out, slash, (size_t)namelen); out[namelen] = '\0'; } /* ------------------------------------------------------------------ */ /* Find the rom_type_t that matches a filename extension */ /* ------------------------------------------------------------------ */ static const rom_type_t *match_rom_type(const char *filename) { char lower[PATH_BUF]; snprintf(lower, sizeof(lower), "%s", filename); str_lower_tail(lower, 6); /* extensions are max 4 chars */ const char *dot = strrchr(lower, '.'); if (!dot) return NULL; for (int i = 0; i < MAX_ROM_TYPES && rom_db[i].ext; i++) { if (strcmp(dot, rom_db[i].ext) == 0) return &rom_db[i]; } return NULL; } /* ------------------------------------------------------------------ */ /* Scan ROM_DIR recursively and populate roms[] */ /* ------------------------------------------------------------------ */ static void scan_roms_recursive(const char *basepath) { DIR *dir = opendir(basepath); if (!dir) return; struct dirent *de; while ((de = readdir(dir)) != NULL) { if (de->d_name[0] == '.') continue; /* skip hidden / . / .. */ char fullpath[PATH_BUF]; snprintf(fullpath, sizeof(fullpath), "%s/%s", basepath, de->d_name); struct stat st; if (lstat(fullpath, &st) != 0) continue; if (S_ISDIR(st.st_mode)) { /* recurse into subdirectories (system-named folders) */ scan_roms_recursive(fullpath); continue; } if (!S_ISREG(st.st_mode)) continue; const rom_type_t *t = match_rom_type(de->d_name); if (!t) continue; /* unknown extension, skip */ if (rom_count >= MAX_ROMS) break; rom_entry_t *r = &roms[rom_count]; snprintf(r->path, PATH_BUF, "%s", fullpath); extract_filename(de->d_name, r->name, (int)sizeof(r->name)); r->type = t; rom_count++; } closedir(dir); } /* ------------------------------------------------------------------ */ /* System filter state */ /* ------------------------------------------------------------------ */ static const char *system_filters[] = { "ALL", "NES", "FDS", "SNES", "GameBoy", "GBC", "GBA", "Genesis", "SMS", "GameGear", "PCE", "Atari2600", "NeoGeoP", NULL /* sentinel */ }; #define NUM_FILTERS (sizeof(system_filters) / sizeof(system_filters[0]) - 1) static int current_filter = 0; /* index into system_filters, 0 = ALL */ /* ------------------------------------------------------------------ */ /* LVGL UI objects (module-global for event loop access) */ /* ------------------------------------------------------------------ */ static lv_obj_t *title_label = NULL; static lv_obj_t *filter_label = NULL; static lv_obj_t *count_label = NULL; static lv_obj_t *rom_list = NULL; static lv_obj_t *list_btns[MAX_ROMS]; static int visible_count = 0; static int scroll_index = 0; /* ------------------------------------------------------------------ */ /* Build the ROM list UI (called on filter change) */ /* ------------------------------------------------------------------ */ static void build_rom_list(void) { /* clear existing list buttons */ if (rom_list) { lv_obj_clean(rom_list); } visible_count = 0; scroll_index = 0; const char *filter = system_filters[current_filter]; int is_all = (current_filter == 0); for (int i = 0; i < rom_count && visible_count < MAX_ROMS; i++) { if (!is_all && strcmp(roms[i].type->system, filter) != 0) continue; /* build display text: "[SYSTEM] name" */ char display[80]; snprintf(display, sizeof(display), "%s %s", roms[i].type->system, roms[i].name); list_btns[visible_count] = lv_list_add_btn(rom_list, LV_SYMBOL_PLAY, display); lv_obj_set_style_text_color(list_btns[visible_count], COLOR_TEXT, LV_PART_MAIN); lv_obj_set_style_text_font(list_btns[visible_count], &lv_font_montserrat_12, LV_PART_MAIN); visible_count++; } /* update count label */ char count_buf[48]; snprintf(count_buf, sizeof(count_buf), "%d / %d ROMs", visible_count, rom_count); lv_label_set_text(count_label, count_buf); } /* ------------------------------------------------------------------ */ /* Scroll the list */ /* ------------------------------------------------------------------ */ static void scroll_rom_list(int delta) { scroll_index += delta; if (scroll_index < 0) scroll_index = 0; if (scroll_index >= visible_count) scroll_index = visible_count - 1; if (visible_count > 0 && scroll_index < visible_count) { lv_obj_scroll_to_view(list_btns[scroll_index], LV_ANIM_ON); } } /* ------------------------------------------------------------------ */ /* Cycle system filter (rapid-rotate = cycle filter) */ /* ------------------------------------------------------------------ */ static void cycle_filter(void) { current_filter++; if (system_filters[current_filter] == NULL) current_filter = 0; char fbuf[48]; snprintf(fbuf, sizeof(fbuf), "Filter: %s", system_filters[current_filter]); lv_label_set_text(filter_label, fbuf); build_rom_list(); LOG_INF("filter changed to: %s", system_filters[current_filter]); } /* ------------------------------------------------------------------ */ /* Launch the selected ROM */ /* ------------------------------------------------------------------ */ static void launch_rom(int index) { if (index < 0 || index >= visible_count) return; /* find the actual rom_entry_t (skip filtered-out entries) */ const char *filter = system_filters[current_filter]; int is_all = (current_filter == 0); int real_index = -1; int target = 0; for (int i = 0; i < rom_count; i++) { if (!is_all && strcmp(roms[i].type->system, filter) != 0) continue; if (target == index) { real_index = i; break; } target++; } if (real_index < 0) return; const rom_entry_t *rom = &roms[real_index]; const rom_type_t *type = rom->type; LOG_INF("launching ROM: %s [%s]", rom->name, type->system); /* clear screen before launch */ lv_obj_clean(lv_scr_act()); lv_obj_t *launch_lbl = lv_label_create(lv_scr_act()); char launch_buf[80]; snprintf(launch_buf, sizeof(launch_buf), "Launching %s...\n%s\n[BACK] to exit", type->system, rom->name); lv_label_set_text(launch_lbl, launch_buf); lv_obj_set_style_text_color(launch_lbl, COLOR_ACCENT, LV_PART_MAIN); lv_obj_align(launch_lbl, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(launch_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); /* flush the "Launching..." screen */ for (int i = 0; i < 10; i++) lv_timer_handler(); pid_t pid = fork(); if (pid == 0) { /* * Child: exec the emulator with the ROM path. * If the emulator has a core flag (e.g. mednafen -ss), pass it. */ if (type->core) { char *args[] = { (char *)type->emulator, (char *)type->core, (char *)rom->path, NULL }; execv(type->emulator, args); } else { char *args[] = { (char *)type->emulator, (char *)rom->path, NULL }; execv(type->emulator, args); } /* exec failed -- emulator binary not found */ _exit(127); } if (pid < 0) { LOG_ERR("fork failed for emulator launch"); return; /* fork failed */ } LOG_INF("emulator child pid %d started", (int)pid); /* * Parent: non-blocking wait in event loop. * BACK key sends SIGTERM to the emulator child. * Rebuilds the ROM list when child exits. */ int input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK); struct input_event ev; int child_running = 1; while (child_running) { lv_timer_handler(); /* non-blocking child check */ int status; pid_t w = waitpid(pid, &status, WNOHANG); if (w == pid) { child_running = 0; LOG_INF("emulator child %d exited", (int)pid); break; } /* read input */ if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_KEY && ev.value == 1) { if (ev.code == H2_KEY_BACK) { kill(pid, SIGTERM); /* give emulator 300ms to clean up */ usleep(300000); waitpid(pid, &status, WNOHANG); child_running = 0; LOG_INF("emulator child %d terminated by user", (int)pid); } } } usleep(15000); } if (input_fd >= 0) close(input_fd); /* rebuild the UI for the ROM picker */ lv_obj_clean(lv_scr_act()); } /* ------------------------------------------------------------------ */ /* main -- module entry point */ /* ------------------------------------------------------------------ */ int main(void) { init_h2_graphics_runtime("RETRO GAME LAUNCHER"); LOG_INF("retro module started"); lv_obj_t *scr = lv_scr_act(); /* ---- header ---- */ title_label = lv_label_create(scr); lv_label_set_text(title_label, "RETRO GAME LAUNCHER"); lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 8); lv_obj_set_style_text_color(title_label, COLOR_PRIMARY, LV_PART_MAIN); /* ---- filter indicator (PLAY = launch, rapid-rotate = filter) ---- */ filter_label = lv_label_create(scr); lv_label_set_text(filter_label, "Filter: ALL"); lv_obj_align(filter_label, LV_ALIGN_TOP_LEFT, 8, 30); lv_obj_set_style_text_color(filter_label, COLOR_ACCENT, LV_PART_MAIN); lv_obj_set_style_text_font(filter_label, &lv_font_montserrat_10, LV_PART_MAIN); /* ---- ROM count ---- */ count_label = lv_label_create(scr); lv_obj_align(count_label, LV_ALIGN_TOP_RIGHT, -8, 30); lv_obj_set_style_text_color(count_label, lv_color_make(120, 120, 140), LV_PART_MAIN); lv_obj_set_style_text_font(count_label, &lv_font_montserrat_10, LV_PART_MAIN); /* ---- ROM list ---- */ rom_list = lv_list_create(scr); lv_obj_set_size(rom_list, 296, 168); lv_obj_align(rom_list, LV_ALIGN_TOP_MID, 0, 46); lv_obj_set_style_bg_color(rom_list, lv_color_make(18, 22, 32), LV_PART_MAIN); lv_obj_set_style_border_width(rom_list, 1, LV_PART_MAIN); lv_obj_set_style_border_color(rom_list, lv_color_make(50, 55, 70), LV_PART_MAIN); /* ---- status bar ---- */ lv_obj_t *status = lv_label_create(scr); lv_label_set_text(status, "ROTATE=scroll PLAY=launch BACK=exit"); lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, -4); lv_obj_set_style_text_color(status, lv_color_make(80, 85, 100), LV_PART_MAIN); lv_obj_set_style_text_font(status, &lv_font_montserrat_10, LV_PART_MAIN); /* ---- scan ROMs ---- */ scan_roms_recursive(ROM_DIR); LOG_INF("ROM scan complete: %d ROM(s) found", rom_count); build_rom_list(); if (rom_count == 0) { lv_obj_t *empty = lv_label_create(rom_list); lv_label_set_text(empty, LV_SYMBOL_WARNING " No ROMs found.\n\n" "Place ROM files in:\n" " /data/roms/\n\n" "Organize by folder:\n" " /data/roms/nes/\n" " /data/roms/snes/\n" " /data/roms/gb/\n" " ...etc"); lv_obj_set_style_text_color(empty, lv_color_make(160, 100, 60), LV_PART_MAIN); lv_obj_set_style_text_align(empty, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); } /* ---- input loop ---- */ int input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK); struct input_event ev; /* rotary accumulator for filter cycling: * 8 fast clicks in < 200ms each = cycle filter * (avoids needing a separate button for filter) */ int rotary_clicks = 0; long long last_click_ms = 0; while (1) { lv_timer_handler(); if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_REL && ev.code == 0) { if (ev.value != 0) { /* track rapid scrolling for filter toggle */ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); long long now = (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; if (now - last_click_ms < 200) { rotary_clicks++; if (rotary_clicks >= 8) { cycle_filter(); rotary_clicks = 0; } } else { rotary_clicks = 1; } last_click_ms = now; scroll_rom_list(ev.value > 0 ? 1 : -1); } } if (ev.type == EV_KEY && ev.value == 1) { if (ev.code == H2_KEY_PLAY && visible_count > 0) { /* PLAY: launch selected ROM */ launch_rom(scroll_index); /* after launch returns, rebuild UI */ lv_obj_clean(lv_scr_act()); /* re-create widgets */ title_label = lv_label_create(scr); lv_label_set_text(title_label, "RETRO GAME LAUNCHER"); lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 8); lv_obj_set_style_text_color(title_label, COLOR_PRIMARY, LV_PART_MAIN); filter_label = lv_label_create(scr); char fbuf[48]; snprintf(fbuf, sizeof(fbuf), "Filter: %s", system_filters[current_filter]); lv_label_set_text(filter_label, fbuf); lv_obj_align(filter_label, LV_ALIGN_TOP_LEFT, 8, 30); lv_obj_set_style_text_color(filter_label, COLOR_ACCENT, LV_PART_MAIN); lv_obj_set_style_text_font(filter_label, &lv_font_montserrat_10, LV_PART_MAIN); count_label = lv_label_create(scr); lv_obj_align(count_label, LV_ALIGN_TOP_RIGHT, -8, 30); lv_obj_set_style_text_color(count_label, lv_color_make(120, 120, 140), LV_PART_MAIN); lv_obj_set_style_text_font(count_label, &lv_font_montserrat_10, LV_PART_MAIN); rom_list = lv_list_create(scr); lv_obj_set_size(rom_list, 296, 168); lv_obj_align(rom_list, LV_ALIGN_TOP_MID, 0, 46); lv_obj_set_style_bg_color(rom_list, lv_color_make(18, 22, 32), LV_PART_MAIN); lv_obj_set_style_border_width(rom_list, 1, LV_PART_MAIN); lv_obj_set_style_border_color(rom_list, lv_color_make(50, 55, 70), LV_PART_MAIN); status = lv_label_create(scr); lv_label_set_text(status, "ROTATE=scroll PLAY=launch BACK=exit"); lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, -4); lv_obj_set_style_text_color(status, lv_color_make(80, 85, 100), LV_PART_MAIN); lv_obj_set_style_text_font(status, &lv_font_montserrat_10, LV_PART_MAIN); build_rom_list(); } if (ev.code == H2_KEY_BACK) { /* BACK: exit module */ break; } } } usleep(15000); } if (input_fd >= 0) close(input_fd); LOG_INF("retro module exited"); return 0; }