From 4b2a22bf3ce7e0dc812b1f40fdf7e6004194fa6b Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Wed, 29 Jan 2025 07:21:09 +0300 Subject: [PATCH] twn_filewatch.c: file and directory change api, initial support for texture reload --- CMakeLists.txt | 1 + src/twn_amalgam.c | 1 + src/twn_filewatch.c | 171 ++++++++++++++++++++++++++++++ src/twn_filewatch_c.h | 23 ++++ src/twn_loop.c | 31 ++++-- src/twn_textures.c | 4 + third-party/x-watcher/x-watcher.h | 44 ++++---- 7 files changed, 246 insertions(+), 29 deletions(-) create mode 100644 src/twn_filewatch.c create mode 100644 src/twn_filewatch_c.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e5e667e..03d82e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,7 @@ set(TWN_NONOPT_SOURCE_FILES src/twn_input.c include/twn_input.h src/twn_camera.c src/twn_camera_c.h src/twn_textures.c src/twn_textures_c.h + src/twn_filewatch.c src/twn_filewatch_c.h src/rendering/twn_draw.c src/rendering/twn_draw_c.h src/rendering/twn_quads.c diff --git a/src/twn_amalgam.c b/src/twn_amalgam.c index f8e0613..09d7608 100644 --- a/src/twn_amalgam.c +++ b/src/twn_amalgam.c @@ -9,6 +9,7 @@ #include "twn_main.c" #include "twn_textures.c" #include "twn_util.c" +#include "twn_filewatch.c" #include "rendering/twn_circles.c" #include "rendering/twn_draw.c" diff --git a/src/twn_filewatch.c b/src/twn_filewatch.c new file mode 100644 index 0000000..a640fda --- /dev/null +++ b/src/twn_filewatch.c @@ -0,0 +1,171 @@ +#include "twn_filewatch_c.h" +#include "twn_util.h" +#include "twn_engine_context_c.h" + +#include +#include +#include + + +struct FilewatchPathToCallback { + char *path; + FileatchCallback callback; + float tick_last_reported; + enum FilewatchAction action_pending : 3; + bool action_processed : 1; +}; + +static struct FilewatchPathToCallback *filewatch_directories; +static struct FilewatchPathToCallback *filewatch_files; +/* note: it gets rebuilt on every addition, as api is such */ +/* TODO: test whether it's possible to miss on update while watcher is being rebuilt */ +/* we might have to enumerate things, just to make sure */ +static x_watcher *filewatcher; +static SDL_mutex *filewatcher_lock; + +/* TODO: check whether path and p->path match? */ +static void filewatch_dispatch(XWATCHER_FILE_EVENT event, + const char *path, + int context, + void *additional_data +) { + (void)additional_data; (void)path; + + enum FilewatchAction action; + + switch (event) { + case XWATCHER_FILE_CREATED: + action = FILEWATCH_ACTION_FILE_CREATED; + break; + case XWATCHER_FILE_REMOVED: + action = FILEWATCH_ACTION_FILE_DELETED; + break; + case XWATCHER_FILE_MODIFIED: + action = FILEWATCH_ACTION_FILE_MODIFIED; + break; + case XWATCHER_FILE_NONE: + case XWATCHER_FILE_OPENED: + case XWATCHER_FILE_RENAMED: + case XWATCHER_FILE_UNSPECIFIED: + case XWATCHER_FILE_ATTRIBUTES_CHANGED: + default: + return; + } + + SDL_LockMutex(filewatcher_lock); + + struct FilewatchPathToCallback *p; + + if (context < 0) + p = &filewatch_files[-context - 1]; + else + p = &filewatch_directories[context]; + + p->tick_last_reported = ctx.game.frame_number; + p->action_processed = false; + p->action_pending = action; + + SDL_UnlockMutex(filewatcher_lock); +} + + +static bool filewatcher_rebuild(void) { + if (filewatcher) + xWatcher_destroy(filewatcher); + + filewatcher = xWatcher_create(); + if (!filewatcher) { + log_warn("Error creating xWatcher instance, no file watching is done"); + return false; + } + + int const filewatch_directories_len = arrlen(filewatch_directories); + for (int i = 0; i < filewatch_directories_len; ++i) { + xWatcher_reference ref = { + .path = filewatch_directories[i].path, + .callback_func = filewatch_dispatch, + .context = i, + }; + if (!xWatcher_appendDir(filewatcher, &ref)) + log_warn("Error watching dir contents: %s", filewatch_directories[i].path); + } + + int const filewatch_files_len = arrlen(filewatch_files); + for (int i = 0; i < filewatch_files_len; ++i) { + xWatcher_reference ref = { + .path = filewatch_files[i].path, + .callback_func = filewatch_dispatch, + .context = -(i + 1), /* in negative range to allow inrefence */ + }; + if (!xWatcher_appendDir(filewatcher, &ref)) + log_warn("Error watching file: %s", filewatch_files[i].path); + } + + if (!xWatcher_start(filewatcher)) { + log_warn("Error creating xWatcher instance, no file watching is done"); + xWatcher_destroy(filewatcher); + filewatcher = NULL; + return false; + } + + return true; +} + + +bool filewatch_add_directory(char *dir, FileatchCallback callback) { + SDL_assert(dir && callback); + + struct FilewatchPathToCallback const w = { + .callback = callback, + .path = SDL_strdup(dir), + .action_processed = true, + .tick_last_reported = ctx.game.frame_number, + }; + arrpush(filewatch_directories, w); + + return filewatcher_rebuild(); +} + + +bool filewatch_add_file(char *filepath, FileatchCallback callback) { + SDL_assert(filepath && callback); + + struct FilewatchPathToCallback const f = { + .callback = callback, + .path = SDL_strdup(filepath), + .action_processed = true, + .tick_last_reported = ctx.game.frame_number, + }; + arrpush(filewatch_files, f); + + return filewatcher_rebuild(); +} + + +void filewatch_poll(void) { + SDL_LockMutex(filewatcher_lock); + + int const filewatch_directories_len = arrlen(filewatch_directories); + for (int i = 0; i < filewatch_directories_len; ++i) { + if (!filewatch_directories[i].action_processed + && (ctx.game.frame_number - filewatch_directories[i].tick_last_reported > FILEWATCH_MODIFIED_TICKS_MERGED)) { + SDL_assert(filewatch_directories[i].action_pending != FILEWATCH_ACTION_FILE_NONE); + filewatch_directories[i].callback(filewatch_directories[i].path, filewatch_directories[i].action_pending); + filewatch_directories[i].action_pending = FILEWATCH_ACTION_FILE_NONE; + filewatch_directories[i].action_processed = true; + } + } + + int const filewatch_files_len = arrlen(filewatch_files); + for (int i = 0; i < filewatch_files_len; ++i) { + if (!filewatch_files[i].action_processed + && (ctx.game.frame_number - filewatch_files[i].tick_last_reported > FILEWATCH_MODIFIED_TICKS_MERGED)) { + SDL_assert(filewatch_files[i].action_pending != FILEWATCH_ACTION_FILE_NONE); + filewatch_files[i].callback(filewatch_files[i].path, filewatch_files[i].action_pending); + filewatch_files[i].action_pending = FILEWATCH_ACTION_FILE_NONE; + filewatch_files[i].action_processed = true; + } + } + + SDL_UnlockMutex(filewatcher_lock); +} diff --git a/src/twn_filewatch_c.h b/src/twn_filewatch_c.h new file mode 100644 index 0000000..04a90fb --- /dev/null +++ b/src/twn_filewatch_c.h @@ -0,0 +1,23 @@ +#ifndef TWN_FILEWATCH_C_H +#define TWN_FILEWATCH_C_H + +#include + +#define FILEWATCH_MODIFIED_TICKS_MERGED 10 + +enum FilewatchAction { + FILEWATCH_ACTION_FILE_NONE, + FILEWATCH_ACTION_FILE_CREATED, + FILEWATCH_ACTION_FILE_DELETED, + FILEWATCH_ACTION_FILE_MODIFIED, +}; + +typedef void (*FileatchCallback)(char const *path, enum FilewatchAction action); + +bool filewatch_add_directory(char *dir, FileatchCallback callback); + +bool filewatch_add_file(char *file, FileatchCallback callback); + +void filewatch_poll(void); + +#endif diff --git a/src/twn_loop.c b/src/twn_loop.c index a769862..80a269b 100644 --- a/src/twn_loop.c +++ b/src/twn_loop.c @@ -1,5 +1,6 @@ #include "twn_loop.h" #include "twn_engine_context_c.h" +#include "twn_filewatch_c.h" #include "twn_input_c.h" #include "twn_util.h" #include "twn_util_c.h" @@ -733,8 +734,6 @@ static void clean_up(void) { text_cache_deinit(&ctx.text_cache); textures_cache_deinit(&ctx.texture_cache); - textures_reset_state(); - arrfree(ctx.render_queue_2d); toml_free(ctx.config_table); @@ -754,6 +753,25 @@ static void reset_state(void) { } +static void pack_contents_modified(char const *path, enum FilewatchAction action) { + log_info("Pack contents invalidated: %s, action: %i", path, action); + reset_state(); +} + + +/* TODO: handle .btw packs */ +static bool try_mounting_root_pack(char *path) { + log_info("Mounting %s", path); + + if (!PHYSFS_mount(path, NULL, true)) + return false; + + filewatch_add_directory(path, &pack_contents_modified); + + return true; +} + + int enter_loop(int argc, char **argv) { profile_start("startup"); @@ -795,10 +813,8 @@ int enter_loop(int argc, char **argv) { return EXIT_FAILURE; } - if (!PHYSFS_mount(argv[i+1], NULL, true)) { - CRY_PHYSFS("Data dir mount override failed."); + if (!try_mounting_root_pack(argv[i+1])) return EXIT_FAILURE; - } data_dir_mounted = true; @@ -823,10 +839,10 @@ int enter_loop(int argc, char **argv) { /* try mouning data folder first, relative to executable root */ char *full_path; SDL_asprintf(&full_path, "%sdata", ctx.base_dir); - if (!PHYSFS_mount(full_path, NULL, true)) { + if (!try_mounting_root_pack(full_path)) { SDL_free(full_path); SDL_asprintf(&full_path, "%sdata.btw", ctx.base_dir); - if (!PHYSFS_mount(full_path, NULL, true)) { + if (!try_mounting_root_pack(full_path)) { SDL_free(full_path); CRY_PHYSFS("Cannot find data.btw or data directory in root. Please create them or specify with --data-dir parameter."); return EXIT_FAILURE; @@ -857,6 +873,7 @@ int enter_loop(int argc, char **argv) { reset_state(); } + filewatch_poll(); main_loop(); } diff --git a/src/twn_textures.c b/src/twn_textures.c index 4ef1197..d4c69e3 100644 --- a/src/twn_textures.c +++ b/src/twn_textures.c @@ -308,6 +308,8 @@ static void update_texture_rects_in_atlas(TextureCache *cache, stbrp_rect *rects void textures_cache_init(TextureCache *cache, SDL_Window *window) { + SDL_zero(*cache); + cache->window = window; sh_new_arena(cache->hash); @@ -581,4 +583,6 @@ size_t textures_get_num_atlases(const TextureCache *cache) { } void textures_reset_state(void) { + textures_cache_deinit(&ctx.texture_cache); + textures_cache_init(&ctx.texture_cache, ctx.window); } diff --git a/third-party/x-watcher/x-watcher.h b/third-party/x-watcher/x-watcher.h index 0eebd9b..d04f802 100644 --- a/third-party/x-watcher/x-watcher.h +++ b/third-party/x-watcher/x-watcher.h @@ -37,7 +37,7 @@ typedef struct xWatcher_reference { void *additional_data; } xWatcher_reference; -struct file { +typedef struct xWatcher_file { // just the file name alone char *name; // used for adding (additional) context in the handler (if needed) @@ -50,11 +50,11 @@ struct file { const char *path, int context, void *additional_data); -} file; +} xWatcher_file; -struct directory { +typedef struct xWatcher_directory { // list of files - struct file *files; + struct xWatcher_file *files; char *path; // used for adding (additional) context in the handler (if needed) @@ -78,10 +78,10 @@ struct directory { #else #error "Unsupported" #endif -} directory; +} xWatcher_directory; typedef struct x_watcher { - struct directory *directories; + struct xWatcher_directory *directories; pthread_t thread; int thread_id; bool alive; @@ -118,7 +118,7 @@ typedef struct x_watcher { x_watcher *watcher = (x_watcher*) argument; char buffer[BUF_LEN]; ssize_t lenght; - struct directory *directories = watcher->directories; + struct xWatcher_directory *directories = watcher->directories; while(watcher->alive) { // poll for events @@ -150,7 +150,7 @@ typedef struct x_watcher { &buffer[i]; // find directory for which this even matches via the descriptor - struct directory *directory = NULL; + struct xWatcher_directory *directory = NULL; for(size_t j = 0; j < arr_count(directories); j++) { if(directories[j].inotify_watch_fd == event->wd) { directory = &directories[j]; @@ -163,7 +163,7 @@ typedef struct x_watcher { } // find matching file (if any) - struct file *file = NULL; + struct xWatcher_file *file = NULL; for(size_t j = 0; j < arr_count(directory->files); j++) { if(strcmp(directory->files[j].name, event->name) == 0) { file = &directory->files[j]; @@ -225,9 +225,9 @@ typedef struct x_watcher { // cleanup time for(size_t i = 0; i < arr_count(watcher->directories); i++) { - struct directory *directory = &watcher->directories[i]; + struct xWatcher_directory *directory = &watcher->directories[i]; for(size_t j = 0; j < arr_count(directory->files); j++) { - struct file *file = &directory->files[j]; + struct xWatcher_file *file = &directory->files[j]; free(file->name); } arr_free(directory->files); @@ -247,7 +247,7 @@ typedef struct x_watcher { static inline void *__internal_xWatcherProcess(void *argument) { x_watcher *watcher = (x_watcher*) argument; - struct directory *directories = watcher->directories; + struct xWatcher_directory *directories = watcher->directories; // create an event list so we can still make use of the Windows API HANDLE events[arr_count(directories)]; @@ -281,7 +281,7 @@ typedef struct x_watcher { } // shorhand for convenience - struct directory *dir = &directories[object_index]; + struct xWatcher_directory *dir = &directories[object_index]; // retrieve event data DWORD bytes_transferred; @@ -326,7 +326,7 @@ typedef struct x_watcher { } // find matching file (if any) - struct file *file = NULL; + struct xWatcher_file *file = NULL; for(size_t j = 0; j < arr_count(dir->files); j++) { if(strcmp(dir->files[j].name, name_char) == 0) { file = &dir->files[j]; @@ -423,9 +423,9 @@ typedef struct x_watcher { } // cleanup time for(size_t i = 0; i < arr_count(watcher->directories); i++) { - struct directory *directory = &watcher->directories[i]; + struct xWatcher_directory *directory = &watcher->directories[i]; for(size_t j = 0; j < arr_count(directory->files); j++) { - struct file *file = &directory->files[j]; + struct xWatcher_file *file = &directory->files[j]; free(file->name); } arr_free(directory->files); @@ -487,7 +487,7 @@ static inline bool xWatcher_appendFile( filename = path; } - struct directory *dir = NULL; + struct xWatcher_directory *dir = NULL; // check against the database of (pre-existing) directories for(size_t i = 0; i < arr_count(watcher->directories); i++) { @@ -499,7 +499,7 @@ static inline bool xWatcher_appendFile( // directory exists, check if an callback has been already added if(dir == NULL) { - struct directory new_dir; + struct xWatcher_directory new_dir; new_dir.callback_func = NULL; // DO NOT add callbacks if it's a file new_dir.context = 0; // context should be invalid as well @@ -524,7 +524,7 @@ static inline bool xWatcher_appendFile( } // search for the file - struct file *file = NULL; + struct xWatcher_file *file = NULL; for(size_t i = 0; i < arr_count(dir->files); i++) { if(strcmp(dir->files[i].name, filename) == 0) { file = &dir->files[i]; @@ -535,7 +535,7 @@ static inline bool xWatcher_appendFile( return false; // file already exists, that's an ERROR } - struct file new_file; + struct xWatcher_file new_file; // avoid an invalid free because this shares the memory space // of the full path string new_file.name = strdup(filename); @@ -667,7 +667,7 @@ static inline bool xWatcher_appendDir( if(path[strlen(path)-1] == DIRBRK) path[strlen(path)-1] = '\0'; - struct directory *dir = NULL; + struct xWatcher_directory *dir = NULL; // check against the database of (pre-existing) directories for(size_t i=0; i < arr_count(watcher->directories); i++) { @@ -689,7 +689,7 @@ static inline bool xWatcher_appendDir( dir->additional_data = reference->additional_data; } else { // keep an eye for this one as it's on the stack - struct directory dir; + struct xWatcher_directory dir; dir.path = path; dir.callback_func = reference->callback_func;