ogl_beamforming

Ultrasound Beamforming Implemented with OpenGL
git clone anongit@rnpnr.xyz:ogl_beamforming.git
Log | Files | Refs | Feed | Submodules | LICENSE

Commit: a3edbfbb675ec3f06797d6bb68612ca7dde63c66
Parent: f108b6dcfc00fe769911babdd6b206f0319615d1
Author: Randy Palamar
Date:   Tue, 21 Jan 2025 11:10:38 -0700

core: automatic hot reloading

* shaders reload on file save
* w32 debug reloading now works without jank

Diffstat:
Mbeamformer.c | 17+++++++++--------
Mbeamformer.h | 4++--
Mbuild.sh | 6+++---
Dmain_generic.c | 80-------------------------------------------------------------------------------
Amain_linux.c | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain_w32.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mos_unix.c | 51+++++++++++++++++++++++++++++++--------------------
Mos_win32.c | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mstatic.c | 233+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mui.c | 2+-
Mutil.c | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mutil.h | 40+++++++++++++++++++++++++++++++++++++---
12 files changed, 610 insertions(+), 242 deletions(-)

diff --git a/beamformer.c b/beamformer.c @@ -599,7 +599,7 @@ do_beamform_work(BeamformerCtx *ctx, Arena *a) work->compute_ctx.export_handle = INVALID_FILE; } - ctx->flags |= GEN_MIPMAPS; + ctx->fsctx.gen_mipmaps = 1; } break; } @@ -671,6 +671,12 @@ DEBUG_EXPORT BEAMFORMER_FRAME_STEP_FN(beamformer_frame_step) ui_init(ctx, ctx->ui_backing_store); } + if (ctx->flags & START_COMPUTE) { + if (ui_can_start_compute(ctx)) + ui_start_compute(ctx); + ctx->flags &= ~START_COMPUTE; + } + /* NOTE: Store the compute time for the last frame. */ check_compute_timers(&ctx->csctx, &ctx->partial_compute_ctx, ctx->params); @@ -767,21 +773,16 @@ DEBUG_EXPORT BEAMFORMER_FRAME_STEP_FN(beamformer_frame_step) EndTextureMode(); /* NOTE: regenerate mipmaps only when the output has actually changed */ - if (ctx->flags & GEN_MIPMAPS) { + if (ctx->fsctx.gen_mipmaps) { /* NOTE: shut up raylib's reporting on mipmap gen */ SetTraceLogLevel(LOG_NONE); GenTextureMipmaps(&ctx->fsctx.output.texture); SetTraceLogLevel(LOG_INFO); - ctx->flags &= ~GEN_MIPMAPS; + ctx->fsctx.gen_mipmaps = 0; } draw_ui(ctx, input, frame_to_draw); - if (IsKeyPressed(KEY_R)) { - ctx->flags |= RELOAD_SHADERS; - if (ui_can_start_compute(ctx)) - ui_start_compute(ctx); - } if (WindowShouldClose()) ctx->flags |= SHOULD_EXIT; } diff --git a/beamformer.h b/beamformer.h @@ -35,8 +35,7 @@ enum program_flags { SHOULD_EXIT = 1 << 0, - RELOAD_SHADERS = 1 << 1, - GEN_MIPMAPS = 1 << 30, + START_COMPUTE = 1 << 1, }; enum gl_vendor_ids { @@ -221,6 +220,7 @@ typedef struct { i32 threshold_id; f32 db; f32 threshold; + b32 gen_mipmaps; } FragmentShaderCtx; typedef struct { diff --git a/build.sh b/build.sh @@ -1,5 +1,4 @@ #!/bin/sh -set -e cflags="-march=native -std=c11 -O3 -Wall -I./external/include" #cflags="${cflags} -fproc-stat-report" @@ -10,7 +9,6 @@ ldflags="-lm" debug=${DEBUG} cc=${CC:-cc} -main=main_generic.c case $(uname -sm) in MINGW64*) @@ -20,6 +18,7 @@ MINGW64*) extra_ldflags="-llibmat -llibmex" fi libname="beamformer.dll" + main="main_w32.c" ${cc} $libcflags helpers/ogl_beamformer_lib.c -o helpers/ogl_beamformer_lib.dll \ -L'C:/Program Files/MATLAB/R2022a/extern/lib/win64/microsoft' \ $extra_ldflags @@ -27,6 +26,7 @@ MINGW64*) Linux*) cflags="$cflags -D_DEFAULT_SOURCE" libname="beamformer.so" + main="main_linux.c" ${cc} $libcflags helpers/ogl_beamformer_lib.c -o helpers/ogl_beamformer_lib.so ;; esac @@ -64,4 +64,4 @@ else ldflags="./external/lib/libraylib.a ${ldflags}" fi -${cc} $cflags -o ogl $main $ldflags +${cc} ${cflags} -o ogl ${main} ${ldflags} diff --git a/main_generic.c b/main_generic.c @@ -1,80 +0,0 @@ -/* See LICENSE for license details. */ -#include "beamformer.h" - -#if defined(__unix__) - #include "os_unix.c" - - #define OS_DEBUG_LIB_NAME "./beamformer.so" - #define OS_DEBUG_LIB_TEMP_NAME "./beamformer_temp.so" - - #define OS_CUDA_LIB_NAME "./external/cuda_toolkit.so" - #define OS_CUDA_LIB_TEMP_NAME "./external/cuda_toolkit_temp.so" - - #define OS_PIPE_NAME "/tmp/beamformer_data_fifo" - #define OS_SMEM_NAME "/ogl_beamformer_parameters" - -#elif defined(_WIN32) - #include "os_win32.c" - - #define OS_DEBUG_LIB_NAME "beamformer.dll" - #define OS_DEBUG_LIB_TEMP_NAME "beamformer_temp.dll" - - #define OS_CUDA_LIB_NAME "external\\cuda_toolkit.dll" - #define OS_CUDA_LIB_TEMP_NAME "external\\cuda_toolkit_temp.dll" - - #define OS_PIPE_NAME "\\\\.\\pipe\\beamformer_data_fifo" - #define OS_SMEM_NAME "Local\\ogl_beamformer_parameters" -#else - #error Unsupported Platform! -#endif - -#include "static.c" - -int -main(void) -{ - BeamformerCtx ctx = {0}; - BeamformerInput input = {0}; - Arena temp_memory = os_alloc_arena((Arena){0}, 16 * MEGABYTE); - ctx.error_stream = stream_alloc(&temp_memory, 1 * MEGABYTE); - - ctx.ui_backing_store = sub_arena(&temp_memory, 2 * MEGABYTE); - - Pipe data_pipe = os_open_named_pipe(OS_PIPE_NAME); - input.pipe_handle = data_pipe.file; - ASSERT(data_pipe.file != INVALID_FILE); - - input.executable_reloaded = 1; - - #define X(name) ctx.platform.name = os_ ## name; - PLATFORM_FNS - #undef X - - setup_beamformer(&ctx, temp_memory); - - while(!(ctx.flags & SHOULD_EXIT)) { - do_debug(&input, &ctx.error_stream); - if (ctx.gl.vendor_id == GL_VENDOR_NVIDIA) - check_and_load_cuda_lib(&ctx.cuda_lib, &ctx.error_stream); - - if (ctx.flags & RELOAD_SHADERS) { - ctx.flags &= ~RELOAD_SHADERS; - reload_shaders(&ctx, temp_memory); - } - - input.last_mouse = input.mouse; - input.mouse.rl = GetMousePosition(); - input.pipe_data_available = os_poll_pipe(data_pipe); - - beamformer_frame_step(&ctx, &temp_memory, &input); - - input.executable_reloaded = 0; - } - - /* NOTE: make sure this will get cleaned up after external - * programs release their references */ - os_remove_shared_memory(OS_SMEM_NAME); - - /* NOTE: garbage code needed for Linux */ - os_close_named_pipe(data_pipe); -} diff --git a/main_linux.c b/main_linux.c @@ -0,0 +1,110 @@ +/* See LICENSE for license details. */ +#ifndef __linux__ +#error This file is only meant to be compiled for Linux +#endif + +#include "beamformer.h" + +#include "os_unix.c" + +#define OS_DEBUG_LIB_NAME "./beamformer.so" +#define OS_DEBUG_LIB_TEMP_NAME "./beamformer_temp.so" + +#define OS_CUDA_LIB_NAME "./external/cuda_toolkit.so" +#define OS_CUDA_LIB_TEMP_NAME "./external/cuda_toolkit_temp.so" + +#define OS_PIPE_NAME "/tmp/beamformer_data_fifo" +#define OS_SMEM_NAME "/ogl_beamformer_parameters" + +#define OS_PATH_SEPERATOR "/" + +#include "static.c" + +static void +dispatch_file_watch_events(FileWatchContext *fwctx, Arena arena) +{ + u8 *mem = alloc_(&arena, 4096, 64, 1); + Stream path = stream_alloc(&arena, 256); + struct inotify_event *event; + + size rlen; + while ((rlen = read(fwctx->handle, mem, 4096)) > 0) { + for (u8 *data = mem; data < mem + rlen; data += sizeof(*event) + event->len) { + event = (struct inotify_event *)data; + for (u32 i = 0; i < fwctx->directory_watch_count; i++) { + FileWatchDirectory *dir = fwctx->directory_watches + i; + if (event->wd != dir->handle) + continue; + + s8 file = cstr_to_s8(event->name); + u64 hash = s8_hash(file); + for (u32 i = 0; i < dir->file_watch_count; i++) { + FileWatch *fw = dir->file_watches + i; + if (fw->hash == hash) { + stream_append_s8(&path, dir->name); + stream_append_byte(&path, '/'); + stream_append_s8(&path, file); + stream_append_byte(&path, 0); + path.widx--; + fw->callback(stream_to_s8(&path), + fw->user_data, arena); + path.widx = 0; + break; + } + } + } + } + } +} + +int +main(void) +{ + BeamformerCtx ctx = {0}; + BeamformerInput input = {.executable_reloaded = 1}; + Arena temp_memory = os_alloc_arena((Arena){0}, 16 * MEGABYTE); + ctx.error_stream = stream_alloc(&temp_memory, 1 * MEGABYTE); + + ctx.ui_backing_store = sub_arena(&temp_memory, 2 * MEGABYTE, 4096); + + Pipe data_pipe = os_open_named_pipe(OS_PIPE_NAME); + input.pipe_handle = data_pipe.file; + ASSERT(data_pipe.file != INVALID_FILE); + + #define X(name) ctx.platform.name = os_ ## name; + PLATFORM_FNS + #undef X + + ctx.platform.file_watch_context.handle = inotify_init1(O_NONBLOCK|O_CLOEXEC); + + setup_beamformer(&ctx, &temp_memory); + debug_init(&ctx.platform, (iptr)&input, &temp_memory); + + struct pollfd fds[2] = {{0}, {0}}; + fds[0].fd = ctx.platform.file_watch_context.handle; + fds[0].events = POLLIN; + fds[1].fd = data_pipe.file; + fds[1].events = POLLIN; + + while (!(ctx.flags & SHOULD_EXIT)) { + poll(fds, 2, 0); + if (fds[0].revents & POLLIN) + dispatch_file_watch_events(&ctx.platform.file_watch_context, temp_memory); + + input.pipe_data_available = !!(fds[1].revents & POLLIN); + input.last_mouse = input.mouse; + input.mouse.rl = GetMousePosition(); + + beamformer_frame_step(&ctx, &temp_memory, &input); + + input.executable_reloaded = 0; + } + + /* NOTE: make sure this will get cleaned up after external + * programs release their references */ + shm_unlink(OS_SMEM_NAME); + + /* NOTE: garbage code needed for Linux */ + close(data_pipe.file); + unlink(data_pipe.name); +} diff --git a/main_w32.c b/main_w32.c @@ -0,0 +1,150 @@ +/* See LICENSE for license details. */ +#ifndef _WIN32 +#error This file is only meant to be compiled for Win32 +#endif + +#include "beamformer.h" + +typedef struct { + iptr io_completion_handle; +} w32_context; + +enum w32_io_events { + W32_IO_FILE_WATCH, + W32_IO_PIPE, +}; + +typedef struct { + u64 tag; + iptr context; +} w32_io_completion_event; + +#include "os_win32.c" + +#define OS_DEBUG_LIB_NAME ".\\beamformer.dll" +#define OS_DEBUG_LIB_TEMP_NAME ".\\beamformer_temp.dll" + +#define OS_CUDA_LIB_NAME "external\\cuda_toolkit.dll" +#define OS_CUDA_LIB_TEMP_NAME "external\\cuda_toolkit_temp.dll" + +#define OS_PIPE_NAME "\\\\.\\pipe\\beamformer_data_fifo" +#define OS_SMEM_NAME "Local\\ogl_beamformer_parameters" + +#define OS_PATH_SEPERATOR "\\" + +#include "static.c" + +static void +w32_wide_char_to_mb(Stream *s, u16 *wstr, u32 wide_char_length) +{ + /* NOTE(rnp): this assumes the wstr is strictly ASCII */ + s->errors |= (s->cap - s->widx) < wide_char_length; + if (!s->errors) { + for (u32 i = 0; i < wide_char_length; i++) + s->data[s->widx++] = wstr[i] & 0xFF; + } +} + +static void +dispatch_file_watch(FileWatchDirectory *fw_dir, u8 *buf, Arena arena) +{ + i64 offset = 0; + Stream path = stream_alloc(&arena, 256); + w32_file_notify_info *fni = (w32_file_notify_info *)buf; + do { + if (fni->action != FILE_ACTION_MODIFIED) { + path.widx = 0; + stream_append_s8(&path, s8("unknown file watch event: ")); + stream_append_u64(&path, fni->action); + stream_append_byte(&path, '\n'); + os_write_err_msg(stream_to_s8(&path)); + } + + path.widx = 0; + stream_append_s8(&path, fw_dir->name); + stream_append_byte(&path, '\\'); + + s8 file_name = {.data = path.data + path.widx, .len = fni->filename_size / 2}; + w32_wide_char_to_mb(&path, fni->filename, fni->filename_size / 2); + stream_append_byte(&path, 0); + path.widx--; + + u64 hash = s8_hash(file_name); + for (u32 i = 0; i < fw_dir->file_watch_count; i++) { + FileWatch *fw = fw_dir->file_watches + i; + if (fw->hash == hash) { + fw->callback(stream_to_s8(&path), + fw->user_data, arena); + break; + } + } + + offset = fni->next_entry_offset; + fni = (w32_file_notify_info *)((u8 *)fni + offset); + } while (offset); +} + +static void +clear_io_queue(Platform *platform, BeamformerInput *input, Arena arena) +{ + w32_context *ctx = (w32_context *)platform->os_context; + + iptr handle = ctx->io_completion_handle; + w32_overlapped *overlapped; + u32 bytes_read; + uptr user_data; + while (GetQueuedCompletionStatus(handle, &bytes_read, &user_data, &overlapped, 0)) { + w32_io_completion_event *event = (w32_io_completion_event *)user_data; + switch (event->tag) { + case W32_IO_FILE_WATCH: { + FileWatchDirectory *dir = (FileWatchDirectory *)event->context; + dispatch_file_watch(dir, dir->buffer.beg, arena); + zero_struct(overlapped); + ReadDirectoryChangesW(dir->handle, dir->buffer.beg, 4096, 0, + FILE_NOTIFY_CHANGE_LAST_WRITE, 0, overlapped, 0); + } break; + case W32_IO_PIPE: break; + } + } +} + +int +main(void) +{ + BeamformerCtx ctx = {0}; + BeamformerInput input = {.executable_reloaded = 1}; + Arena temp_memory = os_alloc_arena((Arena){0}, 16 * MEGABYTE); + ctx.error_stream = stream_alloc(&temp_memory, 1 * MEGABYTE); + + ctx.ui_backing_store = sub_arena(&temp_memory, 2 * MEGABYTE, 4096); + + Pipe data_pipe = os_open_named_pipe(OS_PIPE_NAME); + input.pipe_handle = data_pipe.file; + ASSERT(data_pipe.file != INVALID_FILE); + + #define X(name) ctx.platform.name = os_ ## name; + PLATFORM_FNS + #undef X + + w32_context w32_ctx = {0}; + w32_ctx.io_completion_handle = CreateIoCompletionPort(INVALID_FILE, 0, 0, 0); + ctx.platform.os_context = (iptr)&w32_ctx; + + setup_beamformer(&ctx, &temp_memory); + debug_init(&ctx.platform, (iptr)&input, &temp_memory); + + while (!(ctx.flags & SHOULD_EXIT)) { + clear_io_queue(&ctx.platform, &input, temp_memory); + + input.last_mouse = input.mouse; + input.mouse.rl = GetMousePosition(); + + i32 bytes_available = 0; + input.pipe_data_available = PeekNamedPipe(data_pipe.file, 0, 1 * MEGABYTE, 0, + &bytes_available, 0) && bytes_available; + + beamformer_frame_step(&ctx, &temp_memory, &input); + + input.executable_reloaded = 0; + } +} diff --git a/os_unix.c b/os_unix.c @@ -1,9 +1,14 @@ /* See LICENSE for license details. */ + +/* NOTE(rnp): provides the platform layer for the beamformer. This code must + * be provided by any platform the beamformer is ported to. */ + #include "util.h" #include <dlfcn.h> #include <fcntl.h> #include <poll.h> +#include <sys/inotify.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> @@ -118,20 +123,6 @@ os_open_named_pipe(char *name) return (Pipe){.file = open(name, O_RDONLY|O_NONBLOCK), .name = name}; } -static void -os_close_named_pipe(Pipe p) -{ - close(p.file); - unlink(p.name); -} - -static PLATFORM_POLL_PIPE_FN(os_poll_pipe) -{ - struct pollfd pfd = {.fd = p.file, .events = POLLIN}; - poll(&pfd, 1, 0); - return !!(pfd.revents & POLLIN); -} - static PLATFORM_READ_PIPE_FN(os_read_pipe) { size r = 0, total_read = 0; @@ -164,12 +155,6 @@ os_open_shared_memory_area(char *name, size cap) return new; } -static void -os_remove_shared_memory(char *name) -{ - shm_unlink(name); -} - /* NOTE: complete garbage because there is no standarized copyfile() in POSix */ static b32 os_copy_file(char *name, char *new) @@ -246,3 +231,29 @@ os_unload_library(void *h) if (h) dlclose(h); } + +static PLATFORM_ADD_FILE_WATCH_FN(os_add_file_watch) +{ + s8 directory = path; + directory.len = s8_scan_backwards(path, '/'); + ASSERT(directory.len > 0); + + u64 hash = s8_hash(directory); + FileWatchContext *fwctx = &platform->file_watch_context; + FileWatchDirectory *dir = lookup_file_watch_directory(fwctx, hash); + if (!dir) { + ASSERT(path.data[directory.len] == '/'); + + dir = fwctx->directory_watches + fwctx->directory_watch_count++; + dir->hash = hash; + + dir->name = push_s8(a, directory); + arena_commit(a, 1); + dir->name.data[dir->name.len] = 0; + + i32 mask = IN_MOVED_TO|IN_CLOSE_WRITE; + dir->handle = inotify_add_watch(fwctx->handle, (c8 *)dir->name.data, mask); + } + + insert_file_watch(dir, s8_cut_head(path, dir->name.len + 1), user_data, callback); +} diff --git a/os_win32.c b/os_win32.c @@ -8,13 +8,21 @@ #define MEM_COMMIT 0x1000 #define MEM_RESERVE 0x2000 #define MEM_RELEASE 0x8000 + #define GENERIC_WRITE 0x40000000 #define GENERIC_READ 0x80000000 #define PIPE_TYPE_BYTE 0x00 #define PIPE_ACCESS_INBOUND 0x01 -#define FILE_MAP_ALL_ACCESS 0x000F001F +#define FILE_SHARE_READ 0x00000001 +#define FILE_MAP_ALL_ACCESS 0x000F001F +#define FILE_FLAG_BACKUP_SEMANTICS 0x02000000 +#define FILE_FLAG_OVERLAPPED 0x40000000 + +#define FILE_NOTIFY_CHANGE_LAST_WRITE 0x00000010 + +#define FILE_ACTION_MODIFIED 0x00000003 #define CREATE_ALWAYS 2 #define OPEN_EXISTING 3 @@ -50,11 +58,28 @@ typedef struct __attribute__((packed)) { u32 nFileIndexLow; } w32_file_info; +typedef struct { + u32 next_entry_offset; + u32 action; + u32 filename_size; + u16 filename[]; +} w32_file_notify_info; + +typedef struct { + uptr internal, internal_high; + union { + struct {u32 off, off_high;}; + iptr pointer; + }; + iptr event_handle; +} w32_overlapped; + #define W32(r) __declspec(dllimport) r __stdcall W32(b32) CloseHandle(iptr); W32(b32) CopyFileA(c8 *, c8 *, b32); W32(iptr) CreateFileA(c8 *, u32, u32, void *, u32, u32, void *); W32(iptr) CreateFileMappingA(iptr, void *, u32, u32, u32, c8 *); +W32(iptr) CreateIoCompletionPort(iptr, iptr, uptr, u32); W32(iptr) CreateNamedPipeA(c8 *, u32, u32, u32, u32, u32, u32, void *); W32(b32) DeleteFileA(c8 *); W32(void) ExitProcess(i32); @@ -62,11 +87,13 @@ W32(b32) FreeLibrary(void *); W32(b32) GetFileInformationByHandle(iptr, w32_file_info *); W32(i32) GetLastError(void); W32(void *) GetProcAddress(void *, c8 *); +W32(b32) GetQueuedCompletionStatus(iptr, u32 *, uptr *, w32_overlapped **, u32); W32(iptr) GetStdHandle(i32); W32(void) GetSystemInfo(void *); W32(void *) LoadLibraryA(c8 *); W32(void *) MapViewOfFile(iptr, u32, u32, u32, u64); W32(b32) PeekNamedPipe(iptr, u8 *, i32, i32 *, i32 *, i32 *); +W32(b32) ReadDirectoryChangesW(iptr, u8 *, u32, b32, u32, u32 *, void *, void *); W32(b32) ReadFile(iptr, u8 *, i32, i32 *, void *); W32(b32) WriteFile(iptr, u8 *, i32, i32 *, void *); W32(void *) VirtualAlloc(u8 *, size, u32, u32); @@ -203,18 +230,6 @@ os_open_named_pipe(char *name) return (Pipe){.file = h, .name = name}; } -/* NOTE: win32 doesn't pollute the filesystem so no need to waste the user's time */ -static void -os_close_named_pipe(Pipe p) -{ -} - -static PLATFORM_POLL_PIPE_FN(os_poll_pipe) -{ - i32 bytes_available = 0; - return PeekNamedPipe(p.file, 0, 1 * MEGABYTE, 0, &bytes_available, 0) && bytes_available; -} - static PLATFORM_READ_PIPE_FN(os_read_pipe) { i32 total_read = 0; @@ -232,12 +247,6 @@ os_open_shared_memory_area(char *name, size cap) return MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, cap); } -/* NOTE: closing the handle releases the memory and this happens when program terminates */ -static void -os_remove_shared_memory(char *name) -{ -} - static void * os_load_library(char *name, char *temp_name, Stream *e) { @@ -284,3 +293,43 @@ os_unload_library(void *h) { FreeLibrary(h); } + +static PLATFORM_ADD_FILE_WATCH_FN(os_add_file_watch) +{ + s8 directory = path; + directory.len = s8_scan_backwards(path, '\\'); + ASSERT(directory.len > 0); + + u64 hash = s8_hash(directory); + FileWatchContext *fwctx = &platform->file_watch_context; + FileWatchDirectory *dir = lookup_file_watch_directory(fwctx, hash); + if (!dir) { + ASSERT(path.data[directory.len] == '\\'); + + dir = fwctx->directory_watches + fwctx->directory_watch_count++; + dir->hash = hash; + + dir->name = push_s8(a, directory); + arena_commit(a, 1); + dir->name.data[dir->name.len] = 0; + + dir->handle = CreateFileA((c8 *)dir->name.data, GENERIC_READ, FILE_SHARE_READ, 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED, 0); + + w32_context *ctx = (w32_context *)platform->os_context; + w32_io_completion_event *event = push_struct(a, typeof(*event)); + event->tag = W32_IO_FILE_WATCH; + event->context = (iptr)dir; + CreateIoCompletionPort(dir->handle, ctx->io_completion_handle, (uptr)event, 0); + + dir->buffer = sub_arena(a, 4096 + sizeof(w32_overlapped), 64); + w32_overlapped *overlapped = (w32_overlapped *)(dir->buffer.beg + 4096); + zero_struct(overlapped); + + ReadDirectoryChangesW(dir->handle, dir->buffer.beg, 4096, 0, + FILE_NOTIFY_CHANGE_LAST_WRITE, 0, overlapped, 0); + } + + insert_file_watch(dir, s8_cut_head(path, dir->name.len + 1), user_data, callback); +} diff --git a/static.c b/static.c @@ -1,42 +1,39 @@ /* See LICENSE for license details. */ -static struct { - s8 label; - s8 path; - b32 needs_header; -} compute_shaders[CS_LAST] = { - #define X(e, n, s, h, pn) [CS_##e] = {s8(#e), s8("shaders/" s ".glsl"), h}, - COMPUTE_SHADERS - #undef X -}; - #ifndef _DEBUG #include "beamformer.c" -#define do_debug(...) +#define debug_init(...) #else static void *debug_lib; static beamformer_frame_step_fn *beamformer_frame_step; +static FILE_WATCH_CALLBACK_FN(debug_reload) +{ + BeamformerInput *input = (BeamformerInput *)user_data; + Stream err = arena_stream(&tmp); + + os_unload_library(debug_lib); + debug_lib = os_load_library(OS_DEBUG_LIB_NAME, OS_DEBUG_LIB_TEMP_NAME, &err); + beamformer_frame_step = os_lookup_dynamic_symbol(debug_lib, "beamformer_frame_step", &err); + os_write_err_msg(s8("Reloaded Main Executable\n")); + input->executable_reloaded = 1; + + return 1; +} + static void -do_debug(BeamformerInput *input, Stream *error_stream) +debug_init(Platform *p, iptr input, Arena *arena) { - static f64 updated_time; - FileStats test_stats = os_get_file_stats(OS_DEBUG_LIB_NAME); - if (test_stats.filesize > 32 && test_stats.timestamp > updated_time) { - os_unload_library(debug_lib); - debug_lib = os_load_library(OS_DEBUG_LIB_NAME, OS_DEBUG_LIB_TEMP_NAME, error_stream); - beamformer_frame_step = os_lookup_dynamic_symbol(debug_lib, "beamformer_frame_step", error_stream); - updated_time = test_stats.timestamp; - - input->executable_reloaded = 1; - os_write_err_msg(s8("Reloaded Main Executable\n")); - } + p->add_file_watch(p, arena, s8(OS_DEBUG_LIB_NAME), debug_reload, input); + debug_reload((s8){0}, input, *arena); } #endif /* _DEBUG */ +#define static_path_join(a, b) (a OS_PATH_SEPERATOR b) + static void gl_debug_logger(u32 src, u32 type, u32 id, u32 lvl, i32 len, const char *msg, const void *userctx) { @@ -154,13 +151,13 @@ compile_shader(Arena a, u32 type, s8 shader) i32 res = 0; glGetShaderiv(sid, GL_COMPILE_STATUS, &res); - char *stype; - switch (type) { - case GL_COMPUTE_SHADER: stype = "Compute"; break; - case GL_FRAGMENT_SHADER: stype = "Fragment"; break; - } - if (res == GL_FALSE) { + char *stype; + switch (type) { + case GL_COMPUTE_SHADER: stype = "Compute"; break; + case GL_FRAGMENT_SHADER: stype = "Fragment"; break; + } + TraceLog(LOG_WARNING, "SHADER: [ID %u] %s shader failed to compile", sid, stype); i32 len = 0; glGetShaderiv(sid, GL_INFO_LOG_LENGTH, &len); @@ -168,94 +165,103 @@ compile_shader(Arena a, u32 type, s8 shader) glGetShaderInfoLog(sid, len, (int *)&err.len, (char *)err.data); TraceLog(LOG_WARNING, "SHADER: [ID %u] Compile error: %s", sid, (char *)err.data); glDeleteShader(sid); - } else { - TraceLog(LOG_INFO, "SHADER: [ID %u] %s shader compiled successfully", sid, stype); + + sid = 0; } return sid; } -static void -reload_shaders(BeamformerCtx *ctx, Arena a) +static FILE_WATCH_CALLBACK_FN(reload_render_shader) { - ComputeShaderCtx *csctx = &ctx->csctx; - s8 header_in_arena = push_s8(&a, s8(COMPUTE_SHADER_HEADER)); - for (u32 i = 0; i < ARRAY_COUNT(csctx->programs); i++) { - FileStats fs = os_get_file_stats((char *)compute_shaders[i].path.data); - if (fs.filesize <= 0) - continue; - - Arena tmp = a; - s8 shader_text = os_read_file(&tmp, (char *)compute_shaders[i].path.data, fs.filesize); - if (shader_text.len == -1) { - stream_append_s8(&ctx->error_stream, s8("failed to read shader: ")); - stream_append_s8(&ctx->error_stream, compute_shaders[i].path); - stream_append_byte(&ctx->error_stream, '\n'); - /* TODO: maybe we don't need to fail here */ - os_fatal(stream_to_s8(&ctx->error_stream)); - } - /* NOTE: arena works as stack (since everything here is 1 byte aligned) */ - if (compute_shaders[i].needs_header) { - shader_text.data -= header_in_arena.len; - shader_text.len += header_in_arena.len; - ASSERT(shader_text.data == header_in_arena.data); - } + FragmentShaderCtx *ctx = (FragmentShaderCtx *)user_data; + Shader updated_fs = LoadShader(0, (c8 *)path.data); + + if (updated_fs.id) { + UnloadShader(ctx->shader); + LABEL_GL_OBJECT(GL_PROGRAM, updated_fs.id, s8("Render Shader")); + ctx->shader = updated_fs; + ctx->db_cutoff_id = GetShaderLocation(updated_fs, "u_db_cutoff"); + ctx->threshold_id = GetShaderLocation(updated_fs, "u_threshold"); + ctx->gen_mipmaps = 1; + } - u32 shader_id = compile_shader(tmp, GL_COMPUTE_SHADER, shader_text); + return 1; +} - if (shader_id) { - glDeleteProgram(csctx->programs[i]); - csctx->programs[i] = rlLoadComputeShaderProgram(shader_id); - glUseProgram(csctx->programs[i]); - glBindBufferBase(GL_UNIFORM_BUFFER, 0, csctx->shared_ubo); - LABEL_GL_OBJECT(GL_PROGRAM, csctx->programs[i], compute_shaders[i].label); - } +struct compute_shader_reload_ctx { + BeamformerCtx *ctx; + s8 label; + u32 shader; + b32 needs_header; +}; - glDeleteShader(shader_id); - } +static FILE_WATCH_CALLBACK_FN(reload_compute_shader) +{ + struct compute_shader_reload_ctx *ctx = (struct compute_shader_reload_ctx *)user_data; + ComputeShaderCtx *cs = &ctx->ctx->csctx; - #define X(idx, name) csctx->name##_id = glGetUniformLocation(csctx->programs[idx], "u_" #name); - CS_UNIFORMS - #undef X + b32 result = 1; + + /* NOTE: arena works as stack (since everything here is 1 byte aligned) */ + s8 header_in_arena = {.data = tmp.beg}; + if (ctx->needs_header) + header_in_arena = push_s8(&tmp, s8(COMPUTE_SHADER_HEADER)); + + size fs = os_get_file_stats((c8 *)path.data).filesize; + + s8 shader_text = os_read_file(&tmp, (c8 *)path.data, fs); + shader_text.data -= header_in_arena.len; + shader_text.len += header_in_arena.len; + ASSERT(shader_text.data == header_in_arena.data); - Shader updated_fs = LoadShader(NULL, "shaders/render.glsl"); - if (updated_fs.id != rlGetShaderIdDefault()) { - UnloadShader(ctx->fsctx.shader); - LABEL_GL_OBJECT(GL_PROGRAM, updated_fs.id, s8("Render")); - ctx->fsctx.shader = updated_fs; - ctx->fsctx.db_cutoff_id = GetShaderLocation(updated_fs, "u_db_cutoff"); - ctx->fsctx.threshold_id = GetShaderLocation(updated_fs, "u_threshold"); + u32 shader_id = compile_shader(tmp, GL_COMPUTE_SHADER, shader_text); + if (shader_id) { + glDeleteProgram(cs->programs[ctx->shader]); + cs->programs[ctx->shader] = rlLoadComputeShaderProgram(shader_id); + glUseProgram(cs->programs[ctx->shader]); + glBindBufferBase(GL_UNIFORM_BUFFER, 0, cs->shared_ubo); + LABEL_GL_OBJECT(GL_PROGRAM, cs->programs[ctx->shader], ctx->label); + + TraceLog(LOG_INFO, "%s loaded", path.data); + + ctx->ctx->flags |= START_COMPUTE; + } else { + result = 0; } -} -static void -validate_cuda_lib(CudaLib *cl) -{ - #define X(name) if (!cl->name) cl->name = name ## _stub; - CUDA_LIB_FNS - #undef X + glDeleteShader(shader_id); + + return result; } -static void -check_and_load_cuda_lib(CudaLib *cl, Stream *error_stream) +static FILE_WATCH_CALLBACK_FN(load_cuda_lib) { - FileStats current = os_get_file_stats(OS_CUDA_LIB_NAME); - if (cl->timestamp == current.timestamp || current.filesize < 32) - return; - - TraceLog(LOG_INFO, "Loading CUDA lib: %s", OS_CUDA_LIB_NAME); + CudaLib *cl = (CudaLib *)user_data; + b32 result = 0; + size fs = os_get_file_stats((c8 *)path.data).filesize; + if (fs > 0) { + TraceLog(LOG_INFO, "Loading CUDA lib: %s", OS_CUDA_LIB_NAME); + + Stream err = arena_stream(&tmp); + os_unload_library(cl->lib); + cl->lib = os_load_library((c8 *)path.data, OS_CUDA_LIB_TEMP_NAME, &err); + #define X(name) cl->name = os_lookup_dynamic_symbol(cl->lib, #name, &err); + CUDA_LIB_FNS + #undef X + + result = 1; + } - cl->timestamp = current.timestamp; - os_unload_library(cl->lib); - cl->lib = os_load_library(OS_CUDA_LIB_NAME, OS_CUDA_LIB_TEMP_NAME, error_stream); - #define X(name) cl->name = os_lookup_dynamic_symbol(cl->lib, #name, error_stream); + #define X(name) if (!cl->name) cl->name = name ## _stub; CUDA_LIB_FNS #undef X - validate_cuda_lib(cl); + + return result; } static void -setup_beamformer(BeamformerCtx *ctx, Arena temp_memory) +setup_beamformer(BeamformerCtx *ctx, Arena *memory) { ctx->window_size = (uv2){.w = 1280, .h = 840}; @@ -267,7 +273,7 @@ setup_beamformer(BeamformerCtx *ctx, Arena temp_memory) /* NOTE: Gather information about the GPU */ get_gl_params(&ctx->gl, &ctx->error_stream); - dump_gl_params(&ctx->gl, temp_memory); + dump_gl_params(&ctx->gl, *memory); validate_gl_requirements(&ctx->gl); ctx->fsctx.db = -50.0f; @@ -283,8 +289,16 @@ setup_beamformer(BeamformerCtx *ctx, Arena temp_memory) ctx->params->compute_stages[2] = CS_MIN_MAX; ctx->params->compute_stages_count = 3; - /* NOTE: make sure function pointers are valid even if we are not using the cuda lib */ - validate_cuda_lib(&ctx->cuda_lib); + if (ctx->gl.vendor_id == GL_VENDOR_NVIDIA + && load_cuda_lib(s8(OS_CUDA_LIB_NAME), (iptr)&ctx->cuda_lib, *memory)) + { + os_add_file_watch(&ctx->platform, memory, s8(OS_CUDA_LIB_NAME), load_cuda_lib, + (iptr)&ctx->cuda_lib); + } else { + #define X(name) if (!ctx->cuda_lib.name) ctx->cuda_lib.name = name ## _stub; + CUDA_LIB_FNS + #undef X + } /* NOTE: set up OpenGL debug logging */ glDebugMessageCallback(gl_debug_logger, &ctx->error_stream); @@ -300,5 +314,26 @@ setup_beamformer(BeamformerCtx *ctx, Arena temp_memory) glGenQueries(ARRAY_COUNT(ctx->csctx.timer_fences) * CS_LAST, (u32 *)ctx->csctx.timer_ids); glGenQueries(ARRAY_COUNT(ctx->partial_compute_ctx.timer_ids), ctx->partial_compute_ctx.timer_ids); - reload_shaders(ctx, temp_memory); + #define X(e, sn, f, nh, pretty_name) do if (s8(f).len > 0) { \ + struct compute_shader_reload_ctx *csr = push_struct(memory, typeof(*csr)); \ + csr->ctx = ctx; \ + csr->label = s8("CS_" #e); \ + csr->shader = sn; \ + csr->needs_header = nh; \ + s8 shader = s8(static_path_join("shaders", f ".glsl")); \ + reload_compute_shader(shader, (iptr)csr, *memory); \ + os_add_file_watch(&ctx->platform, memory, shader, reload_compute_shader, (iptr)csr); \ + } while (0); + COMPUTE_SHADERS + #undef X + + s8 render = s8(static_path_join("shaders", "render.glsl")); + reload_render_shader(render, (iptr)&ctx->fsctx, *memory); + os_add_file_watch(&ctx->platform, memory, render, reload_render_shader, (iptr)&ctx->fsctx); + + /* TODO(rnp): remove this */ + ComputeShaderCtx *csctx = &ctx->csctx; + #define X(idx, name) csctx->name##_id = glGetUniformLocation(csctx->programs[idx], "u_" #name); + CS_UNIFORMS + #undef X } diff --git a/ui.c b/ui.c @@ -880,7 +880,7 @@ static void ui_gen_mipmaps(BeamformerCtx *ctx) { if (ctx->fsctx.output.texture.id) - ctx->flags |= GEN_MIPMAPS; + ctx->fsctx.gen_mipmaps = 1; } static void diff --git a/util.c b/util.c @@ -14,6 +14,7 @@ static i32 hadamard_12_12_transpose[] = { 1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, -1, }; +#define zero_struct(s) mem_clear(s, 0, sizeof(*s)); static void * mem_clear(void *p_, u8 c, size len) { @@ -37,6 +38,14 @@ mem_move(u8 *src, u8 *dest, size n) else while (n) { n--; dest[n] = src[n]; } } + +static void +arena_commit(Arena *a, size size) +{ + ASSERT(a->end - a->beg >= size); + a->beg += size; +} + #define alloc(a, t, n) (t *)alloc_(a, sizeof(t), _Alignof(t), n) #define push_struct(a, t) (t *)alloc_(a, sizeof(t), _Alignof(t), 1) static void * @@ -57,14 +66,15 @@ alloc_(Arena *a, size len, size align, size count) } static Arena -sub_arena(Arena *a, size size) +sub_arena(Arena *a, size len, size align) { Arena result = {0}; - if ((a->end - a->beg) >= size) { - result.beg = a->beg; - result.end = a->beg + size; - a->beg += size; - } + + size padding = -(uintptr_t)a->beg & (align - 1); + result.beg = a->beg + padding; + result.end = result.beg + len; + arena_commit(a, len + padding); + return result; } @@ -246,6 +256,18 @@ stream_append_variable(Stream *s, Variable *var) } } +/* NOTE(rnp): FNV-1a hash */ +static u64 +s8_hash(s8 v) +{ + u64 h = 0x3243f6a8885a308d; /* digits of pi */ + for (; v.len; v.len--) { + h ^= v.data[v.len - 1] & 0xFF; + h *= 1111111111111111111; /* random prime */ + } + return h; +} + static s8 cstr_to_s8(char *cstr) { @@ -254,13 +276,23 @@ cstr_to_s8(char *cstr) return result; } +/* NOTE(rnp): returns < 0 if byte is not found */ +static size +s8_scan_backwards(s8 s, u8 byte) +{ + size result = s.len; + while (result && s.data[result - 1] != byte) result--; + result--; + return result; +} + static s8 s8_cut_head(s8 s, size cut) { s8 result = s; if (cut > 0) { result.data += cut; - result.len -= cut; + result.len -= cut; } return result; } @@ -406,6 +438,32 @@ parse_f64(s8 s) return result; } +static FileWatchDirectory * +lookup_file_watch_directory(FileWatchContext *ctx, u64 hash) +{ + FileWatchDirectory *result = 0; + + for (u32 i = 0; i < ctx->directory_watch_count; i++) { + FileWatchDirectory *test = ctx->directory_watches + i; + if (test->hash == hash) { + result = test; + break; + } + } + + return result; +} + +static void +insert_file_watch(FileWatchDirectory *dir, s8 name, iptr user_data, file_watch_callback *callback) +{ + ASSERT(dir->file_watch_count < ARRAY_COUNT(dir->file_watches)); + FileWatch *fw = dir->file_watches + dir->file_watch_count++; + fw->hash = s8_hash(name); + fw->user_data = user_data; + fw->callback = callback; +} + static void fill_kronecker_sub_matrix(i32 *out, i32 out_stride, i32 scale, i32 *b, uv2 b_dim) { diff --git a/util.h b/util.h @@ -68,6 +68,7 @@ typedef float f32; typedef double f64; typedef ptrdiff_t size; typedef ptrdiff_t iptr; +typedef size_t uptr; typedef struct { u8 *beg, *end; } Arena; typedef struct { Arena *arena; u8 *old_beg; } TempArena; @@ -160,6 +161,30 @@ typedef struct { } FileStats; #define ERROR_FILE_STATS (FileStats){.filesize = -1} +#define FILE_WATCH_CALLBACK_FN(name) b32 name(s8 path, iptr user_data, Arena tmp) +typedef FILE_WATCH_CALLBACK_FN(file_watch_callback); + +typedef struct { + iptr user_data; + u64 hash; + file_watch_callback *callback; +} FileWatch; + +typedef struct { + u64 hash; + iptr handle; + s8 name; + FileWatch file_watches[16]; + u32 file_watch_count; + Arena buffer; +} FileWatchDirectory; + +typedef struct { + FileWatchDirectory directory_watches[4]; + iptr handle; + u32 directory_watch_count; +} FileWatchContext; + typedef struct { u8 *data; u32 widx; @@ -202,17 +227,21 @@ typedef struct { #define NULL_VARIABLE (Variable){.store = 0, .type = VT_NULL} +typedef struct Platform Platform; + #define PLATFORM_ALLOC_ARENA_FN(name) Arena name(Arena old, size capacity) typedef PLATFORM_ALLOC_ARENA_FN(platform_alloc_arena_fn); +#define PLATFORM_ADD_FILE_WATCH_FN(name) void name(Platform *platform, Arena *a, s8 path, \ + file_watch_callback *callback, iptr user_data) +typedef PLATFORM_ADD_FILE_WATCH_FN(platform_add_file_watch_fn); + #define PLATFORM_CLOSE_FN(name) void name(iptr file) typedef PLATFORM_CLOSE_FN(platform_close_fn); #define PLATFORM_OPEN_FOR_WRITE_FN(name) iptr name(c8 *fname) typedef PLATFORM_OPEN_FOR_WRITE_FN(platform_open_for_write_fn); -#define PLATFORM_POLL_PIPE_FN(name) b32 name(Pipe p) - #define PLATFORM_READ_PIPE_FN(name) size name(iptr pipe, void *buf, size len) typedef PLATFORM_READ_PIPE_FN(platform_read_pipe_fn); @@ -223,6 +252,7 @@ typedef PLATFORM_WRITE_NEW_FILE_FN(platform_write_new_file_fn); typedef PLATFORM_WRITE_FILE_FN(platform_write_file_fn); #define PLATFORM_FNS \ + X(add_file_watch) \ X(alloc_arena) \ X(close) \ X(open_for_write) \ @@ -231,7 +261,11 @@ typedef PLATFORM_WRITE_FILE_FN(platform_write_file_fn); X(write_file) #define X(name) platform_ ## name ## _fn *name; -typedef struct { PLATFORM_FNS } Platform; +struct Platform { + PLATFORM_FNS + FileWatchContext file_watch_context; + iptr os_context; +}; #undef X typedef struct {