Commit: 904252067b7f7d1d79f4096b7f2363042d70dcd6
Parent: 2ad319b13c7fe4d86031fc7bdbe663fca728611e
Author: Randy Palamar
Date: Wed, 11 Dec 2024 07:57:30 -0700
make hot reloading a little more bulletproof
First, we need to guard against broken linkers (LLD) deleting the
shared library long before they are ready to write the output
(instead of just moving a temporary file overtop of the existing
one).
Second we shouldn't unload the code that the render thread is
executing while it is in the middle of executing it.
Diffstat:
4 files changed, 131 insertions(+), 46 deletions(-)
diff --git a/platform_linux_common.c b/platform_linux_common.c
@@ -57,11 +57,12 @@
struct __attribute__((aligned(16))) stack_base {
void (*entry)(struct stack_base *stack);
- Arena thread_arena;
- void *window;
- TerminalMemory *terminal_memory;
- TerminalInput *input;
- i32 work_futex;
+ Arena thread_arena;
+ void *window;
+ TerminalMemory *terminal_memory;
+ TerminalInput *input;
+ i32 work_futex;
+ b32 thread_asleep;
};
typedef struct {
@@ -72,6 +73,13 @@ typedef struct {
i32 handle;
} linux_file_watch;
+typedef struct linux_deferred_file_reload_queue {
+ struct linux_deferred_file_reload_queue *next;
+ struct linux_deferred_file_reload_queue *last;
+ i32 index;
+ i32 failures;
+} linux_deferred_file_reload_queue;
+
typedef struct {
iptr handle;
iptr process_id;
@@ -90,11 +98,15 @@ typedef struct {
i32 inotify_fd;
i32 win_fd;
+ linux_deferred_file_reload_queue file_reload_queue;
+ linux_deferred_file_reload_queue *file_reload_free_list;
linux_file_watch file_watches[32];
i32 file_watch_count;
Stream error_stream;
+ struct stack_base *render_stack;
+
#ifdef _DEBUG
void *library_handle;
#endif
@@ -354,14 +366,72 @@ static PLATFORM_ADD_FILE_WATCH_FN(linux_add_file_watch)
syscall2(SYS_stat, (iptr)path, (iptr)sb);
i32 wd = syscall3(SYS_inotify_add_watch, linux_ctx.inotify_fd, (iptr)path, LINUX_INOTIFY_MASK);
- i32 idx = linux_ctx.file_watch_count++;
- ASSERT(idx < ARRAY_COUNT(linux_ctx.file_watches));
-
- linux_ctx.file_watches[idx].fn = fn;
- linux_ctx.file_watches[idx].path = path;
- linux_ctx.file_watches[idx].handle = wd;
- linux_ctx.file_watches[idx].inode = STAT_INODE(sb);
- linux_ctx.file_watches[idx].user_ctx = user_ctx;
+ if (wd < 4096UL) {
+ i32 idx = linux_ctx.file_watch_count++;
+ ASSERT(idx < ARRAY_COUNT(linux_ctx.file_watches));
+ linux_ctx.file_watches[idx].fn = fn;
+ linux_ctx.file_watches[idx].path = path;
+ linux_ctx.file_watches[idx].handle = wd;
+ linux_ctx.file_watches[idx].inode = STAT_INODE(sb);
+ linux_ctx.file_watches[idx].user_ctx = user_ctx;
+ }
+}
+
+static void
+try_deferred_file_loads(PlatformCtx *ctx)
+{
+ linux_deferred_file_reload_queue *file = ctx->file_reload_queue.next;
+ while (file) {
+ linux_file_watch *fw = ctx->file_watches + file->index;
+
+ stat_buffer sb;
+ syscall2(SYS_stat, (iptr)fw->path, (iptr)sb);
+
+ fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path,
+ LINUX_INOTIFY_MASK);
+ fw->inode = STAT_INODE(sb);
+
+ if (fw->handle < -4096UL) {
+ fw->fn(fw->path, fw->user_ctx);
+ file->last->next = file->next;
+ file->next = ctx->file_reload_free_list;
+ ctx->file_reload_free_list = file;
+ file = file->last;
+ } else {
+ file->failures++;
+ #if 0
+ TODO
+ if (file->failures > MAX_FILE_RELOAD_TRIES) {
+ log
+ remove from list
+ }
+ #endif
+ }
+ file = file->next;
+ }
+}
+
+static b32
+defer_file_reload(PlatformCtx *ctx, i32 file_watch_index, stat_buffer *sb)
+{
+ b32 result = 1;
+ linux_file_watch *fw = ctx->file_watches + file_watch_index;
+
+ fw->inode = STAT_INODE(*sb);
+ fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path, LINUX_INOTIFY_MASK);
+
+ if (fw->handle >= -4096UL) {
+ result = 0;
+
+ linux_deferred_file_reload_queue *new = ctx->file_reload_free_list;
+ if (new) ctx->file_reload_free_list = new->next;
+ else new = push_struct(&ctx->platform_memory, typeof(*new));
+ new->index = file_watch_index;
+ new->failures = 0;
+ DLLPushDown(&ctx->file_reload_queue, new);
+ }
+
+ return result;
}
static void
@@ -385,27 +455,23 @@ dispatch_file_watch_events(PlatformCtx *ctx)
ie = (void *)data;
for (i32 i = 0; i < ctx->file_watch_count; i++) {
linux_file_watch *fw = ctx->file_watches + i;
- if (fw->handle == ie->wd) {
- b32 file_changed = (ie->mask & IN_CLOSE_WRITE) != 0;
- file_changed |= (ie->mask & IN_MODIFY) != 0;
- /* NOTE: some editors and the compiler will rewrite a file
- * completely and thus the inode will change; here we
- * detect that and restart the watch */
- stat_buffer sb;
- syscall2(SYS_stat, (iptr)fw->path, (iptr)sb);
- if (fw->inode != STAT_INODE(sb)) {
- syscall2(SYS_inotify_rm_watch, ctx->inotify_fd,
- (iptr)fw->handle);
- fw->inode = STAT_INODE(sb);
- fw->handle = syscall3(SYS_inotify_add_watch,
- ctx->inotify_fd,
- (iptr)fw->path,
- LINUX_INOTIFY_MASK);
- file_changed = 1;
- }
- if (file_changed)
- fw->fn(fw->path, fw->user_ctx);
+ if (fw->handle != ie->wd)
+ continue;
+
+ b32 file_changed = (ie->mask & IN_CLOSE_WRITE) != 0;
+ file_changed |= (ie->mask & IN_MODIFY) != 0;
+ /* NOTE: some editors and the compiler will rewrite a file
+ * completely and thus the inode will change; here we
+ * detect that and restart the watch */
+ stat_buffer sb = {0};
+ syscall2(SYS_stat, (iptr)fw->path, (iptr)sb);
+ if (fw->inode != STAT_INODE(sb)) {
+ syscall2(SYS_inotify_rm_watch, ctx->inotify_fd, fw->handle);
+ fw->handle = INVALID_FILE;
+ file_changed = defer_file_reload(ctx, i, &sb);
}
+ if (file_changed)
+ fw->fn(fw->path, fw->user_ctx);
}
}
}
diff --git a/platform_linux_x11.c b/platform_linux_x11.c
@@ -42,6 +42,10 @@ static PLATFORM_FILE_WATCH_CALLBACK_FN(debug_reload_library)
if (ctx->input.executable_reloaded)
return;
+
+ /* NOTE(rnp): spin until render thread finishes its work */
+ while (!ctx->render_stack->thread_asleep);
+
ctx->input.executable_reloaded = 1;
s8 nl = s8("\n");
/* NOTE: glibc sucks and will crash if this is NULL */
@@ -277,6 +281,7 @@ update_input(PlatformCtx *ctx)
input->data_available = FD_ISSET(ctx->child.handle, rfd) != 0;
+ try_deferred_file_loads(ctx);
if (FD_ISSET(ctx->inotify_fd, rfd))
dispatch_file_watch_events(ctx);
@@ -348,7 +353,9 @@ linux_render_thread_entry(struct stack_base *stack)
}
for (;;) {
+ stack->thread_asleep = 1;
syscall4(SYS_futex, (iptr)&stack->work_futex, FUTEX_WAIT, 0, 0);
+ stack->thread_asleep = 0;
vtgl_render_frame(stack->terminal_memory, stack->input, stack->thread_arena);
glfwSwapBuffers(stack->window);
}
@@ -405,9 +412,10 @@ main(i32 argc, char *argv[], char *envp[])
linux_ctx.error_stream.widx = 0;
}
- struct stack_base *render_stack = new_stack(KB(256));
- render_stack->entry = linux_render_thread_entry;
- new_thread(render_stack);
+ linux_ctx.render_stack = new_stack(KB(256));
+ linux_ctx.render_stack->entry = linux_render_thread_entry;
+ linux_ctx.render_stack->thread_asleep = 1;
+ new_thread(linux_ctx.render_stack);
{
Arena tmp = linux_ctx.platform_memory;
@@ -491,11 +499,11 @@ main(i32 argc, char *argv[], char *envp[])
linux_ctx.input.window_size = window_size;
- render_stack->input = &linux_ctx.input;
- render_stack->terminal_memory = &linux_ctx.memory;
- render_stack->thread_arena = arena_from_memory_block(linux_block_alloc(MB(8)));
- render_stack->window = linux_ctx.window;
- syscall3(SYS_futex, (iptr)&render_stack->work_futex, FUTEX_WAKE, 1);
+ linux_ctx.render_stack->input = &linux_ctx.input;
+ linux_ctx.render_stack->terminal_memory = &linux_ctx.memory;
+ linux_ctx.render_stack->thread_arena = arena_from_memory_block(linux_block_alloc(MB(8)));
+ linux_ctx.render_stack->window = linux_ctx.window;
+ syscall3(SYS_futex, (iptr)&linux_ctx.render_stack->work_futex, FUTEX_WAKE, 1);
Range last_sel = {0};
f64 last_time = os_get_time();
@@ -510,7 +518,7 @@ main(i32 argc, char *argv[], char *envp[])
update_input(&linux_ctx);
if (vtgl_frame_step(&linux_ctx.memory, &linux_ctx.input))
- syscall3(SYS_futex, (iptr)&render_stack->work_futex, FUTEX_WAKE, 1);
+ syscall3(SYS_futex, (iptr)&linux_ctx.render_stack->work_futex, FUTEX_WAKE, 1);
Range current_sel = vtgl_active_selection(&linux_ctx.memory, 0);
if (is_valid_range(current_sel) && !equal_range(current_sel, last_sel)) {
diff --git a/util.c b/util.c
@@ -159,6 +159,7 @@ mem_copy(void *restrict src, void *restrict dest, size len)
for (; len; len--) *d++ = *s++;
}
+#define zero_struct(s) mem_clear(s, 0, sizeof(typeof(*s0)))
static void *
mem_clear(void *p_, u8 c, size len)
{
@@ -526,7 +527,7 @@ construct_c_str_array(Arena *a, SLLVariableVector vars)
{
c8 **result = alloc(a, c8 *, vars.count + 1);
- VariableLink *chain = vars.first;
+ VariableLink *chain = vars.next;
for (u32 i = 0; i < vars.count; i++) {
ASSERT(chain->var->type == VT_S8);
result[i] = (c8 *)chain->var->s8.data;
diff --git a/util.h b/util.h
@@ -46,16 +46,26 @@ typedef struct VariableLink {
} VariableLink;
typedef struct {
- VariableLink *first;
+ VariableLink *next;
u32 count;
} SLLVariableVector;
+#define SLLPush(sll, new) do { \
+ (new)->next = (sll)->next; \
+ (sll)->next = (new); \
+} while(0)
+
#define SLLVariableVectorPush(a, sll, var) do { \
VariableLink *vl = push_struct(a, VariableLink); \
+ SLLPush(sll, vl); \
(sll)->count++; \
- vl->next = (sll)->first; \
vl->var = var; \
- (sll)->first = vl; \
+} while(0)
+
+#define DLLPushDown(head, new) do { \
+ (new)->last = (head); \
+ (new)->next = (head)->next; \
+ (head)->next = (new); \
} while(0)
typedef struct Variable {