/** * @file main.c * @brief GameBoy DMG Emulator – Tactility (Peanut-GB) – I2 canvas version * Fixes black screen after reboot: previous RGB565 PSRAM buffer required flush_cache handler which is NULL on ESP32S3 LVGL port. * I2 (2bpp indexed) = 160*144*2/8 = 5760 bytes – fits internal RAM, no cache coherency, no WiFi/SD starvation. * Uses lv_canvas_set_palette for 4 grays per GB_PALETTE. */ #include #include #include #include #include #include void lv_image_cache_drop(const void * src); #include #undef ESP_LOGI #undef ESP_LOGW #undef ESP_LOGE #define ESP_LOGI(tag, fmt, ...) do { (void)(tag); } while (0) #define ESP_LOGW(tag, fmt, ...) do { (void)(tag); } while (0) #define ESP_LOGE(tag, fmt, ...) do { (void)(tag); } while (0) #include #include #include #include #include #include #include #include #include #define ENABLE_SOUND 0 #define ENABLE_LCD 1 #include "peanut_gb.h" #define TAG "GameBoy" #define ROMS_DIR_LEGACY "/data/roms/gb" #define DEFAULT_ROM_PATH_1 "/sdcard/Roms/GB/default.gb" #define DEFAULT_ROM_PATH_2 "/sdcard/roms/gb/default.gb" #define DEFAULT_ROM_PATH_3 "/data/Roms/GB/default.gb" #define DEFAULT_ROM_PATH_4 "/data/roms/gb/default.gb" #define DEFAULT_ROM_PATH DEFAULT_ROM_PATH_1 #define ROMS_DIR ROMS_DIR_LEGACY static const char* ROMS_CANDIDATE_DIRS[] = { "/sdcard/Roms/GB", "/sdcard/Roms", "/sdcard/roms/gb", "/sdcard/ROMS/GB", "/data/Roms/GB", "/data/roms/gb", "/data/Roms", ROMS_DIR_LEGACY, }; #define MAX_ROMS 64 #define MAX_PATH 512 #define MAX_ROM_SIZE (2 * 1024 * 1024) #define FRAME_W 160 #define FRAME_H 144 #define TICK_MS 16 typedef enum { APP_MODE_BROWSER, APP_MODE_EMU } AppMode; typedef struct { char filename[MAX_PATH]; char fullpath[MAX_PATH]; } RomEntry; typedef struct { struct gb_s gb; uint8_t* rom_data; size_t rom_size; uint8_t* cart_ram; size_t cart_ram_size; char rom_path[MAX_PATH]; char rom_title[32]; uint8_t joypad_state; uint8_t* fb_indexed; lv_draw_buf_t* fb_draw_buf; lv_obj_t* canvas; lv_obj_t* root_wrapper; lv_obj_t* browser_wrapper; lv_obj_t* emu_wrapper; lv_obj_t* toolbar; lv_obj_t* status_label; lv_obj_t* rom_list; lv_obj_t* controls_cont; lv_timer_t* emu_timer; RomEntry roms[MAX_ROMS]; int rom_count; int selected_rom_idx; lv_obj_t* btn_up; lv_obj_t* btn_down; lv_obj_t* btn_left; lv_obj_t* btn_right; lv_obj_t* btn_a; lv_obj_t* btn_b; lv_obj_t* btn_start; lv_obj_t* btn_select; AppHandle app_handle; AppMode mode; bool emu_running; bool framebuffer_allocated; bool rom_loaded; uint32_t fps_frames; int64_t fps_last_us; uint32_t lines_drawn; } AppCtx; typedef struct { AppCtx* ctx; uint8_t joypad_bit; } BtnUserData; static void* alloc_psram(size_t size) { void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!p) p = heap_caps_malloc(size, MALLOC_CAP_8BIT); return p; } static void* alloc_internal(size_t size) { void* p = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); if (!p) p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!p) p = heap_caps_malloc(size, MALLOC_CAP_8BIT); return p; } static uint8_t gb_rom_read(struct gb_s* gb, const uint_fast32_t addr) { AppCtx* ctx = (AppCtx*)gb->direct.priv; if (!ctx || !ctx->rom_data) return 0xFF; if (addr < ctx->rom_size) return ctx->rom_data[addr]; return 0xFF; } static uint8_t gb_cart_ram_read(struct gb_s* gb, const uint_fast32_t addr) { AppCtx* ctx = (AppCtx*)gb->direct.priv; if (!ctx || !ctx->cart_ram) return 0xFF; if (addr < ctx->cart_ram_size) return ctx->cart_ram[addr]; return 0xFF; } static void gb_cart_ram_write(struct gb_s* gb, const uint_fast32_t addr, const uint8_t val) { AppCtx* ctx = (AppCtx*)gb->direct.priv; if (!ctx || !ctx->cart_ram) return; if (addr < ctx->cart_ram_size) ctx->cart_ram[addr] = val; } static void gb_error_handler(struct gb_s* gb, const enum gb_error_e err, const uint16_t addr) { (void)gb;(void)err;(void)addr; } static void lcd_draw_line(struct gb_s* gb, const uint8_t* pixels, const uint_fast8_t line) { AppCtx* ctx = (AppCtx*)gb->direct.priv; if (!ctx || !ctx->fb_indexed) return; if (line >= FRAME_H) return; uint8_t* row = &ctx->fb_indexed[(line * FRAME_W) / 4]; for (int x=0;x> 2; int shift = 6 - ((x & 0x03) * 2); row[byte_idx] = (uint8_t)((row[byte_idx] & ~(0x03 << shift)) | ((shade & 0x03) << shift)); } ctx->lines_drawn++; } static void get_save_path(const char* rom_path, char* out, size_t out_len) { if (!rom_path || !out) return; strncpy(out, rom_path, out_len-1); out[out_len-1]='\0'; char* dot=strrchr(out,'.'); char* sl=strrchr(out,'/'); if (dot && (!sl || dot>sl)) { snprintf(dot, out_len-(dot-out), ".sav"); } else { strncat(out,".sav",out_len-strlen(out)-1); } } static void load_cart_ram(AppCtx* ctx) { if (!ctx || !ctx->rom_path[0]) return; char save_path[MAX_PATH]; get_save_path(ctx->rom_path, save_path, sizeof(save_path)); size_t save_sz=0; if (gb_get_save_size_s(&ctx->gb, &save_sz)!=0) save_sz=gb_get_save_size(&ctx->gb); if (save_sz==0) return; if (ctx->cart_ram) { heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; } ctx->cart_ram_size=save_sz; ctx->cart_ram=alloc_psram(save_sz); if (!ctx->cart_ram){ ctx->cart_ram_size=0; return; } memset(ctx->cart_ram,0,save_sz); FILE* f=fopen(save_path,"rb"); if (!f) return; fread(ctx->cart_ram,1,save_sz,f); fclose(f); } static void save_cart_ram(AppCtx* ctx) { if (!ctx || !ctx->cart_ram || ctx->cart_ram_size==0) return; if (!ctx->rom_path[0]) return; char save_path[MAX_PATH]; get_save_path(ctx->rom_path, save_path, sizeof(save_path)); FILE* f=fopen(save_path,"wb"); if (!f) return; fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f); } static bool load_rom_file(AppCtx* ctx, const char* path) { if (!ctx || !path) return false; FILE* f=fopen(path,"rb"); if (!f) return false; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); if (sz<=0 || sz>MAX_ROM_SIZE){ fclose(f); return false; } uint8_t* buf=alloc_psram((size_t)sz); if (!buf){ fclose(f); return false; } size_t read=fread(buf,1,(size_t)sz,f); fclose(f); if (read!=(size_t)sz){ heap_caps_free(buf); return false; } if (ctx->rom_data) { if (ctx->cart_ram) save_cart_ram(ctx); heap_caps_free(ctx->rom_data); } if (ctx->cart_ram){ heap_caps_free(ctx->cart_ram); ctx->cart_ram=NULL; ctx->cart_ram_size=0; } ctx->rom_data=buf; ctx->rom_size=(size_t)sz; strncpy(ctx->rom_path,path,sizeof(ctx->rom_path)-1); ctx->rom_path[sizeof(ctx->rom_path)-1]='\0'; memset(&ctx->gb,0,sizeof(ctx->gb)); ctx->joypad_state=0xFF; enum gb_init_error_e err=gb_init(&ctx->gb, gb_rom_read, gb_cart_ram_read, gb_cart_ram_write, gb_error_handler, ctx); if (err!=GB_INIT_NO_ERROR){ heap_caps_free(ctx->rom_data); ctx->rom_data=NULL; ctx->rom_size=0; return false; } gb_init_lcd(&ctx->gb, lcd_draw_line); load_cart_ram(ctx); gb_reset(&ctx->gb); char title[32]={0}; gb_get_rom_name(&ctx->gb, title); strncpy(ctx->rom_title,title,sizeof(ctx->rom_title)-1); ctx->rom_loaded=true; return true; } static void scan_rom_dir(AppCtx* ctx) { ctx->rom_count=0; for (size_t d=0; d < sizeof(ROMS_CANDIDATE_DIRS)/sizeof(ROMS_CANDIDATE_DIRS[0]) && ctx->rom_count < MAX_ROMS; d++) { const char* dir_path = ROMS_CANDIDATE_DIRS[d]; DIR* dir=opendir(dir_path); if (!dir) continue; struct dirent* ent; while ((ent=readdir(dir))!=NULL && ctx->rom_countd_name[0]=='.') continue; size_t len=strlen(ent->d_name); if (len<3) continue; bool is_gb = (strcasecmp(ent->d_name+len-3,".gb")==0) || (len>=4 && (strcasecmp(ent->d_name+len-4,".gbc")==0 || strcasecmp(ent->d_name+len-4,".sgb")==0 || strcasecmp(ent->d_name+len-4,".bin")==0)); if (!is_gb) continue; char full[MAX_PATH]; snprintf(full,sizeof(full),"%s/%s",dir_path,ent->d_name); bool dup=false; for(int i=0;irom_count;i++){ if (strcasecmp(ctx->roms[i].filename, ent->d_name)==0) { dup=true; break; } } if (dup) continue; RomEntry* e=&ctx->roms[ctx->rom_count++]; strncpy(e->filename, ent->d_name, sizeof(e->filename)-1); e->filename[sizeof(e->filename)-1]='\0'; strncpy(e->fullpath, full, sizeof(e->fullpath)-1); e->fullpath[sizeof(e->fullpath)-1]='\0'; } closedir(dir); } } static void emu_timer_cb(lv_timer_t* timer) { AppCtx* ctx=(AppCtx*)lv_timer_get_user_data(timer); if (!ctx || !ctx->emu_running || !ctx->rom_loaded || !ctx->canvas) return; ctx->lines_drawn = 0; ctx->gb.direct.joypad = ctx->joypad_state; gb_run_frame(&ctx->gb); if (ctx->lines_drawn == 0) { static uint32_t tick=0; tick++; for (int y=0;yfb_indexed[(y*FRAME_W)/4]; for (int x=0;x>2; int sh=6-((x&3)*2); uint8_t shade = (uint8_t)((x + y + tick) & 0x03); row[bi] = (uint8_t)((row[bi] & ~(0x03<fb_draw_buf) lv_image_cache_drop(ctx->fb_draw_buf); lv_obj_invalidate(ctx->canvas); ctx->fps_frames++; int64_t now = esp_timer_get_time(); if (ctx->fps_last_us == 0) ctx->fps_last_us = now; int64_t elapsed = now - ctx->fps_last_us; if (elapsed >= 1000000) { uint32_t fps = (uint32_t)((ctx->fps_frames * 1000000ULL) / (uint64_t)elapsed); if (ctx->status_label) lv_label_set_text_fmt(ctx->status_label, "GB: %s FPS:%lu L:%lu", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps, (unsigned long)ctx->lines_drawn); printf("GAMEBOY_FPS %lu LINES %lu\n", (unsigned long)fps, (unsigned long)ctx->lines_drawn); ctx->fps_frames = 0; ctx->fps_last_us = now; } } static void set_joypad_bit(AppCtx* ctx, uint8_t bit, bool pressed) { if (!ctx) return; if (pressed) ctx->joypad_state &= (uint8_t)~bit; else ctx->joypad_state |= bit; } static void input_down_cb(lv_event_t* e){ BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e); if (!ud) return; set_joypad_bit(ud->ctx, ud->joypad_bit, true); } static void input_up_cb(lv_event_t* e){ BtnUserData* ud=(BtnUserData*)lv_event_get_user_data(e); if (!ud) return; set_joypad_bit(ud->ctx, ud->joypad_bit, false); } static void key_press_cb(lv_event_t* e){ AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); uint32_t key=lv_event_get_key(e); switch(key){ case LV_KEY_UP: set_joypad_bit(ctx, JOYPAD_UP, true); break; case LV_KEY_DOWN: set_joypad_bit(ctx, JOYPAD_DOWN, true); break; case LV_KEY_LEFT: set_joypad_bit(ctx, JOYPAD_LEFT, true); break; case LV_KEY_RIGHT: set_joypad_bit(ctx, JOYPAD_RIGHT, true); break; case LV_KEY_ENTER: set_joypad_bit(ctx, JOYPAD_START, true); break; case LV_KEY_ESC: set_joypad_bit(ctx, JOYPAD_SELECT, true); break; default: { if (key== (uint32_t)'z' || key== (uint32_t)'Z') set_joypad_bit(ctx, JOYPAD_A, true); else if (key== (uint32_t)'x' || key== (uint32_t)'X') set_joypad_bit(ctx, JOYPAD_B, true); break; } } } static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent); static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent); static void switch_to_browser(AppCtx* ctx); static void switch_to_emu(AppCtx* ctx); static void rom_button_event(lv_event_t* e){ AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); lv_obj_t* btn=lv_event_get_target_obj(e); int* pIdx=(int*)lv_obj_get_user_data(btn); if (!pIdx) return; ctx->selected_rom_idx=*pIdx; if (ctx->selected_rom_idx<0 || ctx->selected_rom_idx>=ctx->rom_count) return; const char* path=ctx->roms[ctx->selected_rom_idx].fullpath; if (load_rom_file(ctx, path)) switch_to_emu(ctx); else if (ctx->status_label) lv_label_set_text_fmt(ctx->status_label, "Failed: %s", ctx->roms[ctx->selected_rom_idx].filename); } static void default_rom_event(lv_event_t* e){ AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); if (load_rom_file(ctx, DEFAULT_ROM_PATH)) switch_to_emu(ctx); else if (ctx->status_label) lv_label_set_text(ctx->status_label, "default.gb not found"); } static void back_to_menu_event(lv_event_t* e){ AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); switch_to_browser(ctx); } static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent){ lv_obj_clean(parent); ctx->rom_list=NULL; lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(parent, 4, 0); lv_obj_set_style_bg_color(parent, lv_color_black(), 0); lv_obj_t* info=lv_label_create(parent); lv_label_set_text_fmt(info, "GameBoy I2 fix – Place ROMs in Roms/GB – Found %d ROMs", ctx->rom_count); lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP); lv_obj_set_width(info, LV_PCT(100)); lv_obj_set_style_text_color(info, lv_color_white(), 0); ctx->status_label=lv_label_create(parent); lv_label_set_text(ctx->status_label, "Select a ROM – tap .gb in Files app also works"); lv_obj_set_style_text_color(ctx->status_label, lv_palette_main(LV_PALETTE_ORANGE), 0); lv_obj_t* list=lv_list_create(parent); lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_flex_grow(list, 1); ctx->rom_list=list; if (ctx->rom_count==0){ lv_obj_t* lbl=lv_label_create(parent); lv_label_set_text(lbl, "No ROMs. Put .gb in /sdcard/Roms/GB"); lv_obj_set_style_text_color(lbl, lv_color_white(), 0); } else { for (int i=0;irom_count;i++){ lv_obj_t* btn=lv_list_add_btn(list, LV_SYMBOL_FILE, ctx->roms[i].filename); int* idx=(int*)malloc(sizeof(int)); *idx=i; lv_obj_set_user_data(btn, idx); lv_obj_add_event_cb(btn, rom_button_event, LV_EVENT_CLICKED, ctx); } } lv_obj_t* default_btn=lv_btn_create(parent); lv_obj_set_width(default_btn, LV_PCT(100)); lv_obj_t* dlbl=lv_label_create(default_btn); lv_label_set_text(dlbl, "Try default.gb"); lv_obj_center(dlbl); lv_obj_add_event_cb(default_btn, default_rom_event, LV_EVENT_CLICKED, ctx); } static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){ lv_obj_clean(parent); lv_obj_set_style_bg_color(parent, lv_color_black(), 0); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(parent, 0, 0); lv_obj_set_style_pad_row(parent, 0, 0); lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t* info_bar=lv_obj_create(parent); lv_obj_set_width(info_bar, LV_PCT(100)); lv_obj_set_height(info_bar, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(info_bar, 2, 0); lv_obj_set_style_border_width(info_bar, 0, 0); lv_obj_set_flex_flow(info_bar, LV_FLEX_FLOW_ROW); lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x222222), 0); lv_obj_t* title_lbl=lv_label_create(info_bar); ctx->status_label = title_lbl; lv_label_set_text_fmt(title_lbl, "GB: %s FPS:--", ctx->rom_title[0]?ctx->rom_title:"GameBoy"); lv_obj_set_style_text_color(title_lbl, lv_color_white(), 0); lv_obj_t* spacer=lv_obj_create(info_bar); lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP,0); lv_obj_set_style_border_width(spacer,0,0); lv_obj_set_flex_grow(spacer,1); lv_obj_t* back_btn=lv_btn_create(info_bar); lv_obj_set_size(back_btn, 60, 28); lv_obj_t* bl=lv_label_create(back_btn); lv_label_set_text(bl,"Menu"); lv_obj_center(bl); lv_obj_add_event_cb(back_btn, back_to_menu_event, LV_EVENT_CLICKED, ctx); lv_obj_t* canvas_cont=lv_obj_create(parent); lv_obj_set_width(canvas_cont, LV_PCT(100)); lv_obj_set_flex_grow(canvas_cont,1); lv_obj_set_style_bg_color(canvas_cont, lv_color_black(),0); lv_obj_set_style_border_width(canvas_cont,0,0); lv_obj_set_style_pad_all(canvas_cont,2,0); lv_obj_set_flex_flow(canvas_cont, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(canvas_cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_remove_flag(canvas_cont, LV_OBJ_FLAG_SCROLLABLE); if (!ctx->fb_indexed){ size_t fb_bytes = (FRAME_W * FRAME_H) / 4; ctx->fb_indexed=(uint8_t*)alloc_internal(fb_bytes); if (ctx->fb_indexed){ ctx->framebuffer_allocated=true; memset(ctx->fb_indexed,0,fb_bytes); } } if (ctx->fb_indexed){ lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL); lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL); int avail_h = disp_h - 160; int avail_w = disp_w - 8; int scale = 1; if (avail_w >= FRAME_W * 3 && avail_h >= FRAME_H * 3) scale = 3; else if (avail_w >= FRAME_W * 2 && avail_h >= FRAME_H * 2) scale = 2; ctx->canvas = lv_canvas_create(canvas_cont); lv_canvas_set_buffer(ctx->canvas, ctx->fb_indexed, FRAME_W, FRAME_H, LV_COLOR_FORMAT_I2); lv_color32_t c0 = { .red=0xFF, .green=0xFF, .blue=0xFF, .alpha=0xFF }; lv_color32_t c1 = { .red=0xAA, .green=0xAA, .blue=0xAA, .alpha=0xFF }; lv_color32_t c2 = { .red=0x55, .green=0x55, .blue=0x55, .alpha=0xFF }; lv_color32_t c3 = { .red=0x00, .green=0x00, .blue=0x00, .alpha=0xFF }; lv_canvas_set_palette(ctx->canvas, 0, c0); lv_canvas_set_palette(ctx->canvas, 1, c1); lv_canvas_set_palette(ctx->canvas, 2, c2); lv_canvas_set_palette(ctx->canvas, 3, c3); ctx->fb_draw_buf = lv_canvas_get_draw_buf(ctx->canvas); if (ctx->fb_draw_buf) lv_image_cache_drop(ctx->fb_draw_buf); if (scale > 1) { lv_image_set_scale(ctx->canvas, (uint32_t)(256 * scale)); lv_image_set_pivot(ctx->canvas, FRAME_W / 2, FRAME_H / 2); } lv_obj_set_style_border_width(ctx->canvas, 1, 0); lv_obj_set_style_border_color(ctx->canvas, lv_color_hex(0x00FF00), 0); lv_obj_center(ctx->canvas); } else { lv_obj_t* err=lv_label_create(canvas_cont); lv_label_set_text(err,"FB alloc failed"); lv_obj_set_style_text_color(err, lv_color_white(),0); } lv_obj_t* ctrl=lv_obj_create(parent); ctx->controls_cont=ctrl; lv_obj_set_width(ctrl, LV_PCT(100)); lv_obj_set_height(ctrl, 110); lv_obj_set_style_pad_all(ctrl,2,0); lv_obj_set_style_bg_color(ctrl, lv_color_hex(0x111111),0); lv_obj_set_style_border_width(ctrl,0,0); lv_obj_set_flex_flow(ctrl, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(ctrl, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); static BtnUserData btn_ud[8]; static bool ud_init=false; if (!ud_init){ memset(btn_ud,0,sizeof(btn_ud)); ud_init=true; } lv_obj_t* dpad=lv_obj_create(ctrl); lv_obj_set_size(dpad,96,96); lv_obj_set_style_bg_opa(dpad,LV_OPA_TRANSP,0); lv_obj_set_style_border_width(dpad,0,0); lv_obj_set_style_pad_all(dpad,0,0); lv_obj_t* up=lv_btn_create(dpad); lv_obj_set_size(up,32,32); lv_obj_set_pos(up,32,0); lv_obj_t* left=lv_btn_create(dpad); lv_obj_set_size(left,32,32); lv_obj_set_pos(left,0,32); lv_obj_t* right=lv_btn_create(dpad); lv_obj_set_size(right,32,32); lv_obj_set_pos(right,64,32); lv_obj_t* down=lv_btn_create(dpad); lv_obj_set_size(down,32,32); lv_obj_set_pos(down,32,64); lv_obj_t* lbl; lbl=lv_label_create(up); lv_label_set_text(lbl, LV_SYMBOL_UP); lv_obj_center(lbl); lbl=lv_label_create(down); lv_label_set_text(lbl, LV_SYMBOL_DOWN); lv_obj_center(lbl); lbl=lv_label_create(left); lv_label_set_text(lbl, LV_SYMBOL_LEFT); lv_obj_center(lbl); lbl=lv_label_create(right); lv_label_set_text(lbl, LV_SYMBOL_RIGHT); lv_obj_center(lbl); ctx->btn_up=up; ctx->btn_down=down; ctx->btn_left=left; ctx->btn_right=right; btn_ud[0].ctx=ctx; btn_ud[0].joypad_bit=JOYPAD_UP; lv_obj_add_event_cb(up, input_down_cb, LV_EVENT_PRESSED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_RELEASED, &btn_ud[0]); lv_obj_add_event_cb(up, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[0]); btn_ud[1].ctx=ctx; btn_ud[1].joypad_bit=JOYPAD_DOWN; lv_obj_add_event_cb(down, input_down_cb, LV_EVENT_PRESSED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_RELEASED, &btn_ud[1]); lv_obj_add_event_cb(down, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[1]); btn_ud[2].ctx=ctx; btn_ud[2].joypad_bit=JOYPAD_LEFT; lv_obj_add_event_cb(left, input_down_cb, LV_EVENT_PRESSED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_RELEASED, &btn_ud[2]); lv_obj_add_event_cb(left, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[2]); btn_ud[3].ctx=ctx; btn_ud[3].joypad_bit=JOYPAD_RIGHT;lv_obj_add_event_cb(right, input_down_cb, LV_EVENT_PRESSED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_RELEASED, &btn_ud[3]); lv_obj_add_event_cb(right, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[3]); lv_obj_t* center_col=lv_obj_create(ctrl); lv_obj_set_size(center_col,64,96); lv_obj_set_style_bg_opa(center_col,LV_OPA_TRANSP,0); lv_obj_set_style_border_width(center_col,0,0); lv_obj_set_flex_flow(center_col, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(center_col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(center_col,4,0); lv_obj_t* sel_btn=lv_btn_create(center_col); lv_obj_set_size(sel_btn,60,28); lbl=lv_label_create(sel_btn); lv_label_set_text(lbl,"Sel"); lv_obj_center(lbl); lv_obj_t* sta_btn=lv_btn_create(center_col); lv_obj_set_size(sta_btn,60,28); lbl=lv_label_create(sta_btn); lv_label_set_text(lbl,"Sta"); lv_obj_center(lbl); ctx->btn_select=sel_btn; ctx->btn_start=sta_btn; btn_ud[4].ctx=ctx; btn_ud[4].joypad_bit=JOYPAD_SELECT; lv_obj_add_event_cb(sel_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[4]); lv_obj_add_event_cb(sel_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[4]); btn_ud[5].ctx=ctx; btn_ud[5].joypad_bit=JOYPAD_START; lv_obj_add_event_cb(sta_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[5]); lv_obj_add_event_cb(sta_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[5]); lv_obj_t* ab=lv_obj_create(ctrl); lv_obj_set_size(ab,96,96); lv_obj_set_style_bg_opa(ab,LV_OPA_TRANSP,0); lv_obj_set_style_border_width(ab,0,0); lv_obj_set_style_pad_all(ab,0,0); lv_obj_t* b_btn=lv_btn_create(ab); lv_obj_set_size(b_btn,40,40); lv_obj_set_pos(b_btn,0,24); lv_obj_set_style_radius(b_btn,20,0); lv_obj_t* a_btn=lv_btn_create(ab); lv_obj_set_size(a_btn,40,40); lv_obj_set_pos(a_btn,48,8); lv_obj_set_style_radius(a_btn,20,0); lbl=lv_label_create(b_btn); lv_label_set_text(lbl,"B"); lv_obj_center(lbl); lbl=lv_label_create(a_btn); lv_label_set_text(lbl,"A"); lv_obj_center(lbl); ctx->btn_a=a_btn; ctx->btn_b=b_btn; btn_ud[6].ctx=ctx; btn_ud[6].joypad_bit=JOYPAD_B; lv_obj_add_event_cb(b_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[6]); lv_obj_add_event_cb(b_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[6]); btn_ud[7].ctx=ctx; btn_ud[7].joypad_bit=JOYPAD_A; lv_obj_add_event_cb(a_btn, input_down_cb, LV_EVENT_PRESSED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_RELEASED, &btn_ud[7]); lv_obj_add_event_cb(a_btn, input_up_cb, LV_EVENT_PRESS_LOST, &btn_ud[7]); lv_obj_add_event_cb(parent, key_press_cb, LV_EVENT_KEY, ctx); if (tt_lvgl_hardware_keyboard_is_available()){ lv_group_t* g=lv_group_get_default(); if (g){ lv_group_add_obj(g, parent); lv_group_focus_obj(parent); lv_group_set_editing(g, true); } } if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } ctx->fps_frames = 0; ctx->fps_last_us = esp_timer_get_time(); ctx->lines_drawn = 0; ctx->emu_timer=lv_timer_create(emu_timer_cb, TICK_MS, ctx); ctx->emu_running=true; ctx->mode=APP_MODE_EMU; } static void switch_to_browser(AppCtx* ctx){ if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } ctx->emu_running=false; save_cart_ram(ctx); ctx->mode=APP_MODE_BROWSER; if (!ctx->browser_wrapper || !lv_obj_is_valid(ctx->browser_wrapper)) return; if (ctx->emu_wrapper) lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); scan_rom_dir(ctx); build_browser_ui(ctx, ctx->browser_wrapper); } static void switch_to_emu(AppCtx* ctx){ if (!ctx->rom_loaded) return; if (ctx->browser_wrapper) lv_obj_add_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); if (!ctx->emu_wrapper) return; lv_obj_remove_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); build_emu_ui(ctx, ctx->emu_wrapper); } static void* create_data(void){ AppCtx* ctx=(AppCtx*)calloc(1,sizeof(AppCtx)); if (ctx){ ctx->joypad_state=0xFF; ctx->mode=APP_MODE_BROWSER; ctx->selected_rom_idx=-1; } return ctx; } static void destroy_data(void* data){ AppCtx* ctx=(AppCtx*)data; if (!ctx) return; if (ctx->emu_timer) lv_timer_delete(ctx->emu_timer); if (ctx->fb_indexed) heap_caps_free(ctx->fb_indexed); if (ctx->rom_data) heap_caps_free(ctx->rom_data); if (ctx->cart_ram) heap_caps_free(ctx->cart_ram); free(ctx); } static void on_create(AppHandle app, void* data){ AppCtx* ctx=(AppCtx*)data; if (ctx) ctx->app_handle=app; } static void on_destroy(AppHandle app, void* data){ (void)app;(void)data; } static void on_show(AppHandle app, void* data, lv_obj_t* parent){ AppCtx* ctx=(AppCtx*)data; if (!ctx) return; ctx->app_handle=app; lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(parent,0,0); lv_obj_set_style_pad_row(parent,0,0); lv_obj_set_style_bg_color(parent, lv_color_black(),0); ctx->toolbar=tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_set_style_bg_color(ctx->toolbar, lv_color_hex(0x111111),0); ctx->root_wrapper=lv_obj_create(parent); lv_obj_set_width(ctx->root_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(ctx->root_wrapper,1); lv_obj_set_style_pad_all(ctx->root_wrapper,0,0); lv_obj_set_style_border_width(ctx->root_wrapper,0,0); lv_obj_set_style_bg_color(ctx->root_wrapper, lv_color_black(),0); lv_obj_set_flex_flow(ctx->root_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_remove_flag(ctx->root_wrapper, LV_OBJ_FLAG_SCROLLABLE); ctx->browser_wrapper=lv_obj_create(ctx->root_wrapper); lv_obj_set_width(ctx->browser_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(ctx->browser_wrapper,1); lv_obj_set_style_pad_all(ctx->browser_wrapper,0,0); lv_obj_set_style_border_width(ctx->browser_wrapper,0,0); ctx->emu_wrapper=lv_obj_create(ctx->root_wrapper); lv_obj_set_width(ctx->emu_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(ctx->emu_wrapper,1); lv_obj_set_style_pad_all(ctx->emu_wrapper,0,0); lv_obj_set_style_border_width(ctx->emu_wrapper,0,0); lv_obj_add_flag(ctx->emu_wrapper, LV_OBJ_FLAG_HIDDEN); BundleHandle bundle = tt_app_get_parameters(app); char file_param[512] = {0}; bool hasFileParam = false; const char* file_to_try = NULL; if (bundle && tt_bundle_opt_string(bundle, "file", file_param, sizeof(file_param))) { hasFileParam = file_param[0] != '\0'; file_to_try = file_param; } if (hasFileParam && file_to_try) { char resolved[MAX_PATH]; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" if (strncmp(file_to_try, "/sdcard", 7) == 0 || strncmp(file_to_try, "/data", 5) == 0) { snprintf(resolved, sizeof(resolved), "%s", file_to_try); } else if (file_to_try[0] == '/') { snprintf(resolved, sizeof(resolved), "/sdcard%s", file_to_try); } else { char truncated_file[400]; snprintf(truncated_file, sizeof(truncated_file), "%s", file_to_try); snprintf(resolved, sizeof(resolved), "/sdcard/%s", truncated_file); } #pragma GCC diagnostic pop struct stat st; if (stat(resolved, &st)==0 && load_rom_file(ctx, resolved)) { build_browser_ui(ctx, ctx->browser_wrapper); switch_to_emu(ctx); return; } if (load_rom_file(ctx, file_to_try)) { build_browser_ui(ctx, ctx->browser_wrapper); switch_to_emu(ctx); return; } } scan_rom_dir(ctx); const char* default_candidates[] = { DEFAULT_ROM_PATH_1, DEFAULT_ROM_PATH_2, DEFAULT_ROM_PATH_3, DEFAULT_ROM_PATH_4, ROMS_DIR_LEGACY "/default.gb" }; for (size_t i=0;ibrowser_wrapper); switch_to_emu(ctx); return; } } build_browser_ui(ctx, ctx->browser_wrapper); lv_obj_remove_flag(ctx->browser_wrapper, LV_OBJ_FLAG_HIDDEN); ctx->mode=APP_MODE_BROWSER; } static void on_hide(AppHandle app, void* data){ (void)app; AppCtx* ctx=(AppCtx*)data; if (!ctx) return; if (ctx->emu_timer){ lv_timer_delete(ctx->emu_timer); ctx->emu_timer=NULL; } ctx->emu_running=false; if (ctx->rom_loaded) save_cart_ram(ctx); ctx->canvas=NULL; ctx->fb_draw_buf=NULL; ctx->toolbar=NULL; ctx->root_wrapper=NULL; ctx->browser_wrapper=NULL; ctx->emu_wrapper=NULL; ctx->status_label=NULL; ctx->rom_list=NULL; ctx->controls_cont=NULL; ctx->btn_up=ctx->btn_down=ctx->btn_left=ctx->btn_right=NULL; ctx->btn_a=ctx->btn_b=ctx->btn_start=ctx->btn_select=NULL; ctx->app_handle=NULL; } int main(int argc, char* argv[]){ (void)argc; (void)argv; tt_app_register((AppRegistration){ .createData=create_data, .destroyData=destroy_data, .onCreate=on_create, .onDestroy=on_destroy, .onShow=on_show, .onHide=on_hide }); return 0; }