Add GameBoy emulator prototype
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS Source ../../../Libraries/PeanutGB
|
||||
REQUIRES TactilitySDK
|
||||
)
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* @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>
|
||||
#include <esp_heap_caps.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 "/sdcard/roms/gb/default.gb"
|
||||
#define ROMS_DIR "/sdcard/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;
|
||||
} 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) return;
|
||||
if (!ctx->canvas || !ctx->fb_native) return;
|
||||
ctx->gb.direct.joypad=ctx->joypad_state;
|
||||
gb_run_frame(&ctx->gb);
|
||||
lv_obj_invalidate(ctx->canvas);
|
||||
}
|
||||
|
||||
/* 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 /sdcard/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 /sdcard/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);
|
||||
lv_label_set_text_fmt(title_lbl, "GB: %s", 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){
|
||||
lv_coord_t disp_w=lv_display_get_horizontal_resolution(NULL);
|
||||
lv_coord_t disp_h=lv_display_get_vertical_resolution(NULL);
|
||||
int scale=1;
|
||||
if (disp_w>=FRAME_W*2 && disp_h>=FRAME_H*3) scale=2;
|
||||
if (disp_w>=FRAME_W*3 && disp_h>=FRAME_H*4) scale=3;
|
||||
lv_obj_t* canvas=lv_canvas_create(canvas_cont);
|
||||
ctx->canvas=canvas;
|
||||
lv_canvas_set_buffer(canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565);
|
||||
(void)scale; /* Prototype: avoid non-exported LVGL transform-scale symbols in side-loaded app. */
|
||||
lv_obj_set_style_border_width(canvas,1,0);
|
||||
lv_obj_set_style_border_color(canvas, lv_color_hex(0x555555),0);
|
||||
lv_obj_center(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);
|
||||
|
||||
// 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->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;
|
||||
}
|
||||
Reference in New Issue
Block a user