Seamless hot-reloading

This commit is contained in:
Abdulmujeeb Raji 2024-08-14 12:53:51 +01:00
parent 4201ca7d59
commit e85a2facef
9 changed files with 315 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vscode/
build/
ols.json

View File

@ -2,7 +2,7 @@
Powervessel is a batteries-included framework made for real-time applications,
particularly games. It provides:
- [ ] Seamless hot-reloading of user code
- [x] Seamless hot-reloading of user code
- [ ] Cross-platform windowing and graphics via [GLFW](https://glfw.org)
- [ ] An Extensible WebGPU Renderer that supports both 2D and 3D
- [ ] A Uniform Keyboard + Gamepad Input System

55
app.odin Normal file
View File

@ -0,0 +1,55 @@
package app
// NOTE: Restart means it's like the program's *just* been opened. Typically happens when memory layout changes
// Reload means that the code has changed, but state is still the same. More seamless and you're in the same place in your program.
// All global data in your program MUST be in this struct
App_Memory :: struct {
current_frame: int,
}
g_mem: ^App_Memory
// Called upon first run OR full restart. Use it to set starting values (i.e. Player position, menu state)
@(export)
app_init :: proc() {
g_mem = new(App_Memory)
}
// Typical update loop
@(export)
app_update :: proc() -> bool {
g_mem.current_frame += 1
return true
}
// Called when program closes OR full restart. Primarily for clearing memory
@(export)
app_shutdown :: proc() {
free(g_mem)
}
// You can set conditions in your program for it to reload other than recompiling the DLL
// e.g. check for a keypress, or if the user clicks a button
@(export)
app_should_reload :: proc() -> bool {
return false
}
// Same as app_should_reload, but for complete restart
@(export)
app_should_restart :: proc() -> bool {
return false
}
// Returns the global memory pointer and the size of the struct. Used to tell if we need to full restart
@(export)
app_memory :: proc() -> (rawptr, int) {
return g_mem, size_of(g_mem)
}
// Replace global state. Used when reloading in order to maintain state.
@(export)
app_memory_set :: proc(mem: ^App_Memory) {
g_mem = mem
}

75
releaser/releaser.odin Normal file
View File

@ -0,0 +1,75 @@
package releaser
import "core:log"
import "core:os"
import "core:mem"
import app ".."
USE_TRACKING_ALLOCATOR :: ODIN_DEBUG
main :: proc() {
when USE_TRACKING_ALLOCATOR {
default_allocator := context.allocator
tracking_allocator: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
context.allocator = mem.tracking_allocator(&tracking_allocator)
}
mode: int = 0
when ODIN_OS == .Linux || ODIN_OS == .Darwin {
mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH
}
logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode)
if logh_err == os.ERROR_NONE {
os.stdout = logh
os.stderr = logh
}
logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger()
context.logger = logger
// TODO Create Windoww
app.app_init()
window_open := true
for window_open {
window_open = app.app_update()
when USE_TRACKING_ALLOCATOR {
for b in tracking_allocator.bad_free_array {
log.error("Bad free at: %v", b.location)
}
clear(&tracking_allocator.bad_free_array)
}
free_all(context.temp_allocator)
}
free_all(context.temp_allocator)
app.app_shutdown()
// TODO Shutdown Window
if logh_err == os.ERROR_NONE {
log.destroy_file_logger(logger)
}
when USE_TRACKING_ALLOCATOR {
for key, value in tracking_allocator.allocation_map {
log.error("%v: Leaked %v bytes\n", value.location, value.size)
}
mem.tracking_allocator_destroy(&tracking_allocator)
}
}
// make app use good GPU on laptops etc
@(export)
NvOptimusEnablement: u32 = 1
@(export)
AmdPowerXpressRequestHighPerformance: i32 = 1

168
reloader/reloader.odin Normal file
View File

@ -0,0 +1,168 @@
/*
A simple hot reloading app that loads a dll of the application as it is compiled
https://zylinski.se/posts/hot-reload-gameplay-code
*/
package reloader
import "core:dynlib"
import "core:os"
import "core:fmt"
import "core:log"
import "core:c/libc"
import "core:mem"
when ODIN_OS == .Windows {
DLL_EXT :: ".dll"
COPY_CMD :: "copy"
} else when ODIN_OS == .Darwin {
DLL_EXT :: ".dylib"
COPY_CMD :: "cp"
} else {
DLL_EXT :: ".so"
COPY_CMD :: "cp"
}
Api :: struct {
init: proc(),
update: proc() -> bool,
shutdown: proc(),
memory: proc() -> (rawptr, int),
memory_set: proc(rawptr),
should_reload: proc() -> bool,
should_restart: proc() -> bool,
lib: dynlib.Library,
dll_time: os.File_Time,
version: int
}
api_load :: proc(version: int) -> (Api, bool) {
dll_time, dll_time_err := os.last_write_time_by_name("app" + DLL_EXT)
if dll_time_err != os.ERROR_NONE {
log.errorf("Could not fetch last write date of game" + DLL_EXT)
return {}, false
}
dll_name := fmt.tprintf("app_{0}" + DLL_EXT, version)
copy_cmd := fmt.ctprintf("{0} app{1} {2}", COPY_CMD, DLL_EXT, dll_name)
if libc.system(copy_cmd) != 0 {
log.errorf("Failed to copy app{0} to {1}", DLL_EXT, dll_name)
return {}, false
}
api: Api
symbols_loaded, ok := dynlib.initialize_symbols(&api, dll_name, "app_", "lib")
if !ok {
log.errorf("Failed to load symbols from {0}", dll_name)
return {}, false
}
api.dll_time = dll_time
api.version = version
return api, true
}
api_unload :: proc(api: Api) {
if api.lib != nil {
dynlib.unload_library(api.lib)
}
del_cmd := fmt.ctprintf("del app_{0}" + DLL_EXT, api.version)
if libc.system(del_cmd) != 0 {
fmt.println("Failed to remove app_{0}.dll copy", api.version)
}
}
main :: proc() {
context.logger = log.create_console_logger()
default_allocator := context.allocator
tracking_allocator: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracking_allocator, default_allocator)
context.allocator = mem.tracking_allocator(&tracking_allocator)
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
}
api_version := 0
api, api_ok := api_load(api_version)
if !api_ok {
log.fatalf("Failed to load Application API")
return
}
api_version += 1
// TODO Create Window
api.init()
for {
if api.update() == false {
break
}
dll_time, dll_time_err := os.last_write_time_by_name("app" + DLL_EXT)
restart := api.should_restart()
reload := api.should_reload() || restart
if dll_time_err == os.ERROR_NONE && api.dll_time != dll_time {
reload = true
}
if reload {
new_api, new_api_ok := api_load(api_version)
if new_api_ok {
memory, size := api.memory()
_, new_size := new_api.memory()
if size != new_size || restart {
api.shutdown()
reset_tracking_allocator(&tracking_allocator)
api_unload(api)
api = new_api
api.init()
} else {
api_unload(api)
api = new_api
api.memory_set(memory)
}
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)
}
libc.getchar()
panic("Bad free detected")
}
free_all(context.temp_allocator)
}
free_all(context.temp_allocator)
api.shutdown()
if reset_tracking_allocator(&tracking_allocator) {
libc.getchar()
}
// TODO: Shutdown Window
api_unload(api)
mem.tracking_allocator_destroy(&tracking_allocator)
}

3
scripts/build_debug.bat Normal file
View File

@ -0,0 +1,3 @@
@echo off
odin build releaser -debug -out:build/app_debug.exe

4
scripts/build_dll.bat Normal file
View File

@ -0,0 +1,4 @@
@echo off
if not exist build\ mkdir build
odin build app.odin -file -build-mode:dll -out:build/app.dll

View File

@ -0,0 +1,3 @@
@echo off
odin build releaser -out:build/app.exe

View File

@ -0,0 +1,3 @@
@echo off
odin build reloader -debug -out:build/app_reloader.exe