From 761794f594d497014dd3af2c894bb569712f9019 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 22 Jan 2024 03:47:53 -0500 Subject: [PATCH] Hot reload works --- code/api.odin | 46 ++++++++++-------- code/env.odin | 3 ++ code/filesystem.odin | 20 ++++++-- code/host/host.odin | 108 ++++++++++++++++++++++++++++--------------- code/text.odin | 13 +++--- scripts/build.ps1 | 37 +++++++++------ 6 files changed, 147 insertions(+), 80 deletions(-) diff --git a/code/api.odin b/code/api.odin index e93b823..c7b7281 100644 --- a/code/api.odin +++ b/code/api.odin @@ -11,7 +11,7 @@ Path_Assets :: "../assets/" ModuleAPI :: struct { lib : dynlib.Library, - load_time : os.File_Time, + write_time : os.File_Time, lib_version : i32, startup : type_of( startup ), @@ -33,22 +33,21 @@ memory : Memory startup :: proc( persistent, transient, temp : ^ mem.Arena ) { memory.persistent = persistent + state := cast(^State) memory.persistent; using state // Anything allocated by default is considered transient. context.allocator = mem.arena_allocator( transient ) context.temp_allocator = mem.arena_allocator( temp ) - state := cast(^State) memory.persistent - // Rough setup of window with rl stuff - screen_width : i32 = 1280 - screen_height : i32 = 1000 + screen_width = 1280 + screen_height = 1000 win_title : cstring = "Sectr Prototype" rl.InitWindow( screen_width, screen_height, win_title ) // Determining current monitor and setting the target frametime based on it.. - monitor_id := rl.GetCurrentMonitor() - monitor_refresh_hz := rl.GetMonitorRefreshRate( monitor_id ) + monitor_id = rl.GetCurrentMonitor() + monitor_refresh_hz = rl.GetMonitorRefreshRate( monitor_id ) rl.SetTargetFPS( monitor_refresh_hz ) fmt.println( "Set target FPS to: %v", monitor_refresh_hz ) @@ -68,7 +67,11 @@ startup :: proc( persistent, transient, temp : ^ mem.Arena ) @export sectr_shutdown :: proc() { - rl.UnloadFont( font_rec_mono_semicasual_reg ) + if memory.persistent == nil { + return + } + state := cast( ^ State ) memory.persistent + rl.UnloadFont( state.font_rec_mono_semicasual_reg ) rl.CloseWindow() } @@ -77,21 +80,25 @@ reload :: proc( persistent, transient, temp : ^ mem.Arena ) { memory.persistent = persistent memory.transient = transient - context.allocator = mem.arena_allocator( persistent ) - context.temp_allocator = mem.arena_allocator( transient ) + memory.temp = temp + context.allocator = mem.arena_allocator( transient ) + context.temp_allocator = mem.arena_allocator( temp ) } @export update :: proc() -> b32 { - should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose() + state := cast( ^ State ) memory.persistent + + should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose() return should_shutdown } -draw_text_y : f32 = 50 @export render :: proc() { + state := cast( ^ State ) memory.persistent; using state + rl.BeginDrawing() rl.ClearBackground( Color_BG ) defer { @@ -102,21 +109,22 @@ render :: proc() draw_text :: proc( format : string, args : ..any ) { - @static - draw_text_scratch : [Kilobyte * 64]u8 - if ( draw_text_y > 500 ) { - draw_text_y = 50 + @static draw_text_scratch : [Kilobyte * 64]u8 + + state := cast( ^ State ) memory.persistent; using state + if ( draw_debug_text_y > 800 ) { + draw_debug_text_y = 50 } content := fmt.bprintf( draw_text_scratch[:], format, ..args ) - debug_text( content, 25, draw_text_y ) + debug_text( content, 25, draw_debug_text_y ) - draw_text_y += 16 + draw_debug_text_y += 16 } draw_text( "Monitor : %v", rl.GetMonitorName(0) ) draw_text( "Screen Width : %v", rl.GetScreenWidth() ) draw_text( "Screen Height: %v", rl.GetScreenHeight() ) - draw_text_y = 50 + draw_debug_text_y = 50 } diff --git a/code/env.odin b/code/env.odin index 3c60c8e..cf34794 100644 --- a/code/env.odin +++ b/code/env.odin @@ -12,6 +12,9 @@ State :: struct { engine_refresh_hz : i32, engine_refresh_target : i32, + font_rec_mono_semicasual_reg : Font, + default_font : Font, + draw_debug_text_y : f32 } diff --git a/code/filesystem.odin b/code/filesystem.odin index 6fa8801..868f375 100644 --- a/code/filesystem.odin +++ b/code/filesystem.odin @@ -4,9 +4,9 @@ import "core:fmt" import "core:os" import "core:runtime" -copy_file_sync :: proc( path_src, path_dst: string ) -> bool +copy_file_sync :: proc( path_src, path_dst: string ) -> b32 { - file_size : i64 + file_size : i64 { path_info, result := os.stat( path_src, context.temp_allocator ) if result != os.ERROR_NONE { @@ -29,5 +29,19 @@ copy_file_sync :: proc( path_src, path_dst: string ) -> bool runtime.debug_trap() return false } - return true + return true +} + +is_file_locked :: proc( file_path: string ) -> b32 { + // Try to open the file for read access without sharing. + // If the file is locked, the call will fail. + handle, err := os.open(file_path, os.O_RDONLY) + if err != os.ERROR_NONE { + // If the error indicates the file is in use, return true. + return true + } + + // If the file opens successfully, close it and return false. + os.close(handle) + return false } diff --git a/code/host/host.odin b/code/host/host.odin index c190c07..8ffd622 100644 --- a/code/host/host.odin +++ b/code/host/host.odin @@ -16,15 +16,13 @@ import "core:mem/virtual" import "core:os" import "core:runtime" import "core:strings" +import "core:time" import rl "vendor:raylib" import sectr "../." RuntimeState :: struct { - running : b32, - - - memory : VMemChunk, - + running : b32, + memory : VMemChunk, sectr_api : sectr.ModuleAPI, } @@ -39,8 +37,7 @@ VMemChunk :: struct { setup_engine_memory :: proc () -> VMemChunk { - memory : VMemChunk - using memory + memory : VMemChunk; using memory arena_init :: mem.arena_init ptr_offset :: mem.ptr_offset @@ -58,24 +55,21 @@ setup_engine_memory :: proc () -> VMemChunk } // For now I'm making persistent sections each 128 meg and transient sections w/e is left over / 2 (one for engine the other for the env) - persistent_size :: Megabyte * 128 * 2 - transient_size :: (Gigabyte * 2 - persistent_size * 2) / 2 - + persistent_size :: Megabyte * 128 * 2 + transient_size :: (Gigabyte * 2 - persistent_size * 2) / 2 eng_persistent_size :: persistent_size / 4 eng_transient_size :: transient_size / 4 - env_persistent_size :: persistent_size - eng_persistent_size env_trans_temp_size :: (transient_size - eng_transient_size) / 2 block := memory.sarena.curr_block // Try to get a slice for each segment - eng_persistent_slice := slice_ptr( block.base, eng_persistent_size) + eng_persistent_slice := slice_ptr( block.base, eng_persistent_size) eng_transient_slice := slice_ptr( & eng_persistent_slice[ eng_persistent_size - 1], eng_transient_size) env_persistent_slice := slice_ptr( & eng_transient_slice [ eng_transient_size - 1], env_persistent_size) env_transient_slice := slice_ptr( & env_persistent_slice[ env_persistent_size - 1], env_trans_temp_size) env_temp_slice := slice_ptr( & env_transient_slice [ env_trans_temp_size - 1], env_trans_temp_size) - arena_init( & eng_persistent, eng_persistent_slice ) arena_init( & eng_transient, eng_transient_slice ) arena_init( & env_persistent, env_persistent_slice ) @@ -88,7 +82,7 @@ load_sectr_api :: proc ( version_id : i32 ) -> sectr.ModuleAPI { loaded_module : sectr.ModuleAPI - load_time, + write_time, result := os.last_write_time_by_name("sectr.dll") if result != os.ERROR_NONE { fmt. println("Could not resolve the last write time for sectr.dll") @@ -106,26 +100,53 @@ load_sectr_api :: proc ( version_id : i32 ) -> sectr.ModuleAPI return {} } + startup := cast( type_of( sectr.startup )) dynlib.symbol_address( lib, "startup" ) + shutdown := cast( type_of( sectr.sectr_shutdown )) dynlib.symbol_address( lib, "sectr_shutdown" ) + reload := cast( type_of( sectr.reload )) dynlib.symbol_address( lib, "reload" ) + update := cast( type_of( sectr.update )) dynlib.symbol_address( lib, "update" ) + render := cast( type_of( sectr.render )) dynlib.symbol_address( lib, "render" ) + + missing_symbol : b32 = false + if startup == nil do fmt.println("Failed to load sectr.startup symbol") + if shutdown == nil do fmt.println("Failed to load sectr.shutdown symbol") + if reload == nil do fmt.println("Failed to load sectr.reload symbol") + if update == nil do fmt.println("Failed to load sectr.update symbol") + if render == nil do fmt.println("Failed to load sectr.render symbol") + if missing_symbol { + runtime.debug_trap() + return {} + } + loaded_module = { lib = lib, - load_time = load_time, + write_time = write_time, lib_version = version_id, - startup = cast( type_of( sectr.startup )) dynlib.symbol_address( lib, "startup" ), - shutdown = cast( type_of( sectr.sectr_shutdown )) dynlib.symbol_address( lib, "sectr_shutdown" ), - reload = cast( type_of( sectr.reload )) dynlib.symbol_address( lib, "reload" ), - update = cast( type_of( sectr.update )) dynlib.symbol_address( lib, "update" ), - render = cast( type_of( sectr.render )) dynlib.symbol_address( lib, "render" ) + startup = startup, + shutdown = shutdown, + reload = reload, + update = update, + render = render } return loaded_module } +unload_sectr_api :: proc ( module : ^ sectr.ModuleAPI ) +{ + lock_file := fmt.tprintf( "sectr_{0}_locked.dll", module.lib_version ) + dynlib.unload_library( module.lib ) + // os.remove( lock_file ) + module^ = {} +} + main :: proc() { fmt.println("Hellope!") + state : RuntimeState + using state + // Basic Giant VMem Block - memory : VMemChunk { // By default odin uses a growing arena for the runtime context // We're going to make it static for the prototype and separate it from the 'project' memory. @@ -137,7 +158,6 @@ main :: proc() } // Load the Enviornment API for the first-time - sectr_api : sectr.ModuleAPI { sectr_api = load_sectr_api( 1 ) if sectr_api.lib_version == 0 { @@ -147,27 +167,40 @@ main :: proc() } } - state : RuntimeState - state.running = true; - state.memory = memory - state.sectr_api = sectr_api + running = true; + memory = memory + sectr_api = sectr_api + sectr_api.startup( & memory.env_persistent, & memory.env_transient, & memory.env_temp ) - state.sectr_api.startup( & memory.env_persistent, & memory.env_transient, & memory.env_temp ) - - // TODO(Ed) : This should return a end status so that we know the reason the engine stopped. - for ; state.running ; + // TODO(Ed) : This should have an end status so that we know the reason the engine stopped. + for ; running ; { // Hot-Reload - // TODO(ED) : Detect if currently loaded code is outdated. + if write_time, result := os.last_write_time_by_name("sectr.dll"); + result == os.ERROR_NONE && sectr_api.write_time != write_time { - // state.sectr_api.reload() + version_id := sectr_api.lib_version + 1 + unload_sectr_api( & sectr_api ) + + // Wait for pdb to unlock (linker may still be writting) + for ; sectr.is_file_locked( "sectr.pdb" ); { + } + time.sleep( time.Millisecond ) + + sectr_api = load_sectr_api( version_id ) + if sectr_api.lib_version == 0 { + fmt.println("Failed to hot-reload the sectr module") + runtime.debug_trap() + os.exit(-1) + } + sectr_api.reload( & memory.env_persistent, & memory.env_transient, & memory.env_temp ) } - // Logic Update - state.running = state.sectr_api.update() + running = sectr_api.update() + sectr_api.render() - // Rendering - state.sectr_api.render() + free_all( mem.arena_allocator( & memory.env_temp ) ) + // free_all( mem.arena_allocator( & memory.env_transient ) ) } // Determine how the run_cyle completed, if it failed due to an error, @@ -176,5 +209,6 @@ main :: proc() // TODO(Ed): Implement this. } - state.sectr_api.shutdown() + sectr_api.shutdown() + unload_sectr_api( & sectr_api ) } diff --git a/code/text.odin b/code/text.odin index c265678..d35635e 100644 --- a/code/text.odin +++ b/code/text.odin @@ -1,19 +1,20 @@ package sectr -import "core:unicode/utf8" - +import "core:unicode/utf8" import rl "vendor:raylib" -font_rec_mono_semicasual_reg : Font; -default_font : Font - -debug_text :: proc( content : string, x, y : f32, size : f32 = 16.0, color : rl.Color = rl.WHITE, font : rl.Font = default_font ) +debug_text :: proc( content : string, x, y : f32, size : f32 = 16.0, color : rl.Color = rl.WHITE, font : rl.Font = {} ) { if len( content ) == 0 { return } runes := utf8.string_to_runes( content, context.temp_allocator ) + font := font + if ( font.chars == nil ) { + font = ( cast( ^ State) memory.persistent ).default_font + } + rl.DrawTextCodepoints( font, raw_data(runes), cast(i32) len(runes), position = rl.Vector2 { x, y }, diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 0e064ae..e6ce130 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -57,22 +57,35 @@ push-location $path_root } function build-prototype { + $host_process_active = Get-Process | Where-Object {$_.Name -like 'sectr_host*'} + push-location $path_code $project_name = 'sectr' $executable = join-path $path_build ($project_name + '_host.exe') $pdb = join-path $path_build ($project_name + '_host.pdb') - $build_args = @() - $build_args += $flag_build - $build_args += './host' - $build_args += $flag_output_path + $executable - $build_args += $flag_optimize_none - $build_args += $flag_debug - $build_args += $flag_pdb_name + $pdb - $build_args += $flag_subsystem + 'windows' + if ( -not($host_process_active)) { + $build_args = @() + $build_args += $flag_build + $build_args += './host' + $build_args += $flag_output_path + $executable + $build_args += $flag_optimize_none + $build_args += $flag_debug + $build_args += $flag_pdb_name + $pdb + $build_args += $flag_subsystem + 'windows' - & odin $build_args + & odin $build_args + + $third_party_dlls = Get-ChildItem -Path $path_thirdparty -Filter '*.dll' + foreach ($dll in $third_party_dlls) { + $destination = join-path $path_build $dll.Name + Copy-Item $dll.FullName -Destination $destination -Force + } + } + else { + write-host 'Skipping sectr_host build, process is active' + } $module_dll = join-path $path_build ( $project_name + '.dll' ) $pdb = join-path $path_build ( $project_name + '.pdb' ) @@ -88,12 +101,6 @@ push-location $path_root & odin $build_args - $third_party_dlls = Get-ChildItem -Path $path_thirdparty -Filter '*.dll' - foreach ($dll in $third_party_dlls) { - $destination = join-path $path_build $dll.Name - Copy-Item $dll.FullName -Destination $destination -Force - } - Pop-Location } build-prototype