Files
tactility_apps/Apps/GameBoy/main/Source/main.c
T
2026-07-17 18:11:05 -04:00

631 lines
28 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file main.c
* @brief GameBoy DMG Emulator for Tactility (Peanut-GB prototype, no audio)
*
* MIT licensed Peanut-GB core vendored in Libraries/PeanutGB/peanut_gb.h
* See app README for notes.
*/
#include <tt_app.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <tt_lvgl_keyboard.h>
#include <lvgl.h>
#include <esp_log.h>
/* Board firmware 0.8.0-dev/IDF 5.3.2 does not export esp_log for side-loaded ELFs. */
#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 <esp_heap_caps.h>
#include <esp_timer.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#define ENABLE_SOUND 0
#define ENABLE_LCD 1
#include "peanut_gb.h"
#define TAG "GameBoy"
#define DEFAULT_ROM_PATH "/data/roms/gb/default.gb"
#define ROMS_DIR "/data/roms/gb"
#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
static const uint16_t GB_PALETTE[4] = {
0xFFFF, // white
0x8C51, // light gray
0x4A49, // dark gray
0x0000 // black
};
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;
lv_color16_t* fb_native;
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;
} AppCtx;
typedef struct {
AppCtx* ctx;
uint8_t joypad_bit;
} BtnUserData;
/* PSRAM helpers */
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);
if (!p) p = malloc(size);
return p;
}
/* Peanut-GB callbacks */
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) {
const char* err_str = "UNKNOWN";
switch (err) {
case GB_INVALID_OPCODE: err_str = "INVALID OPCODE"; break;
case GB_INVALID_READ: err_str = "INVALID READ"; break;
case GB_INVALID_WRITE: err_str = "INVALID WRITE"; break;
default: break;
}
ESP_LOGE(TAG, "GB error %s (%d) @ %04X", err_str, err, addr);
AppCtx* ctx = (AppCtx*)gb->direct.priv;
if (ctx && ctx->cart_ram_size) {
char sp[MAX_PATH];
strncpy(sp, ctx->rom_path, MAX_PATH-1); sp[MAX_PATH-1]='\0';
char* dot=strrchr(sp,'.'); char* sl=strrchr(sp,'/');
if (dot && (!sl || dot>sl)) snprintf(dot, MAX_PATH-(dot-sp), ".sav"); else strncat(sp,".sav",MAX_PATH-strlen(sp)-1);
FILE* f=fopen(sp,"wb"); if(f){fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);}
}
}
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_native) return;
if (line >= FRAME_H) return;
lv_color16_t* dst = &ctx->fb_native[line * FRAME_W];
for (int x=0;x<FRAME_W;x++) {
uint8_t shade = pixels[x] & 0x03;
uint16_t v = GB_PALETTE[shade];
dst[x] = (lv_color16_t){ .blue = v & 0x1F, .green = (v >> 5) & 0x3F, .red = (v >> 11) & 0x1F };
}
}
/* Save path */
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) { ESP_LOGI(TAG,"No save RAM"); 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){ ESP_LOGE(TAG,"cart RAM alloc fail %zu",save_sz); ctx->cart_ram_size=0; return; }
memset(ctx->cart_ram,0,save_sz);
FILE* f=fopen(save_path,"rb");
if (!f){ ESP_LOGI(TAG,"No save %s",save_path); return; }
size_t r=fread(ctx->cart_ram,1,save_sz,f); fclose(f);
ESP_LOGI(TAG,"Loaded save %s %zu/%zu",save_path,r,save_sz);
}
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){ ESP_LOGE(TAG,"Save open fail %s",save_path); return; }
size_t w=fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);
ESP_LOGI(TAG,"Saved RAM %s %zu",save_path,w);
}
/* ROM loading */
static bool load_rom_file(AppCtx* ctx, const char* path) {
if (!ctx || !path) return false;
ESP_LOGI(TAG,"Loading ROM %s",path);
FILE* f=fopen(path,"rb");
if (!f){ ESP_LOGE(TAG,"Open fail %s",path); return false; }
fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET);
if (sz<=0 || sz>MAX_ROM_SIZE){ ESP_LOGE(TAG,"Bad size %ld %s",sz,path); fclose(f); return false; }
uint8_t* buf=alloc_psram((size_t)sz);
if (!buf){ ESP_LOGE(TAG,"Alloc fail %ld",sz); fclose(f); return false; }
size_t read=fread(buf,1,(size_t)sz,f); fclose(f);
if (read!=(size_t)sz){ ESP_LOGE(TAG,"Short read %zu vs %ld",read,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){ ESP_LOGE(TAG,"gb_init %d",err); 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);
ESP_LOGI(TAG,"ROM OK title='%s' size=%zu save=%zu",ctx->rom_title,ctx->rom_size,ctx->cart_ram_size);
ctx->rom_loaded=true;
return true;
}
static void scan_rom_dir(AppCtx* ctx) {
ctx->rom_count=0;
DIR* dir=opendir(ROMS_DIR);
if (!dir){ ESP_LOGW(TAG,"ROMS dir missing %s",ROMS_DIR); return; }
struct dirent* ent;
while ((ent=readdir(dir))!=NULL && ctx->rom_count<MAX_ROMS){
if (ent->d_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,".bin")==0));
if (!is_gb) 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';
snprintf(e->fullpath, sizeof(e->fullpath), "%s/%s", ROMS_DIR, ent->d_name);
}
closedir(dir);
ESP_LOGI(TAG,"Found %d ROMs",ctx->rom_count);
}
/* Emu timer */
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->gb.direct.joypad = ctx->joypad_state;
gb_run_frame(&ctx->gb);
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", ctx->rom_title[0] ? ctx->rom_title : "GameBoy", (unsigned long)fps);
}
printf("GAMEBOY_FPS %lu\n", (unsigned long)fps);
ctx->fps_frames = 0;
ctx->fps_last_us = now;
}
}
/* Input helpers */
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: {
// LVGL may deliver ASCII via key code >127? Check second method via indev? Keep z/x mapping via key char if lower ascii
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;
}
}
}
/* Forward declarations for UI switching */
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);
/* ROM selection events */
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 in /data/roms/gb/");
}
}
static void back_to_menu_event(lv_event_t* e){
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
switch_to_browser(ctx);
}
/* UI builders */
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 (no audio) - Peanut-GB\nPlace ROMs in %s\nDefault: %s\nFound %d ROMs", ROMS_DIR, DEFAULT_ROM_PATH, 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 to start");
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 found in /data/roms/gb\nPut .gb files there.");
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
} else {
for (int i=0;i<ctx->rom_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_native){
size_t fb_bytes=FRAME_W*FRAME_H*sizeof(lv_color16_t);
ctx->fb_native=(lv_color16_t*)alloc_psram(fb_bytes);
if (ctx->fb_native){ ctx->framebuffer_allocated=true; memset(ctx->fb_native,0,fb_bytes); for(int i=0;i<FRAME_W*FRAME_H;i++) ctx->fb_native[i]=(lv_color16_t){ .blue = 0x4208 & 0x1F, .green = (0x4208 >> 5) & 0x3F, .red = (0x4208 >> 11) & 0x1F }; }
}
if (ctx->fb_native){
ctx->canvas = lv_canvas_create(canvas_cont);
lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565);
lv_obj_set_style_border_width(ctx->canvas, 1, 0);
lv_obj_set_style_border_color(ctx->canvas, lv_color_hex(0x444444), 0);
} 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);
// Storage for button user data keep static to survive hide/show? Use dynamic per UI build but tied to ctx lifetime via malloc, but for prototype use static array inside function with static lifetime
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->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);
}
/* Lifecycle */
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_native) heap_caps_free(ctx->fb_native);
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);
scan_rom_dir(ctx);
struct stat st;
bool default_exists=(stat(DEFAULT_ROM_PATH,&st)==0);
if (default_exists){
if (load_rom_file(ctx, DEFAULT_ROM_PATH)){
build_browser_ui(ctx, ctx->browser_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->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;
}