Initial version of single binary hot-reloading
This commit is contained in:
+34
-29
@@ -32,8 +32,6 @@ PROJECT_NAME :: "sindri"
|
|||||||
BUILD_DIRECTORY :: "out/"
|
BUILD_DIRECTORY :: "out/"
|
||||||
|
|
||||||
GAME_SRC_DIR :: "game/"
|
GAME_SRC_DIR :: "game/"
|
||||||
GAME_LIB_PATH :: BUILD_DIRECTORY + "game" + LIB_EXT
|
|
||||||
GAME_LIB_TMP_PATH :: BUILD_DIRECTORY + "game_tmp" + LIB_EXT
|
|
||||||
|
|
||||||
ENGINE_DIR :: "src/"
|
ENGINE_DIR :: "src/"
|
||||||
ENGINE_OUTPUT :: BUILD_DIRECTORY + PROJECT_NAME
|
ENGINE_OUTPUT :: BUILD_DIRECTORY + PROJECT_NAME
|
||||||
@@ -47,6 +45,7 @@ Options :: struct {
|
|||||||
debug: bool `args:"optional" usage:"Build project in debug-mode."`,
|
debug: bool `args:"optional" usage:"Build project in debug-mode."`,
|
||||||
release: bool `args:"optional" usage:"Build project in release-mode."`,
|
release: bool `args:"optional" usage:"Build project in release-mode."`,
|
||||||
run: bool `args:"optional" usage:"Run project."`,
|
run: bool `args:"optional" usage:"Run project."`,
|
||||||
|
hot_reload: bool `args:"optional" usage:"Hot-reload project."`,
|
||||||
wgpu_init: bool `args:"optional" usage:"Prepare wgpu-native."`,
|
wgpu_init: bool `args:"optional" usage:"Prepare wgpu-native."`,
|
||||||
wgpu_version: string `args:"optional" usage:"Override the wgpu release version to init."`,
|
wgpu_version: string `args:"optional" usage:"Override the wgpu release version to init."`,
|
||||||
fmt: bool `args:"optional" usage:"Run odinfmt."`,
|
fmt: bool `args:"optional" usage:"Run odinfmt."`,
|
||||||
@@ -77,6 +76,7 @@ main :: proc() {
|
|||||||
if opts.debug do build_project(true, opts.overflow[:])
|
if opts.debug do build_project(true, opts.overflow[:])
|
||||||
if opts.release do build_project(false, opts.overflow[:])
|
if opts.release do build_project(false, opts.overflow[:])
|
||||||
if opts.run do run_project(opts.overflow[:])
|
if opts.run do run_project(opts.overflow[:])
|
||||||
|
if opts.hot_reload do hot_reload(!opts.release, opts.overflow[:])
|
||||||
if opts.wgpu_init do wgpu_init(opts.wgpu_version)
|
if opts.wgpu_init do wgpu_init(opts.wgpu_version)
|
||||||
if opts.fmt do format_source()
|
if opts.fmt do format_source()
|
||||||
if opts.pre_commit do pre_commit()
|
if opts.pre_commit do pre_commit()
|
||||||
@@ -91,17 +91,12 @@ main :: proc() {
|
|||||||
// Build project
|
// Build project
|
||||||
|
|
||||||
// Build the game library ('game/') and the engine ('src/')
|
// Build the game library ('game/') and the engine ('src/')
|
||||||
build_project :: proc(debug: bool, odin_extra_args: []string) {
|
build_project :: proc(debug: bool, extra_args: []string) {
|
||||||
version := project_version()
|
version := project_version()
|
||||||
|
|
||||||
// Setup compiled wgpu binary
|
// Setup compiled wgpu binary
|
||||||
wgpu_init()
|
wgpu_init()
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Game compile
|
|
||||||
|
|
||||||
build_game(debug, version, odin_extra_args)
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Engine compile
|
// Engine compile
|
||||||
|
|
||||||
@@ -111,7 +106,7 @@ build_project :: proc(debug: bool, odin_extra_args: []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
build_engine(debug, version, odin_extra_args)
|
build_engine(debug, version, extra_args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared builder between game/engine
|
// Shared builder between game/engine
|
||||||
@@ -124,6 +119,7 @@ build_common :: proc(
|
|||||||
b := brokkr.builder_make()
|
b := brokkr.builder_make()
|
||||||
brokkr.build(&b, dir)
|
brokkr.build(&b, dir)
|
||||||
brokkr.append(&b, "-show-timings")
|
brokkr.append(&b, "-show-timings")
|
||||||
|
brokkr.collection(&b, "game", ".")
|
||||||
brokkr.collection(&b, PROJECT_NAME, "src")
|
brokkr.collection(&b, PROJECT_NAME, "src")
|
||||||
brokkr.collection(&b, "gram", "vendor/gram/src")
|
brokkr.collection(&b, "gram", "vendor/gram/src")
|
||||||
brokkr.collection(&b, "wgpu", "vendor")
|
brokkr.collection(&b, "wgpu", "vendor")
|
||||||
@@ -144,24 +140,6 @@ build_common :: proc(
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the game as a dynamic library loaded by the engine
|
|
||||||
build_game :: proc(debug: bool, version: string, extra_args: []string) {
|
|
||||||
b := build_common(GAME_SRC_DIR, debug, version, extra_args)
|
|
||||||
defer brokkr.builder_destroy(&b)
|
|
||||||
brokkr.mode(&b, .Dynamic)
|
|
||||||
// Use a temp file because it first writes an empty file, which the engine
|
|
||||||
// would load before it is actually fully written
|
|
||||||
brokkr.out(&b, GAME_LIB_TMP_PATH)
|
|
||||||
if err := brokkr.execute(&b); err != nil {
|
|
||||||
os.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !os.is_file(GAME_LIB_TMP_PATH) {
|
|
||||||
die("build produced no game lib %q", GAME_LIB_TMP_PATH)
|
|
||||||
}
|
|
||||||
os.rename(GAME_LIB_TMP_PATH, GAME_LIB_PATH)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the engine executable
|
// Build the engine executable
|
||||||
build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
||||||
b := build_common(ENGINE_DIR, debug, version, extra_args)
|
b := build_common(ENGINE_DIR, debug, version, extra_args)
|
||||||
@@ -174,8 +152,7 @@ build_engine :: proc(debug: bool, version: string, extra_args: []string) {
|
|||||||
|
|
||||||
run_project :: proc(extra_args: []string) {
|
run_project :: proc(extra_args: []string) {
|
||||||
// Build project if it wasnt already
|
// Build project if it wasnt already
|
||||||
if !os.exists(ENGINE_OUTPUT) ||
|
if !os.exists(output_path(ENGINE_OUTPUT)) do build_project(true, {})
|
||||||
!os.exists(GAME_LIB_PATH) { build_project(true, {}) }
|
|
||||||
|
|
||||||
cmd := []string{output_path(ENGINE_OUTPUT)}
|
cmd := []string{output_path(ENGINE_OUTPUT)}
|
||||||
line := slice.concatenate(
|
line := slice.concatenate(
|
||||||
@@ -206,6 +183,34 @@ run_project :: proc(extra_args: []string) {
|
|||||||
os.exit(res.exit_code)
|
os.exit(res.exit_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hot_reload :: proc(debug: bool, extra_args: []string) {
|
||||||
|
if engine_is_running() {
|
||||||
|
process := output_path(PROJECT_NAME)
|
||||||
|
when ODIN_OS == .Windows {
|
||||||
|
// Ask process to shut down gracefully (equivalent to SIGTERM)
|
||||||
|
_, ok, _ := exec([]string{"taskkill", "/IM", process + ".exe"})
|
||||||
|
if !ok {
|
||||||
|
die("hot reload: failed to terminate running %s", process)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, ok, _ := exec([]string{"pkill", "-TERM", "-x", process})
|
||||||
|
if !ok {
|
||||||
|
die(
|
||||||
|
"hot reload: failed to send SIGTERM to running %s",
|
||||||
|
process,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for process to terminate
|
||||||
|
for engine_is_running() {
|
||||||
|
time.sleep(time.Millisecond * 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_project(extra_args)
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
// wgpu setup
|
// wgpu setup
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Hot Reloading
|
||||||
|
|
||||||
|
Instead of having the engine as an exe and game as a dll, and having to jump
|
||||||
|
through hoops to share libraries internal state between them we do it
|
||||||
|
differently. The engine and game live in the same binary, compiled fully each
|
||||||
|
time. We rely mostly on static memory, and register these memory regions. Then
|
||||||
|
when we want to hot reload: we send a close signal to the running instance, it
|
||||||
|
writes a snapshot of the active memory to disk, instance closes, recompile,
|
||||||
|
restart the application, it restores the memory from the snapshot.
|
||||||
|
|
||||||
|
## Byte Layout On Disk
|
||||||
|
|
||||||
|
```odin
|
||||||
|
Snapshot_Header :: struct #packed #all_or_none {
|
||||||
|
magic_string: u32,
|
||||||
|
version: u16, // spec of the snapshot
|
||||||
|
app_version: [4 + time.MIN_YYYY_DATE_LEN + 8]u8, // example: `dev-2026-09-23-a4084b3`
|
||||||
|
region_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Region_Header :: struct #packed #all_or_none {
|
||||||
|
name_size: u32,
|
||||||
|
data_size: u32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[ Snapshot_Header ]
|
||||||
|
[ Region_Header ]
|
||||||
|
[ name bytes ]
|
||||||
|
[ data bytes ]
|
||||||
|
[ Region_Header ]
|
||||||
|
[ name bytes ]
|
||||||
|
[ data bytes ]
|
||||||
|
[ etc.. ]
|
||||||
|
|
||||||
|
## Diff Checking
|
||||||
|
|
||||||
|
Use hashing per memory segment to do a sanity diff-check.
|
||||||
|
|
||||||
|
```odin
|
||||||
|
import "core:crypto/hash"
|
||||||
|
|
||||||
|
data: []byte = ...
|
||||||
|
|
||||||
|
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
||||||
|
defer delete(digest)
|
||||||
|
```
|
||||||
+35
-30
@@ -7,39 +7,42 @@ import "sindri:base"
|
|||||||
import "sindri:core"
|
import "sindri:core"
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
// Types
|
||||||
|
|
||||||
Game_Memory :: struct {
|
Game_Memory :: struct {
|
||||||
should_close: bool,
|
should_close: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
g: ^Game_Memory
|
Test_Memory :: struct {
|
||||||
|
asd: int,
|
||||||
|
qwe: int,
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
// Variables
|
||||||
|
|
||||||
@(export)
|
g: Game_Memory
|
||||||
memory :: proc() -> rawptr {
|
t: Test_Memory
|
||||||
return g
|
|
||||||
|
game_api := core.Game_API {
|
||||||
|
settings = settings,
|
||||||
|
init_once = init_once,
|
||||||
|
init = init,
|
||||||
|
update = update,
|
||||||
|
render = render,
|
||||||
|
destroy = destroy,
|
||||||
|
should_close = should_close,
|
||||||
|
force_reload = force_reload,
|
||||||
|
force_restart = force_restart,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
// -----------------------------------------
|
||||||
memory_free :: proc() {
|
// Public functions
|
||||||
free(g)
|
|
||||||
|
api :: proc() -> ^core.Game_API {
|
||||||
|
return &game_api
|
||||||
}
|
}
|
||||||
|
|
||||||
@(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() -> base.Settings {
|
settings :: proc() -> base.Settings {
|
||||||
return base.Settings {
|
return base.Settings {
|
||||||
width = 960,
|
width = 960,
|
||||||
@@ -51,36 +54,40 @@ settings :: proc() -> base.Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
|
||||||
init_once :: proc() {
|
init_once :: proc() {
|
||||||
fmt.println("init once")
|
fmt.println("init once")
|
||||||
|
|
||||||
|
base.register_region("game_memory", &g, size_of(Game_Memory))
|
||||||
|
base.register_region("test_memory", &t, size_of(Test_Memory))
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
|
||||||
init :: proc() {
|
init :: proc() {
|
||||||
fmt.println("hello from .dll!")
|
fmt.println("hello from .dll!")
|
||||||
|
|
||||||
g = new(Game_Memory)
|
g.should_close = false
|
||||||
memory_set(g)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
|
||||||
update :: proc(dt: f32) {
|
update :: proc(dt: f32) {
|
||||||
fmt.println("dt:", dt)
|
fmt.println("dt:", dt)
|
||||||
|
|
||||||
if core.key_state(.Key_Escape) == .Press {
|
if core.key_state(.Key_Escape) == .Press {
|
||||||
g.should_close = true
|
g.should_close = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if core.key_state(.Key_F5) == .Press do t.asd = 555
|
||||||
|
if core.key_state(.Key_F6) == .Press do t.asd = 666
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
render :: proc() { }
|
||||||
|
|
||||||
destroy :: proc() {
|
destroy :: proc() {
|
||||||
fmt.println("bye")
|
fmt.println("bye")
|
||||||
|
|
||||||
|
fmt.println(t.asd, t.qwe)
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.tick_now()
|
start := time.tick_now()
|
||||||
|
|
||||||
@(export)
|
|
||||||
should_close :: proc() -> bool {
|
should_close :: proc() -> bool {
|
||||||
elapsed := time.tick_since(start)
|
elapsed := time.tick_since(start)
|
||||||
seconds := time.duration_seconds(elapsed)
|
seconds := time.duration_seconds(elapsed)
|
||||||
@@ -89,12 +96,10 @@ should_close :: proc() -> bool {
|
|||||||
return g.should_close
|
return g.should_close
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
|
||||||
force_reload :: proc() -> bool {
|
force_reload :: proc() -> bool {
|
||||||
return core.key_state(.Key_F5) == .Press
|
return core.key_state(.Key_F5) == .Press
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
|
||||||
force_restart :: proc() -> bool {
|
force_restart :: proc() -> bool {
|
||||||
return core.key_state(.Key_F6) == .Press
|
return core.key_state(.Key_F6) == .Press
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"collections": [
|
"collections": [
|
||||||
|
{ "name": "game", "path": "." },
|
||||||
{ "name": "sindri", "path": "src" },
|
{ "name": "sindri", "path": "src" },
|
||||||
{ "name": "gram", "path": "vendor/gram/src" },
|
{ "name": "gram", "path": "vendor/gram/src" },
|
||||||
{ "name": "wgpu", "path": "vendor" }
|
{ "name": "wgpu", "path": "vendor" }
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
should_close: bool
|
||||||
@@ -158,8 +158,6 @@ on_event :: proc(data: Event_Data) {
|
|||||||
drain_event_queue :: proc() {
|
drain_event_queue :: proc() {
|
||||||
assert(event_bus.queue != nil)
|
assert(event_bus.queue != nil)
|
||||||
|
|
||||||
fmt.println("toby!")
|
|
||||||
|
|
||||||
for &event in event_bus.queue {
|
for &event in event_bus.queue {
|
||||||
|
|
||||||
#partial switch e in event.data {
|
#partial switch e in event.data {
|
||||||
|
|||||||
+259
-188
@@ -1,231 +1,302 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import "core:c"
|
import "base:runtime"
|
||||||
import "core:dynlib"
|
import "core:crypto/hash"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
|
||||||
when ODIN_OS == .Windows {
|
SNAPSHOT_NAME :: "snapshot"
|
||||||
LIB_EXT :: ".dll"
|
SNAPSHOT_EXTENSION :: "sndr"
|
||||||
} else when ODIN_OS == .Darwin {
|
|
||||||
LIB_EXT :: ".dylib"
|
|
||||||
} else {
|
|
||||||
LIB_EXT :: ".so"
|
|
||||||
}
|
|
||||||
|
|
||||||
GAME_LIB_DIR :: "out/"
|
MAGIC_STRING :: 0x72646E73 // "rdns" -> "sndr" in little endian
|
||||||
GAME_LIB_PATH :: GAME_LIB_DIR + "game" + LIB_EXT
|
HOT_RELOAD_VERSION :: 1
|
||||||
|
APP_VERSION_SIZE :: 4 + time.MIN_YYYY_DATE_LEN + 8 + 6 // example: `dev-2026-09-23-a4084b3-debug`
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
// Types
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
Snapshot_Header :: struct #packed #all_or_none {
|
||||||
|
magic_string: u32,
|
||||||
|
version: u16, // spec of the snapshot
|
||||||
|
app_version: [APP_VERSION_SIZE]u8,
|
||||||
|
region_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
Region_Header :: struct #packed #all_or_none {
|
||||||
|
name_size: u32,
|
||||||
|
data_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
Memory_Region :: struct {
|
||||||
|
ptr_location: rawptr,
|
||||||
|
// TODO: How do we do hash compare without having to hash current memory every frame?
|
||||||
|
digest: []u8, // hash of this region in last snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
Hot_Reload :: struct {
|
Hot_Reload :: struct {
|
||||||
loaded_libs: [dynamic]Game_API,
|
allocator: runtime.Allocator,
|
||||||
}
|
snapshot_path: string,
|
||||||
|
snapshot_header: Snapshot_Header,
|
||||||
Hot_Reload_Error :: enum u8 {
|
region_headers: map[string]Region_Header, // hash of name:size
|
||||||
None = 0,
|
regions: map[string]Memory_Region, // hash of name:size
|
||||||
Load_Game_Lib_Failed = 1,
|
write_full_snapshot: bool,
|
||||||
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() -> 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
// Variables
|
||||||
|
|
||||||
hot_reload_init :: proc() -> (hr: Hot_Reload, error: Error) {
|
@(private)
|
||||||
api: Game_API
|
hr: Hot_Reload
|
||||||
|
|
||||||
api_ok := load_game_lib(&api)
|
// -----------------------------------------
|
||||||
if !api_ok {
|
// Constructor & destructor
|
||||||
fmt.println("error: failed to load Game API")
|
|
||||||
return {}, .Load_Game_Lib_Failed
|
hot_reload_init :: proc(
|
||||||
|
app_version: string,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> os.Error {
|
||||||
|
// Get snapshot path
|
||||||
|
out := os.get_executable_directory(allocator) or_return
|
||||||
|
defer delete(out, allocator)
|
||||||
|
snapshot_path := fmt.tprintf(
|
||||||
|
"%v/%v.%v",
|
||||||
|
out,
|
||||||
|
SNAPSHOT_NAME,
|
||||||
|
SNAPSHOT_EXTENSION,
|
||||||
|
)
|
||||||
|
snapshot_path_cpy := strings.clone(snapshot_path, allocator)
|
||||||
|
|
||||||
|
// Copy version string
|
||||||
|
version: [APP_VERSION_SIZE]u8
|
||||||
|
n := min(len(app_version), APP_VERSION_SIZE)
|
||||||
|
copy(version[:n], app_version[:n])
|
||||||
|
|
||||||
|
hr = {
|
||||||
|
allocator = allocator,
|
||||||
|
snapshot_path = snapshot_path_cpy,
|
||||||
|
snapshot_header = {
|
||||||
|
magic_string = MAGIC_STRING,
|
||||||
|
version = HOT_RELOAD_VERSION,
|
||||||
|
app_version = version,
|
||||||
|
region_count = 0,
|
||||||
|
},
|
||||||
|
region_headers = make(map[string]Region_Header, 1, allocator), // initial capacity for nil checks
|
||||||
|
regions = make(map[string]Memory_Region, 1, allocator), // initial capacity for nil checks
|
||||||
|
write_full_snapshot = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
hr.loaded_libs = make([dynamic]Game_API, 0, 0, context.allocator)
|
return nil
|
||||||
append(&hr.loaded_libs, api)
|
|
||||||
|
|
||||||
return hr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hot_reload_destroy :: proc(state: ^Hot_Reload) {
|
hot_reload_destroy :: proc() {
|
||||||
for &api in state.loaded_libs {
|
assert(
|
||||||
unload_game_lib(&api)
|
hr.region_headers != nil,
|
||||||
|
"[hot-reload] destroying uninitialized data",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(hr.regions != nil, "[hot-reload] destroying uninitialized data")
|
||||||
|
|
||||||
|
delete(hr.snapshot_path, hr.allocator)
|
||||||
|
|
||||||
|
for key, region in hr.region_headers do delete(key, hr.allocator)
|
||||||
|
for key, region in hr.regions {
|
||||||
|
if region.digest != nil do delete(region.digest, hr.allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(state.loaded_libs)
|
delete(hr.region_headers)
|
||||||
|
delete(hr.regions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
|
// Public functions
|
||||||
|
|
||||||
active_lib :: proc(hr: ^Hot_Reload) -> ^Game_API {
|
register_region :: proc(
|
||||||
return &hr.loaded_libs[len(hr.loaded_libs) - 1]
|
name: string,
|
||||||
}
|
ptr_location: rawptr,
|
||||||
|
data_size: u32,
|
||||||
|
) -> os.Error {
|
||||||
|
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
||||||
|
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
||||||
|
assert(ptr_location != nil, "[hot_reload] invalid location")
|
||||||
|
assert(data_size > 0, "[hot_reload] invalid type size")
|
||||||
|
|
||||||
should_close :: proc(hr: ^Hot_Reload) -> bool {
|
hash := fmt.tprintf("%v:%d", name, data_size)
|
||||||
api := active_lib(hr)
|
assert(!(hash in hr.regions), "[hot_reload] double registration")
|
||||||
if api.lib == nil do return true
|
hash_cpy := strings.clone(hash, hr.allocator)
|
||||||
return api.should_close()
|
hr.region_headers[hash_cpy] = Region_Header {
|
||||||
}
|
name_size = cast(u32)len(name),
|
||||||
|
data_size = data_size,
|
||||||
copy_lib :: proc(to: string) -> bool {
|
}
|
||||||
copy_err := os.copy_file(to, GAME_LIB_PATH)
|
hr.regions[hash_cpy] = Memory_Region {
|
||||||
if copy_err != nil {
|
ptr_location = ptr_location,
|
||||||
fmt.printfln(
|
|
||||||
"error: failed to copy {0} to {1}: {2:v}",
|
|
||||||
GAME_LIB_PATH,
|
|
||||||
to,
|
|
||||||
copy_err,
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
hr.snapshot_header.region_count += 1
|
||||||
|
|
||||||
|
hr.write_full_snapshot = true
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load game lib symbols
|
read_snapshot :: proc() -> os.Error {
|
||||||
load_game_lib :: proc(api: ^Game_API) -> bool {
|
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
||||||
mod_time, mod_time_err := os.last_write_time_by_name(GAME_LIB_PATH)
|
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
||||||
if mod_time_err != os.ERROR_NONE {
|
|
||||||
fmt.printfln(
|
if !os.exists(hr.snapshot_path) {
|
||||||
"error: failed getting last write time of {0}, error code: {1}",
|
hr.write_full_snapshot = true
|
||||||
GAME_LIB_PATH,
|
return nil
|
||||||
mod_time_err,
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
api.modification_time = mod_time
|
|
||||||
|
|
||||||
lib_name := fmt.tprintf(
|
snapshot := os.open(hr.snapshot_path, {.Read}) or_return
|
||||||
GAME_LIB_DIR + "game_{0}" + LIB_EXT,
|
defer os.close(snapshot)
|
||||||
api.api_version,
|
|
||||||
)
|
|
||||||
copy_lib(lib_name) or_return
|
|
||||||
|
|
||||||
// Match the names of the fields in Game_API to symbols in the game DLL
|
offset: i64
|
||||||
_, ok := dynlib.initialize_symbols(api, lib_name, "", "lib")
|
|
||||||
if !ok {
|
// Read snapshot header
|
||||||
fmt.printfln(
|
snapshot_header: Snapshot_Header
|
||||||
"error: failed initializing symbols: {0}",
|
header_bytes := mem.ptr_to_bytes(&snapshot_header)
|
||||||
dynlib.last_error(),
|
os.read_at(snapshot, header_bytes, offset) or_return
|
||||||
|
offset += size_of(Snapshot_Header)
|
||||||
|
assert(
|
||||||
|
snapshot_header.magic_string == MAGIC_STRING,
|
||||||
|
"[hot-reload] unknown filetype",
|
||||||
)
|
)
|
||||||
|
assert(
|
||||||
|
snapshot_header.version == HOT_RELOAD_VERSION,
|
||||||
|
"[hot-reload] incompatible snapshot version",
|
||||||
|
)
|
||||||
|
|
||||||
|
region_header: Region_Header
|
||||||
|
for i in 0 ..< snapshot_header.region_count {
|
||||||
|
|
||||||
|
// Read region header
|
||||||
|
header_bytes := mem.ptr_to_bytes(®ion_header)
|
||||||
|
os.read_at(snapshot, header_bytes, offset) or_return
|
||||||
|
offset += size_of(Region_Header)
|
||||||
|
|
||||||
|
// Read region name
|
||||||
|
name_bytes := make([]byte, region_header.name_size, hr.allocator)
|
||||||
|
defer delete(name_bytes, hr.allocator)
|
||||||
|
os.read_at(snapshot, name_bytes, offset) or_return
|
||||||
|
offset += cast(i64)region_header.name_size
|
||||||
|
name := transmute(string)name_bytes[:]
|
||||||
|
|
||||||
|
// Read region data
|
||||||
|
data_bytes := make([]byte, region_header.data_size, hr.allocator)
|
||||||
|
defer delete(data_bytes, hr.allocator)
|
||||||
|
os.read_at(snapshot, data_bytes, offset) or_return
|
||||||
|
offset += cast(i64)region_header.data_size
|
||||||
|
|
||||||
|
// Compare currently registered region with the loaded region
|
||||||
|
key := fmt.tprintf("%v:%d", name, region_header.data_size)
|
||||||
|
new_region_header, ok := hr.region_headers[key]
|
||||||
|
if !ok || region_header.data_size != new_region_header.data_size {
|
||||||
|
hr.write_full_snapshot = true
|
||||||
|
fmt.println("[hot-reload] bail loading region:", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create digest and store in registered regions
|
||||||
|
digest := hash.hash_bytes(
|
||||||
|
hash.Algorithm.SHA256,
|
||||||
|
data_bytes,
|
||||||
|
hr.allocator,
|
||||||
|
)
|
||||||
|
region := &hr.regions[key]
|
||||||
|
region.digest = digest
|
||||||
|
|
||||||
|
// Write snapshot memory to the memory region
|
||||||
|
dest := mem.byte_slice(
|
||||||
|
region.ptr_location,
|
||||||
|
cast(int)region_header.data_size,
|
||||||
|
)
|
||||||
|
copy(dest, data_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next write will be a full memory snapshot
|
||||||
|
set_write_full_snapshot :: proc() {
|
||||||
|
hr.write_full_snapshot = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write memory snapshot, save on writes by diff-ing
|
||||||
|
write_snapshot :: proc() -> os.Error {
|
||||||
|
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
|
||||||
|
assert(hr.regions != nil, "[hot-reload] uninitialized")
|
||||||
|
|
||||||
|
snapshot: ^os.File
|
||||||
|
if hr.write_full_snapshot || !os.exists(hr.snapshot_path) {
|
||||||
|
snapshot = os.create(hr.snapshot_path) or_return
|
||||||
} else {
|
} else {
|
||||||
// Odin's shared libraries don't register a constructor, so the lib's
|
snapshot = os.open(hr.snapshot_path, {.Read, .Write}) or_return
|
||||||
// runtime init (@init procs like os.stdout setup) never runs on load.
|
}
|
||||||
// Call it explicitly.
|
defer os.close(snapshot)
|
||||||
// Windows doesn't need this: its dynamic build exports DllMain, which
|
|
||||||
// the OS calls on load and which runs _startup_runtime itself.
|
offset: i64
|
||||||
when ODIN_OS != .Windows {
|
|
||||||
Entry :: #type proc "c" () -> c.int
|
// Write header
|
||||||
entry_ptr, entry_ok := dynlib.symbol_address(
|
if hr.write_full_snapshot {
|
||||||
api.lib,
|
header_bytes := mem.ptr_to_bytes(&hr.snapshot_header)
|
||||||
"_odin_entry_point",
|
n := os.write_at(snapshot, header_bytes, offset) or_return
|
||||||
|
}
|
||||||
|
offset += size_of(Snapshot_Header)
|
||||||
|
|
||||||
|
for key, ®ion in hr.regions {
|
||||||
|
header := hr.region_headers[key]
|
||||||
|
|
||||||
|
// Write region header
|
||||||
|
if hr.write_full_snapshot {
|
||||||
|
header_bytes := mem.ptr_to_bytes(&header)
|
||||||
|
os.write_at(snapshot, header_bytes, offset) or_return
|
||||||
|
}
|
||||||
|
offset += size_of(Region_Header)
|
||||||
|
|
||||||
|
// Write region name
|
||||||
|
if hr.write_full_snapshot {
|
||||||
|
name := key[:header.name_size] // get name from first part of the hash
|
||||||
|
name_bytes := transmute([]byte)name
|
||||||
|
os.write_at(snapshot, name_bytes, offset) or_return
|
||||||
|
}
|
||||||
|
offset += cast(i64)header.name_size
|
||||||
|
|
||||||
|
// Write region data (if bytes differ)
|
||||||
|
data_bytes := mem.byte_slice(region.ptr_location, header.data_size)
|
||||||
|
digest := hash.hash_bytes(
|
||||||
|
hash.Algorithm.SHA256,
|
||||||
|
data_bytes,
|
||||||
|
hr.allocator,
|
||||||
)
|
)
|
||||||
if entry_ok do (transmute(Entry)entry_ptr)()
|
if hr.write_full_snapshot || mem.compare(digest, region.digest) != 0 {
|
||||||
}
|
delete(region.digest, hr.allocator)
|
||||||
}
|
region.digest = digest
|
||||||
|
os.write_at(snapshot, data_bytes, offset) or_return
|
||||||
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 {
|
} else {
|
||||||
// This does a full reset. That's basically like opening and
|
delete(digest, hr.allocator)
|
||||||
// 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)
|
offset += cast(i64)header.data_size
|
||||||
|
|
||||||
new_api.init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
append(&hr.loaded_libs, new_api)
|
hr.write_full_snapshot = false
|
||||||
|
|
||||||
return active_lib(hr), nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
force_reload :: proc() {
|
||||||
|
should_close = true
|
||||||
|
}
|
||||||
|
|
||||||
|
force_restart :: proc() -> os.Error {
|
||||||
|
should_close = true
|
||||||
|
os.remove(hr.snapshot_path) or_return
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "sindri:base"
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Types
|
||||||
|
|
||||||
|
Game_API :: struct {
|
||||||
|
settings: proc() -> base.Settings,
|
||||||
|
init_once: proc(),
|
||||||
|
init: proc(),
|
||||||
|
update: proc(dt: f32),
|
||||||
|
render: proc(),
|
||||||
|
destroy: proc(),
|
||||||
|
should_close: proc() -> bool,
|
||||||
|
force_reload: proc() -> bool,
|
||||||
|
force_restart: proc() -> bool,
|
||||||
|
}
|
||||||
+34
-15
@@ -3,6 +3,7 @@ package sindri
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
|
||||||
|
import "game:game"
|
||||||
import "sindri:base"
|
import "sindri:base"
|
||||||
import "sindri:platform"
|
import "sindri:platform"
|
||||||
|
|
||||||
@@ -14,8 +15,13 @@ main :: proc() {
|
|||||||
fmt.println("hello world!", VERSION)
|
fmt.println("hello world!", VERSION)
|
||||||
|
|
||||||
// Hot reload development functionality
|
// Hot reload development functionality
|
||||||
hr, err := base.hot_reload_init()
|
base.hot_reload_init(VERSION)
|
||||||
defer base.hot_reload_destroy(&hr)
|
defer base.hot_reload_destroy()
|
||||||
|
|
||||||
|
// Load game API & settings
|
||||||
|
api := game.api()
|
||||||
|
defer api.destroy()
|
||||||
|
settings := api.settings()
|
||||||
|
|
||||||
// Event system
|
// Event system
|
||||||
base.event_bus_init()
|
base.event_bus_init()
|
||||||
@@ -23,8 +29,9 @@ main :: proc() {
|
|||||||
|
|
||||||
base.event_subscribe(base.Key_Press_Event, test)
|
base.event_subscribe(base.Key_Press_Event, test)
|
||||||
|
|
||||||
api := base.active_lib(&hr)
|
// Listen for OS signals (SIGINT, SIGTERM)
|
||||||
settings := api.settings()
|
platform.signals_init()
|
||||||
|
defer platform.signals_destroy()
|
||||||
|
|
||||||
// Initialize Window
|
// Initialize Window
|
||||||
platform.os_init(settings)
|
platform.os_init(settings)
|
||||||
@@ -36,29 +43,41 @@ main :: proc() {
|
|||||||
defer platform.instance_destroy()
|
defer platform.instance_destroy()
|
||||||
|
|
||||||
api.init_once()
|
api.init_once()
|
||||||
|
|
||||||
|
base.read_snapshot()
|
||||||
|
|
||||||
api.init()
|
api.init()
|
||||||
|
|
||||||
gt: f64 = 0
|
dt: f64
|
||||||
dt: f32
|
gt: f64
|
||||||
|
st: f64
|
||||||
|
|
||||||
|
for !api.should_close() &&
|
||||||
|
!platform.os_should_close() &&
|
||||||
|
!base.should_close {
|
||||||
|
|
||||||
for !platform.os_should_close() && !base.should_close(&hr) {
|
|
||||||
start := time.tick_now()
|
start := time.tick_now()
|
||||||
|
|
||||||
platform.os_poll_events()
|
platform.os_poll_events()
|
||||||
base.drain_event_queue()
|
base.drain_event_queue()
|
||||||
api.update(dt)
|
api.update(cast(f32)dt)
|
||||||
|
base.write_snapshot()
|
||||||
|
|
||||||
platform.frame(dt)
|
platform.frame()
|
||||||
|
api.render()
|
||||||
|
|
||||||
dt = f32(time.duration_seconds(time.tick_since(start)))
|
dt = time.duration_seconds(time.tick_since(start))
|
||||||
gt += f64(dt)
|
gt += dt
|
||||||
|
st += dt
|
||||||
|
|
||||||
// Hot reload
|
if st > 5 {
|
||||||
api, err = base.reload_game_lib(&hr)
|
st -= 5
|
||||||
if err != nil do break
|
base.set_write_full_snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
api.destroy()
|
if api.force_reload() do base.force_reload()
|
||||||
|
if api.force_restart() do base.force_restart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
|
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
import "base:intrinsics"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "vendor:glfw"
|
import "vendor:glfw"
|
||||||
@@ -113,7 +114,8 @@ os_set_callback_proc :: proc(window: rawptr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
os_should_close :: proc() -> bool {
|
os_should_close :: proc() -> bool {
|
||||||
return bool(glfw.WindowShouldClose(state.os.window))
|
window_close := bool(glfw.WindowShouldClose(state.os.window))
|
||||||
|
return window_close || intrinsics.atomic_load(&quit_signal_received)
|
||||||
}
|
}
|
||||||
|
|
||||||
os_set_should_close :: proc(val: bool) {
|
os_set_should_close :: proc(val: bool) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import "base:intrinsics"
|
||||||
|
import "core:c/libc"
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
// Read with atomic_load, safe across threads / interrupt contexts
|
||||||
|
quit_signal_received: bool
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Public functions
|
||||||
|
|
||||||
|
// Register signal handlers
|
||||||
|
signals_init :: proc() {
|
||||||
|
// SIGINT = Interrupt, typically Ctrl+C
|
||||||
|
if libc.signal(libc.SIGINT, signal_handler) ==
|
||||||
|
cast(proc "c" (_: i32))libc.SIG_ERR {
|
||||||
|
fmt.eprintln("error: failed to register SIGINT handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGTERM = Normal termination
|
||||||
|
if libc.signal(libc.SIGTERM, signal_handler) ==
|
||||||
|
cast(proc "c" (_: i32))libc.SIG_ERR {
|
||||||
|
fmt.eprintln("error: failed to register SIGTERM handler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore default signal handlers
|
||||||
|
signals_destroy :: proc() {
|
||||||
|
libc.signal(libc.SIGINT, cast(proc "c" (_: i32))libc.SIG_DFL)
|
||||||
|
libc.signal(libc.SIGTERM, cast(proc "c" (_: i32))libc.SIG_DFL)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
signal_handler :: proc "c" (sig: i32) {
|
||||||
|
switch sig {
|
||||||
|
case libc.SIGINT:
|
||||||
|
fallthrough
|
||||||
|
case libc.SIGTERM:
|
||||||
|
intrinsics.atomic_store(&quit_signal_received, true)
|
||||||
|
case:
|
||||||
|
context = state.ctx
|
||||||
|
panic("[signal] unhandled signal type")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ resize :: proc "c" () {
|
|||||||
wgpu.SurfaceConfigure(state.surface, &state.config)
|
wgpu.SurfaceConfigure(state.surface, &state.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame :: proc "c" (dt: f32) {
|
frame :: proc "c" () {
|
||||||
context = state.ctx
|
context = state.ctx
|
||||||
|
|
||||||
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
|
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)
|
||||||
|
|||||||
Reference in New Issue
Block a user