Compare commits

...
2 Commits
Author SHA1 Message Date
Riyyi 6edcce16c6 Continue work on hot-reload functionality 2026-09-06 18:06:12 +02:00
Riyyi aea791fe2b Start working on hot-reload functionality 2026-09-06 16:37:49 +02:00
7 changed files with 214 additions and 17 deletions
+12
View File
@@ -36,3 +36,15 @@ vendor
- microui (temp)
- box3d?
- build.sh -> build.odin
## Hot Reload
build.sh -> hot reload enabled, debug mode enabled
build_release.sh -> hot reload disabled, debug mode disabled
build_debug.sh -> hot reload disabled, debug mode enabled
hot reload builds engine as .exe, then game as dynamic lib.
skips building the engine if its already running (pgrep).
no hot reload does not use game.dll, instead it imports the game source
as a normal Odin package during build.
+7 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh
set -e
set -eu
PROJECT="sindri"
VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
@@ -14,6 +14,12 @@ VERSION="dev-$(date -u '+%Y-%m-%d')-$(git rev-parse --short HEAD)"
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 "$@"
if [ "$1" = "debug" ]; then
shift
+20
View File
@@ -0,0 +1,20 @@
package game
import "core:time"
import "core:fmt"
@(export)
init :: proc() {
fmt.println("hello from .dll!")
}
start := time.tick_now()
@(export)
should_close :: proc() -> bool {
elapsed := time.tick_since(start)
seconds := time.duration_seconds(elapsed)
if seconds > 4 do return true
return false
}
+10 -12
View File
@@ -1,6 +1,5 @@
package sindri
import "core:time"
import "vendor:glfw"
import "wgpu:wgpu"
@@ -27,20 +26,19 @@ os_init :: proc() {
glfw.SetFramebufferSizeCallback(state.os.window, size_callback)
}
os_run :: proc() {
dt: f32
for !glfw.WindowShouldClose(state.os.window) {
start := time.tick_now()
glfw.PollEvents()
frame(dt)
dt = f32(time.duration_seconds(time.tick_since(start)))
os_should_close :: proc() -> bool {
return bool(glfw.WindowShouldClose(state.os.window))
}
instance_destroy()
os_set_should_close :: proc(val: bool) {
glfw.SetWindowShouldClose(state.os.window, b32(val))
}
os_poll_events :: proc() {
glfw.PollEvents()
}
os_destroy :: proc() {
glfw.DestroyWindow(state.os.window)
glfw.Terminate()
}
+138
View File
@@ -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,
)
}
}
+27
View File
@@ -1,6 +1,9 @@
package sindri
import "core:fmt"
import "core:time"
import "sindri:hot_reload"
VERSION :: #config(VERSION, "dev")
@@ -9,5 +12,29 @@ VERSION :: #config(VERSION, "dev")
main :: proc() {
fmt.println("hello world!")
// Hot reload development functionality
hr, err := hot_reload.hot_reload_init()
defer hot_reload.hot_reload_destroy(&hr)
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() && !hot_reload.should_close(&hr) {
start := time.tick_now()
os_poll_events()
frame(gt)
gt = f32(time.duration_seconds(time.tick_since(start)))
}
}
-4
View File
@@ -27,8 +27,6 @@ state: struct {
instance_init :: proc() {
state.ctx = context
os_init()
state.instance = wgpu.CreateInstance(nil)
if state.instance == nil {
panic("WebGPU is not supported")
@@ -130,8 +128,6 @@ instance_init :: proc() {
multisample = {count = 1, mask = 0xFFFFFFFF},
},
)
os_run()
}
}