vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

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:
Mplatform_linux_common.c | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mplatform_linux_x11.c | 26+++++++++++++++++---------
Mutil.c | 3++-
Mutil.h | 16+++++++++++++---
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 {