Initial version of single binary hot-reloading

This commit is contained in:
Riyyi
2026-10-12 11:11:51 +02:00
parent 089ee28682
commit 3549b0be95
12 changed files with 491 additions and 274 deletions
+34 -29
View File
@@ -32,8 +32,6 @@ PROJECT_NAME :: "sindri"
BUILD_DIRECTORY :: "out/"
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_OUTPUT :: BUILD_DIRECTORY + PROJECT_NAME
@@ -47,6 +45,7 @@ Options :: struct {
debug: bool `args:"optional" usage:"Build project in debug-mode."`,
release: bool `args:"optional" usage:"Build project in release-mode."`,
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_version: string `args:"optional" usage:"Override the wgpu release version to init."`,
fmt: bool `args:"optional" usage:"Run odinfmt."`,
@@ -77,6 +76,7 @@ main :: proc() {
if opts.debug do build_project(true, opts.overflow[:])
if opts.release do build_project(false, 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.fmt do format_source()
if opts.pre_commit do pre_commit()
@@ -91,17 +91,12 @@ main :: proc() {
// Build project
// 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()
// Setup compiled wgpu binary
wgpu_init()
// ----------------------------------------
// Game compile
build_game(debug, version, odin_extra_args)
// ----------------------------------------
// Engine compile
@@ -111,7 +106,7 @@ build_project :: proc(debug: bool, odin_extra_args: []string) {
return
}
build_engine(debug, version, odin_extra_args)
build_engine(debug, version, extra_args)
}
// Shared builder between game/engine
@@ -124,6 +119,7 @@ build_common :: proc(
b := brokkr.builder_make()
brokkr.build(&b, dir)
brokkr.append(&b, "-show-timings")
brokkr.collection(&b, "game", ".")
brokkr.collection(&b, PROJECT_NAME, "src")
brokkr.collection(&b, "gram", "vendor/gram/src")
brokkr.collection(&b, "wgpu", "vendor")
@@ -144,24 +140,6 @@ build_common :: proc(
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_engine :: proc(debug: bool, version: string, extra_args: []string) {
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) {
// Build project if it wasnt already
if !os.exists(ENGINE_OUTPUT) ||
!os.exists(GAME_LIB_PATH) { build_project(true, {}) }
if !os.exists(output_path(ENGINE_OUTPUT)) do build_project(true, {})
cmd := []string{output_path(ENGINE_OUTPUT)}
line := slice.concatenate(
@@ -206,6 +183,34 @@ run_project :: proc(extra_args: []string) {
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
+47
View File
@@ -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
View File
@@ -7,39 +7,42 @@ import "sindri:base"
import "sindri:core"
// -----------------------------------------
// Types
Game_Memory :: struct {
should_close: bool,
}
g: ^Game_Memory
Test_Memory :: struct {
asd: int,
qwe: int,
}
// -----------------------------------------
// Variables
@(export)
memory :: proc() -> rawptr {
return g
g: Game_Memory
t: Test_Memory
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() {
free(g)
// -----------------------------------------
// Public functions
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 {
return base.Settings {
width = 960,
@@ -51,36 +54,40 @@ settings :: proc() -> base.Settings {
}
}
@(export)
init_once :: proc() {
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() {
fmt.println("hello from .dll!")
g = new(Game_Memory)
memory_set(g)
g.should_close = false
}
@(export)
update :: proc(dt: f32) {
fmt.println("dt:", dt)
if core.key_state(.Key_Escape) == .Press {
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() {
fmt.println("bye")
fmt.println(t.asd, t.qwe)
}
start := time.tick_now()
@(export)
should_close :: proc() -> bool {
elapsed := time.tick_since(start)
seconds := time.duration_seconds(elapsed)
@@ -89,12 +96,10 @@ should_close :: proc() -> bool {
return g.should_close
}
@(export)
force_reload :: proc() -> bool {
return core.key_state(.Key_F5) == .Press
}
@(export)
force_restart :: proc() -> bool {
return core.key_state(.Key_F6) == .Press
}
+2 -1
View File
@@ -1,7 +1,8 @@
{
"collections": [
{ "name": "game", "path": "." },
{ "name": "sindri", "path": "src" },
{ "name": "gram", "path": "vendor/gram/src" },
{ "name": "wgpu", "path": "vendor" }
{ "name": "wgpu", "path": "vendor" }
]
}
+3
View File
@@ -0,0 +1,3 @@
package base
should_close: bool
-2
View File
@@ -158,8 +158,6 @@ on_event :: proc(data: Event_Data) {
drain_event_queue :: proc() {
assert(event_bus.queue != nil)
fmt.println("toby!")
for &event in event_bus.queue {
#partial switch e in event.data {
+265 -194
View File
@@ -1,231 +1,302 @@
package base
import "core:c"
import "core:dynlib"
import "base:runtime"
import "core:crypto/hash"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:strings"
import "core:time"
when ODIN_OS == .Windows {
LIB_EXT :: ".dll"
} else when ODIN_OS == .Darwin {
LIB_EXT :: ".dylib"
} else {
LIB_EXT :: ".so"
}
SNAPSHOT_NAME :: "snapshot"
SNAPSHOT_EXTENSION :: "sndr"
GAME_LIB_DIR :: "out/"
GAME_LIB_PATH :: GAME_LIB_DIR + "game" + LIB_EXT
MAGIC_STRING :: 0x72646E73 // "rdns" -> "sndr" in little endian
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 {
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() -> 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
allocator: runtime.Allocator,
snapshot_path: string,
snapshot_header: Snapshot_Header,
region_headers: map[string]Region_Header, // hash of name:size
regions: map[string]Memory_Region, // hash of name:size
write_full_snapshot: bool,
}
// -----------------------------------------
// Variables
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)
}
@(private)
hr: Hot_Reload
// -----------------------------------------
// Constructor & destructor
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,
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,
)
copy_lib(lib_name) or_return
snapshot_path_cpy := strings.clone(snapshot_path, allocator)
// 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(),
// 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,
}
return nil
}
hot_reload_destroy :: proc() {
assert(
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(hr.region_headers)
delete(hr.regions)
}
// -----------------------------------------
// Public functions
register_region :: proc(
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")
hash := fmt.tprintf("%v:%d", name, data_size)
assert(!(hash in hr.regions), "[hot_reload] double registration")
hash_cpy := strings.clone(hash, hr.allocator)
hr.region_headers[hash_cpy] = Region_Header {
name_size = cast(u32)len(name),
data_size = data_size,
}
hr.regions[hash_cpy] = Memory_Region {
ptr_location = ptr_location,
}
hr.snapshot_header.region_count += 1
hr.write_full_snapshot = true
return nil
}
read_snapshot :: proc() -> os.Error {
assert(hr.region_headers != nil, "[hot-reload] uninitialized")
assert(hr.regions != nil, "[hot-reload] uninitialized")
if !os.exists(hr.snapshot_path) {
hr.write_full_snapshot = true
return nil
}
snapshot := os.open(hr.snapshot_path, {.Read}) or_return
defer os.close(snapshot)
offset: i64
// Read snapshot header
snapshot_header: Snapshot_Header
header_bytes := mem.ptr_to_bytes(&snapshot_header)
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(&region_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,
)
} 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)()
}
}
region := &hr.regions[key]
region.digest = digest
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,
// 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
}
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
// Next write will be a full memory snapshot
set_write_full_snapshot :: proc() {
hr.write_full_snapshot = true
}
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
}
// 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")
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)
snapshot: ^os.File
if hr.write_full_snapshot || !os.exists(hr.snapshot_path) {
snapshot = os.create(hr.snapshot_path) or_return
} 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.
snapshot = os.open(hr.snapshot_path, {.Read, .Write}) or_return
}
defer os.close(snapshot)
api.memory_free()
offset: i64
for &g in hr.loaded_libs {
unload_game_lib(&g)
// Write header
if hr.write_full_snapshot {
header_bytes := mem.ptr_to_bytes(&hr.snapshot_header)
n := os.write_at(snapshot, header_bytes, offset) or_return
}
offset += size_of(Snapshot_Header)
for key, &region 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
}
clear(&hr.loaded_libs)
offset += size_of(Region_Header)
new_api.init()
// 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 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
} else {
delete(digest, hr.allocator)
}
offset += cast(i64)header.data_size
}
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
}
+18
View File
@@ -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,
}
+35 -16
View File
@@ -3,6 +3,7 @@ package sindri
import "core:fmt"
import "core:time"
import "game:game"
import "sindri:base"
import "sindri:platform"
@@ -14,8 +15,13 @@ main :: proc() {
fmt.println("hello world!", VERSION)
// Hot reload development functionality
hr, err := base.hot_reload_init()
defer base.hot_reload_destroy(&hr)
base.hot_reload_init(VERSION)
defer base.hot_reload_destroy()
// Load game API & settings
api := game.api()
defer api.destroy()
settings := api.settings()
// Event system
base.event_bus_init()
@@ -23,8 +29,9 @@ main :: proc() {
base.event_subscribe(base.Key_Press_Event, test)
api := base.active_lib(&hr)
settings := api.settings()
// Listen for OS signals (SIGINT, SIGTERM)
platform.signals_init()
defer platform.signals_destroy()
// Initialize Window
platform.os_init(settings)
@@ -36,29 +43,41 @@ main :: proc() {
defer platform.instance_destroy()
api.init_once()
base.read_snapshot()
api.init()
gt: f64 = 0
dt: f32
dt: f64
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()
platform.os_poll_events()
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)))
gt += f64(dt)
dt = time.duration_seconds(time.tick_since(start))
gt += dt
st += dt
// Hot reload
api, err = base.reload_game_lib(&hr)
if err != nil do break
if st > 5 {
st -= 5
base.set_write_full_snapshot()
}
if api.force_reload() do base.force_reload()
if api.force_restart() do base.force_restart()
}
api.destroy()
}
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
+3 -1
View File
@@ -1,5 +1,6 @@
package platform
import "base:intrinsics"
import "core:fmt"
import "core:strings"
import "vendor:glfw"
@@ -113,7 +114,8 @@ os_set_callback_proc :: proc(window: rawptr) {
}
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) {
+48
View File
@@ -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")
}
}
+1 -1
View File
@@ -96,7 +96,7 @@ resize :: proc "c" () {
wgpu.SurfaceConfigure(state.surface, &state.config)
}
frame :: proc "c" (dt: f32) {
frame :: proc "c" () {
context = state.ctx
surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface)