@@ -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 \n Place ROMs in %s \n Default: %s \n Found %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 \n Put .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 ;
}