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,
|
||||
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
|
||||
|
|
|
@ -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