got multi-laned hot-reload

This commit is contained in:
2025-10-13 02:13:58 -04:00
parent 8ced7cc71e
commit 5f57cea027
18 changed files with 499 additions and 176 deletions

View File

@@ -1,3 +1,5 @@
# Host Module
The sole job of this module is to provide a bare launch pad and runtime module hot-reload support for the client module (sectr). To achieve this the static memory of the client module is tracked by the host and provides an api for the client to reload itself when a change is detected. The client is reponsible for populating the static memory reference and doing anything else it needs via the host api that it cannot do on its own.
Uses the core's Arena allocator.

View File

@@ -1,27 +1,77 @@
package host
import "core:thread"
import "core:sync"
Path_Logs :: "../logs"
when ODIN_OS == .Windows
{
Path_Sectr_Module :: "sectr.dll"
Path_Sectr_Live_Module :: "sectr_live.dll"
Path_Sectr_Debug_Symbols :: "sectr.pdb"
Path_Sectr_Spall_Record :: "sectr.spall"
}
// Only static memory host has.
host_memory: HostMemory
host_memory: ProcessMemory
@(thread_local)
thread_memory: ThreadMemory
load_client_api :: proc(version_id: int) -> (loaded_module: Client_API)
{
write_time, result := file_last_write_time_by_name("sectr.dll")
if result != OS_ERROR_NONE {
panic_contextless( "Could not resolve the last write time for sectr")
}
thread_sleep( Millisecond * 100 )
live_file := Path_Sectr_Live_Module
file_copy_sync( Path_Sectr_Module, live_file, allocator = context.temp_allocator )
lib, load_result := os_lib_load( live_file )
if ! load_result {
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")
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")
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" )
loaded_module.lib = lib
loaded_module.write_time = write_time
loaded_module.lib_version = version_id
loaded_module.startup = startup
loaded_module.tick_lane_startup = tick_lane_startup
loaded_module.hot_reload = hot_reload
loaded_module.tick_lane = tick_lane
loaded_module.clean_frame = clean_frame
return
}
master_prepper_proc :: proc(thread: ^SysThread) {}
main :: proc()
{
// TODO(Ed): Change this
host_scratch: Arena; arena_init(& host_scratch, host_memory.host_scratch[:])
context.allocator = arena_allocator(& host_scratch)
context.temp_allocator = context.allocator
// Setup host arenas
arena_init(& host_memory.host_persist, host_memory.host_persist_buf[:])
arena_init(& host_memory.host_scratch, host_memory.host_scratch_buf[:])
context.allocator = arena_allocator(& host_memory.host_persist)
context.temp_allocator = arena_allocator(& host_memory.host_scratch)
// Setup the profiler
{
buffer_backing := make([]u8, SPALL_BUFFER_DEFAULT_SIZE * 4)
host_memory.spall_profiler.ctx = spall_context_create(Path_Sectr_Spall_Record)
host_memory.spall_profiler.buffer = spall_buffer_create(buffer_backing)
}
// Setu the "Master Prepper" thread
thread_memory.id = .Master_Prepper
thread_id := thread_current_id()
{
@@ -34,62 +84,137 @@ main :: proc()
// system_ctx.win32_thread_id = w32_get_current_thread_id()
system_ctx.id = cast(int) system_ctx.win32_thread_id
}
free_all(context.temp_allocator)
}
write_time, result := file_last_write_time_by_name("sectr.dll")
if result != OS_ERROR_NONE {
panic_contextless( "Could not resolve the last write time for sectr")
}
thread_sleep( Millisecond * 100 )
live_file := Path_Sectr_Live_Module
file_copy_sync( Path_Sectr_Module, live_file, allocator = context.temp_allocator )
// Setup the logger
{
lib, load_result := os_lib_load( live_file )
if ! load_result {
panic( "Failed to load the sectr module." )
fmt_backing := make([]byte, 32 * Kilo)
defer free_all(context.temp_allocator)
// Generating the logger's name, it will be used when the app is shutting down.
path_logger_finalized : string
{
startup_time := time_now()
year, month, day := time_date( startup_time)
hour, min, sec := time_clock_from_time( startup_time)
if ! os_is_directory( Path_Logs ) {
os_make_directory( Path_Logs )
}
timestamp := str_pfmt_buffer( fmt_backing, "%04d-%02d-%02d_%02d-%02d-%02d", year, month, day, hour, min, sec)
path_logger_finalized = str_pfmt_buffer( fmt_backing, "%s/sectr_%v.log", Path_Logs, timestamp)
}
logger_init( & host_memory.logger, "Sectr Host", str_pfmt_buffer( fmt_backing, "%s/sectr.log", Path_Logs ) )
context.logger = to_odin_logger( & host_memory.logger )
{
// Log System Context
builder := strbuilder_from_bytes( fmt_backing )
str_pfmt_builder( & builder, "Core Count: %v, ", os_core_count() )
str_pfmt_builder( & builder, "Page Size: %v", os_page_size() )
log_print( to_str(builder) )
}
startup := cast( type_of( host_memory.client_api.startup)) os_lib_get_proc(lib, "startup")
hot_reload := cast( type_of( host_memory.client_api.hot_reload)) os_lib_get_proc(lib, "hot_reload")
tick_lane_startup := cast( type_of( host_memory.client_api.tick_lane_startup)) os_lib_get_proc(lib, "tick_lane_startup")
if startup == nil do panic("Failed to load sectr.startup symbol" )
if hot_reload == nil do panic("Failed to load sectr.hot_reload symbol" )
if tick_lane_startup == nil do panic("Failed to load sectr.tick_lane_startup symbol" )
host_memory.client_api.lib = lib
host_memory.client_api.startup = startup
host_memory.client_api.hot_reload = hot_reload
host_memory.client_api.tick_lane_startup = tick_lane_startup
}
context.logger = to_odin_logger( & host_memory.logger )
// Load the Enviornment API for the first-time
{
host_memory.client_api = load_client_api( 1 )
verify( host_memory.client_api.lib_version != 0, "Failed to initially load the sectr module" )
}
// Client API Startup
host_memory.host_api.sync_client_module = sync_client_api
host_memory.host_api.launch_tick_lane_thread = launch_tick_lane_thread
host_memory.client_api.startup(& host_memory, & thread_memory)
// Start the tick lanes
thread_wide_startup()
}
@export
sync_client_api :: proc() {
assert_contextless(thread_memory.id == .Master_Prepper)
// Fill out detection and reloading of client api.
// Needs to flag and atomic to spin-lock live helepr threads when reloading
thread_wide_startup :: proc()
{
assert(thread_memory.id == .Master_Prepper)
if THREAD_TICK_LANES > 1 {
launch_tick_lane_thread(.Atomic_Accountant)
sync.barrier_init(& host_memory.client_api_sync_lock, THREAD_TICK_LANES)
}
host_tick_lane_startup(thread_memory.system_ctx)
}
import "core:thread"
@export
launch_tick_lane_thread :: proc(id : WorkerID) {
assert_contextless(thread_memory.id == .Master_Prepper)
// TODO(Ed): We need to make our own version of this that doesn't allocate memory.
lane_thread := thread.create(host_tick_lane_startup, .High)
lane_thread := thread.create(host_tick_lane_startup, .High)
lane_thread.user_index = int(id)
thread.start(lane_thread)
}
host_tick_lane_startup :: proc(lane_thread: ^SysThread) {
thread_memory.system_ctx = lane_thread
thread_memory.id = cast(WorkerID) lane_thread.user_index
thread_memory.id = cast(WorkerID) lane_thread.user_index
host_memory.client_api.tick_lane_startup(& thread_memory)
host_tick_lane()
}
host_tick_lane :: proc()
{
delta_ns: Duration
host_tick := time_tick_now()
running : b64 = true
for ; running ;
{
profile("Host Tick")
sync_client_api()
running = host_memory.client_api.tick_lane( duration_seconds(delta_ns), delta_ns )
// host_memory.client_api.clean_frame()
delta_ns = time_tick_lap_time( & host_tick )
host_tick = time_tick_now()
}
}
@export
sync_client_api :: proc()
{
leader := sync.barrier_wait(& host_memory.client_api_sync_lock)
free_all(context.temp_allocator)
profile(#procedure)
if thread_memory.id == .Master_Prepper
{
write_time, result := file_last_write_time_by_name( Path_Sectr_Module );
if result == OS_ERROR_NONE && host_memory.client_api.write_time != write_time
{
thread_coherent_store(& host_memory.client_api_hot_reloaded, true)
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" )
}
}
leader = sync.barrier_wait(& host_memory.client_api_sync_lock)
if thread_coherent_load(& host_memory.client_api_hot_reloaded)
{
host_memory.client_api.hot_reload(& host_memory, & thread_memory)
if thread_memory.id == .Master_Prepper {
thread_coherent_store(& host_memory.client_api_hot_reloaded, false)
}
}
}
unload_client_api :: proc( module : ^Client_API )
{
os_lib_unload( module.lib )
file_remove( Path_Sectr_Live_Module )
module^ = {}
log_print("Unloaded sectr API")
}

View File

@@ -1,9 +1,9 @@
package host
import "base:builtin"
// import "base:builtin"
// Odin_OS_Type :: type_of(ODIN_OS)
import "base:intrinsics"
// import "base:intrinsics"
// atomic_thread_fence :: intrinsics.atomic_thread_fence
// mem_zero :: intrinsics.mem_zero
// mem_zero_volatile :: intrinsics.mem_zero_volatile
@@ -11,47 +11,142 @@ import "base:intrinsics"
// mem_copy_overlapping :: intrinsics.mem_copy
import "base:runtime"
// Assertion_Failure_Proc :: runtime.Assertion_Failure_Proc
// Logger :: runtime.Logger
debug_trap :: runtime.debug_trap
import "core:dynlib"
os_lib_load :: dynlib.load_library
os_lib_unload :: dynlib.unload_library
os_lib_get_proc :: dynlib.symbol_address
import "core:fmt"
str_pfmt_builder :: fmt.sbprintf
str_pfmt_buffer :: fmt.bprintf
import "core:log"
LoggerLevel :: log.Level
import "core:mem"
Arena :: mem.Arena
arena_allocator :: mem.arena_allocator
arena_init :: mem.arena_init
import core_os "core:os"
file_last_write_time_by_name :: core_os.last_write_time_by_name
OS_ERROR_NONE :: core_os.ERROR_NONE
import "core:os"
FileTime :: os.File_Time
file_last_write_time_by_name :: os.last_write_time_by_name
file_remove :: os.remove
OS_ERROR_NONE :: os.ERROR_NONE
os_is_directory :: os.is_dir
os_make_directory :: os.make_directory
os_core_count :: os.processor_core_count
os_page_size :: os.get_page_size
process_exit :: os.exit
import "core:prof/spall"
SPALL_BUFFER_DEFAULT_SIZE :: spall.BUFFER_DEFAULT_SIZE
spall_context_create :: spall.context_create
spall_buffer_create :: spall.buffer_create
import "core:strings"
strbuilder_from_bytes :: strings.builder_from_bytes
builder_to_str :: strings.to_string
import "core:sync"
thread_current_id :: sync.current_thread_id
thread_current_id :: sync.current_thread_id
thread_coherent_load :: sync.atomic_load
thread_coherent_store :: sync.atomic_store
import "core:time"
Millisecond :: time.Millisecond
Second :: time.Second
Duration :: time.Duration
duration_seconds :: time.duration_seconds
thread_sleep :: time.sleep
Millisecond :: time.Millisecond
Second :: time.Second
Duration :: time.Duration
time_clock_from_time :: time.clock_from_time
duration_seconds :: time.duration_seconds
time_date :: time.date
time_now :: time.now
thread_sleep :: time.sleep
time_tick_now :: time.tick_now
time_tick_lap_time :: time.tick_lap_time
import "core:thread"
SysThread :: thread.Thread
import grime "codebase:grime"
file_copy_sync :: grime.file_copy_sync
DISABLE_PROFILING :: grime.DISABLE_PROFILING
file_copy_sync :: grime.file_copy_sync
file_is_locked :: grime.file_is_locked
logger_init :: grime.logger_init
to_odin_logger :: grime.to_odin_logger
import "codebase:sectr"
MAX_THREADS :: sectr.MAX_THREADS
Client_API :: sectr.ModuleAPI
HostMemory :: sectr.HostMemory
ThreadMemory :: sectr.ThreadMemory
WorkerID :: sectr.WorkerID
MAX_THREADS :: sectr.MAX_THREADS
THREAD_TICK_LANES :: sectr.THREAD_TICK_LANES
Client_API :: sectr.ModuleAPI
ProcessMemory :: sectr.ProcessMemory
ThreadMemory :: sectr.ThreadMemory
WorkerID :: sectr.WorkerID
SpallProfiler :: sectr.SpallProfiler
ensure :: #force_inline proc( condition : b32, msg : string, location := #caller_location )
{
if condition {
return
}
log_print( msg, LoggerLevel.Warning, location )
debug_trap()
}
// TODO(Ed) : Setup exit codes!
fatal :: #force_inline proc( msg : string, exit_code : int = -1, location := #caller_location )
{
log_print( msg, LoggerLevel.Fatal, location )
debug_trap()
process_exit( exit_code )
}
// TODO(Ed) : Setup exit codes!
verify :: #force_inline proc( condition : b32, msg : string, exit_code : int = -1, location := #caller_location )
{
if condition {
return
}
log_print( msg, LoggerLevel.Fatal, location )
debug_trap()
process_exit( exit_code )
}
log_print :: proc( msg : string, level := LoggerLevel.Info, loc := #caller_location ) {
context.allocator = arena_allocator(& host_memory.host_scratch)
context.temp_allocator = arena_allocator(& host_memory.host_scratch)
log.log( level, msg, location = loc )
}
log_print_fmt :: proc( fmt : string, args : ..any, level := LoggerLevel.Info, loc := #caller_location ) {
context.allocator = arena_allocator(& host_memory.host_scratch)
context.temp_allocator = arena_allocator(& host_memory.host_scratch)
log.logf( level, fmt, ..args, location = loc )
}
@(deferred_none = profile_end, disabled = DISABLE_PROFILING)
profile :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
spall._buffer_begin( & host_memory.spall_profiler.ctx, & host_memory.spall_profiler.buffer, name, "", loc )
}
@(disabled = DISABLE_PROFILING)
profile_begin :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
spall._buffer_begin( & host_memory.spall_profiler.ctx, & host_memory.spall_profiler.buffer, name, "", loc )
}
@(disabled = DISABLE_PROFILING)
profile_end :: #force_inline proc "contextless" () {
spall._buffer_end( & host_memory.spall_profiler.ctx, & host_memory.spall_profiler.buffer)
}
Kilo :: 1024
Mega :: Kilo * 1024
Giga :: Mega * 1024
Tera :: Giga * 1024
to_str :: proc {
builder_to_str,
}