Files
tactility_apps/Apps/GameBoy/main/Source/main.c
T
Adolfo 4ff787ff1d fix(GameBoy): I2 2bpp 5.6KB internal canvas fixes black screen after reboot (L:144 FPS:40)
Root cause: RGB565 46KB PSRAM required flush_cache handler which is NULL on ESP32S3 LVGL port -> dirty cache never reached PSRAM -> black. Frame writes L:144 prove core OK, FPS:40 proves timer OK, but PSRAM stale.

Fix: I2 indexed 160*144/4=5760 bytes internal RAM, no cache coherency, no WiFi/SD starvation (previous 46KB internal -> 7KB free -> sdmmc_read_sectors fail + wifi m f null). Uses lv_canvas_set_buffer I2 + lv_canvas_set_palette white/light/dark/black. Preserves Roms/GB capitalized probe + .gb file browser tap via tt_app_get_parameters bundle file.

Installs to 192.168.68.132, survives reboot.
2026-07-20 14:06:08 -04:00

672 lines
30 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 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 <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>
void lv_image_cache_drop(const void * src);
#include <esp_log.h>
#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 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<FRAME_W;x++) {
uint8_t shade = pixels[x] & 0x03;
int byte_idx = 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_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,".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;i<ctx->rom_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;y<FRAME_H;y++) {
uint8_t* row=&ctx->fb_indexed[(y*FRAME_W)/4];
for (int x=0;x<FRAME_W;x++) {
int bi=x>>2; int sh=6-((x&3)*2);
uint8_t shade = (uint8_t)((x + y + tick) & 0x03);
row[bi] = (uint8_t)((row[bi] & ~(0x03<<sh)) | (shade<<sh));
}
}
}
if (ctx->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;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_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;i<sizeof(default_candidates)/sizeof(default_candidates[0]);i++) {
struct stat st;
if (stat(default_candidates[i], &st)==0 && load_rom_file(ctx, default_candidates[i])) {
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->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;
}