From 9f75d080a758ab1c9d14436e39018049809c86e6 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 15 Oct 2025 00:44:14 -0400 Subject: [PATCH] hot reload works with tick lanes and job worker loops! --- code2/grime/profiler.odin | 10 +++--- code2/grime/static_memory.odin | 4 +-- code2/host/host.odin | 56 +++++++++++++++-------------- code2/sectr/engine/client_api.odin | 57 ++++++++++++++++++++++++------ code2/sectr/engine/host_api.odin | 33 +++++++++-------- code2/sectr/pkg_mappings.odin | 11 +++--- 6 files changed, 109 insertions(+), 62 deletions(-) diff --git a/code2/grime/profiler.odin b/code2/grime/profiler.odin index f8a4f93..6b3a61b 100644 --- a/code2/grime/profiler.odin +++ b/code2/grime/profiler.odin @@ -7,24 +7,24 @@ This is just a snippet file, do not use directly. */ set_profiler_module_context :: #force_inline proc "contextless" (profiler : ^Spall_Context) { - sync_store(& static_memory.spall_context, profiler, .Release) + sync_store(& grime_memory.spall_context, profiler, .Release) } set_profiler_thread_buffer :: #force_inline proc "contextless" (buffer: ^Spall_Buffer) { - sync_store(& thread_memory.spall_buffer, buffer, .Release) + sync_store(& grime_thread.spall_buffer, buffer, .Release) } DISABLE_PROFILING :: true @(deferred_none = profile_end, disabled = DISABLE_PROFILING) profile :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) { - spall._buffer_begin( static_memory.spall_context, thread_memory.spall_buffer, name, "", loc ) + spall._buffer_begin( grime_memory.spall_context, grime_thread.spall_buffer, name, "", loc ) } @(disabled = DISABLE_PROFILING) profile_begin :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) { - spall._buffer_begin( static_memory.spall_context, thread_memory.spall_buffer, name, "", loc ) + spall._buffer_begin( grime_memory.spall_context, grime_thread.spall_buffer, name, "", loc ) } @(disabled = DISABLE_PROFILING) profile_end :: #force_inline proc "contextless" () { - spall._buffer_end( static_memory.spall_context, thread_memory.spall_buffer) + spall._buffer_end( grime_memory.spall_context, grime_thread.spall_buffer) } diff --git a/code2/grime/static_memory.odin b/code2/grime/static_memory.odin index 737c5dd..3809e61 100644 --- a/code2/grime/static_memory.odin +++ b/code2/grime/static_memory.odin @@ -1,8 +1,8 @@ package grime //region STATIC MEMORY - static_memory: StaticMemory -@thread_local thread_memory: ThreadMemory + grime_memory: StaticMemory +@thread_local grime_thread: ThreadMemory //endregion STATIC MEMORY StaticMemory :: struct { diff --git a/code2/host/host.odin b/code2/host/host.odin index 7c0dffa..6d18a37 100644 --- a/code2/host/host.odin +++ b/code2/host/host.odin @@ -19,21 +19,23 @@ load_client_api :: proc(version_id: int) -> (loaded_module: Client_API) { //TODO(Ed): Lets try to minimize this... thread_sleep( Millisecond * 100 ) // Get the live dll loaded up - live_file := Path_Sectr_Live_Module - file_copy_sync( Path_Sectr_Module, live_file, allocator = context.temp_allocator ) - did_load: bool; lib, did_load = os_lib_load( Path_Sectr_Module ) + file_copy_sync( Path_Sectr_Module, Path_Sectr_Live_Module, allocator = context.temp_allocator ) + did_load: bool; lib, did_load = os_lib_load( Path_Sectr_Live_Module ) if ! did_load do panic( "Failed to load the sectr module.") startup = cast( type_of( host_memory.client_api.startup)) os_lib_get_proc(lib, "startup") tick_lane_startup = cast( type_of( host_memory.client_api.tick_lane_startup)) os_lib_get_proc(lib, "tick_lane_startup") + job_worker_startup = cast( type_of( host_memory.client_api.job_worker_startup)) os_lib_get_proc(lib, "job_worker_startup") hot_reload = cast( type_of( host_memory.client_api.hot_reload)) os_lib_get_proc(lib, "hot_reload") tick_lane = cast( type_of( host_memory.client_api.tick_lane)) os_lib_get_proc(lib, "tick_lane") clean_frame = cast( type_of( host_memory.client_api.clean_frame)) os_lib_get_proc(lib, "clean_frame") jobsys_worker_tick = cast( type_of( host_memory.client_api.jobsys_worker_tick)) os_lib_get_proc(lib, "jobsys_worker_tick") - if startup == nil do panic("Failed to load sectr.startup symbol" ) - if tick_lane_startup == nil do panic("Failed to load sectr.tick_lane_startup symbol" ) - if hot_reload == nil do panic("Failed to load sectr.hot_reload symbol" ) - if tick_lane == nil do panic("Failed to load sectr.tick_lane symbol" ) - if clean_frame == nil do panic("Failed to load sectr.clean_frmae symbol" ) + if startup == nil do panic("Failed to load sectr.startup symbol" ) + if tick_lane_startup == nil do panic("Failed to load sectr.tick_lane_startup symbol" ) + if job_worker_startup == nil do panic("Failed to load sectr.job_worker_startup symbol" ) + if hot_reload == nil do panic("Failed to load sectr.hot_reload symbol" ) + if tick_lane == nil do panic("Failed to load sectr.tick_lane symbol" ) + if clean_frame == nil do panic("Failed to load sectr.clean_frame symbol" ) + if jobsys_worker_tick == nil do panic("Failed to laod sectr.jobsys_worker_tick") lib_version = version_id return } @@ -132,6 +134,7 @@ main :: proc() thread_start(worker_thread) } } + barrier_init(& host_memory.lane_job_sync, THREAD_TICK_LANES + THREAD_JOB_WORKERS) host_tick_lane() } @@ -187,6 +190,7 @@ host_tick_lane :: proc() delta_ns = time_tick_lap_time( & host_tick ) host_tick = time_tick_now() } + leader := barrier_wait(& host_memory.lane_sync) host_lane_shutdown() } host_lane_shutdown :: proc() @@ -212,8 +216,11 @@ host_job_worker_entrypoint :: proc(worker_thread: ^SysThread) host_memory.client_api.jobsys_worker_tick() // TODO(Ed): We cannot allow job threads to enter the reload barrier until all jobs have drained. if sync_load(& host_memory.client_api_hot_reloaded, .Acquire) { - leader :=barrier_wait(& host_memory.job_hot_reload_sync) - break + // Signals to main hread when all jobs have drained. + leader :=barrier_wait(& host_memory.job_hot_reload_sync) + // Job threads wait here until client module is back + leader =barrier_wait(& host_memory.job_hot_reload_sync) + host_memory.client_api.hot_reload(& host_memory, & thread_memory) } } } @@ -232,28 +239,25 @@ sync_client_api :: proc() profile("Master_Prepper: Reloading client module") sync_store(& host_memory.client_api_hot_reloaded, true, .Release) // We nee to wait for the job queue to drain. - barrier_wait(& host_memory.job_hot_reload_sync) + leader = barrier_wait(& host_memory.job_hot_reload_sync) + { + version_id := host_memory.client_api.lib_version + 1 + unload_client_api( & host_memory.client_api ) + // Wait for pdb to unlock (linker may still be writting) + for ; file_is_locked( Path_Sectr_Debug_Symbols ) && file_is_locked( Path_Sectr_Live_Module ); {} - version_id := host_memory.client_api.lib_version + 1 - unload_client_api( & host_memory.client_api ) - // Wait for pdb to unlock (linker may still be writting) - for ; file_is_locked( Path_Sectr_Debug_Symbols ) && file_is_locked( Path_Sectr_Live_Module ); {} - thread_sleep( Millisecond * 100 ) - host_memory.client_api = load_client_api( version_id ) - verify( host_memory.client_api.lib_version != 0, "Failed to hot-reload the sectr module" ) + thread_sleep( Millisecond * 100 ) - // Don't let jobs continue until after we clear loading. - barrier_wait(& host_memory.job_hot_reload_sync) + host_memory.client_api = load_client_api( version_id ) + verify( host_memory.client_api.lib_version != 0, "Failed to hot-reload the sectr module" ) + } + leader = barrier_wait(& host_memory.job_hot_reload_sync) } } - leader = barrier_wait(& host_memory.lane_sync) + leader = barrier_wait(& host_memory.lane_sync) // Lanes are safe to continue. - if sync_load(& host_memory.client_api_hot_reloaded, .Acquire) - { + if sync_load(& host_memory.client_api_hot_reloaded, .Acquire) { host_memory.client_api.hot_reload(& host_memory, & thread_memory) - if thread_memory.id == .Master_Prepper { - sync_store(& host_memory.client_api_hot_reloaded, false, .Release) - } } } unload_client_api :: proc( module : ^Client_API ) diff --git a/code2/sectr/engine/client_api.odin b/code2/sectr/engine/client_api.odin index 81ce3db..bc13361 100644 --- a/code2/sectr/engine/client_api.odin +++ b/code2/sectr/engine/client_api.odin @@ -18,6 +18,7 @@ ModuleAPI :: struct { startup: type_of( startup ), tick_lane_startup: type_of( tick_lane_startup), + job_worker_startup: type_of( job_worker_startup), hot_reload: type_of( hot_reload ), tick_lane: type_of( tick_lane ), clean_frame: type_of( clean_frame), @@ -47,21 +48,46 @@ Threads will eventually return to their tick_lane upon completion. @export hot_reload :: proc(host_mem: ^ProcessMemory, thread_mem: ^ThreadMemory) { - profile(#procedure) - thread = thread_mem - if thread.id == .Master_Prepper { - grime_set_profiler_module_context(& memory.spall_context) - sync_store(& memory, host_mem, .Release) + // Critical reference synchronization + { + thread = thread_mem + if thread.id == .Master_Prepper { + sync_store(& memory, host_mem, .Release) + grime_set_profiler_module_context(& memory.spall_context) + } + else { + for ; memory == nil; { + sync_load(& memory, .Acquire) + } + for ; thread == nil; { + thread = thread_mem + } + } + grime_set_profiler_thread_buffer(& thread.spall_buffer) + } + profile(#procedure) + // Do hot-reload stuff... + { + + } + // Critical reference synchronization + { + leader := barrier_wait(& memory.lane_job_sync) + if thread.id == .Master_Prepper { + sync_store(& memory.client_api_hot_reloaded, false, .Release) + } + else { + for ; memory.client_api_hot_reloaded == true; { + sync_load(& memory.client_api_hot_reloaded, .Acquire) + } + } + leader = barrier_wait(& memory.lane_job_sync) } - grime_set_profiler_thread_buffer(& thread.spall_buffer) } /* Called by host_tick_lane_startup Used for lane specific startup operations - -The lane tick cannot be handled it, its call must be done by the host module. -(We need threads to not be within a client callstack in the even of a hot-reload) */ @export tick_lane_startup :: proc(thread_mem: ^ThreadMemory) @@ -73,8 +99,19 @@ tick_lane_startup :: proc(thread_mem: ^ThreadMemory) profile(#procedure) } -/* +@export +job_worker_startup :: proc(thread_mem: ^ThreadMemory) +{ + if thread_mem.id != .Master_Prepper { + thread = thread_mem + grime_set_profiler_thread_buffer(& thread.spall_buffer) + } + profile(#procedure) +} +/* +Host handles the loop. +(We need threads to be outside of client callstack in the event of a hot-reload) */ @export tick_lane :: proc(host_delta_time_ms: f64, host_delta_ns: Duration) -> (should_close: b64 = false) diff --git a/code2/sectr/engine/host_api.odin b/code2/sectr/engine/host_api.odin index de8196b..738b4fd 100644 --- a/code2/sectr/engine/host_api.odin +++ b/code2/sectr/engine/host_api.odin @@ -17,39 +17,42 @@ ProcessMemory :: struct { // Host host_persist_buf: [32 * Mega]byte, host_scratch_buf: [64 * Mega]byte, - host_persist: Odin_Arena, - host_scratch: Odin_Arena, - host_api: Host_API, + host_persist: Odin_Arena, // Host Persistent (Non-Wipeable), for bad third-party static object allocation + host_scratch: Odin_Arena, // Host Temporary Wipable + host_api: Host_API, // Client -> Host Interface // Textual Logging - logger: Logger, + logger: Logger, path_logger_finalized: string, // Profiling spall_context: Spall_Context, + // TODO(Ed): Try out Superluminal's API! // Multi-threading - threads: [MAX_THREADS](^SysThread), - job_system: JobSystemContext, - tick_lanes: int, - lane_sync: sync.Barrier, - job_hot_reload_sync: sync.Barrier, // Used to sync jobs with main thread during hot-reload junction. - tick_running: b64, + threads: [MAX_THREADS](^SysThread), // All threads are tracked here. + job_system: JobSystemContext, // State tracking for job system. + tick_running: b64, // When disabled will lead to shutdown of the process. + tick_lanes: int, // Runtime tracker of live tick lane threads + lane_sync: sync.Barrier, // Used to sync tick lanes during wide junctions. + job_hot_reload_sync: sync.Barrier, // Used to sync jobs with main thread during hot-reload junction. + lane_job_sync: sync.Barrier, // Used to sync tick lanes and job workers during hot-reload. // Client Module - client_api_hot_reloaded: b64, - client_api: ModuleAPI, - client_memory: State, + client_api_hot_reloaded: b64, // Used to signal to threads when hot-reload paths should be taken. + client_api: ModuleAPI, // Host -> Client Interface + client_memory: State, } Host_API :: struct { - request_virtual_memory: #type proc(), - request_virtual_mapped_io: #type proc(), + request_virtual_memory: #type proc(), // All dynamic allocations will utilize vmem interfaces + request_virtual_mapped_io: #type proc(), // TODO(Ed): Figure out usage constraints of this. } ThreadMemory :: struct { using _: ThreadWorkerContext, + // Per-thread profiling spall_buffer_backing: [SPALL_BUFFER_DEFAULT_SIZE * 2]byte, spall_buffer: Spall_Buffer, } diff --git a/code2/sectr/pkg_mappings.odin b/code2/sectr/pkg_mappings.odin index f747fe2..b4f78db 100644 --- a/code2/sectr/pkg_mappings.odin +++ b/code2/sectr/pkg_mappings.odin @@ -32,9 +32,10 @@ import "core:prof/spall" Spall_Buffer :: spall.Buffer import "core:sync" - AtomicMutex :: sync.Atomic_Mutex - sync_store :: sync.atomic_store_explicit - sync_load :: sync.atomic_load_explicit + AtomicMutex :: sync.Atomic_Mutex + barrier_wait :: sync.barrier_wait + sync_store :: sync.atomic_store_explicit + sync_load :: sync.atomic_load_explicit import threading "core:thread" SysThread :: threading.Thread @@ -43,7 +44,9 @@ import threading "core:thread" thread_start :: threading.start import "core:time" - Duration :: time.Duration + Millisecond :: time.Millisecond + Duration :: time.Duration + thread_sleep :: time.sleep import "codebase:grime" Logger :: grime.Logger