Last active
February 1, 2021 02:32
-
-
Save dbechrd/822a4e5aab3a5ff06efb686e17b08936 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/////////////////////////////////////////////////////// | |
// asset_watcher.h | |
/////////////////////////////////////////////////////// | |
#pragma once | |
#include "SDL/SDL_thread.h" | |
#include "SDL/SDL_mutex.h" | |
typedef enum ta_watcher_result { | |
TA_WATCHER_SUCCESS = 0, | |
TA_WATCHER_ERR_INVALID_HANDLE = -1, | |
TA_WATCHER_ERR_READ_DIRECTORY_CHANGES = -2, | |
TA_WATCHER_ERR_BUFFER_SIZE_OUT_OF_RANGE = -3, | |
} ta_watcher_result; | |
typedef struct ta_asset_change_record { | |
char *path; // relative name of files with detected changes | |
double changed_at_ms; // elapsed_ms when was detected (used to delay handling to allow file handle to close) | |
} ta_asset_change_record; | |
typedef struct ta_asset_watcher { | |
SDL_Thread *thread; // asset watcher thread | |
SDL_mutex *mutex; // mutex to be used for all access to this data structure | |
bool signal_exit; // if true, main thread is requesting asset watcher to clean up for exit | |
// Protected by mutex, *must* lock before accessing this buffer | |
ta_asset_change_record *changes; // unhandled changes buffer | |
// NOTE: This is not protected by the mutex, it should only be set once before the thread is created | |
const char *dir_path; // directory path to watch for file changes | |
} ta_asset_watcher; | |
void ta_asset_watcher_start(ta_asset_watcher *watcher, const char *directory, size_t directory_len); | |
void ta_asset_watcher_stop(ta_asset_watcher *watcher); | |
/////////////////////////////////////////////////////// | |
// asset_watcher.c | |
/////////////////////////////////////////////////////// | |
#include "ta_asset_watcher.h" | |
#include "tinycthread/source/tinycthread.h" | |
#include <windows.h> | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <tchar.h> | |
static ta_watcher_result ta_open_directory(const char *path, HANDLE *handle) | |
{ | |
// Open directory handle | |
HANDLE hnd = CreateFile( | |
path, | |
FILE_LIST_DIRECTORY, | |
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, | |
NULL, | |
OPEN_EXISTING, | |
FILE_FLAG_BACKUP_SEMANTICS, | |
NULL | |
); | |
if (hnd == INVALID_HANDLE_VALUE) { | |
return TA_WATCHER_ERR_INVALID_HANDLE; | |
} | |
*handle = hnd; | |
return TA_WATCHER_SUCCESS; | |
} | |
static ta_watcher_result ta_asset_watcher_wait_changes(ta_asset_watcher *watcher, HANDLE handle) | |
{ | |
DWORD bytesReturned = 0; | |
char *buffer[1024] = { 0 }; | |
// NOTE: Blocking call, waits for directory changes | |
DWORD success = ReadDirectoryChangesW( | |
handle, | |
buffer, | |
sizeof(buffer), | |
TRUE, | |
FILE_NOTIFY_CHANGE_FILE_NAME | |
//| FILE_NOTIFY_CHANGE_DIR_NAME | |
//| FILE_NOTIFY_CHANGE_ATTRIBUTES | |
//| FILE_NOTIFY_CHANGE_SIZE | |
| FILE_NOTIFY_CHANGE_LAST_WRITE | |
//| FILE_NOTIFY_CHANGE_LAST_ACCESS | |
//| FILE_NOTIFY_CHANGE_CREATION | |
//| FILE_NOTIFY_CHANGE_SECURITY | |
, | |
&bytesReturned, | |
NULL, | |
NULL | |
); | |
if (!success) { | |
DWORD err = GetLastError(); | |
printf("[ASSET_WATCHER] ERROR: ReadDirectoryChangesW failed with error code: %lu\n", err); | |
return TA_WATCHER_ERR_READ_DIRECTORY_CHANGES; | |
} | |
if (bytesReturned == 0) { | |
printf("[ASSET_WATCHER] WARNING: ReadDirectoryChangesW failed, buffer too small or too big.\n"); | |
return TA_WATCHER_ERR_BUFFER_SIZE_OUT_OF_RANGE; | |
} | |
//printf("[ASSET_WATCHER] bytes_returned = %u\n", bytesReturned); | |
FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *)buffer; | |
for (;;) { | |
#if 0 | |
const char *action_str = 0; | |
switch (info->Action) { | |
case FILE_ACTION_ADDED : action_str = "File created "; break; | |
case FILE_ACTION_REMOVED : action_str = "File removed "; break; | |
case FILE_ACTION_MODIFIED : action_str = "File modified "; break; | |
case FILE_ACTION_RENAMED_OLD_NAME: action_str = "File renamed from"; break; | |
case FILE_ACTION_RENAMED_NEW_NAME: action_str = "File renamed to "; break; | |
} | |
// NOTE: This doesn't work properly because %.*s doesn't work properly for wide char strings? | |
if (action_str) { | |
printf("[ASSET_WATCHER] %s '%.*ls'\n", action_str, info->FileNameLength, info->FileName); | |
} else { | |
printf("[ASSET_WATCHER] UNKOWN (%u) '%.*ls'\n", info->Action, info->FileNameLength, info->FileName); | |
} | |
#endif | |
if (info->Action == FILE_ACTION_ADDED | |
|| info->Action == FILE_ACTION_MODIFIED | |
|| info->Action == FILE_ACTION_RENAMED_NEW_NAME) | |
{ | |
DLB_ASSERT(info->FileNameLength); | |
DLB_ASSERT(info->FileName); | |
// If multi-bype size of locale is > 1, the file buffer below could overflow | |
DLB_ASSERT(MB_CUR_MAX == 1); | |
bool is_file = false; | |
int file_len = info->FileNameLength / sizeof(wchar_t); | |
char *file = (char *)dlb_calloc(1, file_len + 1); | |
for (int j = 0; j < file_len; ++j) { | |
int result = wctomb(&file[j], info->FileName[j]); | |
if (result == -1) { | |
file[j] = '?'; | |
} else if (file[j] == '\\') { | |
file[j] = '/'; | |
} | |
if (file[j] == '.') { | |
is_file = true; | |
} | |
} | |
// NOTE: If not empty slot is found, we simply discard the change notification. I don't know how to resize | |
// the buffer in a thread-safe way, and this isn't a vital thing to detect. | |
if (is_file) { | |
// Block until mutex available | |
int lock_status = SDL_LockMutex(watcher->mutex); | |
if (lock_status == 0) { | |
ta_asset_change_record *record = dlb_vec_alloc(watcher->changes); | |
record->path = file; | |
record->changed_at_ms = ta_timer_elapsed_ms();; | |
DLB_ASSERT(SDL_UnlockMutex(watcher->mutex) == 0); | |
} else { | |
DLB_ASSERT(lock_status < 0); | |
printf("SDL_LockMutex failed in ta_asset_watcher_wait_changes: %s\n", SDL_GetError()); | |
DLB_ASSERT(!"Failed to lock mutex due to error!"); | |
} | |
} | |
} | |
if (!info->NextEntryOffset) { | |
break; | |
} | |
info = (FILE_NOTIFY_INFORMATION *)((char *)info + info->NextEntryOffset); | |
} | |
return TA_WATCHER_SUCCESS; | |
} | |
static int ta_asset_watcher_watch(void *data) | |
{ | |
ta_watcher_result err; | |
ta_asset_watcher *watcher = (ta_asset_watcher *)data; | |
HANDLE handle; | |
err = ta_open_directory(watcher->dir_path, &handle); | |
// Watch the directory for file changes | |
while (!err) { | |
// Block until mutex available | |
int lock_status = SDL_LockMutex(watcher->mutex); | |
if (lock_status == 0) { | |
if (watcher->signal_exit) { | |
// NOTE: This should only fail if the mutex was locked by another thread, which shouldn't be possible | |
DLB_ASSERT(SDL_UnlockMutex(watcher->mutex) == 0); | |
break; | |
} | |
// NOTE: This should only fail if the mutex was locked by another thread, which shouldn't be possible | |
DLB_ASSERT(SDL_UnlockMutex(watcher->mutex) == 0); | |
} else { | |
DLB_ASSERT(lock_status < 0); | |
printf("SDL_LockMutex failed in ta_asset_watcher_watch: %s\n", SDL_GetError()); | |
DLB_ASSERT(!"Failed to lock mutex due to error!"); | |
} | |
printf("[ASSET_WATCHER] Querying changes...\n"); | |
err = ta_asset_watcher_wait_changes(watcher, handle); | |
} | |
switch (err) { | |
case TA_WATCHER_ERR_INVALID_HANDLE: | |
printf("[ASSET_WATCHER] ERROR: Failed to open directory handle.\n"); | |
break; | |
case TA_WATCHER_ERR_READ_DIRECTORY_CHANGES: | |
printf("[ASSET_WATCHER] ERROR: Failed to read directory changes.\n"); | |
break; | |
case TA_WATCHER_ERR_BUFFER_SIZE_OUT_OF_RANGE: | |
printf("[ASSET_WATCHER] ERROR: Failed to populate change buffer due to size (out of range).\n"); | |
break; | |
default: | |
break; | |
} | |
// Clean up | |
DLB_ASSERT(SDL_UnlockMutex(watcher->mutex) == 0); | |
SDL_DestroyMutex(watcher->mutex); | |
dlb_vec_free(watcher->changes); | |
CloseHandle(handle); | |
return (int)err; | |
} | |
//----------------------------------------------------- | |
// NOTE: This section is executing in the main thread | |
//----------------------------------------------------- | |
void ta_asset_watcher_start(ta_asset_watcher *watcher, const char *directory, size_t directory_len) | |
{ | |
DLB_ASSERT(directory); | |
DLB_ASSERT(directory_len); | |
watcher->dir_path = directory; | |
DLB_ASSERT(watcher->dir_path[directory_len - 1] == '/'); // Directory must end with slash | |
SDL_Thread *thread = SDL_CreateThread(&ta_asset_watcher_watch, "ta_asset_watcher_watch", watcher); | |
if (!thread) { | |
printf("SDL_CreateThread failed: %s\n", SDL_GetError()); | |
DLB_ASSERT(!"Failed to create asset watcher thread"); | |
return; | |
} | |
watcher->mutex = SDL_CreateMutex(); | |
} | |
void ta_asset_watcher_stop(ta_asset_watcher *watcher) | |
{ | |
DLB_ASSERT(SDL_LockMutex(watcher->mutex) == 0); | |
printf("[ASSET_WATCHER] Stop requested, signaling exit...\n"); | |
watcher->signal_exit = true; | |
DLB_ASSERT(SDL_UnlockMutex(watcher->mutex) == 0); | |
int status = 0; | |
SDL_WaitThread(watcher->thread, &status); | |
} | |
//----------------------------------------------------- | |
/////////////////////////////////////////////////////// | |
// game.c (or any other file, runs on main thread) | |
/////////////////////////////////////////////////////// | |
static void game_hotload_textures() | |
{ | |
// NOTE: Hot reload at most 1 texture per frame | |
int lock_status = SDL_TryLockMutex(tg_game.texture_watcher.mutex); | |
if (lock_status == 0) { | |
if (dlb_vec_len(tg_game.texture_watcher.changes)) { | |
ta_asset_change_record *change = dlb_vec_last(tg_game.texture_watcher.changes); | |
// NOTE: Wait for Paint.NET to finalize it's weird copy/rename nonsense and let go of the file handle | |
if (ta_timer_elapsed_ms() > change->changed_at_ms + 500) { | |
DLB_ASSERT(change); | |
DLB_ASSERT(change->path); | |
ta_texture *tex = (ta_texture *)ta_game_by_name_try(RES_TEXTURE, change->path, strlen(change->path)); | |
if (tex) { | |
printf("[GAME] hot-loading: %s\n", change->path); | |
ta_texture_hot_reload(tex); | |
} else { | |
printf("[GAME] hot-load requested but texture not found: %s\n", change->path); | |
} | |
dlb_free(change->path); | |
dlb_vec_popz(tg_game.texture_watcher.changes); | |
} | |
} | |
// NOTE: This should only fail if the mutex was locked by another thread, which shouldn't be possible | |
DLB_ASSERT(SDL_UnlockMutex(tg_game.texture_watcher.mutex) == 0); | |
} else if (lock_status == SDL_MUTEX_TIMEDOUT) { | |
// Wait until next frame | |
} else { | |
DLB_ASSERT(lock_status < 0); | |
printf("SDL_TryLockMutex failed in game_hotload_textures: %s\n", SDL_GetError()); | |
DLB_ASSERT(!"Failed to lock mutex due to error!"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment