From f6f8fa6a8fdfe04862ae05dc2ee3731a23740d55 Mon Sep 17 00:00:00 2001 From: Shaun Inman Date: Tue, 3 Jan 2023 22:17:49 -0500 Subject: [PATCH] messy experimentation commit --- .gitignore | 4 + src/common/api.c | 415 +++++++++ src/common/api.h | 85 ++ src/common/defines.h | 39 +- src/common/utils.c | 136 +++ src/common/utils.h | 22 + src/minarch/main.c | 504 ++--------- src/minarch/makefile | 2 +- src/minui/main.c | 1569 +++++++++++++++++++++++++++++++++++ src/minui/makefile | 15 + src/test/Test.pak/launch.sh | 5 + src/test/main.c | 87 ++ src/test/makefile | 15 + 13 files changed, 2472 insertions(+), 426 deletions(-) create mode 100644 .gitignore create mode 100644 src/common/api.c create mode 100644 src/common/api.h create mode 100644 src/common/utils.c create mode 100644 src/common/utils.h create mode 100644 src/minui/main.c create mode 100644 src/minui/makefile create mode 100755 src/test/Test.pak/launch.sh create mode 100644 src/test/main.c create mode 100644 src/test/makefile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9418b46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*.o +*.so +*.elf diff --git a/src/common/api.c b/src/common/api.c new file mode 100644 index 0000000..1dd6731 --- /dev/null +++ b/src/common/api.c @@ -0,0 +1,415 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "api.h" +#include "defines.h" + +/////////////////////////////// + +// TODO: tmp +void powerOff(void) { + system("echo u > /proc/sysrq-trigger"); + system("echo s > /proc/sysrq-trigger"); + system("echo o > /proc/sysrq-trigger"); +} +void fauxSleep(void) { } +int preventAutosleep(void) { return 0; } +int isCharging() { return 0; } + +/////////////////////////////// + +void LOG_note(int level, const char* fmt, ...) { + char buf[1024] = {0}; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + switch(level) { +#ifdef DEBUG + case LOG_DEBUG: + printf("DEBUG: %s", buf); + break; +#endif + case LOG_INFO: + printf("INFO: %s", buf); + break; + case LOG_WARN: + fprintf(stderr, "WARN: %s", buf); + break; + case LOG_ERROR: + fprintf(stderr, "ERROR: %s", buf); + break; + default: + break; + } + fflush(stdout); +} + +/////////////////////////////// + +static struct GFX_Context { + int fb; + int pitch; + int buffer; + int buffer_size; + int map_size; + void* map; + struct fb_var_screeninfo vinfo; + struct fb_fix_screeninfo finfo; + + SDL_Surface* screen; +} gfx; +SDL_Surface* GFX_init(void) { + SDL_Init(SDL_INIT_VIDEO); + SDL_ShowCursor(0); + TTF_Init(); + + // char namebuf[MAX_PATH]; + // if (SDL_VideoDriverName(namebuf, MAX_PATH)) { + // printf("SDL_VideoDriverName: %s\n", namebuf); + // } + + // SDL_Rect **modes = SDL_ListModes(NULL, SDL_HWSURFACE); + // if(modes == (SDL_Rect **)0){ + // puts("No modes available!"); + // exit(-1); + // } + // + // if(modes == (SDL_Rect **)-1){ + // puts("All resolutions available."); + // } + // else{ + // puts("Available Modes"); + // for(int i=0; modes[i]; ++i) { + // printf("\t%d x %d\n", modes[i]->w, modes[i]->h); + // } + // } + + // we're drawing to the (triple-buffered) framebuffer directly + // but we still need to set video mode to initialize input events + SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, SDL_HWSURFACE); + + // open framebuffer + gfx.fb = open("/dev/fb0", O_RDWR); + + // configure framebuffer + ioctl(gfx.fb, FBIOGET_VSCREENINFO, &gfx.vinfo); + gfx.vinfo.bits_per_pixel = SCREEN_DEPTH; + gfx.vinfo.xres = SCREEN_WIDTH; + gfx.vinfo.yres = SCREEN_HEIGHT; + gfx.vinfo.xres_virtual = SCREEN_WIDTH; + gfx.vinfo.yres_virtual = SCREEN_HEIGHT * SCREEN_BUFFER_COUNT; + gfx.vinfo.xoffset = 0; + gfx.vinfo.activate = FB_ACTIVATE_VBL; + + // gfx.vinfo.pixclock = 0xb7bb;// 0xc350; // 0xc350=56 0xb7e0=59.94 0xb7bb=60 + // gfx.vinfo.left_margin = 0x10; + // gfx.vinfo.right_margin = 0x14; + // gfx.vinfo.upper_margin = 0x0f; + // gfx.vinfo.lower_margin = 0x05; + // gfx.vinfo.hsync_len = 0x1e; + // gfx.vinfo.vsync_len = 0x02; + + ioctl(gfx.fb, FBIOPUT_VSCREENINFO, &gfx.vinfo); + + // printf("pixclock: 0x%04x\n", gfx.vinfo.pixclock); + // printf("left_margin: 0x%02x\n", gfx.vinfo.left_margin); + // printf("right_margin: 0x%02x\n", gfx.vinfo.right_margin); + // printf("upper_margin: 0x%02x\n", gfx.vinfo.upper_margin); + // printf("lower_margin: 0x%02x\n", gfx.vinfo.lower_margin); + // printf("hsync_len: 0x%02x\n", gfx.vinfo.hsync_len); + // printf("vsync_len: 0x%02x\n", gfx.vinfo.vsync_len); + + // get fixed screen info + ioctl(gfx.fb, FBIOGET_FSCREENINFO, &gfx.finfo); + gfx.map_size = gfx.finfo.smem_len; + gfx.map = mmap(0, gfx.map_size, PROT_READ | PROT_WRITE, MAP_SHARED, gfx.fb, 0); + + // struct fb_vblank vblank; + // ioctl(gfx.fb, FBIOGET_VBLANK, &vblank); + // printf("flags: %i\n", vblank.flags); + // printf("count: %i\n", vblank.count); + // printf("vcount: %i\n", vblank.vcount); + // printf("hcount: %i\n", vblank.hcount); + + // buffer tracking + gfx.buffer = 0; + gfx.buffer_size = SCREEN_PITCH * SCREEN_HEIGHT; + + // return screen + gfx.screen = SDL_CreateRGBSurfaceFrom(gfx.map, SCREEN_WIDTH,SCREEN_HEIGHT, SCREEN_DEPTH,SCREEN_PITCH, 0,0,0,0); + return gfx.screen; +} +void GFX_clear(SDL_Surface* screen) { + memset(screen->pixels, 0, gfx.buffer_size); +} +void GFX_clearAll(void) { + memset(gfx.map, 0, gfx.map_size); +} + +// #define OWL_IOW(num, dtype) _IOW('O', num, dtype) +// #define OWLFB_WAITFORVSYNC OWL_IOW(57,long long) + +void GFX_flip(SDL_Surface* screen) { + // struct fb_vblank vblank; + // ioctl(gfx.fb, FBIOGET_VBLANK, &vblank); + // printf("flags: %i\n", vblank.flags); + // printf("count: %i\n", vblank.count); + // printf("vcount: %i\n", vblank.vcount); + // printf("hcount: %i\n", vblank.hcount); + + // TODO: this would be moved to a thread + // I'm not clear on why that would be necessary + // if it's non-blocking and the pan will wait + // until the next vblank... + // what if the scaling was also moved to a thread? + gfx.vinfo.yoffset = gfx.buffer * SCREEN_HEIGHT; + int arg = 0; + // ioctl(gfx.fb, OWLFB_WAITFORVSYNC, &arg); // TODO: this doesn't wait but it also doesn't error out like FBIO_WAITFORVSYNC... + ioctl(gfx.fb, FBIOPAN_DISPLAY, &gfx.vinfo); + + gfx.buffer += 1; + if (gfx.buffer>=SCREEN_BUFFER_COUNT) gfx.buffer -= SCREEN_BUFFER_COUNT; + screen->pixels = gfx.map + (gfx.buffer * gfx.buffer_size); +} +void GFX_quit(void) { + GFX_clearAll(); + munmap(gfx.map, gfx.map_size); + close(gfx.fb); + SDL_Quit(); +} + +/////////////////////////////// + +// based on picoarch's audio +// implementation, rewritten +// to understand it better + +#define MAX_SAMPLE_RATE 48000 +#define BATCH_SIZE 100 + +typedef int (*SND_Resampler)(const SND_Frame frame); +static struct SND_Context { + double frame_rate; + + int sample_rate_in; + int sample_rate_out; + + int buffer_seconds; // current_audio_buffer_size + SND_Frame* buffer; // buf + size_t frame_count; // buf_len + + int frame_in; // buf_w + int frame_out; // buf_r + int frame_filled; // max_buf_w + + SND_Resampler resample; +} snd; +static void SND_audioCallback(void* userdata, uint8_t* stream, int len) { // plat_sound_callback + if (snd.frame_count==0) return; + + int16_t *out = (int16_t *)stream; + len /= (sizeof(int16_t) * 2); + + while (snd.frame_out!=snd.frame_in && len>0) { + *out++ = snd.buffer[snd.frame_out].left; + *out++ = snd.buffer[snd.frame_out].right; + + snd.frame_filled = snd.frame_out; + + snd.frame_out += 1; + len -= 1; + + if (snd.frame_out>=snd.frame_count) snd.frame_out = 0; + } + + while (len>0) { + *out++ = 0; + *out++ = 0; + len -= 1; + } +} +static void SND_resizeBuffer(void) { // plat_sound_resize_buffer + snd.frame_count = snd.buffer_seconds * snd.sample_rate_in / snd.frame_rate; + if (snd.frame_count==0) return; + + SDL_LockAudio(); + + int buffer_bytes = snd.frame_count * sizeof(SND_Frame); + snd.buffer = realloc(snd.buffer, buffer_bytes); + + memset(snd.buffer, 0, buffer_bytes); + + snd.frame_in = 0; + snd.frame_out = 0; + snd.frame_filled = snd.frame_count - 1; + + SDL_UnlockAudio(); +} +static int SND_resampleNone(SND_Frame frame) { // audio_resample_passthrough + snd.buffer[snd.frame_in++] = frame; + if (snd.frame_in >= snd.frame_count) snd.frame_in = 0; + return 1; +} +static int SND_resampleNear(SND_Frame frame) { // audio_resample_nearest + static int diff = 0; + int consumed = 0; + + if (diff < snd.sample_rate_out) { + snd.buffer[snd.frame_in++] = frame; + if (snd.frame_in >= snd.frame_count) snd.frame_in = 0; + diff += snd.sample_rate_in; + } + + if (diff >= snd.sample_rate_out) { + consumed++; + diff -= snd.sample_rate_out; + } + + return consumed; +} +static void SND_selectResampler(void) { // plat_sound_select_resampler + if (snd.sample_rate_in==snd.sample_rate_out) { + snd.resample = SND_resampleNone; + } + else { + snd.resample = SND_resampleNear; + } +} +size_t SND_batchSamples(const SND_Frame* frames, size_t frame_count) { // plat_sound_write / plat_sound_write_resample + if (snd.frame_count==0) return 0; + + SDL_LockAudio(); + + int consumed = 0; + while (frame_count > 0) { + int tries = 0; + int amount = MIN(BATCH_SIZE, frame_count); + + while (tries < 10 && snd.frame_in==snd.frame_filled) { + tries++; + SDL_UnlockAudio(); + SDL_Delay(1); + SDL_LockAudio(); + } + + while (amount && snd.frame_in != snd.frame_filled) { + consumed = snd.resample(*frames); + frames += consumed; + amount -= consumed; + frame_count -= consumed; + } + } + SDL_UnlockAudio(); + + return consumed; +} + +void SND_init(double sample_rate, double frame_rate) { // plat_sound_init + SDL_InitSubSystem(SDL_INIT_AUDIO); + + snd.frame_rate = frame_rate; + + SDL_AudioSpec spec_in; + SDL_AudioSpec spec_out; + + spec_in.freq = MIN(sample_rate, MAX_SAMPLE_RATE); // TODO: always MAX_SAMPLE_RATE on Miyoo Mini? use #ifdef PLATFORM_MIYOOMINI? + spec_in.format = AUDIO_S16; + spec_in.channels = 2; + spec_in.samples = 512; + spec_in.callback = SND_audioCallback; + + SDL_OpenAudio(&spec_in, &spec_out); + + snd.buffer_seconds = 5; + snd.sample_rate_in = sample_rate; + snd.sample_rate_out = spec_out.freq; + + SND_selectResampler(); + SND_resizeBuffer(); + + SDL_PauseAudio(0); +} +void SND_quit(void) { // plat_sound_finish + SDL_PauseAudio(1); + SDL_CloseAudio(); + + if (snd.buffer) { + free(snd.buffer); + snd.buffer = NULL; + } +} + +/////////////////////////////// + +static struct PAD_Context { + int is_pressed; + int just_pressed; + int just_released; +} pad; +void PAD_reset(void) { + pad.just_pressed = BTN_NONE; + pad.is_pressed = BTN_NONE; + pad.just_released = BTN_NONE; +} +void PAD_poll(void) { + // reset transient state + pad.just_pressed = BTN_NONE; + pad.just_released = BTN_NONE; + + // the actual poll + SDL_Event event; + while (SDL_PollEvent(&event)) { + int btn = BTN_NONE; + if (event.type==SDL_KEYDOWN || event.type==SDL_KEYUP) { + uint8_t code = event.key.keysym.scancode; + if (code==CODE_UP) btn = BTN_UP; + else if (code==CODE_DOWN) btn = BTN_DOWN; + else if (code==CODE_LEFT) btn = BTN_LEFT; + else if (code==CODE_RIGHT) btn = BTN_RIGHT; + else if (code==CODE_A) btn = BTN_A; + else if (code==CODE_B) btn = BTN_B; + else if (code==CODE_X) btn = BTN_X; + else if (code==CODE_Y) btn = BTN_Y; + else if (code==CODE_START) btn = BTN_START; + else if (code==CODE_SELECT) btn = BTN_SELECT; + else if (code==CODE_MENU) btn = BTN_MENU; + else if (code==CODE_L1) btn = BTN_L1; + else if (code==CODE_L2) btn = BTN_L2; + else if (code==CODE_R1) btn = BTN_R1; + else if (code==CODE_R2) btn = BTN_R2; + else if (code==CODE_VOL_UP) btn = BTN_VOL_UP; + else if (code==CODE_VOL_DN) btn = BTN_VOL_DN; + else if (code==CODE_POWER) btn = BTN_POWER; + } + + if (btn==BTN_NONE) continue; + + if (event.type==SDL_KEYUP) { + pad.is_pressed &= ~btn; // unset + pad.just_released |= btn; // set + } + else if ((pad.is_pressed & btn)==BTN_NONE) { + pad.just_pressed |= btn; // set + pad.is_pressed |= btn; // set + } + } +} + +// TODO: switch to macros? not if I want to move it to a separate file +int PAD_anyPressed(void) { return pad.is_pressed!=BTN_NONE; } +int PAD_justPressed(int btn) { return pad.just_pressed & btn; } +int PAD_isPressed(int btn) { return pad.is_pressed & btn; } +int PAD_justReleased(int btn) { return pad.just_released & btn; } \ No newline at end of file diff --git a/src/common/api.h b/src/common/api.h new file mode 100644 index 0000000..7f9b575 --- /dev/null +++ b/src/common/api.h @@ -0,0 +1,85 @@ +#ifndef __API_H__ +#define __API_H__ +#include + +/////////////////////////////// + +// TODO: tmp +void powerOff(void); +void fauxSleep(void); +int preventAutosleep(void); +int isCharging(); +#define PAD_justRepeated PAD_justPressed + +/////////////////////////////// + +enum { + LOG_DEBUG = 0, + LOG_INFO, + LOG_WARN, + LOG_ERROR, +}; + +#define LOG_debug(fmt, ...) LOG_note(LOG_DEBUG, fmt, ##__VA_ARGS__) +#define LOG_info(fmt, ...) LOG_note(LOG_INFO, fmt, ##__VA_ARGS__) +#define LOG_warn(fmt, ...) LOG_note(LOG_WARN, fmt, ##__VA_ARGS__) +#define LOG_error(fmt, ...) LOG_note(LOG_ERROR, fmt, ##__VA_ARGS__) +void LOG_note(int level, const char* fmt, ...); + +/////////////////////////////// + +SDL_Surface* GFX_init(void); +void GFX_clear(SDL_Surface* screen); +void GFX_clearAll(void); +void GFX_flip(SDL_Surface* screen); +void GFX_quit(void); + +/////////////////////////////// + +typedef struct SND_Frame { + int16_t left; + int16_t right; +} SND_Frame; + +void SND_init(double sample_rate, double frame_rate); +size_t SND_batchSamples(const SND_Frame* frames, size_t frame_count); +void SND_quit(void); + +/////////////////////////////// + +enum { + BTN_NONE = 0, + BTN_UP = 1 << 0, + BTN_DOWN = 1 << 1, + BTN_LEFT = 1 << 2, + BTN_RIGHT = 1 << 3, + BTN_A = 1 << 4, + BTN_B = 1 << 5, + BTN_X = 1 << 6, + BTN_Y = 1 << 7, + BTN_START = 1 << 8, + BTN_SELECT = 1 << 9, + BTN_L1 = 1 << 10, + BTN_R1 = 1 << 11, + BTN_L2 = 1 << 12, + BTN_R2 = 1 << 13, + BTN_MENU = 1 << 14, + BTN_VOL_UP = 1 << 15, + BTN_VOL_DN = 1 << 16, + BTN_POWER = 1 << 17, +}; + +// TODO: this belongs in defines.h or better yet a platform.h +#define BTN_RESUME BTN_X +#define BTN_SLEEP BTN_POWER + +void PAD_reset(void); +void PAD_poll(void); +int PAD_anyPressed(void); +int PAD_justPressed(int btn); +int PAD_isPressed(int btn); +int PAD_justReleased(int btn); + +/////////////////////////////// + +#endif diff --git a/src/common/defines.h b/src/common/defines.h index ab77f97..3d7919c 100644 --- a/src/common/defines.h +++ b/src/common/defines.h @@ -1,5 +1,5 @@ -#ifndef __DEFS_H__ -#define __DEFS_H__ +#ifndef __DEFINES_H__ +#define __DEFINES_H__ #define CODE_UP 0x5A #define CODE_DOWN 0x5B @@ -25,18 +25,43 @@ #define BRIGHTNESS_MIN 0 #define BRIGHTNESS_MAX 10 -#define SDCARD_PATH "/mnt/sdcard" -#define SYSTEM_PATH SDCARD_PATH "/.system/" PLATFORM -#define USERDATA_PATH SDCARD_PATH "/.userdata/" PLATFORM #define MAX_PATH 512 +#define SDCARD_PATH "/mnt/sdcard" +#define ROMS_PATH SDCARD_PATH "/Roms" +#define SYSTEM_PATH SDCARD_PATH "/.system/" PLATFORM +#define RES_PATH SDCARD_PATH "/.system/res" +#define FONT_PATH RES_PATH "/BPreplayBold-unhinted.otf" +#define USERDATA_PATH SDCARD_PATH "/.userdata/" PLATFORM +#define PAKS_PATH SYSTEM_PATH "/paks" +#define RECENT_PATH USERDATA_PATH "/.minui/recent.txt" +#define FAUX_RECENT_PATH SDCARD_PATH "/Recently Played" +#define COLLECTIONS_PATH SDCARD_PATH "/Collections" + +#define LAST_PATH "/tmp/last.txt" // transient +#define CHANGE_DISC_PATH "/tmp/change_disc.txt" +#define RESUME_SLOT_PATH "/tmp/mmenu_slot.txt" +#define AUTO_RESUME_PATH USERDATA_PATH "/.miniui/auto_resume.txt" +#define AUTO_RESUME_SLOT "9" +#define ENABLE_SIMPLE_MODE_PATH USERDATA_PATH "/enable-simple-mode" + #define SCREEN_WIDTH 640 #define SCREEN_HEIGHT 480 + +// GBA +// #define SCREEN_WIDTH 960 +// #define SCREEN_HEIGHT 720 + +// GB converted to 4:3 with full height +// #define SCREEN_WIDTH 768 +// #define SCREEN_HEIGHT 576 + #define SCREEN_DEPTH 16 -#define SCREEN_PITCH 1280 #define SCREEN_BPP 2 +#define SCREEN_PITCH SCREEN_WIDTH * SCREEN_BPP #define SCREEN_BUFFER_COUNT 3 +#define MAIN_ROW_COUNT 7 /////////////////////////////// @@ -46,4 +71,4 @@ #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) -#endif // __DEFS_H__ \ No newline at end of file +#endif \ No newline at end of file diff --git a/src/common/utils.c b/src/common/utils.c new file mode 100644 index 0000000..0a2de9f --- /dev/null +++ b/src/common/utils.c @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "defines.h" + +/////////////////////////////////////// + +int prefixMatch(char* pre, char* str) { + return (strncasecmp(pre,str,strlen(pre))==0); +} +int suffixMatch(char* suf, char* str) { + int len = strlen(suf); + int offset = strlen(str)-len; + return (offset>=0 && strncasecmp(suf, str+offset, len)==0); +} +int exactMatch(char* str1, char* str2) { + int len1 = strlen(str1); + if (len1!=strlen(str2)) return 0; + return (strncmp(str1,str2,len1)==0); +} +int hide(char* file_name) { + return file_name[0]=='.'; +} + +void getDisplayName(const char* in_name, char* out_name) { + char* tmp; + strcpy(out_name, in_name); + + // extract just the filename if necessary + tmp = strrchr(in_name, '/'); + if (tmp) strcpy(out_name, tmp+1); + + // remove extension + tmp = strrchr(out_name, '.'); + if (tmp && strlen(tmp)<=4) tmp[0] = '\0'; // 3 letter extension plus dot + + // remove trailing parens (round and square) + char safe_name[256]; + strcpy(safe_name,out_name); + while ((tmp=strrchr(out_name, '('))!=NULL || (tmp=strrchr(out_name, '['))!=NULL) { + if (tmp==out_name) break; + tmp[0] = '\0'; + tmp = out_name; + } + + // make sure we haven't nuked the entire name + if (out_name[0]=='\0') strcpy(out_name, safe_name); + + // remove trailing whitespace + tmp = out_name + strlen(out_name) - 1; + while(tmp>out_name && isspace((unsigned char)*tmp)) tmp--; + tmp[1] = '\0'; +} +void getEmuName(const char* in_name, char* out_name) { // NOTE: both char arrays need to be MAX_PATH length! + char* tmp; + strcpy(out_name, in_name); + tmp = out_name; + + // extract just the Roms folder name if necessary + if (prefixMatch(ROMS_PATH, tmp)) { + tmp += strlen(ROMS_PATH) + 1; + char* tmp2 = strchr(tmp, '/'); + if (tmp2) tmp2[0] = '\0'; + } + + // finally extract pak name from parenths if present + tmp = strrchr(tmp, '('); + if (tmp) { + tmp += 1; + strcpy(out_name, tmp); + tmp = strchr(out_name,')'); + tmp[0] = '\0'; + } +} + +void normalizeNewline(char* line) { + int len = strlen(line); + if (len>1 && line[len-1]=='\n' && line[len-2]=='\r') { // windows! + line[len-2] = '\n'; + line[len-1] = '\0'; + } +} +void trimTrailingNewlines(char* line) { + int len = strlen(line); + while (len>0 && line[len-1]=='\n') { + line[len-1] = '\0'; // trim newline + len -= 1; + } +} + +/////////////////////////////////////// + +int exists(char* path) { + return access(path, F_OK)==0; +} +void touch(char* path) { + close(open(path, O_RDWR|O_CREAT, 0777)); +} +void putFile(char* path, char* contents) { + FILE* file = fopen(path, "w"); + if (file) { + fputs(contents, file); + fclose(file); + } +} +void getFile(char* path, char* buffer, size_t buffer_size) { + FILE *file = fopen(path, "r"); + if (file) { + fseek(file, 0L, SEEK_END); + size_t size = ftell(file); + if (size>buffer_size-1) size = buffer_size - 1; + rewind(file); + fread(buffer, sizeof(char), size, file); + fclose(file); + buffer[size] = '\0'; + } +} +int getInt(char* path) { + int i = 0; + FILE *file = fopen(path, "r"); + if (file!=NULL) { + fscanf(file, "%i", &i); + fclose(file); + } + return i; +} +void putInt(char* path, int value) { + char buffer[8]; + sprintf(buffer, "%d", value); + putFile(path, buffer); +} diff --git a/src/common/utils.h b/src/common/utils.h new file mode 100644 index 0000000..e3962e1 --- /dev/null +++ b/src/common/utils.h @@ -0,0 +1,22 @@ +#ifndef UTILS_H +#define UTILS_H + +int prefixMatch(char* pre, char* str); +int suffixMatch(char* suf, char* str); +int exactMatch(char* str1, char* str2); +int hide(char* file_name); + +void getDisplayName(const char* in_name, char* out_name); +void getEmuName(const char* in_name, char* out_name); + +void normalizeNewline(char* line); +void trimTrailingNewlines(char* line); + +int exists(char* path); +void touch(char* path); +void putFile(char* path, char* contents); +void getFile(char* path, char* buffer, size_t buffer_size); +void putInt(char* path, int value); +int getInt(char* path); + +#endif diff --git a/src/minarch/main.c b/src/minarch/main.c index 4f6c8d3..8139c1e 100644 --- a/src/minarch/main.c +++ b/src/minarch/main.c @@ -1,386 +1,25 @@ #include #include #include -#include -#include -#include -#include -#include -#include #include #include #include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include "libretro.h" #include "defines.h" +#include "utils.h" +#include "api.h" #include "scaler_neon.h" -/////////////////////////////// - -enum { - LOG_DEBUG = 0, - LOG_INFO, - LOG_WARN, - LOG_ERROR, -}; -#define LOG_debug(fmt, ...) LOG_note(LOG_DEBUG, fmt, ##__VA_ARGS__) -#define LOG_info(fmt, ...) LOG_note(LOG_INFO, fmt, ##__VA_ARGS__) -#define LOG_warn(fmt, ...) LOG_note(LOG_WARN, fmt, ##__VA_ARGS__) -#define LOG_error(fmt, ...) LOG_note(LOG_ERROR, fmt, ##__VA_ARGS__) -void LOG_note(int level, const char* fmt, ...) { - char buf[1024] = {0}; - va_list args; - va_start(args, fmt); - vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - - switch(level) { -#ifdef DEBUG - case LOG_DEBUG: - printf("DEBUG: %s", buf); - break; -#endif - case LOG_INFO: - printf("INFO: %s", buf); - break; - case LOG_WARN: - fprintf(stderr, "WARN: %s", buf); - break; - case LOG_ERROR: - fprintf(stderr, "ERROR: %s", buf); - break; - default: - break; - } - fflush(stdout); -} - -/////////////////////////////// - -static struct GFX_Context { - int fb; - int pitch; - int buffer; - int buffer_size; - int map_size; - void* map; - struct fb_var_screeninfo vinfo; - struct fb_fix_screeninfo finfo; - - SDL_Surface* screen; -} gfx; -SDL_Surface* GFX_init(void) { - SDL_Init(SDL_INIT_VIDEO); - - // we're drawing to the (triple-buffered) framebuffer directly - // but we still need to set video mode to initialize input events - SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, SDL_SWSURFACE); - SDL_ShowCursor(0); - - // open framebuffer - gfx.fb = open("/dev/fb0", O_RDWR); - - // configure framebuffer - ioctl(gfx.fb, FBIOGET_VSCREENINFO, &gfx.vinfo); - gfx.vinfo.bits_per_pixel = SCREEN_DEPTH; - gfx.vinfo.xres = SCREEN_WIDTH; - gfx.vinfo.yres = SCREEN_HEIGHT; - gfx.vinfo.xres_virtual = SCREEN_WIDTH; - gfx.vinfo.yres_virtual = SCREEN_HEIGHT * SCREEN_BUFFER_COUNT; - gfx.vinfo.xoffset = 0; - gfx.vinfo.activate = FB_ACTIVATE_VBL; - ioctl(gfx.fb, FBIOPUT_VSCREENINFO, &gfx.vinfo); - - // get fixed screen info - ioctl(gfx.fb, FBIOGET_FSCREENINFO, &gfx.finfo); - gfx.map_size = gfx.finfo.smem_len; - gfx.map = mmap(0, gfx.map_size, PROT_READ | PROT_WRITE, MAP_SHARED, gfx.fb, 0); - - // buffer tracking - gfx.buffer = 0; - gfx.buffer_size = SCREEN_PITCH * SCREEN_HEIGHT; - - // return screen - gfx.screen = SDL_CreateRGBSurfaceFrom(gfx.map, SCREEN_WIDTH,SCREEN_HEIGHT, SCREEN_DEPTH,SCREEN_PITCH, 0,0,0,0); - return gfx.screen; -} -void GFX_clear(SDL_Surface* screen) { - memset(screen->pixels, 0, gfx.buffer_size); -} -void GFX_clearAll(void) { - memset(gfx.map, 0, gfx.map_size); -} -void GFX_flip(SDL_Surface* screen) { - // TODO: this would be moved to a thread - // I'm not clear on why that would be necessary - // if it's non-blocking and the pan will wait - // until the next vblank... - gfx.vinfo.yoffset = gfx.buffer * SCREEN_HEIGHT; - ioctl(gfx.fb, FBIOPAN_DISPLAY, &gfx.vinfo); - - gfx.buffer += 1; - if (gfx.buffer>=SCREEN_BUFFER_COUNT) gfx.buffer -= SCREEN_BUFFER_COUNT; - screen->pixels = gfx.map + (gfx.buffer * gfx.buffer_size); -} -void GFX_quit(void) { - GFX_clearAll(); - munmap(gfx.map, gfx.map_size); - close(gfx.fb); - SDL_Quit(); -} - -/////////////////////////////// - -// based on picoarch's audio -// implementation, rewritten -// to understand it better - -#define MAX_SAMPLE_RATE 48000 -#define BATCH_SIZE 100 - -typedef struct SND_Frame { - int16_t left; - int16_t right; -} SND_Frame; -typedef int (*SND_Resampler)(const SND_Frame frame); -static struct SND_Context { - double frame_rate; - - int sample_rate_in; - int sample_rate_out; - - int buffer_seconds; // current_audio_buffer_size - SND_Frame* buffer; // buf - size_t frame_count; // buf_len - - int frame_in; // buf_w - int frame_out; // buf_r - int frame_filled; // max_buf_w - - SND_Resampler resample; -} snd; -void SND_audioCallback(void* userdata, uint8_t* stream, int len) { // plat_sound_callback - if (snd.frame_count==0) return; - - int16_t *out = (int16_t *)stream; - len /= (sizeof(int16_t) * 2); - - while (snd.frame_out!=snd.frame_in && len>0) { - *out++ = snd.buffer[snd.frame_out].left; - *out++ = snd.buffer[snd.frame_out].right; - - snd.frame_filled = snd.frame_out; - - snd.frame_out += 1; - len -= 1; - - if (snd.frame_out>=snd.frame_count) snd.frame_out = 0; - } - - while (len>0) { - *out++ = 0; - *out++ = 0; - len -= 1; - } -} -void SND_resizeBuffer(void) { // plat_sound_resize_buffer - snd.frame_count = snd.buffer_seconds * snd.sample_rate_in / snd.frame_rate; - if (snd.frame_count==0) return; - - SDL_LockAudio(); - - int buffer_bytes = snd.frame_count * sizeof(SND_Frame); - snd.buffer = realloc(snd.buffer, buffer_bytes); - - memset(snd.buffer, 0, buffer_bytes); - - snd.frame_in = 0; - snd.frame_out = 0; - snd.frame_filled = snd.frame_count - 1; - - SDL_UnlockAudio(); -} -int SND_resampleNone(SND_Frame frame) { // audio_resample_passthrough - snd.buffer[snd.frame_in++] = frame; - if (snd.frame_in >= snd.frame_count) snd.frame_in = 0; - return 1; -} -int SND_resampleNear(SND_Frame frame) { // audio_resample_nearest - static int diff = 0; - int consumed = 0; - - if (diff < snd.sample_rate_out) { - snd.buffer[snd.frame_in++] = frame; - if (snd.frame_in >= snd.frame_count) snd.frame_in = 0; - diff += snd.sample_rate_in; - } - - if (diff >= snd.sample_rate_out) { - consumed++; - diff -= snd.sample_rate_out; - } - - return consumed; -} -void SND_selectResampler(void) { // plat_sound_select_resampler - if (snd.sample_rate_in==snd.sample_rate_out) { - snd.resample = SND_resampleNone; - } - else { - snd.resample = SND_resampleNear; - } -} -size_t SND_batchSamples(const SND_Frame* frames, size_t frame_count) { // plat_sound_write / plat_sound_write_resample - if (snd.frame_count==0) return 0; - - SDL_LockAudio(); - - int consumed = 0; - while (frame_count > 0) { - int tries = 0; - int amount = MIN(BATCH_SIZE, frame_count); - - while (tries < 10 && snd.frame_in==snd.frame_filled) { - tries++; - SDL_UnlockAudio(); - SDL_Delay(1); - SDL_LockAudio(); - } - - while (amount && snd.frame_in != snd.frame_filled) { - consumed = snd.resample(*frames); - frames += consumed; - amount -= consumed; - frame_count -= consumed; - } - } - SDL_UnlockAudio(); - - return consumed; -} - -void SND_init(double sample_rate, double frame_rate) { // plat_sound_init - SDL_InitSubSystem(SDL_INIT_AUDIO); - - snd.frame_rate = frame_rate; - - SDL_AudioSpec spec_in; - SDL_AudioSpec spec_out; - - spec_in.freq = MIN(sample_rate, MAX_SAMPLE_RATE); // TODO: always MAX_SAMPLE_RATE on Miyoo Mini? use #ifdef PLATFORM_MIYOOMINI? - spec_in.format = AUDIO_S16; - spec_in.channels = 2; - spec_in.samples = 512; - spec_in.callback = SND_audioCallback; - - SDL_OpenAudio(&spec_in, &spec_out); - - snd.buffer_seconds = 5; - snd.sample_rate_in = sample_rate; - snd.sample_rate_out = spec_out.freq; - - SND_selectResampler(); - SND_resizeBuffer(); - - SDL_PauseAudio(0); -} -void SND_quit(void) { // plat_sound_finish - SDL_PauseAudio(1); - SDL_CloseAudio(); - - if (snd.buffer) { - free(snd.buffer); - snd.buffer = NULL; - } -} - -/////////////////////////////// - -enum { - BTN_NONE = 0, - BTN_UP = 1 << 0, - BTN_DOWN = 1 << 1, - BTN_LEFT = 1 << 2, - BTN_RIGHT = 1 << 3, - BTN_A = 1 << 4, - BTN_B = 1 << 5, - BTN_X = 1 << 6, - BTN_Y = 1 << 7, - BTN_START = 1 << 8, - BTN_SELECT = 1 << 9, - BTN_L1 = 1 << 10, - BTN_R1 = 1 << 11, - BTN_L2 = 1 << 12, - BTN_R2 = 1 << 13, - BTN_MENU = 1 << 14, - BTN_VOL_UP = 1 << 15, - BTN_VOL_DN = 1 << 16, - BTN_POWER = 1 << 17, -}; -static struct PAD_Context { - int is_pressed; - int just_pressed; - int just_released; -} pad; -void PAD_poll(void) { - // reset transient state - pad.just_pressed = 0; - pad.just_released = 0; - - // the actual poll - SDL_Event event; - while (SDL_PollEvent(&event)) { - int btn = BTN_NONE; - if (event.type==SDL_KEYDOWN || event.type==SDL_KEYUP) { - uint8_t code = event.key.keysym.scancode; - if (code==CODE_UP) btn = BTN_UP; - else if (code==CODE_DOWN) btn = BTN_DOWN; - else if (code==CODE_LEFT) btn = BTN_LEFT; - else if (code==CODE_RIGHT) btn = BTN_RIGHT; - else if (code==CODE_A) btn = BTN_A; - else if (code==CODE_B) btn = BTN_B; - else if (code==CODE_X) btn = BTN_X; - else if (code==CODE_Y) btn = BTN_Y; - else if (code==CODE_START) btn = BTN_START; - else if (code==CODE_SELECT) btn = BTN_SELECT; - else if (code==CODE_MENU) btn = BTN_MENU; - else if (code==CODE_L1) btn = BTN_L1; - else if (code==CODE_L2) btn = BTN_L2; - else if (code==CODE_R1) btn = BTN_R1; - else if (code==CODE_R2) btn = BTN_R2; - else if (code==CODE_VOL_UP) btn = BTN_VOL_UP; - else if (code==CODE_VOL_DN) btn = BTN_VOL_DN; - else if (code==CODE_POWER) btn = BTN_POWER; - } - - if (btn==BTN_NONE) continue; - - if (event.type==SDL_KEYUP) { - pad.is_pressed &= ~btn; // unset - pad.just_released |= btn; // set - } - else if ((pad.is_pressed & btn)==BTN_NONE) { - pad.just_pressed |= btn; // set - pad.is_pressed |= btn; // set - } - } -} - -// TODO: switch to macros? not if I want to move it to a separate file -int PAD_anyPressed(void) { return pad.is_pressed!=BTN_NONE; } -int PAD_justPressed(int btn) { return pad.just_pressed & btn; } -int PAD_isPressed(int btn) { return pad.is_pressed & btn; } -int PAD_justReleased(int btn) { return pad.just_released & btn; } - -// #define PAD_anyPressed() (pad.is_pressed!=BTN_NONE) -// #define PAD_justPressed(btn) (pad.just_pressed & (btn)) -// #define PAD_isPressed(btn) (pad.is_pressed & (btn)) -// #define PAD_justReleased(btn) (pad.just_released & (btn)) +static SDL_Surface* screen; /////////////////////////////////////// @@ -460,6 +99,7 @@ static void SRAM_read(void) { char filename[MAX_PATH]; SRAM_getPath(filename); + printf("sav path (read): %s\n", filename); FILE *sram_file = fopen(filename, "r"); if (!sram_file) return; @@ -478,6 +118,7 @@ static void SRAM_write(void) { char filename[MAX_PATH]; SRAM_getPath(filename); + printf("sav path (write): %s\n", filename); FILE *sram_file = fopen(filename, "w"); if (!sram_file) { @@ -615,6 +256,7 @@ static bool environment_callback(unsigned cmd, void *data) { // copied from pico // TODO: core.tag isn't available at this point // TODO: it only becomes available after we open the game... sprintf(sys_dir, SDCARD_PATH "/.userdata/%s/%s-%s", PLATFORM, core.tag, core.name); + puts(sys_dir); fflush(stdout); *out = sys_dir; break; } @@ -752,9 +394,13 @@ static bool environment_callback(unsigned cmd, void *data) { // copied from pico for (int i=0; vars[i].key; i++) { const struct retro_core_option_definition *var = &vars[i]; // printf("set key: %s to value: %s (%s)\n", var->key, var->default_value, var->desc); - printf("set core (intl) key: %s to value: %s\n", var->key, var->default_value); + char *default_value = (char*)var->default_value; + if (!strcmp("gpsp_save_method", var->key)) { + default_value = "libretro"; // TODO: tmp, patch or override gpsp + } + printf("set core (intl) key: %s to value: %s\n", var->key, default_value); strcpy(tmp_options[i].key, var->key); - strcpy(tmp_options[i].value, var->default_value); + strcpy(tmp_options[i].value, default_value); } } break; @@ -1019,6 +665,9 @@ static void scale(const void* src, int width, int height, int pitch, void* dst) dst += (oy * SCREEN_PITCH) + (ox * SCREEN_BPP); + // TODO: trying to identify source of the framepacing issue + // scale1x(width,height,pitch,src,dst); + switch (scale) { case 4: scale4x_n16((void*)src,dst,width,height,pitch,SCREEN_PITCH); break; case 3: scale3x_n16((void*)src,dst,width,height,pitch,SCREEN_PITCH); break; @@ -1032,6 +681,52 @@ static void scale(const void* src, int width, int height, int pitch, void* dst) // case 2: scale2x(width,height,pitch,src,dst); break; // default: scale1x(width,height,pitch,src,dst); break; } + + // TODO: diagnosing framepacing issues + if (1) { + static int frame = 0; + int w = 8; + int h = 16; + int fps = 60; + int x = frame * w; + + dst -= (oy * SCREEN_PITCH) + (ox * SCREEN_BPP); + + dst += (SCREEN_WIDTH - (w * fps)) / 2 * SCREEN_BPP; + + void* _dst = dst; + memset(_dst, 0, (h * SCREEN_PITCH)); + for (int y=0; y=fps) frame -= fps; + } + + if (0) { + // measure framerate + static int start = -1; + static int ticks = 0; + ticks += 1; + int now = SDL_GetTicks(); + if (start==-1) start = now; + if (now-start>=1000) { + start = now; + printf("fps: %i\n", ticks); + fflush(stdout); + ticks = 0; + } + } } static void video_refresh_callback(const void *data, unsigned width, unsigned height, size_t pitch) { @@ -1043,8 +738,8 @@ static void video_refresh_callback(const void *data, unsigned width, unsigned he last_height = height; GFX_clearAll(); } - scale(data,width,height,pitch,gfx.screen->pixels); - GFX_flip(gfx.screen); + scale(data,width,height,pitch,screen->pixels); + // GFX_flip(screen); } static void audio_sample_callback(int16_t left, int16_t right) { @@ -1198,53 +893,19 @@ void Core_close(void) { } int main(int argc , char* argv[]) { - // system("touch /tmp/wait"); - - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/gambatte_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Game Boy Color (GBC)/Legend of Zelda, The - Link's Awakening DX (USA, Europe) (Rev 2) (SGB Enhanced) (GB Compatible).gbc"; - // char* rom_path = "/mnt/sdcard/Roms/Game Boy Color (GBC)/Dragon Warrior I & II (USA) (SGB Enhanced).gbc"; - // char* tag_name = "GBC"; - // char* rom_path = "/mnt/sdcard/Roms/Game Boy (GB)/Super Mario Land (World) (Rev A).gb"; - // char* rom_path = "/mnt/sdcard/Roms/Game Boy (GB)/Dr. Mario (World).gb"; - // char* tag_name = "GB"; + char core_path[MAX_PATH]; + char rom_path[MAX_PATH]; + char tag_name[MAX_PATH]; - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/gpsp_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Game Boy Advance (GBA)/Metroid Zero Mission.gba"; - // char* tag_name = "GBA"; - - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/fceumm_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Nintendo (FC)/Castlevania 3 - Dracula's Curse (U).nes"; - // char* rom_path = "/mnt/sdcard/Roms/Nintendo (FC)/Mega Man 2 (U).nes"; - // char* tag_name = "FC"; - - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/picodrive_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Genesis (MD)/Sonic The Hedgehog (USA, Europe).md"; - // char* tag_name = "MD"; - - char* core_path = "/mnt/sdcard/.system/rg35xx/cores/snes9x2005_plus_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Super Nintendo (SFC)/Super Mario World (USA).sfc"; - // char* rom_path = "/mnt/sdcard/Roms/Super Nintendo (SFC)/Super Mario World 2 - Yoshi's Island (USA, Asia) (Rev 1).sfc"; - char* rom_path = "/mnt/sdcard/Roms/Super Nintendo (SFC)/Final Fantasy III (USA) (Rev 1).sfc"; - char* tag_name = "SFC"; - - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/pcsx_rearmed_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/PlayStation (PS)/Castlevania - Symphony of the Night (USA)/Castlevania - Symphony of the Night (USA).cue"; - // char* rom_path = "/mnt/sdcard/Roms/PlayStation (PS)/Final Fantasy VII (USA)/Final Fantasy VII (USA).m3u"; - // char* tag_name = "PS"; - - // char* core_path = "/mnt/sdcard/.system/rg35xx/cores/pokemini_libretro.so"; - // char* rom_path = "/mnt/sdcard/Roms/Pokémon mini (PKM)/Pokemon Tetris (Europe) (En,Ja,Fr).min"; - // char* tag_name = "PKM"; - - // char core_path[MAX_PATH]; strcpy(core_path, argv[1]); - // char rom_path[MAX_PATH]; strcpy(rom_path, argv[2]); - // char tag_name[MAX_PATH]; strcpy(tag_name, argv[3]); + strcpy(core_path, argv[1]); + strcpy(rom_path, argv[2]); + getEmuName(rom_path, tag_name); LOG_info("core_path: %s\n", core_path); LOG_info("rom_path: %s\n", rom_path); LOG_info("tag_name: %s\n", tag_name); - SDL_Surface* screen = GFX_init(); + screen = GFX_init(); Core_open(core_path, tag_name); LOG_info("after Core_open\n"); Core_init(); LOG_info("after Core_init\n"); Game_open(rom_path); LOG_info("after Game_open\n"); @@ -1252,16 +913,23 @@ int main(int argc , char* argv[]) { SND_init(core.sample_rate, core.fps); LOG_info("after SND_init\n"); State_read(); LOG_info("after State_read\n"); - while (1) { + int start = SDL_GetTicks(); + while (1) { + unsigned long frame_start = SDL_GetTicks(); + if (PAD_justPressed(BTN_POWER)) { - system("rm /tmp/minui_exec"); + // system("rm /tmp/minui_exec"); break; } - // still not working // if (PAD_justPressed(BTN_L1)) State_read(); // else if (PAD_justPressed(BTN_R1)) State_write(); core.run(); + + unsigned long frame_duration = SDL_GetTicks() - frame_start; + #define TARGET_FRAME_DURATION 15 + if (frame_duration +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defines.h" +#include "utils.h" +#include "api.h" + +/////////////////////////////////////// + +#define dump(msg) puts((msg));fflush(stdout); + +/////////////////////////////////////// + +typedef struct Array { + int count; + int capacity; + void** items; +} Array; + +static Array* Array_new(void) { + Array* self = malloc(sizeof(Array)); + self->count = 0; + self->capacity = 8; + self->items = malloc(sizeof(void*) * self->capacity); + return self; +} +static void Array_push(Array* self, void* item) { + if (self->count>=self->capacity) { + self->capacity *= 2; + self->items = realloc(self->items, sizeof(void*) * self->capacity); + } + self->items[self->count++] = item; +} +static void Array_unshift(Array* self, void* item) { + if (self->count==0) return Array_push(self, item); + Array_push(self, NULL); // ensures we have enough capacity + for (int i=self->count-2; i>=0; i--) { + self->items[i+1] = self->items[i]; + } + self->items[0] = item; +} +static void* Array_pop(Array* self) { + if (self->count==0) return NULL; + return self->items[--self->count]; +} +static void Array_reverse(Array* self) { + int end = self->count-1; + int mid = self->count/2; + for (int i=0; iitems[i]; + self->items[i] = self->items[end-i]; + self->items[end-i] = item; + } +} +static void Array_free(Array* self) { + free(self->items); + free(self); +} + +static int StringArray_indexOf(Array* self, char* str) { + for (int i=0; icount; i++) { + if (exactMatch(self->items[i], str)) return i; + } + return -1; +} +static void StringArray_free(Array* self) { + for (int i=0; icount; i++) { + free(self->items[i]); + } + Array_free(self); +} + +/////////////////////////////////////// + +typedef struct Hash { + Array* keys; + Array* values; +} Hash; // not really a hash + +static Hash* Hash_new(void) { + Hash* self = malloc(sizeof(Hash)); + self->keys = Array_new(); + self->values = Array_new(); + return self; +} +static void Hash_free(Hash* self) { + StringArray_free(self->keys); + StringArray_free(self->values); + free(self); +} +static void Hash_set(Hash* self, char* key, char* value) { + Array_push(self->keys, strdup(key)); + Array_push(self->values, strdup(value)); +} +static char* Hash_get(Hash* self, char* key) { + int i = StringArray_indexOf(self->keys, key); + if (i==-1) return NULL; + return self->values->items[i]; +} + +/////////////////////////////////////// + +enum EntryType { + ENTRY_DIR, + ENTRY_PAK, + ENTRY_ROM, +}; +typedef struct Entry { + char* path; + char* name; + char* unique; + int type; + int alpha; // index in parent Directory's alphas Array, which points to the index of an Entry in its entries Array :sweat_smile: + + int has_alt; + int use_alt; +} Entry; + +static Entry* Entry_new(char* path, int type) { + char display_name[256]; + getDisplayName(path, display_name); + Entry* self = malloc(sizeof(Entry)); + self->path = strdup(path); + self->name = strdup(display_name); + self->unique = NULL; + self->type = type; + self->alpha = 0; + self->has_alt = type!=ENTRY_ROM?0:-1; + self->use_alt = type!=ENTRY_ROM?0:-1; + return self; +} +static void Entry_free(Entry* self) { + free(self->path); + free(self->name); + if (self->unique) free(self->unique); + free(self); +} + +static int EntryArray_indexOf(Array* self, char* path) { + for (int i=0; icount; i++) { + Entry* entry = self->items[i]; + if (exactMatch(entry->path, path)) return i; + } + return -1; +} +static int EntryArray_sortEntry(const void* a, const void* b) { + Entry* item1 = *(Entry**)a; + Entry* item2 = *(Entry**)b; + return strcasecmp(item1->name, item2->name); +} +static void EntryArray_sort(Array* self) { + qsort(self->items, self->count, sizeof(void*), EntryArray_sortEntry); +} + +static void EntryArray_free(Array* self) { + for (int i=0; icount; i++) { + Entry_free(self->items[i]); + } + Array_free(self); +} + +/////////////////////////////////////// + +#define INT_ARRAY_MAX 27 +typedef struct IntArray { + int count; + int items[INT_ARRAY_MAX]; +} IntArray; +static IntArray* IntArray_new(void) { + IntArray* self = malloc(sizeof(IntArray)); + self->count = 0; + memset(self->items, 0, sizeof(int) * INT_ARRAY_MAX); + return self; +} +static void IntArray_push(IntArray* self, int i) { + self->items[self->count++] = i; +} +static void IntArray_free(IntArray* self) { + free(self); +} + +/////////////////////////////////////// + +typedef struct Directory { + char* path; + char* name; + Array* entries; + IntArray* alphas; + // rendering + int selected; + int start; + int end; +} Directory; + +static int getIndexChar(char* str) { + char i = 0; + char c = tolower(str[0]); + if (c>='a' && c<='z') i = (c-'a')+1; + return i; +} + +static void getUniqueName(Entry* entry, char* out_name) { + char* filename = strrchr(entry->path, '/')+1; + char emu_tag[256]; + getEmuName(entry->path, emu_tag); + + char *tmp; + strcpy(out_name, entry->name); + tmp = out_name + strlen(out_name); + strcpy(tmp, " ("); + tmp = out_name + strlen(out_name); + strcpy(tmp, emu_tag); + tmp = out_name + strlen(out_name); + strcpy(tmp, ")"); +} + +static void Directory_index(Directory* self) { + int skip_index = exactMatch(FAUX_RECENT_PATH, self->path) || prefixMatch(COLLECTIONS_PATH, self->path); // not alphabetized + + Entry* prior = NULL; + int alpha = -1; + int index = 0; + for (int i=0; ientries->count; i++) { + Entry* entry = self->entries->items[i]; + if (prior!=NULL && exactMatch(prior->name, entry->name)) { + if (prior->unique) free(prior->unique); + if (entry->unique) free(entry->unique); + + char* prior_filename = strrchr(prior->path, '/')+1; + char* entry_filename = strrchr(entry->path, '/')+1; + if (exactMatch(prior_filename, entry_filename)) { + char prior_unique[256]; + char entry_unique[256]; + getUniqueName(prior, prior_unique); + getUniqueName(entry, entry_unique); + + prior->unique = strdup(prior_unique); + entry->unique = strdup(entry_unique); + } + else { + prior->unique = strdup(prior_filename); + entry->unique = strdup(entry_filename); + } + } + + if (!skip_index) { + int a = getIndexChar(entry->name); + if (a!=alpha) { + index = self->alphas->count; + IntArray_push(self->alphas, i); + alpha = a; + } + entry->alpha = index; + } + + prior = entry; + } +} + +static Array* getRoot(void); +static Array* getRecents(void); +static Array* getCollection(char* path); +static Array* getDiscs(char* path); +static Array* getEntries(char* path); + +static Directory* Directory_new(char* path, int selected) { + char display_name[256]; + getDisplayName(path, display_name); + + Directory* self = malloc(sizeof(Directory)); + self->path = strdup(path); + self->name = strdup(display_name); + if (exactMatch(path, SDCARD_PATH)) { + self->entries = getRoot(); + } + else if (exactMatch(path, FAUX_RECENT_PATH)) { + self->entries = getRecents(); + } + else if (!exactMatch(path, COLLECTIONS_PATH) && prefixMatch(COLLECTIONS_PATH, path)) { + self->entries = getCollection(path); + } + else if (suffixMatch(".m3u", path)) { + self->entries = getDiscs(path); + } + else { + self->entries = getEntries(path); + } + self->alphas = IntArray_new(); + self->selected = selected; + Directory_index(self); + return self; +} +static void Directory_free(Directory* self) { + free(self->path); + free(self->name); + EntryArray_free(self->entries); + IntArray_free(self->alphas); + free(self); +} + +static void DirectoryArray_pop(Array* self) { + Directory_free(Array_pop(self)); +} +static void DirectoryArray_free(Array* self) { + for (int i=0; icount; i++) { + Directory_free(self->items[i]); + } + Array_free(self); +} + +/////////////////////////////////////// + +typedef struct Recent { + char* path; // NOTE: this is without the SDCARD_PATH prefix! + int available; +} Recent; +static int hasEmu(char* emu_name); +static Recent* Recent_new(char* path) { + Recent* self = malloc(sizeof(Recent)); + + char sd_path[256]; // only need to get emu name + sprintf(sd_path, "%s%s", SDCARD_PATH, path); + + char emu_name[256]; + getEmuName(sd_path, emu_name); + + self->path = strdup(path); + self->available = hasEmu(emu_name); + return self; +} +static void Recent_free(Recent* self) { + free(self->path); + free(self); +} + +static int RecentArray_indexOf(Array* self, char* str) { + for (int i=0; icount; i++) { + Recent* item = self->items[i]; + if (exactMatch(item->path, str)) return i; + } + return -1; +} +static void RecentArray_free(Array* self) { + for (int i=0; icount; i++) { + Recent_free(self->items[i]); + } + Array_free(self); +} + +/////////////////////////////////////// + +static Directory* top; +static Array* stack; // DirectoryArray +static Array* recents; // RecentArray + +static int is_simple = 0; +static int quit = 0; +static int can_resume = 0; +static int should_resume = 0; // set to 1 on BTN_RESUME but only if can_resume==1 +static char slot_path[256]; + +static int restore_depth = -1; +static int restore_relative = -1; +static int restore_selected = 0; +static int restore_start = 0; +static int restore_end = 0; + +/////////////////////////////////////// + +#define MAX_RECENTS 24 // a multiple of all menu rows +static void saveRecents(void) { + FILE* file = fopen(RECENT_PATH, "w"); + if (file) { + for (int i=0; icount; i++) { + Recent* recent = recents->items[i]; + fputs(recent->path, file); + putc('\n', file); + } + fclose(file); + } +} +static void addRecent(char* path) { + path += strlen(SDCARD_PATH); // makes paths platform agnostic + int id = RecentArray_indexOf(recents, path); + if (id==-1) { // add + while (recents->count>=MAX_RECENTS) { + Recent_free(Array_pop(recents)); + } + Array_unshift(recents, Recent_new(path)); + } + else if (id>0) { // bump to top + for (int i=id; i>0; i--) { + void* tmp = recents->items[i-1]; + recents->items[i-1] = recents->items[i]; + recents->items[i] = tmp; + } + } + saveRecents(); +} + +static int hasEmu(char* emu_name) { + char pak_path[256]; + sprintf(pak_path, "%s/Emus/%s.pak/launch.sh", PAKS_PATH, emu_name); + if (exists(pak_path)) return 1; + + sprintf(pak_path, "%s/Emus/%s.pak/launch.sh", SDCARD_PATH, emu_name); + return exists(pak_path); +} +static void getEmuPath(char* emu_name, char* pak_path) { + sprintf(pak_path, "%s/Emus/%s.pak/launch.sh", SDCARD_PATH, emu_name); + if (exists(pak_path)) return; + sprintf(pak_path, "%s/Emus/%s.pak/launch.sh", PAKS_PATH, emu_name); +} +static int hasAlt(char* emu_name) { + char pak_path[256]; + sprintf(pak_path, "%s/Emus/%s.pak/has-alt", PAKS_PATH, emu_name); + return exists(pak_path); +} +static int hasCue(char* dir_path, char* cue_path) { // NOTE: dir_path not rom_path + char* tmp = strrchr(dir_path, '/') + 1; // folder name + sprintf(cue_path, "%s/%s.cue", dir_path, tmp); + return exists(cue_path); +} +static int hasM3u(char* rom_path, char* m3u_path) { // NOTE: rom_path not dir_path + char* tmp; + + strcpy(m3u_path, rom_path); + tmp = strrchr(m3u_path, '/') + 1; + tmp[0] = '\0'; + + // path to parent directory + char base_path[256]; + strcpy(base_path, m3u_path); + + tmp = strrchr(m3u_path, '/'); + tmp[0] = '\0'; + + // get parent directory name + char dir_name[256]; + tmp = strrchr(m3u_path, '/'); + strcpy(dir_name, tmp); + + // dir_name is also our m3u file name + tmp = m3u_path + strlen(m3u_path); + strcpy(tmp, dir_name); + + // add extension + tmp = m3u_path + strlen(m3u_path); + strcpy(tmp, ".m3u"); + + return exists(m3u_path); +} + +static int Entry_hasAlt(Entry* self) { + // has_alt can be set by getEntries() + // but won't be set by getRecents() + // otherwise delayed until selected + if (self->has_alt==-1) { + // check + char emu_name[256]; + getEmuName(self->path, emu_name); + self->has_alt = hasAlt(emu_name); + } + return self->has_alt; +} +static int Entry_useAlt(Entry* self) { + // has to be checked on an individual basis + // but delayed until selected + + if (self->use_alt==-1) { + // check + char emu_name[256]; + getEmuName(self->path, emu_name); + + char rom_name[256]; + char* tmp = strrchr(self->path, '/'); + if (tmp) strcpy(rom_name, tmp+1); + + char use_alt[256]; + sprintf(use_alt, "%s/.mmenu/%s/%s.use-alt", USERDATA_PATH, emu_name, rom_name); + + self->use_alt = exists(use_alt); + } + return self->use_alt; +} +static int Entry_toggleAlt(Entry* self) { + if (!Entry_hasAlt(self)) return 0; + + self->use_alt = !Entry_useAlt(self); + + char emu_name[256]; + getEmuName(self->path, emu_name); + + char rom_name[256]; + char* tmp = strrchr(self->path, '/'); + if (tmp) strcpy(rom_name, tmp+1); + + char use_alt_path[256]; + sprintf(use_alt_path, "%s/.mmenu/%s/%s.use-alt", USERDATA_PATH, emu_name, rom_name); + + if (self->use_alt==1) touch(use_alt_path); + else unlink(use_alt_path); + + return 1; +} + +static int hasRecents(void) { + int has = 0; + + Array* parent_paths = Array_new(); + if (exists(CHANGE_DISC_PATH)) { + char sd_path[256]; + getFile(CHANGE_DISC_PATH, sd_path, 256); + if (exists(sd_path)) { + char* disc_path = sd_path + strlen(SDCARD_PATH); // makes path platform agnostic + Recent* recent = Recent_new(disc_path); + if (recent->available) has += 1; + Array_push(recents, recent); + + char parent_path[256]; + strcpy(parent_path, disc_path); + char* tmp = strrchr(parent_path, '/') + 1; + tmp[0] = '\0'; + Array_push(parent_paths, strdup(parent_path)); + } + unlink(CHANGE_DISC_PATH); + } + + FILE* file = fopen(RECENT_PATH, "r"); // newest at top + if (file) { + char line[256]; + while (fgets(line,256,file)!=NULL) { + normalizeNewline(line); + trimTrailingNewlines(line); + if (strlen(line)==0) continue; // skip empty lines + + char sd_path[256]; + sprintf(sd_path, "%s%s", SDCARD_PATH, line); + if (exists(sd_path)) { + if (recents->countcount; i++) { + char* path = parent_paths->items[i]; + if (prefixMatch(path, parent_path)) { + found = 1; + break; + } + } + if (found) continue; + + Array_push(parent_paths, strdup(parent_path)); + } + Recent* recent = Recent_new(line); + if (recent->available) has += 1; + Array_push(recents, recent); + } + } + } + fclose(file); + } + + saveRecents(); + + StringArray_free(parent_paths); + return has>0; +} +static int hasCollections(void) { + int has = 0; + if (!exists(COLLECTIONS_PATH)) return has; + + DIR *dh = opendir(COLLECTIONS_PATH); + struct dirent *dp; + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + has = 1; + break; + } + closedir(dh); + return has; +} +static int hasRoms(char* dir_name) { + int has = 0; + char emu_name[256]; + char rom_path[256]; + + getEmuName(dir_name, emu_name); + + // check for emu pak + if (!hasEmu(emu_name)) return has; + + // check for at least one non-hidden file (we're going to assume it's a rom) + sprintf(rom_path, "%s/%s/", ROMS_PATH, dir_name); + DIR *dh = opendir(rom_path); + if (dh!=NULL) { + struct dirent *dp; + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + has = 1; + break; + } + closedir(dh); + } + // if (!has) printf("No roms for %s!\n", dir_name); + return has; +} +static Array* getRoot(void) { + Array* root = Array_new(); + + if (hasRecents()) Array_push(root, Entry_new(FAUX_RECENT_PATH, ENTRY_DIR)); + + DIR *dh; + + Array* entries = Array_new(); + dh = opendir(ROMS_PATH); + if (dh!=NULL) { + struct dirent *dp; + char* tmp; + char full_path[256]; + sprintf(full_path, "%s/", ROMS_PATH); + tmp = full_path + strlen(full_path); + Array* emus = Array_new(); + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + if (hasRoms(dp->d_name)) { + strcpy(tmp, dp->d_name); + Array_push(emus, Entry_new(full_path, ENTRY_DIR)); + } + } + EntryArray_sort(emus); + Entry* prev_entry = NULL; + for (int i=0; icount; i++) { + Entry* entry = emus->items[i]; + if (prev_entry!=NULL) { + if (exactMatch(prev_entry->name, entry->name)) { + Entry_free(entry); + continue; + } + } + Array_push(entries, entry); + prev_entry = entry; + } + Array_free(emus); // just free the array part, entries now owns emus entries + closedir(dh); + } + + if (hasCollections()) { + if (entries->count) Array_push(root, Entry_new(COLLECTIONS_PATH, ENTRY_DIR)); + else { // no visible systems, promote collections to root + dh = opendir(COLLECTIONS_PATH); + if (dh!=NULL) { + struct dirent *dp; + char* tmp; + char full_path[256]; + sprintf(full_path, "%s/", COLLECTIONS_PATH); + tmp = full_path + strlen(full_path); + Array* collections = Array_new(); + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + strcpy(tmp, dp->d_name); + Array_push(collections, Entry_new(full_path, ENTRY_DIR)); // yes, collections are fake directories + } + EntryArray_sort(collections); + for (int i=0; icount; i++) { + Array_push(entries, collections->items[i]); + } + Array_free(collections); // just free the array part, entries now owns collections entries + closedir(dh); + } + } + } + + // add systems to root + for (int i=0; icount; i++) { + Array_push(root, entries->items[i]); + } + Array_free(entries); // root now owns entries' entries + + char tools_path[256]; + sprintf(tools_path, "%s/Tools", SDCARD_PATH); + if (!is_simple && exists(tools_path)) Array_push(root, Entry_new(tools_path, ENTRY_DIR)); + + return root; +} +static Array* getRecents(void) { + Array* entries = Array_new(); + for (int i=0; icount; i++) { + Recent* recent = recents->items[i]; + if (!recent->available) continue; + + char sd_path[256]; + sprintf(sd_path, "%s%s", SDCARD_PATH, recent->path); + int type = suffixMatch(".pak", sd_path) ? ENTRY_PAK : ENTRY_ROM; // ??? + Array_push(entries, Entry_new(sd_path, type)); + } + return entries; +} +static Array* getCollection(char* path) { + Array* entries = Array_new(); + FILE* file = fopen(path, "r"); + if (file) { + char line[256]; + while (fgets(line,256,file)!=NULL) { + normalizeNewline(line); + trimTrailingNewlines(line); + if (strlen(line)==0) continue; // skip empty lines + + char sd_path[256]; + sprintf(sd_path, "%s%s", SDCARD_PATH, line); + if (exists(sd_path)) { + int type = suffixMatch(".pak", sd_path) ? ENTRY_PAK : ENTRY_ROM; // ??? + Array_push(entries, Entry_new(sd_path, type)); + + // char emu_name[256]; + // getEmuName(sd_path, emu_name); + // if (hasEmu(emu_name)) { + // Array_push(entries, Entry_new(sd_path, ENTRY_ROM)); + // } + } + } + fclose(file); + } + return entries; +} +static Array* getDiscs(char* path){ + + // TODO: does path have SDCARD_PATH prefix? + + Array* entries = Array_new(); + + char base_path[256]; + strcpy(base_path, path); + char* tmp = strrchr(base_path, '/') + 1; + tmp[0] = '\0'; + + // TODO: limit number of discs supported (to 9?) + FILE* file = fopen(path, "r"); + if (file) { + char line[256]; + int disc = 0; + while (fgets(line,256,file)!=NULL) { + normalizeNewline(line); + trimTrailingNewlines(line); + if (strlen(line)==0) continue; // skip empty lines + + char disc_path[256]; + sprintf(disc_path, "%s%s", base_path, line); + + if (exists(disc_path)) { + disc += 1; + Entry* entry = Entry_new(disc_path, ENTRY_ROM); + free(entry->name); + char name[16]; + sprintf(name, "Disc %i", disc); + entry->name = strdup(name); + Array_push(entries, entry); + } + } + fclose(file); + } + return entries; +} +static int getFirstDisc(char* m3u_path, char* disc_path) { // based on getDiscs() natch + int found = 0; + + char base_path[256]; + strcpy(base_path, m3u_path); + char* tmp = strrchr(base_path, '/') + 1; + tmp[0] = '\0'; + + FILE* file = fopen(m3u_path, "r"); + if (file) { + char line[256]; + while (fgets(line,256,file)!=NULL) { + normalizeNewline(line); + trimTrailingNewlines(line); + if (strlen(line)==0) continue; // skip empty lines + + sprintf(disc_path, "%s%s", base_path, line); + + if (exists(disc_path)) found = 1; + break; + } + fclose(file); + } + return found; +} + +static void addEntries(Array* entries, char* path) { + DIR *dh = opendir(path); + if (dh!=NULL) { + struct dirent *dp; + char* tmp; + char full_path[256]; + sprintf(full_path, "%s/", path); + tmp = full_path + strlen(full_path); + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + strcpy(tmp, dp->d_name); + int is_dir = dp->d_type==DT_DIR; + int type; + if (is_dir) { + // TODO: this should make sure launch.sh exists + if (suffixMatch(".pak", dp->d_name)) { + type = ENTRY_PAK; + } + else { + type = ENTRY_DIR; + } + } + else { + if (prefixMatch(COLLECTIONS_PATH, full_path)) { + type = ENTRY_DIR; // :shrug: + } + else { + type = ENTRY_ROM; + } + } + Array_push(entries, Entry_new(full_path, type)); + } + closedir(dh); + } +} + +static int isConsoleDir(char* path) { + char* tmp; + char parent_dir[256]; + strcpy(parent_dir, path); + tmp = strrchr(parent_dir, '/'); + tmp[0] = '\0'; + + return exactMatch(parent_dir, ROMS_PATH); +} + +static Array* getEntries(char* path){ + Array* entries = Array_new(); + + if (isConsoleDir(path)) { // top-level console folder, might collate + char collated_path[256]; + strcpy(collated_path, path); + char* tmp = strrchr(collated_path, '('); + if (tmp) { + tmp[1] = '\0'; // 1 because we want to keep the opening parenthesis to avoid collating "Game Boy Color" and "Game Boy Advance" into "Game Boy" + + DIR *dh = opendir(ROMS_PATH); + if (dh!=NULL) { + struct dirent *dp; + char full_path[256]; + sprintf(full_path, "%s/", ROMS_PATH); + tmp = full_path + strlen(full_path); + // while loop so we can collate paths, see above + while((dp = readdir(dh)) != NULL) { + if (hide(dp->d_name)) continue; + if (dp->d_type!=DT_DIR) continue; + strcpy(tmp, dp->d_name); + + if (!prefixMatch(collated_path, full_path)) continue; + addEntries(entries, full_path); + } + closedir(dh); + } + } + } + else addEntries(entries, path); // just a subfolder + + EntryArray_sort(entries); + return entries; +} + +/////////////////////////////////////// + +static void queueNext(char* cmd) { + putFile("/tmp/next", cmd); + quit = 1; +} +static char* escapeSingleQuotes(char* str) { + // based on https://stackoverflow.com/a/31775567/145965 + int replaceString(char *line, const char *search, const char *replace) { + char *sp; // start of pattern + if ((sp = strstr(line, search)) == NULL) { + return 0; + } + int count = 1; + int sLen = strlen(search); + int rLen = strlen(replace); + if (sLen > rLen) { + // move from right to left + char *src = sp + sLen; + char *dst = sp + rLen; + while((*dst = *src) != '\0') { dst++; src++; } + } else if (sLen < rLen) { + // move from left to right + int tLen = strlen(sp) - sLen; + char *stop = sp + rLen; + char *src = sp + sLen + tLen; + char *dst = sp + rLen + tLen; + while(dst >= stop) { *dst = *src; dst--; src--; } + } + memcpy(sp, replace, rLen); + count += replaceString(sp + rLen, search, replace); + return count; + } + replaceString(str, "'", "'\\''"); + return str; +} + +/////////////////////////////////////// + +static void readyResumePath(char* rom_path, int type) { + char* tmp; + can_resume = 0; + char path[256]; + strcpy(path, rom_path); + + if (!prefixMatch(ROMS_PATH, path)) return; + + char auto_path[256]; + if (type==ENTRY_DIR) { + if (!hasCue(path, auto_path)) { // no cue? + tmp = strrchr(auto_path, '.') + 1; // extension + strcpy(tmp, "m3u"); // replace with m3u + if (!exists(auto_path)) return; // no m3u + } + strcpy(path, auto_path); // cue or m3u if one exists + } + + if (!suffixMatch(".m3u", path)) { + char m3u_path[256]; + if (hasM3u(path, m3u_path)) { + // change path to m3u path + strcpy(path, m3u_path); + } + } + + char emu_name[256]; + getEmuName(path, emu_name); + + char rom_file[256]; + tmp = strrchr(path, '/') + 1; + strcpy(rom_file, tmp); + + sprintf(slot_path, "%s/.mmenu/%s/%s.txt", USERDATA_PATH, emu_name, rom_file); // /.userdata/.mmenu//.ext.txt + + can_resume = exists(slot_path); +} +static void readyResume(Entry* entry) { + readyResumePath(entry->path, entry->type); +} + +static void saveLast(char* path); +static void loadLast(void); + +static int autoResume(void) { + // NOTE: bypasses recents + + if (!exists(AUTO_RESUME_PATH)) return 0; + + char path[256]; + getFile(AUTO_RESUME_PATH, path, 256); + unlink(AUTO_RESUME_PATH); + sync(); + + // make sure rom still exists + char sd_path[256]; + sprintf(sd_path, "%s%s", SDCARD_PATH, path); + if (!exists(sd_path)) return 0; + + // make sure emu still exists + char emu_name[256]; + getEmuName(sd_path, emu_name); + + char emu_path[256]; + getEmuPath(emu_name, emu_path); + + if (!exists(emu_path)) return 0; + + // putFile(LAST_PATH, FAUX_RECENT_PATH); // saveLast() will crash here because top is NULL + + char cmd[256]; + sprintf(cmd, "'%s' '%s'", escapeSingleQuotes(emu_path), escapeSingleQuotes(sd_path)); + putFile(RESUME_SLOT_PATH, AUTO_RESUME_SLOT); + queueNext(cmd); + return 1; +} + +static void openPak(char* path) { + // NOTE: escapeSingleQuotes() modifies the passed string + // so we need to save the path before we call that + // if (prefixMatch(ROMS_PATH, path)) { + // addRecent(path); + // } + saveLast(path); + + char cmd[256]; + sprintf(cmd, "'%s/launch.sh'", escapeSingleQuotes(path)); + queueNext(cmd); +} +static void openRom(char* path, char* last) { + char sd_path[256]; + strcpy(sd_path, path); + + char m3u_path[256]; + int has_m3u = hasM3u(sd_path, m3u_path); + + char recent_path[256]; + strcpy(recent_path, has_m3u ? m3u_path : sd_path); + + if (has_m3u && suffixMatch(".m3u", sd_path)) { + getFirstDisc(m3u_path, sd_path); + } + + char emu_name[256]; + getEmuName(sd_path, emu_name); + + if (should_resume) { + char slot[16]; + getFile(slot_path, slot, 16); + putFile(RESUME_SLOT_PATH, slot); + should_resume = 0; + + if (has_m3u) { + char rom_file[256]; + strcpy(rom_file, strrchr(m3u_path, '/') + 1); + + // get disc for state + char disc_path_path[256]; + sprintf(disc_path_path, "%s/.mmenu/%s/%s.%s.txt", USERDATA_PATH, emu_name, rom_file, slot); // /.userdata/.mmenu//.ext.0.txt + + if (exists(disc_path_path)) { + // switch to disc path + char disc_path[256]; + getFile(disc_path_path, disc_path, 256); + if (disc_path[0]=='/') strcpy(sd_path, disc_path); // absolute + else { // relative + strcpy(sd_path, m3u_path); + char* tmp = strrchr(sd_path, '/') + 1; + strcpy(tmp, disc_path); + } + } + } + } + else putInt(RESUME_SLOT_PATH,8); // resume hidden default state + + char emu_path[256]; + getEmuPath(emu_name, emu_path); + + // NOTE: escapeSingleQuotes() modifies the passed string + // so we need to save the path before we call that + addRecent(recent_path); + saveLast(last==NULL ? sd_path : last); + + char cmd[256]; + sprintf(cmd, "'%s' '%s'", escapeSingleQuotes(emu_path), escapeSingleQuotes(sd_path)); + queueNext(cmd); +} +static void openDirectory(char* path, int auto_launch) { + char auto_path[256]; + if (hasCue(path, auto_path) && auto_launch) { + openRom(auto_path, path); + return; + } + + char m3u_path[256]; + strcpy(m3u_path, auto_path); + char* tmp = strrchr(m3u_path, '.') + 1; // extension + strcpy(tmp, "m3u"); // replace with m3u + if (exists(m3u_path) && auto_launch) { + auto_path[0] = '\0'; + if (getFirstDisc(m3u_path, auto_path)) { + openRom(auto_path, path); + return; + } + // TODO: doesn't handle empty m3u files + } + + int selected = 0; + int start = selected; + int end = 0; + if (top && top->entries->count>0) { + if (restore_depth==stack->count && top->selected==restore_relative) { + selected = restore_selected; + start = restore_start; + end = restore_end; + } + } + + top = Directory_new(path, selected); + top->start = start; + top->end = end ? end : ((top->entries->countentries->count : MAIN_ROW_COUNT); + + Array_push(stack, top); +} +static void closeDirectory(void) { + restore_selected = top->selected; + restore_start = top->start; + restore_end = top->end; + DirectoryArray_pop(stack); + restore_depth = stack->count; + top = stack->items[stack->count-1]; + restore_relative = top->selected; +} + +static void Entry_open(Entry* self) { + if (self->type==ENTRY_ROM) { + char *last = NULL; + if (prefixMatch(COLLECTIONS_PATH, top->path)) { + char* tmp; + char filename[256]; + + tmp = strrchr(self->path, '/'); + if (tmp) strcpy(filename, tmp+1); + + char last_path[256]; + sprintf(last_path, "%s/%s", top->path, filename); + last = last_path; + } + openRom(self->path, last); + } + else if (self->type==ENTRY_PAK) { + openPak(self->path); + } + else if (self->type==ENTRY_DIR) { + openDirectory(self->path, 1); + } +} + +/////////////////////////////////////// + +static void saveLast(char* path) { + // special case for recently played + if (exactMatch(top->path, FAUX_RECENT_PATH)) { + // NOTE: that we don't have to save the file because + // your most recently played game will always be at + // the top which is also the default selection + path = FAUX_RECENT_PATH; + } + putFile(LAST_PATH, path); +} +static void loadLast(void) { // call after loading root directory + if (!exists(LAST_PATH)) return; + + char last_path[256]; + getFile(LAST_PATH, last_path, 256); + + char full_path[256]; + strcpy(full_path, last_path); + + char* tmp; + char filename[256]; + tmp = strrchr(last_path, '/'); + if (tmp) strcpy(filename, tmp); + + Array* last = Array_new(); + while (!exactMatch(last_path, SDCARD_PATH)) { + Array_push(last, strdup(last_path)); + + char* slash = strrchr(last_path, '/'); + last_path[(slash-last_path)] = '\0'; + } + + while (last->count>0) { + char* path = Array_pop(last); + if (!exactMatch(path, ROMS_PATH)) { // romsDir is effectively root as far as restoring state after a game + char collated_path[256]; + collated_path[0] = '\0'; + if (suffixMatch(")", path) && isConsoleDir(path)) { + strcpy(collated_path, path); + tmp = strrchr(collated_path, '('); + if (tmp) tmp[1] = '\0'; // 1 because we want to keep the opening parenthesis to avoid collating "Game Boy Color" and "Game Boy Advance" into "Game Boy" + } + + for (int i=0; ientries->count; i++) { + Entry* entry = top->entries->items[i]; + + // NOTE: strlen() is required for collated_path, '\0' wasn't reading as NULL for some reason + if (exactMatch(entry->path, path) || (strlen(collated_path) && prefixMatch(collated_path, entry->path)) || (prefixMatch(COLLECTIONS_PATH, full_path) && suffixMatch(filename, entry->path))) { + top->selected = i; + if (i>=top->end) { + top->start = i; + top->end = top->start + MAIN_ROW_COUNT; + if (top->end>top->entries->count) { + top->end = top->entries->count; + top->start = top->end - MAIN_ROW_COUNT; + } + } + if (last->count==0 && !exactMatch(entry->path, FAUX_RECENT_PATH) && !(!exactMatch(entry->path, COLLECTIONS_PATH) && prefixMatch(COLLECTIONS_PATH, entry->path))) break; // don't show contents of auto-launch dirs + + if (entry->type==ENTRY_DIR) { + openDirectory(entry->path, 0); + break; + } + } + } + } + free(path); // we took ownership when we popped it + } + + StringArray_free(last); +} + +/////////////////////////////////////// + +static void Menu_init(void) { + stack = Array_new(); // array of open Directories + recents = Array_new(); + + openDirectory(SDCARD_PATH, 0); + loadLast(); // restore state when available +} +static void Menu_quit(void) { + RecentArray_free(recents); + DirectoryArray_free(stack); +} + +/////////////////////////////////////// + +int main (int argc, char *argv[]) { + if (autoResume()) return 0; // nothing to do + + is_simple = exists(ENABLE_SIMPLE_MODE_PATH); + + dump("MinUI"); + + SDL_Surface* screen = GFX_init(); + InitSettings(); + + SDL_Surface* version = NULL; + + Menu_init(); + + // TODO: tmp + TTF_Font* font = TTF_OpenFont(FONT_PATH, 32); + + PAD_reset(); + int dirty = 1; + int was_charging = isCharging(); + unsigned long charge_start = SDL_GetTicks(); + int show_version = 0; + int show_setting = 0; // 1=brightness,2=volume + int setting_value = 0; + int setting_min = 0; + int setting_max = 0; + int delay_start = 0; + int delay_select = 0; + unsigned long cancel_start = SDL_GetTicks(); + unsigned long power_start = 0; + while (!quit) { + unsigned long frame_start = SDL_GetTicks(); + + PAD_poll(); + + int selected = top->selected; + int total = top->entries->count; + + if (show_version) { + if (PAD_justPressed(BTN_B) || PAD_justPressed(BTN_MENU)) { + show_version = 0; + dirty = 1; + } + } + else { + if (!is_simple && PAD_justPressed(BTN_MENU)) { + show_version = 1; + dirty = 1; + } + + if (total>0) { + if (PAD_justRepeated(BTN_UP)) { + selected -= 1; + if (selected<0) { + selected = total-1; + int start = total - MAIN_ROW_COUNT; + top->start = (start<0) ? 0 : start; + top->end = total; + } + else if (selectedstart) { + top->start -= 1; + top->end -= 1; + } + } + else if (PAD_justRepeated(BTN_DOWN)) { + selected += 1; + if (selected>=total) { + selected = 0; + top->start = 0; + top->end = (total=top->end) { + top->start += 1; + top->end += 1; + } + } + if (PAD_justRepeated(BTN_LEFT)) { + selected -= MAIN_ROW_COUNT; + if (selected<0) { + selected = 0; + top->start = 0; + top->end = (totalstart) { + top->start -= MAIN_ROW_COUNT; + if (top->start<0) top->start = 0; + top->end = top->start + MAIN_ROW_COUNT; + } + } + else if (PAD_justRepeated(BTN_RIGHT)) { + selected += MAIN_ROW_COUNT; + if (selected>=total) { + selected = total-1; + int start = total - MAIN_ROW_COUNT; + top->start = (start<0) ? 0 : start; + top->end = total; + } + else if (selected>=top->end) { + top->end += MAIN_ROW_COUNT; + if (top->end>total) top->end = total; + top->start = top->end - MAIN_ROW_COUNT; + } + } + } + + if (!PAD_isPressed(BTN_START) && !PAD_isPressed(BTN_SELECT)) { + if (PAD_justRepeated(BTN_L1)) { // previous alpha + Entry* entry = top->entries->items[selected]; + int i = entry->alpha-1; + if (i>=0) { + selected = top->alphas->items[i]; + if (total>MAIN_ROW_COUNT) { + top->start = selected; + top->end = top->start + MAIN_ROW_COUNT; + if (top->end>total) top->end = total; + top->start = top->end - MAIN_ROW_COUNT; + } + } + } + else if (PAD_justRepeated(BTN_R1)) { // next alpha + Entry* entry = top->entries->items[selected]; + int i = entry->alpha+1; + if (ialphas->count) { + selected = top->alphas->items[i]; + if (total>MAIN_ROW_COUNT) { + top->start = selected; + top->end = top->start + MAIN_ROW_COUNT; + if (top->end>total) top->end = total; + top->start = top->end - MAIN_ROW_COUNT; + } + } + } + } + + if (selected!=top->selected) { + top->selected = selected; + dirty = 1; + } + + if (dirty && total>0) readyResume(top->entries->items[top->selected]); + + if (total>0 && PAD_justReleased(BTN_RESUME)) { + if (can_resume) { + should_resume = 1; + Entry_open(top->entries->items[top->selected]); + dirty = 1; + } + } + else if (total>0 && PAD_justPressed(BTN_A)) { + Entry_open(top->entries->items[top->selected]); + total = top->entries->count; + dirty = 1; + + if (total>0) readyResume(top->entries->items[top->selected]); + } + else if (PAD_justPressed(BTN_B) && stack->count>1) { + closeDirectory(); + total = top->entries->count; + dirty = 1; + // can_resume = 0; + if (total>0) readyResume(top->entries->items[top->selected]); + } + } + + unsigned long now = SDL_GetTicks(); + if (PAD_anyPressed()) cancel_start = now; + + #define CHARGE_DELAY 1000 + if (dirty || now-charge_start>=CHARGE_DELAY) { + int is_charging = isCharging(); + if (was_charging!=is_charging) { + was_charging = is_charging; + dirty = 1; + } + charge_start = now; + } + + if (power_start && now-power_start>=1000) { + powerOff(); + // return 0; // we should never reach this point + } + if (PAD_justPressed(BTN_SLEEP)) { + power_start = now; + } + + #define SLEEP_DELAY 30000 + if (now-cancel_start>=SLEEP_DELAY && preventAutosleep()) cancel_start = now; + + if (now-cancel_start>=SLEEP_DELAY || PAD_justReleased(BTN_SLEEP)) // || PAD_justPressed(BTN_MENU)) + { + fauxSleep(); + cancel_start = SDL_GetTicks(); + power_start = 0; + dirty = 1; + } + + int was_dirty = dirty; // dirty list (not including settings/battery) + + int old_setting = show_setting; + int old_value = setting_value; + show_setting = 0; + if (PAD_isPressed(BTN_START) && PAD_isPressed(BTN_SELECT)) { + // buh + } + else if (PAD_isPressed(BTN_START) && !delay_start) { + show_setting = 1; + setting_value = GetBrightness(); + setting_min = BRIGHTNESS_MIN; + setting_max = BRIGHTNESS_MAX; + } + else if (PAD_isPressed(BTN_SELECT) && !delay_select) { + show_setting = 2; + setting_value = GetVolume(); + setting_min = VOLUME_MIN; + setting_max = VOLUME_MAX; + } + if (old_setting!=show_setting || old_value!=setting_value) dirty = 1; + + if (dirty) { + GFX_clear(screen); + + if (show_setting) { + // GFX_blitSettings(screen, Screen.main.settings.x, Screen.main.settings.y, show_setting==1?0:(setting_value>0?1:2), setting_value,setting_min,setting_max); + } + else { + // GFX_blitBattery(screen, Screen.main.battery.x, Screen.main.battery.y); + } + // GFX_blitRule(screen, Screen.main.rule.top_y); + + if (show_version) { + if (!version) { + // char release[256]; + // getFile("./version.txt", release, 256); + char* release = "20230102"; + + // char *tmp,*commit; + // commit = strrchr(release, '\n'); + // commit[0] = '\0'; + // commit = strrchr(release, '\n')+1; + // tmp = strchr(release, '\n'); + // tmp[0] = '\0'; + char* commit = "deadbeef"; + + SDL_Surface* release_txt = TTF_RenderUTF8_Blended(font, "Release", (SDL_Color){0xff,0xff,0xff}); // was gold + SDL_Surface* version_txt = TTF_RenderUTF8_Blended(font, release, (SDL_Color){0xff,0xff,0xff}); + SDL_Surface* commit_txt = TTF_RenderUTF8_Blended(font, "Commit", (SDL_Color){0xff,0xff,0xff}); // was gold + SDL_Surface* hash_txt = TTF_RenderUTF8_Blended(font, commit, (SDL_Color){0xff,0xff,0xff}); + + int x = version_txt->w + 12; + int w = x + version_txt->w; + int h = 96; // * 2; + version = SDL_CreateRGBSurface(0,w,h,16,0,0,0,0); + + SDL_BlitSurface(release_txt, NULL, version, &(SDL_Rect){0, 0}); + SDL_BlitSurface(version_txt, NULL, version, &(SDL_Rect){x,0}); + SDL_BlitSurface(commit_txt, NULL, version, &(SDL_Rect){0,48}); + SDL_BlitSurface(hash_txt, NULL, version, &(SDL_Rect){x,48}); + + SDL_FreeSurface(release_txt); + SDL_FreeSurface(version_txt); + SDL_FreeSurface(commit_txt); + SDL_FreeSurface(hash_txt); + } + SDL_BlitSurface(version, NULL, screen, &(SDL_Rect){(640-version->w)/2,(480-version->h)/2}); + } + else { + if (total>0) { + int selected_row = top->selected - top->start; + for (int i=top->start,j=0; iend; i++,j++) { + Entry* entry = top->entries->items[i]; + char* display_name = entry->unique ? entry->unique : entry->name; + SDL_Color text_color = {0xff,0xff,0xff}; + if (j==selected_row) { + SDL_FillRect(screen, &(SDL_Rect){0,16+(j*64),SCREEN_WIDTH,64}, SDL_MapRGB(screen->format, 0xff,0xff,0xff)); + text_color = (SDL_Color){0x00,0x00,0x00}; + } + SDL_Surface* text = TTF_RenderUTF8_Blended(font, display_name, text_color); + SDL_BlitSurface(text, &(SDL_Rect){0,0,SCREEN_WIDTH-32,text->h}, screen, &(SDL_Rect){16,16+(j*64)+8}); + SDL_FreeSurface(text); + + // GFX_blitMenu(screen, entry->name, entry->path, entry->unique, j, selected_row); + } + } + else { + // GFX_blitBodyCopy(screen, "Empty folder", 0,0,Screen.width,Screen.height); + } + } + + // GFX_blitRule(screen, Screen.main.rule.bottom_y); + if (can_resume && !show_version) { + // if (strlen(HINT_RESUME)>1) GFX_blitPill(screen, HINT_RESUME, "RESUME", Screen.buttons.left, Screen.buttons.top); + // else GFX_blitButton(screen, HINT_RESUME, "RESUME", Screen.buttons.left, Screen.buttons.top, Screen.button.text.ox_X); + } + else { + // GFX_blitPill(screen, HINT_SLEEP, "SLEEP", Screen.buttons.left, Screen.buttons.top); + } + + if (show_version) { + // GFX_blitButton(screen, "B", "BACK", -Screen.buttons.right, Screen.buttons.top, Screen.button.text.ox_B); + } + else if (total==0) { + if (stack->count>1) { + // GFX_blitButton(screen, "B", "BACK", -Screen.buttons.right, Screen.buttons.top, Screen.button.text.ox_B); + } + } + else { + // int button_width = GFX_blitButton(screen, "A", "OPEN", -Screen.buttons.right, Screen.buttons.top, Screen.button.text.ox_A); + if (stack->count>1) { + // GFX_blitButton(screen, "B", "BACK", -(Screen.buttons.right+button_width+Screen.buttons.gutter),Screen.buttons.top, Screen.button.text.ox_B); + } + } + } + + // scroll long names + if (total>0) { + int selected_row = top->selected - top->start; + Entry* entry = top->entries->items[top->selected]; + // if (GFX_scrollMenu(screen, entry->name, entry->path, entry->unique, selected_row, top->selected, was_dirty, dirty)) dirty = 1; + } + + if (dirty) { + GFX_flip(screen); + dirty = 0; + } + // slow down to 60fps + unsigned long frame_duration = SDL_GetTicks() - frame_start; + #define TARGET_FRAME_DURATION 17 + if (frame_duration "$SDCARD_PATH/log.txt" diff --git a/src/test/main.c b/src/test/main.c new file mode 100644 index 0000000..75a2bd7 --- /dev/null +++ b/src/test/main.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defines.h" +#include "utils.h" +#include "api.h" + +/////////////////////////////////////// + +#include +#include + +static uint64_t GFX_getTicks(void) { + uint64_t ret; + struct timeval tv; + + gettimeofday(&tv, NULL); + + ret = (uint64_t)tv.tv_sec * 1000000; + ret += (uint64_t)tv.tv_usec; + + return ret; +} + +int main (int argc, char *argv[]) { + SDL_Surface* screen = GFX_init(); + PAD_reset(); + + int quit = 0; + while (!quit) { + // unsigned long frame_start = SDL_GetTicks(); + uint64_t frame_start_us = GFX_getTicks(); + + PAD_poll(); + if (PAD_anyPressed()) break; + + // TODO: diagnosing framepacing issues + static int frame = 0; + int x = frame * 8; + + void* dst; + + dst = screen->pixels; + memset(dst, (frame%2)?0x00:0xff, (SCREEN_HEIGHT * SCREEN_PITCH)); + // memset(dst, 0, (16 * SCREEN_PITCH)); + // for (int y=0; y<16; y++) { + // memset(dst+(8 * 60 * SCREEN_BPP), 0xff, SCREEN_BPP); + // dst += SCREEN_PITCH; + // } + // + // dst = screen->pixels; + // dst += (x * SCREEN_BPP); + // + // for (int y=0; y<16; y++) { + // memset(dst, 0xff, 8 * SCREEN_BPP); + // dst += SCREEN_PITCH; + // } + + frame += 1; + if (frame>=60) frame -= 60; + + GFX_flip(screen); + + // SDL_Delay(500); + + // slow down to 60fps + // unsigned long frame_duration = SDL_GetTicks() - frame_start; + uint64_t frame_duration_us = GFX_getTicks() - frame_start_us; + +// #define TARGET_FRAME_DURATION 17 +#define TARGET_FRAME_DURATION_US 16667 + // if (frame_duration