First working version of hot-reloading feature
This commit is contained in:
@@ -11,28 +11,47 @@ VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
|
||||
./scripts/wgpu-init.sh
|
||||
|
||||
# ------------------------------------------
|
||||
# Game compile
|
||||
|
||||
mkdir -p build
|
||||
|
||||
# Game.dll
|
||||
odin build game/ -show-timings \
|
||||
-collection:sindri=src \
|
||||
-build-mode:dynamic \
|
||||
-out:build/game -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
|
||||
-collection:sindri=src \
|
||||
-build-mode:dynamic \
|
||||
-out:build/game_tmp -microarch:native -define:VERSION="$VERSION-debug" -debug "$@"
|
||||
|
||||
# Need to use a temp file on Linux/macOS because it first writes an empty file,
|
||||
# which the engine will load before it is actually fully written.
|
||||
[ "$(uname -s)" = "Darwin" ] && LIB_EXT="dylib" || LIB_EXT="so"
|
||||
[ -f "build/game_tmp.$LIB_EXT" ] || {
|
||||
echo "error: build produced no game lib.$LIB_EXT" >&2
|
||||
exit 1
|
||||
}
|
||||
mv -f "build/game_tmp.$LIB_EXT" "build/game.$LIB_EXT"
|
||||
|
||||
# ------------------------------------------
|
||||
# Engine compile
|
||||
|
||||
# If the executable is already running, then don't try to build and start it.
|
||||
if pgrep -x $PROJECT >/dev/null; then
|
||||
echo "Hot reloading..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "debug" ]; then
|
||||
shift
|
||||
shift
|
||||
|
||||
odin build src/ -show-timings \
|
||||
-collection:sindri=src \
|
||||
-collection:gram=vendor/gram/src \
|
||||
-collection:wgpu=vendor \
|
||||
-out:build/$PROJECT -microarch:native -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@"
|
||||
exit 0
|
||||
odin build src/ -show-timings \
|
||||
-collection:sindri=src \
|
||||
-collection:gram=vendor/gram/src \
|
||||
-collection:wgpu=vendor \
|
||||
-out:build/$PROJECT -microarch:native -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
odin build src/ -show-timings \
|
||||
-collection:sindri=src \
|
||||
-collection:gram=vendor/gram/src \
|
||||
-collection:wgpu=vendor \
|
||||
-out:build/$PROJECT -microarch:native -o:speed -define:VERSION="$VERSION" "$@"
|
||||
-collection:sindri=src \
|
||||
-collection:gram=vendor/gram/src \
|
||||
-collection:wgpu=vendor \
|
||||
-out:build/$PROJECT -microarch:native -o:speed -define:VERSION="$VERSION" "$@"
|
||||
|
||||
@@ -3,9 +3,72 @@ package game
|
||||
import "core:time"
|
||||
import "core:fmt"
|
||||
|
||||
import "sindri:core"
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
Game_Memory :: struct {
|
||||
}
|
||||
|
||||
g: ^Game_Memory
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
@(export)
|
||||
memory :: proc() -> rawptr {
|
||||
return g
|
||||
}
|
||||
|
||||
@(export)
|
||||
memory_free :: proc() {
|
||||
free(g)
|
||||
}
|
||||
|
||||
@(export)
|
||||
memory_size :: proc() -> int {
|
||||
return size_of(Game_Memory)
|
||||
}
|
||||
|
||||
@(export)
|
||||
memory_set :: proc(mem: rawptr) {
|
||||
g = (^Game_Memory)(mem)
|
||||
|
||||
// Here you can also set your own global variables. A good idea is to make
|
||||
// your global variables into pointers that point to something inside `g`.
|
||||
}
|
||||
|
||||
@(export)
|
||||
settings :: proc() -> core.Settings {
|
||||
return core.Settings {
|
||||
width = 960,
|
||||
height = 540,
|
||||
title = "WGPU Native Triangle",
|
||||
mode = core.WindowMode.Windowed,
|
||||
refresh = 60,
|
||||
vsync = true,
|
||||
}
|
||||
}
|
||||
|
||||
@(export)
|
||||
init_once :: proc() {
|
||||
fmt.println("init once")
|
||||
}
|
||||
|
||||
@(export)
|
||||
init :: proc() {
|
||||
fmt.println("hello from .dll!")
|
||||
|
||||
g = new(Game_Memory)
|
||||
}
|
||||
|
||||
@(export)
|
||||
update :: proc(dt: f32) {
|
||||
fmt.println("dt:", dt)
|
||||
}
|
||||
|
||||
@(export)
|
||||
destroy :: proc() {
|
||||
fmt.println("bye")
|
||||
}
|
||||
|
||||
start := time.tick_now()
|
||||
@@ -18,3 +81,15 @@ should_close :: proc() -> bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@(export)
|
||||
force_reload :: proc() -> bool {
|
||||
// TODO: read keypress
|
||||
return false
|
||||
}
|
||||
|
||||
@(export)
|
||||
force_restart :: proc() -> bool {
|
||||
// TODO: read keypress
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package core
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
Settings :: struct {
|
||||
width: u16,
|
||||
height: u16,
|
||||
title: string,
|
||||
mode: WindowMode,
|
||||
refresh: u16,
|
||||
vsync: bool,
|
||||
// TODO:
|
||||
// windowed resizable y/n
|
||||
// exit key (ex: escape)
|
||||
}
|
||||
|
||||
WindowMode :: enum u8 {
|
||||
Windowed = 0,
|
||||
Borderless = 1,
|
||||
Fullscreen = 2,
|
||||
}
|
||||
+64
-4
@@ -1,24 +1,31 @@
|
||||
package sindri
|
||||
|
||||
import "core:strings"
|
||||
import "vendor:glfw"
|
||||
|
||||
import "wgpu:wgpu"
|
||||
import "wgpu:wgpu/glfwglue"
|
||||
|
||||
import "sindri:core"
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
OS :: struct {
|
||||
window: glfw.WindowHandle,
|
||||
}
|
||||
|
||||
os_init :: proc() {
|
||||
// -----------------------------------------
|
||||
|
||||
os_init :: proc(settings: core.Settings) {
|
||||
if !glfw.Init() {
|
||||
panic("[glfw] init failure")
|
||||
}
|
||||
|
||||
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
|
||||
state.os.window = glfw.CreateWindow(
|
||||
960,
|
||||
540,
|
||||
"WGPU Native Triangle",
|
||||
i32(settings.width),
|
||||
i32(settings.height),
|
||||
strings.unsafe_string_to_cstring(settings.title),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
@@ -26,6 +33,59 @@ os_init :: proc() {
|
||||
glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
|
||||
}
|
||||
|
||||
os_set_monitor :: proc(settings: core.Settings) {
|
||||
monitor := glfw.GetPrimaryMonitor()
|
||||
x_pos: i32
|
||||
y_pos: i32
|
||||
width := i32(settings.width)
|
||||
height := i32(settings.height)
|
||||
target_monitor: glfw.MonitorHandle
|
||||
|
||||
mode := glfw.GetVideoMode(monitor)
|
||||
|
||||
switch settings.mode {
|
||||
case .Fullscreen:
|
||||
target_monitor = monitor
|
||||
case .Borderless:
|
||||
// FIXME: On macOS, borderless also fills the notch area
|
||||
|
||||
// Monitor origin on the virtual desktop
|
||||
x_pos, y_pos = glfw.GetMonitorPos(monitor)
|
||||
|
||||
width = mode.width
|
||||
height = mode.height
|
||||
|
||||
glfw.SetWindowAttrib(
|
||||
state.os.window,
|
||||
glfw.DECORATED,
|
||||
i32(glfw.FALSE),
|
||||
)
|
||||
case .Windowed:
|
||||
// Put window in the center of the monitor
|
||||
x_pos = (mode.width - width) / 2
|
||||
y_pos = (mode.height - height) / 2
|
||||
|
||||
glfw.SetWindowAttrib(
|
||||
state.os.window,
|
||||
glfw.DECORATED,
|
||||
i32(glfw.TRUE),
|
||||
)
|
||||
}
|
||||
|
||||
refresh := settings.vsync ? mode.refresh_rate : i32(settings.refresh)
|
||||
if refresh == 0 do refresh = glfw.DONT_CARE
|
||||
|
||||
glfw.SetWindowMonitor(
|
||||
state.os.window,
|
||||
target_monitor,
|
||||
x_pos,
|
||||
y_pos,
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
)
|
||||
}
|
||||
|
||||
os_should_close :: proc() -> bool {
|
||||
return bool(glfw.WindowShouldClose(state.os.window))
|
||||
}
|
||||
|
||||
+100
-20
@@ -3,6 +3,9 @@ package hot_reload
|
||||
import "core:dynlib"
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:time"
|
||||
|
||||
import "sindri:core"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
LIB_EXT :: ".dll"
|
||||
@@ -33,31 +36,39 @@ Error :: union #shared_nil {
|
||||
}
|
||||
|
||||
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
|
||||
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() -> (state: Hot_Reload, error: Error) {
|
||||
hot_reload_init :: proc() -> (hr: 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")
|
||||
fmt.println("error: failed to load Game API")
|
||||
return {}, .Load_Game_Lib_Failed
|
||||
}
|
||||
|
||||
state.loaded_libs = make([dynamic]Game_API)
|
||||
append(&state.loaded_libs, api)
|
||||
hr.loaded_libs = make([dynamic]Game_API, 0, 0, context.allocator)
|
||||
append(&hr.loaded_libs, api)
|
||||
|
||||
return state, nil
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
hot_reload_destroy :: proc(state: ^Hot_Reload) {
|
||||
@@ -84,7 +95,8 @@ 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",
|
||||
"error: failed to copy {0} to {1}: {2:v}",
|
||||
GAME_LIB_PATH,
|
||||
to,
|
||||
copy_err,
|
||||
)
|
||||
@@ -96,8 +108,16 @@ copy_lib :: proc(to: string) -> bool {
|
||||
|
||||
// Load game lib symbols
|
||||
load_game_lib :: proc(api: ^Game_API) -> bool {
|
||||
// Load next iteration
|
||||
api.api_version += 1
|
||||
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,
|
||||
@@ -106,9 +126,12 @@ load_game_lib :: proc(api: ^Game_API) -> bool {
|
||||
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")
|
||||
_, ok := dynlib.initialize_symbols(api, lib_name, "", "lib")
|
||||
if !ok {
|
||||
fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error())
|
||||
fmt.printfln(
|
||||
"error: failed initializing symbols: {0}",
|
||||
dynlib.last_error(),
|
||||
)
|
||||
}
|
||||
|
||||
return ok
|
||||
@@ -119,7 +142,7 @@ unload_game_lib :: proc(api: ^Game_API) {
|
||||
if api.lib != nil {
|
||||
if !dynlib.unload_library(api.lib) {
|
||||
fmt.eprintfln(
|
||||
"error: Failed unloading lib: {0}",
|
||||
"error: failed unloading lib: {0}",
|
||||
dynlib.last_error(),
|
||||
)
|
||||
}
|
||||
@@ -130,9 +153,66 @@ unload_game_lib :: proc(api: ^Game_API) {
|
||||
) !=
|
||||
nil {
|
||||
fmt.printfln(
|
||||
"error: Failed to remove {0}game_{1}" + LIB_EXT + " copy",
|
||||
"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
|
||||
}
|
||||
|
||||
+19
-5
@@ -17,24 +17,38 @@ main :: proc() {
|
||||
defer hot_reload.hot_reload_destroy(&hr)
|
||||
|
||||
api := hot_reload.active_lib(&hr)
|
||||
api.init()
|
||||
settings := api.settings()
|
||||
|
||||
// Initialize Window
|
||||
os_init()
|
||||
os_init(settings)
|
||||
os_set_monitor(settings)
|
||||
defer os_destroy()
|
||||
|
||||
// Initialize GPU resources
|
||||
instance_init()
|
||||
defer instance_destroy()
|
||||
|
||||
gt: f32
|
||||
api.init_once()
|
||||
api.init()
|
||||
|
||||
gt: f64 = 0
|
||||
dt: f32
|
||||
|
||||
for !os_should_close() && !hot_reload.should_close(&hr) {
|
||||
start := time.tick_now()
|
||||
|
||||
os_poll_events()
|
||||
frame(gt)
|
||||
api.update(dt)
|
||||
|
||||
gt = f32(time.duration_seconds(time.tick_since(start)))
|
||||
frame(dt)
|
||||
|
||||
dt = f32(time.duration_seconds(time.tick_since(start)))
|
||||
gt += f64(dt)
|
||||
|
||||
// Hot reload
|
||||
api, err = hot_reload.reload_game_lib(&hr)
|
||||
if err != nil do break
|
||||
}
|
||||
|
||||
api.destroy()
|
||||
}
|
||||
|
||||
@@ -147,6 +147,8 @@ instance_destroy :: proc() {
|
||||
resize :: proc "c" () {
|
||||
context = state.ctx
|
||||
|
||||
if state.surface == nil || state.device == nil do return
|
||||
|
||||
state.config.width, state.config.height = os_get_framebuffer_size()
|
||||
wgpu.SurfaceConfigure(state.surface, &state.config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user