diff --git a/game/game.odin b/game/game.odin index 0f973a1..af93930 100644 --- a/game/game.odin +++ b/game/game.odin @@ -4,7 +4,7 @@ import "core:time" import "core:fmt" @(export) -hello :: proc() { +init :: proc() { fmt.println("hello from .dll!") } diff --git a/src/hot_reload/hot_reload.odin b/src/hot_reload/hot_reload.odin new file mode 100644 index 0000000..8b537fc --- /dev/null +++ b/src/hot_reload/hot_reload.odin @@ -0,0 +1,138 @@ +package hot_reload + +import "core:dynlib" +import "core:fmt" +import "core:os" + +when ODIN_OS == .Windows { + LIB_EXT :: ".dll" +} else when ODIN_OS == .Darwin { + LIB_EXT :: ".dylib" +} else { + LIB_EXT :: ".so" +} + +GAME_LIB_DIR :: "build/" +GAME_LIB_PATH :: GAME_LIB_DIR + "game" + LIB_EXT + +// ----------------------------------------- + +Hot_Reload :: struct { + loaded_libs: [dynamic]Game_API, +} + +Hot_Reload_Error :: enum u8 { + None = 0, + Load_Game_Lib_Failed = 1, + Unimplemented = 127, + Okay = None, +} + +Error :: union #shared_nil { + Hot_Reload_Error, +} + +Game_API :: struct { + lib: dynlib.Library, + init: proc(), + update: proc(), + render: proc(), + destroy: proc(), + should_close: proc() -> bool, + api_version: int, // iteration of the game lib +} + +// ----------------------------------------- + +hot_reload_init :: proc() -> (state: Hot_Reload, error: Error) { + api: Game_API + api.api_version = -1 + + api_ok := load_game_lib(&api) + if !api_ok { + fmt.println("Failed to load Game API") + return {}, .Load_Game_Lib_Failed + } + + state.loaded_libs = make([dynamic]Game_API) + append(&state.loaded_libs, api) + + return state, nil +} + +hot_reload_destroy :: proc(state: ^Hot_Reload) { + for &api in state.loaded_libs { + unload_game_lib(&api) + } + + delete(state.loaded_libs) +} + +// ----------------------------------------- + +active_lib :: proc(hr: ^Hot_Reload) -> ^Game_API { + return &hr.loaded_libs[len(hr.loaded_libs) - 1] +} + +should_close :: proc(hr: ^Hot_Reload) -> bool { + api := active_lib(hr) + if api.lib == nil do return true + return api.should_close() +} + +copy_lib :: proc(to: string) -> bool { + copy_err := os.copy_file(to, GAME_LIB_PATH) + if copy_err != nil { + fmt.printfln( + "error: Failed to copy " + GAME_LIB_PATH + " to {0}: %v", + to, + copy_err, + ) + return false + } + + return true +} + +// Load game lib symbols +load_game_lib :: proc(api: ^Game_API) -> bool { + // Load next iteration + api.api_version += 1 + + lib_name := fmt.tprintf( + GAME_LIB_DIR + "game_{0}" + LIB_EXT, + api.api_version, + ) + copy_lib(lib_name) or_return + + // Match the names of the fields in Game_API to symbols in the game DLL + _, ok := dynlib.initialize_symbols(api, GAME_LIB_PATH, "", "lib") + if !ok { + fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) + } + + return ok +} + +// Unload game lib, used during full restarts +unload_game_lib :: proc(api: ^Game_API) { + if api.lib != nil { + if !dynlib.unload_library(api.lib) { + fmt.eprintfln( + "error: Failed unloading lib: {0}", + dynlib.last_error(), + ) + } + } + + if os.remove( + fmt.tprintf(GAME_LIB_DIR + "game_{0}" + LIB_EXT, api.api_version), + ) != + nil { + fmt.printfln( + "error: Failed to remove {0}game_{1}" + LIB_EXT + " copy", + GAME_LIB_DIR, + api.api_version, + ) + } +} diff --git a/src/main.odin b/src/main.odin index dcffb2e..c6ab779 100644 --- a/src/main.odin +++ b/src/main.odin @@ -1,37 +1,35 @@ package sindri -import "core:dynlib" import "core:fmt" import "core:time" +import "sindri:hot_reload" + VERSION :: #config(VERSION, "dev") // ----------------------------------------- -instance_ready: bool - -Game_API :: struct { - __handle: dynlib.Library, - hello: proc(), - should_close: proc() -> bool, -} - main :: proc() { fmt.println("hello world!") - api: Game_API - load_game_api(&api) - api.hello() + // Hot reload development functionality + hr, err := hot_reload.hot_reload_init() + defer hot_reload.hot_reload_destroy(&hr) - // Initialize window + api := hot_reload.active_lib(&hr) + api.init() + + // Initialize Window os_init() + defer os_destroy() // Initialize GPU resources instance_init() + defer instance_destroy() gt: f32 - for !os_should_close() && !api.should_close() { + for !os_should_close() && !hot_reload.should_close(&hr) { start := time.tick_now() os_poll_events() @@ -39,18 +37,4 @@ main :: proc() { gt = f32(time.duration_seconds(time.tick_since(start))) } - - // Cleanup GPU resources - instance_destroy() - - // Cleanup window - os_destroy() -} - -load_game_api :: proc(api: ^Game_API) { - // Match the names of the fields in Game_API to symbols in the game DLL - _, ok := dynlib.initialize_symbols(api, "build/game.dylib") - if !ok { - fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) - } }