improve script hot reload
This commit is contained in:
parent
01d6e09a91
commit
15041984f1
25 changed files with 1516 additions and 293 deletions
|
|
@ -169,6 +169,12 @@ static const char* pxl8_ffi_cdefs =
|
|||
"typedef struct pxl8_gfx pxl8_gfx;\n"
|
||||
"typedef struct { int x, y, w, h; } pxl8_bounds;\n"
|
||||
"typedef struct { int x, y; } pxl8_point;\n"
|
||||
"typedef struct pxl8_rng { u32 state; } pxl8_rng;\n"
|
||||
"\n"
|
||||
"void pxl8_rng_seed(pxl8_rng* rng, u32 seed);\n"
|
||||
"u32 pxl8_rng_next(pxl8_rng* rng);\n"
|
||||
"f32 pxl8_rng_f32(pxl8_rng* rng);\n"
|
||||
"i32 pxl8_rng_range(pxl8_rng* rng, i32 min, i32 max);\n"
|
||||
"\n"
|
||||
"f32 pxl8_get_fps(const pxl8* sys);\n"
|
||||
"\n"
|
||||
|
|
@ -266,7 +272,7 @@ static const char* pxl8_ffi_cdefs =
|
|||
"} pxl8_raster_bar;\n"
|
||||
"\n"
|
||||
"typedef struct pxl8_particles pxl8_particles;\n"
|
||||
"pxl8_particles* pxl8_particles_create(u32 max_count);\n"
|
||||
"pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng);\n"
|
||||
"void pxl8_particles_destroy(pxl8_particles* particles);\n"
|
||||
"void pxl8_particles_clear(pxl8_particles* particles);\n"
|
||||
"void pxl8_particles_emit(pxl8_particles* particles, u32 count);\n"
|
||||
|
|
@ -460,7 +466,7 @@ static const char* pxl8_ffi_cdefs =
|
|||
"void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);\n"
|
||||
"void pxl8_sfx_node_destroy(pxl8_sfx_node* node);\n"
|
||||
"f32 pxl8_sfx_note_to_freq(u8 note);\n"
|
||||
"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume);\n"
|
||||
"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration);\n"
|
||||
"void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);\n"
|
||||
"pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);\n"
|
||||
"void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);\n"
|
||||
|
|
@ -684,6 +690,14 @@ void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) {
|
|||
}
|
||||
}
|
||||
|
||||
void pxl8_script_set_rng(pxl8_script* script, void* rng) {
|
||||
if (!script) return;
|
||||
if (script->L && rng) {
|
||||
lua_pushlightuserdata(script->L, rng);
|
||||
lua_setglobal(script->L, "_pxl8_rng");
|
||||
}
|
||||
}
|
||||
|
||||
void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) {
|
||||
if (!script) return;
|
||||
script->mixer = mixer;
|
||||
|
|
@ -747,6 +761,53 @@ pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
|
|||
return result;
|
||||
}
|
||||
|
||||
static time_t get_file_mod_time(const char* path) {
|
||||
struct stat file_stat;
|
||||
if (stat(path, &file_stat) == 0) {
|
||||
return file_stat.st_mtime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static time_t get_latest_script_mod_time(const char* dir_path) {
|
||||
DIR* dir = opendir(dir_path);
|
||||
if (!dir) return 0;
|
||||
|
||||
time_t latest = 0;
|
||||
struct dirent* entry;
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.') continue;
|
||||
|
||||
char full_path[512];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
time_t subdir_time = get_latest_script_mod_time(full_path);
|
||||
if (subdir_time > latest) {
|
||||
latest = subdir_time;
|
||||
}
|
||||
} else {
|
||||
size_t len = strlen(entry->d_name);
|
||||
bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) ||
|
||||
(len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0);
|
||||
|
||||
if (is_script) {
|
||||
time_t mod_time = get_file_mod_time(full_path);
|
||||
if (mod_time > latest) {
|
||||
latest = mod_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return latest;
|
||||
}
|
||||
|
||||
void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) {
|
||||
if (!script || !script->L || !cart_path || !original_cwd) return;
|
||||
|
||||
|
|
@ -779,6 +840,9 @@ void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const
|
|||
lua_pop(script->L, 1);
|
||||
}
|
||||
lua_pop(script->L, 1);
|
||||
|
||||
pxl8_strncpy(script->watch_dir, cart_path, sizeof(script->watch_dir));
|
||||
script->latest_mod_time = get_latest_script_mod_time(script->watch_dir);
|
||||
}
|
||||
|
||||
pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
|
||||
|
|
@ -985,7 +1049,7 @@ static bool pxl8_script_is_builtin_global(const char* name) {
|
|||
"print", "pxl8", "rawequal", "rawget", "rawset", "require",
|
||||
"select", "setfenv", "setmetatable", "string", "table",
|
||||
"tonumber", "tostring", "type", "unpack", "update", "frame",
|
||||
"xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_sfx_mixer", "_pxl8_sys",
|
||||
"xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_rng", "_pxl8_sfx_mixer", "_pxl8_sys",
|
||||
NULL
|
||||
};
|
||||
for (int i = 0; builtins[i]; i++) {
|
||||
|
|
@ -1004,6 +1068,57 @@ static void pxl8_script_cleanup(pxl8_script* script) {
|
|||
}
|
||||
}
|
||||
|
||||
static void pxl8_script_save_upvalues(lua_State* L, const char* func_name, int saved_table) {
|
||||
lua_getglobal(L, func_name);
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int func_idx = lua_gettop(L);
|
||||
for (int i = 1; ; i++) {
|
||||
const char* name = lua_getupvalue(L, func_idx, i);
|
||||
if (!name) break;
|
||||
|
||||
int vtype = lua_type(L, -1);
|
||||
if (name[0] != '(' &&
|
||||
vtype != LUA_TFUNCTION &&
|
||||
vtype != LUA_TUSERDATA &&
|
||||
vtype != LUA_TLIGHTUSERDATA &&
|
||||
vtype != LUA_TTHREAD &&
|
||||
vtype != LUA_TTABLE) {
|
||||
lua_pushstring(L, name);
|
||||
lua_pushvalue(L, -2);
|
||||
lua_settable(L, saved_table);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static void pxl8_script_restore_upvalues(lua_State* L, const char* func_name, int saved_table) {
|
||||
lua_getglobal(L, func_name);
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int func_idx = lua_gettop(L);
|
||||
for (int i = 1; ; i++) {
|
||||
const char* name = lua_getupvalue(L, func_idx, i);
|
||||
if (!name) break;
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, saved_table, name);
|
||||
if (!lua_isnil(L, -1)) {
|
||||
lua_setupvalue(L, func_idx, i);
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static void pxl8_script_save_globals(pxl8_script* script) {
|
||||
lua_State* L = script->L;
|
||||
|
||||
|
|
@ -1032,6 +1147,9 @@ static void pxl8_script_save_globals(pxl8_script* script) {
|
|||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
pxl8_script_save_upvalues(L, "update", saved_table);
|
||||
pxl8_script_save_upvalues(L, "frame", saved_table);
|
||||
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
|
||||
pxl8_debug("Hot reload state saved");
|
||||
}
|
||||
|
|
@ -1045,13 +1163,19 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
|||
return;
|
||||
}
|
||||
|
||||
int saved_table = lua_gettop(L);
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
while (lua_next(L, saved_table) != 0) {
|
||||
lua_pushvalue(L, -2);
|
||||
lua_pushvalue(L, -2);
|
||||
lua_settable(L, LUA_GLOBALSINDEX);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
pxl8_script_restore_upvalues(L, "update", saved_table);
|
||||
pxl8_script_restore_upvalues(L, "frame", saved_table);
|
||||
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_pushnil(L);
|
||||
|
|
@ -1059,51 +1183,224 @@ static void pxl8_script_restore_globals(pxl8_script* script) {
|
|||
pxl8_debug("Hot reload state restored");
|
||||
}
|
||||
|
||||
static time_t get_file_mod_time(const char* path) {
|
||||
struct stat file_stat;
|
||||
if (stat(path, &file_stat) == 0) {
|
||||
return file_stat.st_mtime;
|
||||
}
|
||||
return 0;
|
||||
#define SER_NIL 0
|
||||
#define SER_BOOL 1
|
||||
#define SER_NUMBER 2
|
||||
#define SER_STRING 3
|
||||
#define SER_TABLE 4
|
||||
|
||||
typedef struct {
|
||||
u8* data;
|
||||
u32 size;
|
||||
u32 capacity;
|
||||
} ser_buffer;
|
||||
|
||||
static void ser_buffer_init(ser_buffer* buf) {
|
||||
buf->capacity = 1024;
|
||||
buf->data = malloc(buf->capacity);
|
||||
buf->size = 0;
|
||||
}
|
||||
|
||||
static time_t get_latest_script_mod_time(const char* dir_path) {
|
||||
DIR* dir = opendir(dir_path);
|
||||
if (!dir) return 0;
|
||||
static void ser_buffer_grow(ser_buffer* buf, u32 needed) {
|
||||
if (buf->size + needed > buf->capacity) {
|
||||
while (buf->size + needed > buf->capacity) {
|
||||
buf->capacity *= 2;
|
||||
}
|
||||
buf->data = realloc(buf->data, buf->capacity);
|
||||
}
|
||||
}
|
||||
|
||||
time_t latest = 0;
|
||||
struct dirent* entry;
|
||||
static void ser_write_u8(ser_buffer* buf, u8 v) {
|
||||
ser_buffer_grow(buf, 1);
|
||||
buf->data[buf->size++] = v;
|
||||
}
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.') continue;
|
||||
static void ser_write_u32(ser_buffer* buf, u32 v) {
|
||||
ser_buffer_grow(buf, 4);
|
||||
buf->data[buf->size++] = v & 0xFF;
|
||||
buf->data[buf->size++] = (v >> 8) & 0xFF;
|
||||
buf->data[buf->size++] = (v >> 16) & 0xFF;
|
||||
buf->data[buf->size++] = (v >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
char full_path[512];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
static void ser_write_f64(ser_buffer* buf, f64 v) {
|
||||
ser_buffer_grow(buf, 8);
|
||||
memcpy(buf->data + buf->size, &v, 8);
|
||||
buf->size += 8;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
time_t subdir_time = get_latest_script_mod_time(full_path);
|
||||
if (subdir_time > latest) {
|
||||
latest = subdir_time;
|
||||
}
|
||||
} else {
|
||||
size_t len = strlen(entry->d_name);
|
||||
bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) ||
|
||||
(len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0);
|
||||
static void ser_write_bytes(ser_buffer* buf, const void* data, u32 len) {
|
||||
ser_buffer_grow(buf, len);
|
||||
memcpy(buf->data + buf->size, data, len);
|
||||
buf->size += len;
|
||||
}
|
||||
|
||||
if (is_script) {
|
||||
time_t mod_time = get_file_mod_time(full_path);
|
||||
if (mod_time > latest) {
|
||||
latest = mod_time;
|
||||
}
|
||||
}
|
||||
static void ser_write_value(ser_buffer* buf, lua_State* L, int idx, int depth) {
|
||||
int t = lua_type(L, idx);
|
||||
switch (t) {
|
||||
case LUA_TNIL:
|
||||
ser_write_u8(buf, SER_NIL);
|
||||
break;
|
||||
case LUA_TBOOLEAN:
|
||||
ser_write_u8(buf, SER_BOOL);
|
||||
ser_write_u8(buf, lua_toboolean(L, idx) ? 1 : 0);
|
||||
break;
|
||||
case LUA_TNUMBER:
|
||||
ser_write_u8(buf, SER_NUMBER);
|
||||
ser_write_f64(buf, lua_tonumber(L, idx));
|
||||
break;
|
||||
case LUA_TSTRING: {
|
||||
size_t len;
|
||||
const char* str = lua_tolstring(L, idx, &len);
|
||||
ser_write_u8(buf, SER_STRING);
|
||||
ser_write_u32(buf, (u32)len);
|
||||
ser_write_bytes(buf, str, (u32)len);
|
||||
break;
|
||||
}
|
||||
case LUA_TTABLE: {
|
||||
if (depth > 16) { ser_write_u8(buf, SER_NIL); break; }
|
||||
ser_write_u8(buf, SER_TABLE);
|
||||
lua_pushvalue(L, idx);
|
||||
int tbl = lua_gettop(L);
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, tbl) != 0) {
|
||||
ser_write_value(buf, L, -2, depth + 1);
|
||||
ser_write_value(buf, L, -1, depth + 1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
ser_write_u8(buf, SER_NIL);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ser_write_u8(buf, SER_NIL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data) {
|
||||
if (!script || !out_data) return 0;
|
||||
lua_State* L = script->L;
|
||||
|
||||
ser_buffer buf;
|
||||
ser_buffer_init(&buf);
|
||||
|
||||
lua_pushvalue(L, LUA_GLOBALSINDEX);
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
const char* name = lua_tostring(L, -2);
|
||||
int vtype = lua_type(L, -1);
|
||||
|
||||
if (!pxl8_script_is_builtin_global(name) &&
|
||||
vtype != LUA_TFUNCTION &&
|
||||
vtype != LUA_TUSERDATA &&
|
||||
vtype != LUA_TLIGHTUSERDATA &&
|
||||
vtype != LUA_TTHREAD &&
|
||||
vtype != 10) {
|
||||
ser_write_value(&buf, L, -2, 0);
|
||||
ser_write_value(&buf, L, -1, 0);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
ser_write_u8(&buf, SER_NIL);
|
||||
|
||||
*out_data = buf.data;
|
||||
return buf.size;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const u8* data;
|
||||
u32 size;
|
||||
u32 pos;
|
||||
} deser_buffer;
|
||||
|
||||
static u8 deser_read_u8(deser_buffer* buf) {
|
||||
if (buf->pos >= buf->size) return 0;
|
||||
return buf->data[buf->pos++];
|
||||
}
|
||||
|
||||
static u32 deser_read_u32(deser_buffer* buf) {
|
||||
if (buf->pos + 4 > buf->size) return 0;
|
||||
u32 v = buf->data[buf->pos] | (buf->data[buf->pos+1] << 8) |
|
||||
(buf->data[buf->pos+2] << 16) | (buf->data[buf->pos+3] << 24);
|
||||
buf->pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
static f64 deser_read_f64(deser_buffer* buf) {
|
||||
if (buf->pos + 8 > buf->size) return 0;
|
||||
f64 v;
|
||||
memcpy(&v, buf->data + buf->pos, 8);
|
||||
buf->pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
static void deser_read_value(deser_buffer* buf, lua_State* L, int depth) {
|
||||
u8 type = deser_read_u8(buf);
|
||||
switch (type) {
|
||||
case SER_NIL:
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
case SER_BOOL:
|
||||
lua_pushboolean(L, deser_read_u8(buf));
|
||||
break;
|
||||
case SER_NUMBER:
|
||||
lua_pushnumber(L, deser_read_f64(buf));
|
||||
break;
|
||||
case SER_STRING: {
|
||||
u32 len = deser_read_u32(buf);
|
||||
if (buf->pos + len <= buf->size) {
|
||||
lua_pushlstring(L, (const char*)(buf->data + buf->pos), len);
|
||||
buf->pos += len;
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SER_TABLE: {
|
||||
if (depth > 16) { lua_pushnil(L); break; }
|
||||
lua_newtable(L);
|
||||
while (buf->pos < buf->size) {
|
||||
u8 key_type = deser_read_u8(buf);
|
||||
if (key_type == SER_NIL) break;
|
||||
buf->pos--;
|
||||
deser_read_value(buf, L, depth + 1);
|
||||
deser_read_value(buf, L, depth + 1);
|
||||
lua_settable(L, -3);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
lua_pushnil(L);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) {
|
||||
if (!script || !data || size == 0) return;
|
||||
lua_State* L = script->L;
|
||||
|
||||
deser_buffer buf = { .data = data, .size = size, .pos = 0 };
|
||||
|
||||
while (buf.pos < buf.size) {
|
||||
u8 key_type = deser_read_u8(&buf);
|
||||
if (key_type == SER_NIL) break;
|
||||
buf.pos--;
|
||||
|
||||
deser_read_value(&buf, L, 0);
|
||||
deser_read_value(&buf, L, 0);
|
||||
lua_settable(L, LUA_GLOBALSINDEX);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return latest;
|
||||
pxl8_debug("Deserialized globals from %u bytes", size);
|
||||
}
|
||||
|
||||
void pxl8_script_free_serialized(u8* data) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
|
||||
|
|
@ -1188,18 +1485,17 @@ bool pxl8_script_check_reload(pxl8_script* script) {
|
|||
|
||||
if (ext && strcmp(ext, ".fnl") == 0) {
|
||||
if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) {
|
||||
pxl8_script_call_function(script, "init");
|
||||
reloaded = true;
|
||||
}
|
||||
} else if (ext && strcmp(ext, ".lua") == 0) {
|
||||
if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) {
|
||||
pxl8_script_call_function(script, "init");
|
||||
reloaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reloaded) {
|
||||
pxl8_script_restore_globals(script);
|
||||
pxl8_script_call_function(script, "init");
|
||||
}
|
||||
|
||||
return reloaded;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue