Seamless hot-reloading
This commit is contained in:
parent
4201ca7d59
commit
e85a2facef
|
@ -0,0 +1,3 @@
|
||||||
|
.vscode/
|
||||||
|
build/
|
||||||
|
ols.json
|
|
@ -2,7 +2,7 @@
|
||||||
Powervessel is a batteries-included framework made for real-time applications,
|
Powervessel is a batteries-included framework made for real-time applications,
|
||||||
particularly games. It provides:
|
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)
|
- [ ] Cross-platform windowing and graphics via [GLFW](https://glfw.org)
|
||||||
- [ ] An Extensible WebGPU Renderer that supports both 2D and 3D
|
- [ ] An Extensible WebGPU Renderer that supports both 2D and 3D
|
||||||
- [ ] A Uniform Keyboard + Gamepad Input System
|
- [ ] A Uniform Keyboard + Gamepad Input System
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
odin build releaser -debug -out:build/app_debug.exe
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
if not exist build\ mkdir build
|
||||||
|
odin build app.odin -file -build-mode:dll -out:build/app.dll
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
odin build releaser -out:build/app.exe
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
odin build reloader -debug -out:build/app_reloader.exe
|
Loading…
Reference in New Issue