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:
+117
-50
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user