234 lines
5.4 KiB
Odin
234 lines
5.4 KiB
Odin
package hot_reload
|
|
|
|
import "core:c"
|
|
import "core:dynlib"
|
|
import "core:fmt"
|
|
import "core:os"
|
|
import "core:time"
|
|
|
|
import "sindri:core"
|
|
|
|
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,
|
|
memory: proc() -> rawptr,
|
|
memory_free: proc(),
|
|
memory_size: proc() -> int,
|
|
memory_set: proc(mem: rawptr),
|
|
settings: proc() -> core.Settings,
|
|
init_once: proc(),
|
|
init: proc(),
|
|
update: proc(_: f32),
|
|
render: proc(),
|
|
destroy: proc(),
|
|
should_close: proc() -> bool,
|
|
force_reload: proc() -> bool,
|
|
force_restart: proc() -> bool,
|
|
modification_time: time.Time,
|
|
api_version: int, // iteration of the game lib
|
|
}
|
|
|
|
// -----------------------------------------
|
|
|
|
hot_reload_init :: proc() -> (hr: Hot_Reload, error: Error) {
|
|
api: Game_API
|
|
|
|
api_ok := load_game_lib(&api)
|
|
if !api_ok {
|
|
fmt.println("error: failed to load Game API")
|
|
return {}, .Load_Game_Lib_Failed
|
|
}
|
|
|
|
hr.loaded_libs = make([dynamic]Game_API, 0, 0, context.allocator)
|
|
append(&hr.loaded_libs, api)
|
|
|
|
return hr, 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 {0} to {1}: {2:v}",
|
|
GAME_LIB_PATH,
|
|
to,
|
|
copy_err,
|
|
)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Load game lib symbols
|
|
load_game_lib :: proc(api: ^Game_API) -> bool {
|
|
mod_time, mod_time_err := os.last_write_time_by_name(GAME_LIB_PATH)
|
|
if mod_time_err != os.ERROR_NONE {
|
|
fmt.printfln(
|
|
"error: failed getting last write time of {0}, error code: {1}",
|
|
GAME_LIB_PATH,
|
|
mod_time_err,
|
|
)
|
|
return false
|
|
}
|
|
api.modification_time = mod_time
|
|
|
|
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, lib_name, "", "lib")
|
|
if !ok {
|
|
fmt.printfln(
|
|
"error: failed initializing symbols: {0}",
|
|
dynlib.last_error(),
|
|
)
|
|
} else {
|
|
// Odin's shared libraries don't register a constructor, so the lib's
|
|
// runtime init (@init procs like os.stdout setup) never runs on load.
|
|
// Call it explicitly.
|
|
// Windows doesn't need this: its dynamic build exports DllMain, which
|
|
// the OS calls on load and which runs _startup_runtime itself.
|
|
when ODIN_OS != .Windows {
|
|
Entry :: #type proc "c" () -> c.int
|
|
entry_ptr, entry_ok := dynlib.symbol_address(
|
|
api.lib,
|
|
"_odin_entry_point",
|
|
)
|
|
if entry_ok do (transmute(Entry)entry_ptr)()
|
|
}
|
|
}
|
|
|
|
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}{2} copy",
|
|
GAME_LIB_DIR,
|
|
api.api_version,
|
|
LIB_EXT,
|
|
)
|
|
}
|
|
}
|
|
|
|
reload_game_lib :: proc(hr: ^Hot_Reload) -> (lib: ^Game_API, err: Error) {
|
|
api := active_lib(hr)
|
|
force_reload := api.force_reload()
|
|
force_restart := api.force_restart()
|
|
reload := force_reload || force_restart
|
|
|
|
lib_mod, lib_mod_err := os.last_write_time_by_name(GAME_LIB_PATH)
|
|
// TODO: Handle error
|
|
if lib_mod_err == os.ERROR_NONE && api.modification_time != lib_mod {
|
|
reload = true
|
|
}
|
|
|
|
if !reload do return api, nil
|
|
|
|
new_api: Game_API
|
|
new_api.api_version = api.api_version + 1
|
|
new_api_ok := load_game_lib(&new_api)
|
|
if !new_api_ok {
|
|
fmt.println("error: failed to load Game API")
|
|
return nil, .Load_Game_Lib_Failed
|
|
}
|
|
|
|
force_restart = force_restart || api.memory_size() != new_api.memory_size()
|
|
|
|
if !force_restart {
|
|
// This does the normal hot reload
|
|
|
|
// Note that we don't unload the old game APIs because that
|
|
// would unload the lib. The lib can contain stored info
|
|
// such as string literals. The old libs are only unloaded
|
|
// on a full reset or on shutdown.
|
|
memory := api.memory()
|
|
new_api.memory_set(memory)
|
|
} else {
|
|
// This does a full reset. That's basically like opening and
|
|
// closing the game, without having to restart the executable.
|
|
//
|
|
// You end up in here if the game requests a full reset OR
|
|
// if the size of the game memory has changed. That would
|
|
// probably lead to a crash anyways.
|
|
|
|
api.memory_free()
|
|
|
|
for &g in hr.loaded_libs {
|
|
unload_game_lib(&g)
|
|
}
|
|
clear(&hr.loaded_libs)
|
|
|
|
new_api.init()
|
|
}
|
|
|
|
append(&hr.loaded_libs, new_api)
|
|
|
|
return active_lib(hr), nil
|
|
}
|