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.
This commit is contained in:
Adolfo
2026-07-20 14:06:08 -04:00
parent 6d9001fcd7
commit 4ff787ff1d
+143 -151
View File
@@ -1,9 +1,9 @@
/**
* @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.
* @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>
@@ -13,11 +13,9 @@
#include <tt_lvgl_keyboard.h>
#include <lvgl.h>
/* lv_image_cache_drop is not in public LVGL headers but exported by Tactility firmware */
void lv_image_cache_drop(const void * src);
#include <esp_log.h>
/* Board firmware 0.8.0-dev/IDF 5.3.2 does not export esp_log for side-loaded ELFs. */
#include <esp_log.h>
#undef ESP_LOGI
#undef ESP_LOGW
#undef ESP_LOGE
@@ -40,8 +38,23 @@ void lv_image_cache_drop(const void * src);
#include "peanut_gb.h"
#define TAG "GameBoy"
#define DEFAULT_ROM_PATH "/data/roms/gb/default.gb"
#define ROMS_DIR "/data/roms/gb"
#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)
@@ -49,14 +62,6 @@ void lv_image_cache_drop(const void * src);
#define FRAME_H 144
#define TICK_MS 16
/* RGB565 direct no bitfield endian ambiguity */
static const uint16_t GB_PALETTE[4] = {
0xFFFF, // white
0x8C51, // light gray ~ 0b10001 100010 10001 but pre-tuned
0x4A49, // dark gray
0x0000 // black
};
typedef enum { APP_MODE_BROWSER, APP_MODE_EMU } AppMode;
typedef struct {
@@ -74,8 +79,8 @@ typedef struct {
char rom_title[32];
uint8_t joypad_state;
uint16_t* fb_native; /* RGB565 tightly packed 160*144, raw u16 avoids lv_color16_t bitfield */
lv_draw_buf_t* fb_draw_buf; /* draw_buf that canvas src points to owned by us so we can invalidate cache */
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;
@@ -106,7 +111,7 @@ typedef struct {
bool rom_loaded;
uint32_t fps_frames;
int64_t fps_last_us;
uint32_t lines_drawn; /* debug: should be 144 per frame */
uint32_t lines_drawn;
} AppCtx;
typedef struct {
@@ -114,15 +119,18 @@ typedef struct {
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;
}
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;
}
/* 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;
@@ -140,37 +148,22 @@ static void gb_cart_ram_write(struct gb_s* gb, const uint_fast32_t addr, const u
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 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_native) return;
if (!ctx || !ctx->fb_indexed) return;
if (line >= FRAME_H) return;
uint16_t* dst = &ctx->fb_native[line * FRAME_W];
uint8_t* row = &ctx->fb_indexed[(line * FRAME_W) / 4];
for (int x=0;x<FRAME_W;x++) {
uint8_t shade = pixels[x] & 0x03;
dst[x] = GB_PALETTE[shade];
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++;
}
/* 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';
@@ -184,16 +177,15 @@ static void load_cart_ram(AppCtx* ctx) {
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 (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){ ESP_LOGE(TAG,"cart RAM alloc fail %zu",save_sz); ctx->cart_ram_size=0; return; }
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){ 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);
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;
@@ -201,23 +193,20 @@ static void save_cart_ram(AppCtx* ctx) {
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);
if (!f) return;
fwrite(ctx->cart_ram,1,ctx->cart_ram_size,f); fclose(f);
}
/* 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; }
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){ ESP_LOGE(TAG,"Bad size %ld %s",sz,path); fclose(f); return false; }
if (sz<=0 || sz>MAX_ROM_SIZE){ 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; }
if (!buf){ 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 (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; }
@@ -227,35 +216,42 @@ static bool load_rom_file(AppCtx* ctx, const char* path) {
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; }
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);
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; }
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,".bin")==0));
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';
snprintf(e->fullpath, sizeof(e->fullpath), "%s/%s", ROMS_DIR, ent->d_name);
strncpy(e->fullpath, full, sizeof(e->fullpath)-1); e->fullpath[sizeof(e->fullpath)-1]='\0';
}
closedir(dir);
ESP_LOGI(TAG,"Found %d ROMs",ctx->rom_count);
}
}
/* Emu timer THIS IS WHERE "FPS but no image" WAS: missing cache drop */
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;
@@ -263,22 +259,19 @@ static void emu_timer_cb(lv_timer_t* timer) {
ctx->gb.direct.joypad = ctx->joypad_state;
gb_run_frame(&ctx->gb);
/* Critical fix: raw buffer mutated, LVGL image cache is stale.
Without this, canvas shows whatever was first uploaded (gray/black) and FPS label keeps updating,
giving "FPS but no game". */
if (ctx->fb_draw_buf) {
/* Invalidate both D-Cache (PSRAM) and LVGL image cache */
lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL);
lv_image_cache_drop(ctx->fb_draw_buf);
/* Also invalidate area via the canvas src buf if different object */
if (ctx->canvas) {
lv_draw_buf_t* c_db = lv_canvas_get_draw_buf(ctx->canvas);
if (c_db && c_db != ctx->fb_draw_buf) {
lv_draw_buf_invalidate_cache(c_db, NULL);
lv_image_cache_drop(c_db);
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++;
@@ -287,31 +280,20 @@ static void emu_timer_cb(lv_timer_t* timer) {
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);
}
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;
}
}
/* 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 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);
@@ -330,13 +312,11 @@ static void key_press_cb(lv_event_t* e){
}
}
/* 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);
@@ -345,51 +325,37 @@ static void rom_button_event(lv_event_t* e){
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);
}
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);
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); }
/* 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_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 to start");
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 found in /data/roms/gb\nPut .gb files there.");
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++){
@@ -399,7 +365,6 @@ static void build_browser_ui(AppCtx* ctx, lv_obj_t* parent){
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);
@@ -449,18 +414,13 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
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(uint16_t);
ctx->fb_native=(uint16_t*)alloc_psram(fb_bytes);
if (ctx->fb_native){
ctx->framebuffer_allocated=true;
memset(ctx->fb_native,0,fb_bytes);
/* Initial grey fill so we can visually confirm buffer ownership even before first gb_run_frame */
for(int i=0;i<FRAME_W*FRAME_H;i++) ctx->fb_native[i]=0x4208;
}
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_native){
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;
@@ -470,21 +430,23 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
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 creates internal static_buf header from our raw ptr */
lv_canvas_set_buffer(ctx->canvas, ctx->fb_native, FRAME_W, FRAME_H, LV_COLOR_FORMAT_RGB565);
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 && ctx->fb_draw_buf->data) {
/* Force fresh cache state */
lv_draw_buf_invalidate_cache(ctx->fb_draw_buf, NULL);
lv_image_cache_drop(ctx->fb_draw_buf);
}
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(0x444444), 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);
@@ -509,7 +471,6 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
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);
@@ -518,7 +479,6 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
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]);
@@ -532,7 +492,6 @@ static void build_emu_ui(AppCtx* ctx, lv_obj_t* parent){
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;
@@ -586,7 +545,6 @@ static void switch_to_emu(AppCtx* ctx){
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; }
@@ -596,15 +554,13 @@ 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->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_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;
@@ -638,11 +594,47 @@ static void on_show(AppHandle app, void* data, lv_obj_t* parent){
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);
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;
bool default_exists=(stat(DEFAULT_ROM_PATH,&st)==0);
if (default_exists){
if (load_rom_file(ctx, DEFAULT_ROM_PATH)){
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;