254 lines
6.7 KiB
Odin
254 lines
6.7 KiB
Odin
// Development game exe. Loads game.dll and reloads it whenever it changes.
|
|
|
|
package main
|
|
|
|
import "core:c/libc"
|
|
import "core:dynlib"
|
|
import "core:fmt"
|
|
import "core:log"
|
|
import "core:mem"
|
|
import "core:os"
|
|
import "core:os/os2"
|
|
import "core:path/filepath"
|
|
import "libs:tracy"
|
|
|
|
_ :: tracy
|
|
|
|
when ODIN_OS == .Windows {
|
|
DLL_EXT :: ".dll"
|
|
} else when ODIN_OS == .Darwin {
|
|
DLL_EXT :: ".dylib"
|
|
} else {
|
|
DLL_EXT :: ".so"
|
|
}
|
|
|
|
TRACY_ENABLE :: #config(TRACY_ENABLE, false)
|
|
|
|
// We copy the DLL because using it directly would lock it, which would prevent
|
|
// the compiler from writing to it (on windows).
|
|
copy_dll :: proc(bin_dir, to: string) -> bool {
|
|
src := filepath.join({bin_dir, "game" + DLL_EXT}, context.temp_allocator)
|
|
dst := filepath.join({bin_dir, to}, context.temp_allocator)
|
|
err := os2.copy_file(dst, src)
|
|
if err != nil {
|
|
fmt.printfln("Error {0}: Failed to copy {1} to {2}", err, src, dst)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
Game_API :: struct {
|
|
lib: dynlib.Library,
|
|
init_window: proc(args: []string),
|
|
init: proc(),
|
|
update: proc() -> bool,
|
|
shutdown: proc(),
|
|
shutdown_window: proc(),
|
|
memory: proc() -> rawptr,
|
|
memory_size: proc() -> int,
|
|
hot_reloaded: proc(mem: rawptr),
|
|
force_reload: proc() -> bool,
|
|
force_restart: proc() -> bool,
|
|
modification_time: os.File_Time,
|
|
api_version: int,
|
|
}
|
|
|
|
load_game_api :: proc(bin_dir: string, api_version: int) -> (api: Game_API, ok: bool) {
|
|
// NOTE: this needs to be a relative path for Linux to work.
|
|
game_dll_name := fmt.tprintf("game_{0}" + DLL_EXT, api_version)
|
|
game_dll_path := filepath.join({bin_dir, game_dll_name}, context.temp_allocator)
|
|
|
|
mod_time, mod_time_error := os.last_write_time_by_name(
|
|
filepath.join({bin_dir, "game" + DLL_EXT}, context.temp_allocator),
|
|
)
|
|
if mod_time_error != os.ERROR_NONE {
|
|
fmt.printfln(
|
|
"Failed getting last write time of game" + DLL_EXT + ", error code: {1}",
|
|
mod_time_error,
|
|
)
|
|
return
|
|
}
|
|
|
|
|
|
copy_dll(bin_dir, game_dll_name) or_return
|
|
|
|
// This proc matches the names of the fields in Game_API to symbols in the
|
|
// game DLL. It actually looks for symbols starting with `game_`, which is
|
|
// why the argument `"game_"` is there.
|
|
_, ok = dynlib.initialize_symbols(&api, game_dll_path, "game_", "lib")
|
|
if !ok {
|
|
fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error())
|
|
}
|
|
|
|
api.api_version = api_version
|
|
api.modification_time = mod_time
|
|
ok = true
|
|
|
|
return
|
|
}
|
|
|
|
unload_game_api :: proc(bin_dir: string, api: ^Game_API) {
|
|
if api.lib != nil {
|
|
if !dynlib.unload_library(api.lib) {
|
|
fmt.printfln("Failed unloading lib: {0}", dynlib.last_error())
|
|
}
|
|
}
|
|
|
|
game_dll_path := filepath.join(
|
|
{bin_dir, fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)},
|
|
context.temp_allocator,
|
|
)
|
|
|
|
if os2.remove(game_dll_path) != nil {
|
|
fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version)
|
|
}
|
|
}
|
|
|
|
main :: proc() {
|
|
context.logger = log.create_console_logger()
|
|
|
|
bin_dir := filepath.dir(os.args[0])
|
|
defer delete(bin_dir)
|
|
|
|
default_allocator := context.allocator
|
|
tracking_allocator: mem.Tracking_Allocator
|
|
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
|
|
context.allocator = mem.tracking_allocator(&tracking_allocator)
|
|
|
|
|
|
when TRACY_ENABLE {
|
|
context.allocator = tracy.MakeProfiledAllocator(
|
|
self = &tracy.ProfiledAllocatorData{},
|
|
callstack_size = 5,
|
|
backing_allocator = context.allocator,
|
|
secure = true,
|
|
)
|
|
}
|
|
|
|
reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool {
|
|
err := false
|
|
|
|
for _, value in a.allocation_map {
|
|
fmt.printf("%v: Leaked %v bytes\n", value.location, value.size)
|
|
err = true
|
|
}
|
|
|
|
mem.tracking_allocator_clear(a)
|
|
return err
|
|
}
|
|
|
|
game_api_version := 0
|
|
game_api, game_api_ok := load_game_api(bin_dir, game_api_version)
|
|
|
|
if !game_api_ok {
|
|
fmt.println("Failed to load Game API")
|
|
return
|
|
}
|
|
|
|
game_api_version += 1
|
|
game_api.init_window(os.args)
|
|
game_api.init()
|
|
|
|
old_game_apis := make([dynamic]Game_API, default_allocator)
|
|
|
|
window_open := true
|
|
for window_open {
|
|
window_open = game_api.update()
|
|
force_reload := game_api.force_reload()
|
|
force_restart := game_api.force_restart()
|
|
reload := force_reload || force_restart
|
|
game_dll_mod, game_dll_mod_err := os.last_write_time_by_name(
|
|
filepath.join({bin_dir, "game" + DLL_EXT}, context.temp_allocator),
|
|
)
|
|
|
|
if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod {
|
|
reload = true
|
|
}
|
|
|
|
if reload {
|
|
new_game_api, new_game_api_ok := load_game_api(bin_dir, game_api_version)
|
|
|
|
if new_game_api_ok {
|
|
force_restart =
|
|
force_restart || game_api.memory_size() != new_game_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 DLL. The DLL can contain stored info
|
|
// such as string literals. The old DLLs are only unloaded
|
|
// on a full reset or on shutdown.
|
|
append(&old_game_apis, game_api)
|
|
game_memory := game_api.memory()
|
|
game_api = new_game_api
|
|
game_api.hot_reloaded(game_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.
|
|
|
|
game_api.shutdown()
|
|
reset_tracking_allocator(&tracking_allocator)
|
|
|
|
for &g in old_game_apis {
|
|
unload_game_api(bin_dir, &g)
|
|
}
|
|
|
|
clear(&old_game_apis)
|
|
unload_game_api(bin_dir, &game_api)
|
|
game_api = new_game_api
|
|
game_api.init()
|
|
}
|
|
|
|
game_api_version += 1
|
|
}
|
|
}
|
|
|
|
if len(tracking_allocator.bad_free_array) > 0 {
|
|
for b in tracking_allocator.bad_free_array {
|
|
log.errorf("Bad free at: %v", b.location)
|
|
}
|
|
|
|
// This prevents the game from closing without you seeing the bad
|
|
// frees. This is mostly needed because I use Sublime Text and my game's
|
|
// console isn't hooked up into Sublime's console properly.
|
|
libc.getchar()
|
|
panic("Bad free detected")
|
|
}
|
|
|
|
free_all(context.temp_allocator)
|
|
}
|
|
|
|
free_all(context.temp_allocator)
|
|
game_api.shutdown()
|
|
if reset_tracking_allocator(&tracking_allocator) {
|
|
// This prevents the game from closing without you seeing the memory
|
|
// leaks. This is mostly needed because I use Sublime Text and my game's
|
|
// console isn't hooked up into Sublime's console properly.
|
|
libc.getchar()
|
|
}
|
|
|
|
for &g in old_game_apis {
|
|
unload_game_api(bin_dir, &g)
|
|
}
|
|
|
|
delete(old_game_apis)
|
|
|
|
game_api.shutdown_window()
|
|
unload_game_api(bin_dir, &game_api)
|
|
mem.tracking_allocator_destroy(&tracking_allocator)
|
|
}
|
|
|
|
// Make game use good GPU on laptops.
|
|
|
|
@(export)
|
|
NvOptimusEnablement: u32 = 1
|
|
|
|
@(export)
|
|
AmdPowerXpressRequestHighPerformance: i32 = 1
|