feat(LuaGame): fullscreen 320x480, no toolbar, on-demand refresh for solitaire

- Manifest: HideStatusBar flag, version 0.2.0
- app_on_show: remove toolbar, get display resolution via lv_display_get_horizontal_resolution, allocate 320x480 framebuffer in PSRAM
- Dynamic game_w/h, fb_w/h in AppCtx, set_px/fill_rect use dynamic size
- touch_event_cb uses dynamic max_w/h
- On-demand refresh: l_game_set_frame_updates sets wants_frame_updates flag
  - Pico games without set_frame_updates (solitaire) only redraw on touch events
  - Continuous games (breakout, pong) with set_frame_updates(true) use FRAME_TICK every 16ms
  - Initial draw after init for on-demand games
  - FPS tracking only when redraw occurs
- Solitaire optimized: LEFT 37->4, GAP 10->4, TOP_Y 30->4, TAB_Y 98->60, removed draw_terminal_chrome (outer frame + title bar + window controls)
- Uploaded optimized solitaire via /fs/upload?path=/sdcard/lua/solitaire.lua (24430 bytes)
- Tested fullscreen: display 320x480, no toolbar, screenshot shows chrome removed, white pixels 3197 (cards only)
- Logs show no continuous FPS spamming for solitaire (on-demand)
This commit is contained in:
Adolfo
2026-07-31 13:12:32 -04:00
parent 279893eed0
commit 4e4e815995
2 changed files with 159 additions and 91 deletions
+117 -50
View File
@@ -29,6 +29,8 @@
#define TAG "LuaGame"
#define GAME_W 240
#define GAME_H 240
#define GAME_W_DEFAULT 240
#define GAME_H_DEFAULT 240
#define TICK_MS 16
/* stubs for missing libc symbols */
@@ -51,6 +53,10 @@ typedef struct {
lv_obj_t* label_layer;
lv_obj_t* fps_label;
uint16_t* fb;
int fb_w;
int fb_h;
int game_w;
int game_h;
lv_timer_t* timer;
TaskHandle_t lua_task;
bool touch_pressed;
@@ -61,8 +67,10 @@ typedef struct {
bool lua_ok;
bool initialized;
bool is_pico;
bool wants_frame_updates;
uint64_t last_tick_us;
bool should_exit;
bool needs_redraw;
// fps
uint32_t fps_frames;
uint64_t fps_last_us;
@@ -77,8 +85,10 @@ static uint16_t hex_to_rgb565(uint32_t hex){
}
static inline void set_px(AppCtx* c,int x,int y,uint16_t col){
if(!c||!c->fb)return;
if(x<0||y<0||x>=GAME_W||y>=GAME_H)return;
c->fb[y*GAME_W+x]=col;
int w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
int h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
if(x<0||y<0||x>=w||y>=h)return;
c->fb[y*w+x]=col;
}
static void fill_rect(AppCtx* c,int x,int y,int w,int h,uint16_t col,bool filled){
if(w<=0||h<=0)return;
@@ -87,8 +97,10 @@ static void fill_rect(AppCtx* c,int x,int y,int w,int h,uint16_t col,bool filled
for(int j=y;j<y+h;j++){set_px(c,x,j,col);set_px(c,x+w-1,j,col);}
return;
}
int x0=x<0?0:x,y0=y<0?0:y,x1=x+w> GAME_W? GAME_W:x+w,y1=y+h>GAME_H?GAME_H:y+h;
for(int yy=y0;yy<y1;yy++){uint16_t* row=c->fb+yy*GAME_W; for(int xx=x0;xx<x1;xx++) row[xx]=col;}
int fb_w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
int fb_h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
int x0=x<0?0:x,y0=y<0?0:y,x1=x+w> fb_w? fb_w:x+w,y1=y+h>fb_h?fb_h:y+h;
for(int yy=y0;yy<y1;yy++){uint16_t* row=c->fb+yy*fb_w; for(int xx=x0;xx<x1;xx++) row[xx]=col;}
}
static void draw_circle(AppCtx* c,int cx,int cy,int r,uint16_t col,bool filled){
if(r<=0)return;
@@ -126,13 +138,14 @@ static void draw_text8(AppCtx* ctx, int x, int y, const char* txt, uint16_t col)
if(!txt) return;
int cx=x;
int cy=y;
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
for(int i=0; txt[i]; i++){
char ch=txt[i];
if(ch=='\n'){ cx=x; cy+=10; continue; }
draw_char8(ctx, cx, cy, ch, col);
cx+=8;
// wrap if exceeds game width
if(cx+8>GAME_W){ cx=x; cy+=10; }
if(cx+8>max_w){ cx=x; cy+=10; }
}
}
@@ -141,7 +154,9 @@ static int l_sys_log(lua_State* L){const char* m=luaL_checkstring(L,1); printf("
static int l_sys_clear(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
uint32_t hex=0; if(lua_gettop(L)>=1) hex=(uint32_t)(luaL_checknumber(L,1));
fill_rect(ctx,0,0,GAME_W,GAME_H,hex_to_rgb565(hex),true);
int w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
int h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
fill_rect(ctx,0,0,w,h,hex_to_rgb565(hex),true);
clear_labels(ctx);
return 0;
}
@@ -350,14 +365,38 @@ static int l_renderer_text_scaled(lua_State* L){
}
}
cx+=8*scale;
if(cx+8*scale>GAME_W){ cx=x; cy+=8*scale+2; }
{
int max_w = g_ctx && g_ctx->game_w ? g_ctx->game_w : GAME_W_DEFAULT;
if(cx+8*scale>max_w){ cx=x; cy+=8*scale+2; }
}
}
return 0;
}
static int l_game_width(lua_State* L){ lua_pushinteger(L, GAME_W); return 1; }
static int l_game_height(lua_State* L){ lua_pushinteger(L, GAME_H); return 1; }
static int l_game_set_frame_updates(lua_State* L){ (void)L; return 0; }
static int l_game_exit(lua_State* L){ (void)L; printf("[LuaGame] game.exit() called\n"); return 0; }
static int l_game_width(lua_State* L){
AppCtx* ctx=g_ctx;
int w = ctx && ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
lua_pushinteger(L, w); return 1;
}
static int l_game_height(lua_State* L){
AppCtx* ctx=g_ctx;
int h = ctx && ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
lua_pushinteger(L, h); return 1;
}
static int l_game_set_frame_updates(lua_State* L){
AppCtx* ctx=g_ctx;
bool en = false;
if(lua_gettop(L)>=1) en = lua_toboolean(L,1);
if(ctx) ctx->wants_frame_updates = en;
printf("[LuaGame] set_frame_updates %d\n", en?1:0);
return 0;
}
static int l_game_exit(lua_State* L){
(void)L;
AppCtx* ctx=g_ctx;
printf("[LuaGame] game.exit() called\n");
if(ctx) ctx->should_exit = true;
return 0;
}
static void register_sys(lua_State* L){
// sys table (Tactility original)
@@ -521,9 +560,24 @@ static void lua_init_task(void* arg){
ctx->fps_last_us=esp_timer_get_time();
ctx->fps_frames=0;
printf("[LuaGame] init done OK from %s (pico=%d)\n",loaded_from, ctx->is_pico?1:0);
// Initial draw for on-demand games
lua_getglobal(ctx->L,"draw");
if(lua_isfunction(ctx->L,-1)){
if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){
const char* e=lua_tostring(ctx->L,-1);
printf("[LUA ERR initial draw] %s\n",e);
lua_pop(ctx->L,1);
}
} else lua_pop(ctx->L,1);
if(tt_lvgl_lock(portMAX_DELAY)){
invalidate_canvas(ctx);
tt_lvgl_unlock();
}
printf("[LuaGame] entering game loop in same task\n");
// Enter game loop directly
// Enter game loop directly - on-demand for solitaire (no frame updates)
while(!ctx->should_exit){
if(ctx->lua_ok && ctx->initialized && ctx->L){
uint64_t now=esp_timer_get_time();
@@ -531,26 +585,21 @@ static void lua_init_task(void* arg){
if(dt>0.1f) dt=0.1f;
ctx->last_tick_us=now;
lua_State* L=ctx->L;
bool should_draw = false;
if(ctx->is_pico){
// Generate event based on touch
int ev_type = 6; // FRAME_TICK default
bool has_touch_event = false;
int ev_type = 6;
if(!ctx->prev_touch_pressed && ctx->touch_pressed){
ev_type = 1; // TOUCH_DOWN
has_touch_event = true;
ev_type = 1; has_touch_event = true;
} else if(ctx->prev_touch_pressed && !ctx->touch_pressed){
ev_type = 3; // TOUCH_UP
has_touch_event = true;
ev_type = 3; has_touch_event = true;
} else if(ctx->touch_pressed){
ev_type = 2; // TOUCH_MOVE
has_touch_event = true;
} else {
ev_type = 6; // FRAME_TICK
ev_type = 2; has_touch_event = true;
}
// Helper to push event table and call update
// First call with touch event if any
if(ctx->wants_frame_updates){
// Continuous: handle touch event if any, then FRAME_TICK every frame
if(has_touch_event){
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
@@ -561,16 +610,10 @@ static void lua_init_task(void* arg){
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
lua_pushinteger(L,0); lua_setfield(L,-2,"button_id");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else {
bool redraw = lua_toboolean(L,-1);
lua_pop(L,1);
// we will draw regardless, but could check redraw
(void)redraw;
}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
}
// Always also call with FRAME_TICK if not already frame tick
if(ev_type != 6){
// FRAME_TICK
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_newtable(L);
@@ -578,35 +621,37 @@ static void lua_init_task(void* arg){
lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x");
lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y");
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update FRAME_TICK] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else { lua_pop(L,1); }
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update FRAME] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
} else if(!has_touch_event){
// No touch, just frame tick (single call)
} else {
// On-demand: only when touch event
if(has_touch_event){
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_newtable(L);
lua_pushinteger(L, 6); lua_setfield(L,-2,"type");
lua_pushinteger(L, ev_type); lua_setfield(L,-2,"type");
lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x");
lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y");
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
lua_pushinteger(L,0); lua_setfield(L,-2,"button_id");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else {
bool redraw = lua_toboolean(L,-1);
lua_pop(L,1);
(void)redraw;
}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
}
}
ctx->prev_touch_pressed = ctx->touch_pressed;
} else {
// Original sys API
// Original sys API - continuous
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_pushnumber(L,dt);
if(lua_pcall(L,1,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
}else lua_pop(L,1);
should_draw = true;
}
if(should_draw){
lua_getglobal(L,"draw");
if(lua_isfunction(L,-1)){
if(lua_pcall(L,0,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR draw] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
@@ -617,7 +662,7 @@ static void lua_init_task(void* arg){
uint64_t elapsed = now - ctx->fps_last_us;
if(elapsed >= 1000000){
ctx->fps = (float)ctx->fps_frames * 1000000.0f / (float)elapsed;
printf("[LuaGame] FPS %.1f\n", ctx->fps);
printf("[LuaGame] FPS %.1f (on-demand)\n", ctx->fps);
if(ctx->fps_label){
char buf[32];
snprintf(buf,sizeof(buf),"FPS %.0f",ctx->fps);
@@ -629,8 +674,14 @@ static void lua_init_task(void* arg){
tt_lvgl_unlock();
}
}
}
// For on-demand games, sleep longer to save CPU
if(ctx->is_pico && !ctx->wants_frame_updates){
vTaskDelay(pdMS_TO_TICKS(50));
} else {
vTaskDelay(pdMS_TO_TICKS(TICK_MS));
}
}
printf("[LuaGame] game loop exit\n");
vTaskDelete(NULL);
}
@@ -683,14 +734,16 @@ static void touch_event_cb(lv_event_t* e){
if(!ctx)return;
lv_event_code_t code=lv_event_get_code(e);
lv_point_t pt; lv_indev_get_point(lv_indev_active(),&pt);
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
int max_h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
if(ctx->canvas){
lv_area_t area; lv_obj_get_coords(ctx->canvas,&area);
int lx=pt.x-area.x1;
int ly=pt.y-area.y1;
if(lx<0) lx=0;
if(lx>=GAME_W) lx=GAME_W-1;
if(lx>=max_w) lx=max_w-1;
if(ly<0) ly=0;
if(ly>=GAME_H) ly=GAME_H-1;
if(ly>=max_h) ly=max_h-1;
ctx->touch_x=lx; ctx->touch_y=ly;
}else{ctx->touch_x=pt.x; ctx->touch_y=pt.y;}
if(code==LV_EVENT_PRESSING||code==LV_EVENT_PRESSED||code==LV_EVENT_CLICKED) ctx->touch_pressed=true;
@@ -711,13 +764,27 @@ static void app_on_show(AppHandle app,void* data,lv_obj_t* parent){
g_ctx=ctx;
ctx->app=app; ctx->lua_ok=false; ctx->initialized=false; ctx->should_exit=false;
ctx->fps=0; ctx->fps_frames=0;
// Fullscreen: no toolbar, no statusbar (manifest HideStatusBar)
lv_obj_remove_flag(parent,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_pad_all(parent,0,0);
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_row(parent,0,0);
ctx->toolbar=tt_lvgl_toolbar_create_for_app(parent,app);
ctx->toolbar=NULL;
ctx->root=parent;
// Get display resolution for dynamic framebuffer (320x480 on ES3C35P)
lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL);
lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL);
if(disp_w<=0) disp_w = 320;
if(disp_h<=0) disp_h = 480;
ctx->game_w = disp_w;
ctx->game_h = disp_h;
ctx->fb_w = disp_w;
ctx->fb_h = disp_h;
printf("[LuaGame] display %dx%d fullscreen, no toolbar\n", (int)disp_w, (int)disp_h);
ctx->game_area=lv_obj_create(parent);
lv_obj_set_width(ctx->game_area,LV_PCT(100));
lv_obj_set_flex_grow(ctx->game_area,1);
@@ -728,12 +795,12 @@ static void app_on_show(AppHandle app,void* data,lv_obj_t* parent){
lv_obj_set_flex_align(ctx->game_area,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(ctx->game_area,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(ctx->game_area,LV_OBJ_FLAG_CLICKABLE);
size_t fb_bytes=GAME_W*GAME_H*sizeof(uint16_t);
size_t fb_bytes=(size_t)ctx->fb_w * ctx->fb_h * sizeof(uint16_t);
ctx->fb=alloc_psram(fb_bytes);
if(!ctx->fb){printf("[LuaGame] FB alloc fail\n"); return;}
if(!ctx->fb){printf("[LuaGame] FB alloc fail %dx%d\n", ctx->fb_w, ctx->fb_h); return;}
memset(ctx->fb,0,fb_bytes);
lv_obj_t* cont=lv_obj_create(ctx->game_area);
lv_obj_set_size(cont,GAME_W,GAME_H);
lv_obj_set_size(cont,ctx->fb_w,ctx->fb_h);
lv_obj_set_style_bg_color(cont,lv_color_black(),0);
lv_obj_set_style_border_width(cont,0,0);
lv_obj_set_style_pad_all(cont,0,0);
+4 -3
View File
@@ -2,7 +2,8 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32s3
app.id=one.tactility.luagame
app.version.name=0.1.0
app.version.code=1
app.version.name=0.2.0
app.version.code=2
app.name=Lua Game
app.description=Lua interpreter game engine (runs breakout.lua and custom games)
app.description=Lua interpreter game engine - fullscreen, no chrome
app.flags=HideStatusBar