Initial commit
This commit is contained in:
commit
579617b91f
8 changed files with 528 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
*.build/
|
||||
*.DS_Store
|
||||
runtime/*.dll*
|
||||
runtime/*.dylib*
|
||||
runtime/*.so*
|
||||
runtime/*.bin*
|
||||
|
||||
88
README
Normal file
88
README
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
-------------------------------
|
||||
Jai Hot Reload Project Template
|
||||
-------------------------------
|
||||
|
||||
jai build.jai # build the program library
|
||||
jai build.jai - host # build the host executable
|
||||
|
||||
What
|
||||
----
|
||||
|
||||
A very minimal project template that allows code to be
|
||||
reloaded at runtime.
|
||||
|
||||
It only uses modules that come with the compiler and
|
||||
follows conventions I prefer (they're easy to change,
|
||||
though).
|
||||
|
||||
How
|
||||
---
|
||||
|
||||
The program is split into two parts:
|
||||
|
||||
- host.jai: responsible for memory allocation, hot
|
||||
reloading, and the update loop
|
||||
|
||||
- program.jai: the actual program (where most, if not
|
||||
everything, should go)
|
||||
|
||||
When hot reloading is enabled, the host will load the
|
||||
program library at runtime, bind the required procedures,
|
||||
and check if it needs to reload every frame. It creates a
|
||||
temporary copy of the library to ensure we can still
|
||||
overwrite the original when compiling.
|
||||
|
||||
When hot reloading is disabled, the host imports the
|
||||
program module, calling into it in the exact same way
|
||||
(minus the dynamic linking step).
|
||||
|
||||
The program *must* export the following:
|
||||
|
||||
- MaxMemory: a value to tell the host how much memory
|
||||
the program needs to run; if the host can allocate
|
||||
this memory, all allocations within the program will
|
||||
succeed (granted it stays below MaxMemory)
|
||||
|
||||
- StateSize: a value to tell the host how large the
|
||||
program's global state struct is; because type
|
||||
information can change between compilations, this
|
||||
tells the host if memory can safely be reused after
|
||||
reloading.
|
||||
|
||||
- Init: a procedure that's called *after* the host has
|
||||
initialized the programs's memory; it is also called
|
||||
after a reload, with 'reset' denoting what kind of
|
||||
reload just occurred (reset = true means the program
|
||||
should reset its global state)
|
||||
|
||||
- Startup: a procedure that's called *once* after the
|
||||
first call to Init
|
||||
|
||||
- Teardown: a procedure that's called *once* before the
|
||||
program exits; program memory is deallocated *after*
|
||||
Teardown is called
|
||||
|
||||
- Frame: a procedure that's called *once* per frame;
|
||||
the return value tells the host if it should
|
||||
hard/soft reload the program or exit before the next
|
||||
call to Frame
|
||||
|
||||
Because the host and program are completely separate, only
|
||||
sharing the expected exports and an allocator, they can
|
||||
disagree on things like the type table, context
|
||||
structure, etc. without causing issues or crashes at
|
||||
runtime.
|
||||
|
||||
Why
|
||||
---
|
||||
|
||||
I like the workflow hot-reloading allows, and it's very
|
||||
simple to implement if done a certain way. I've been
|
||||
using this template for a while and thought it'd be a
|
||||
good idea to share for those who want something similar.
|
||||
|
||||
|
||||
LICENSE
|
||||
-------
|
||||
|
||||
Public Domain
|
||||
2
TODO
Normal file
2
TODO
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Finish the game
|
||||
- Make $1,000,000
|
||||
42
build.jai
Normal file
42
build.jai
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Control the build without using the command line
|
||||
BUILD_HOST :: false;
|
||||
|
||||
#run {
|
||||
set_build_options_dc(.{ do_output = false });
|
||||
|
||||
ws := compiler_create_workspace();
|
||||
|
||||
options := get_build_options(ws);
|
||||
options.output_path = "runtime";
|
||||
|
||||
args := options.compile_time_command_line;
|
||||
|
||||
build_file := "source/program.jai";
|
||||
if args.count > 0 && args[0] == "host" || BUILD_HOST {
|
||||
#if OS == {
|
||||
case .WINDOWS; EXT :: "exe";
|
||||
case .MACOS; EXT :: "bin";
|
||||
case .LINUX; EXT :: "bin";
|
||||
case .WASM; EXT :: "wasm";
|
||||
}
|
||||
|
||||
print("** Building host executable **\n");
|
||||
build_file = "source/host.jai";
|
||||
options.output_type = .EXECUTABLE;
|
||||
options.output_executable_name = tprint("host.%", EXT);
|
||||
}
|
||||
else {
|
||||
print("** Building library **\n");
|
||||
options.output_type = .DYNAMIC_LIBRARY;
|
||||
options.output_executable_name = "lib";
|
||||
}
|
||||
|
||||
set_build_options(options, ws);
|
||||
|
||||
compiler_begin_intercept(ws);
|
||||
add_build_file(build_file, ws);
|
||||
compiler_end_intercept(ws);
|
||||
}
|
||||
|
||||
#import "Basic";
|
||||
#import "Compiler";
|
||||
0
runtime/.assets-go-here
Normal file
0
runtime/.assets-go-here
Normal file
250
source/host.jai
Normal file
250
source/host.jai
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
main :: () {
|
||||
set_working_directory(path_strip_filename(get_path_of_running_executable()));
|
||||
|
||||
system_allocator := context.allocator;
|
||||
|
||||
// Ensure we're not allocating anywhere unexpected
|
||||
context.allocator = .{ proc = CrashAllocatorProc };
|
||||
|
||||
#if HOT_RELOAD {
|
||||
lib_api, ok := LoadLibrary(0);
|
||||
assert(ok, "host: failed to load library");
|
||||
|
||||
lib_versions: [..]Library;
|
||||
lib_versions.allocator = system_allocator;
|
||||
|
||||
L := lib_api;
|
||||
|
||||
next_lib_version := 1;
|
||||
|
||||
defer {
|
||||
for * lib_versions {
|
||||
UnloadLibrary(it);
|
||||
}
|
||||
|
||||
UnloadLibrary(*L);
|
||||
free(lib_versions.data,, allocator = lib_versions.allocator);
|
||||
}
|
||||
}
|
||||
else {
|
||||
L :: program;
|
||||
}
|
||||
|
||||
print("host: allocating % bytes memory (% for state)\n", L.MaxMemory, L.StateSize);
|
||||
|
||||
lib_memory := alloc(xx L.MaxMemory,, allocator = system_allocator);
|
||||
assert(lib_memory != null, "host: failed to allocate memory!");
|
||||
defer free(lib_memory,, allocator = system_allocator);
|
||||
|
||||
lib_arena: Arena;
|
||||
InitArena(*lib_arena, lib_memory, L.MaxMemory);
|
||||
lib_allocator := Allocator.{ proc = ArenaAllocatorProc, data = *lib_arena };
|
||||
|
||||
lib_state := alloc(xx L.StateSize,, allocator = lib_allocator);
|
||||
|
||||
L.Init(lib_state, lib_allocator, true);
|
||||
L.Startup();
|
||||
|
||||
while true {
|
||||
defer reset_temporary_storage();
|
||||
|
||||
status := L.Frame();
|
||||
if status == .quit break;
|
||||
|
||||
#if HOT_RELOAD {
|
||||
reload := status == .hard_reload || status == .soft_reload;
|
||||
if !reload {
|
||||
default_path := tprint(DefaultLibPath, LibExtension);
|
||||
mod := file_modtime_and_size(default_path);
|
||||
if mod != L.mod_time {
|
||||
reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !reload {
|
||||
continue;
|
||||
}
|
||||
|
||||
new_api, ok := LoadLibrary(next_lib_version);
|
||||
if !ok {
|
||||
print("host: failed to load version % of the library\n", next_lib_version);
|
||||
continue;
|
||||
}
|
||||
|
||||
old_ssize := L.StateSize;
|
||||
old_msize := L.MaxMemory;
|
||||
new_ssize := new_api.StateSize;
|
||||
new_msize := new_api.MaxMemory;
|
||||
|
||||
if old_ssize != new_ssize || old_msize != new_msize {
|
||||
status = .hard_reload;
|
||||
}
|
||||
|
||||
if status == .hard_reload {
|
||||
print("host: performing hard reload...\n");
|
||||
if old_msize <= new_msize {
|
||||
lib_arena.offset = 0;
|
||||
}
|
||||
else {
|
||||
free(lib_memory,, allocator = system_allocator);
|
||||
|
||||
lib_memory = alloc(xx new_msize,, allocator = system_allocator);
|
||||
assert(lib_memory != null, "host: failed to allocate new memory!");
|
||||
|
||||
InitArena(*lib_arena, lib_memory, new_msize);
|
||||
lib_state = alloc(xx new_ssize,, allocator = lib_allocator);
|
||||
}
|
||||
}
|
||||
else {
|
||||
print("host: performing soft reload (in use: %/%)...\n", lib_arena.offset, lib_arena.memory_size);
|
||||
array_add(*lib_versions, L);
|
||||
}
|
||||
|
||||
L = new_api;
|
||||
next_lib_version += 1;
|
||||
|
||||
L.Init(lib_state, lib_allocator, status == .hard_reload);
|
||||
}
|
||||
}
|
||||
|
||||
L.Teardown();
|
||||
}
|
||||
|
||||
// Ensure laptops use the higher performance GPU on windows.
|
||||
#if OS == .WINDOWS {
|
||||
/*
|
||||
https://docs.nvidia.com/gameworks/content/technologies/desktop/optimus.htm
|
||||
|
||||
Starting with the Release 302 drivers, application developers can direct the Optimus driver at runtime to use the High Performance Graphics to render any application —- even those applications for which there is no existing application profile. They can do this by exporting a global variable named NvOptimusEnablement. The Optimus driver looks for the existence and value of the export. Only the LSB of the DWORD matters at this time. A value of 0x00000001 indicates that rendering should be performed using High Performance Graphics. A value of 0x00000000 indicates that this method should be ignored.
|
||||
*/
|
||||
#program_export NvOptimusEnablement: u32 = 0x00000001;
|
||||
|
||||
/*
|
||||
https://gpuopen.com/learn/amdpowerxpressrequesthighperformance/
|
||||
|
||||
Many gaming and workstation laptops are available with both (1) integrated power saving and (2) discrete high performance graphics devices. Unfortunately, 3D intensive application performance may suffer greatly if the best graphics device is not selected. For example, a game may run at 30 Frames Per Second (FPS) on the integrated GPU rather than the 60 FPS the discrete GPU would enable. As a developer you can easily fix this problem by adding only one line to your executable's source code:
|
||||
*/
|
||||
#program_export AmdPowerXpressRequestHighPerformance: u32 = 0x00000001;
|
||||
}
|
||||
|
||||
#if HOT_RELOAD {
|
||||
Library :: struct {
|
||||
MaxMemory: type_of(program.MaxMemory);
|
||||
StateSize: type_of(program.StateSize);
|
||||
Init: type_of(program.Init);
|
||||
Startup: type_of(program.Startup);
|
||||
Teardown: type_of(program.Teardown);
|
||||
Frame: type_of(program.Frame);
|
||||
|
||||
version: int;
|
||||
handle: *void;
|
||||
mod_time: Apollo_Time;
|
||||
}
|
||||
|
||||
DefaultLibPath :: "lib.%";
|
||||
TempLibPathPattern :: "__temp_lib_%.%";
|
||||
|
||||
#if OS == {
|
||||
case .WINDOWS;
|
||||
LibExtension :: "dll";
|
||||
case .MACOS;
|
||||
LibExtension :: "dylib";
|
||||
case .LINUX;
|
||||
LibExtension :: "so";
|
||||
}
|
||||
|
||||
LoadLibrary :: (version: int) -> Library, bool {
|
||||
default_path := tprint(DefaultLibPath, LibExtension);
|
||||
mod, size, exists := file_modtime_and_size(default_path);
|
||||
if !exists {
|
||||
print("host: % did not exist\n", default_path);
|
||||
return .{}, false;
|
||||
}
|
||||
|
||||
new_path := tprint(TempLibPathPattern, version, LibExtension);
|
||||
if !copy_file(default_path, new_path,, allocator = temp) {
|
||||
print("host: could not copy % to %\n", default_path, new_path);
|
||||
return .{}, false;
|
||||
}
|
||||
|
||||
handle := OsLoadLibrary(new_path);
|
||||
if !handle {
|
||||
print("host: failed to load library at %\n", new_path);
|
||||
return .{}, false;
|
||||
}
|
||||
|
||||
return .{
|
||||
MaxMemory = OsFindLibrarySymbol(handle, "MaxMemory").(*type_of(program.MaxMemory)).*,
|
||||
StateSize = OsFindLibrarySymbol(handle, "StateSize").(*type_of(program.StateSize)).*,
|
||||
Init = OsFindLibrarySymbol(handle, "Init"),
|
||||
Startup = OsFindLibrarySymbol(handle, "Startup"),
|
||||
Teardown = OsFindLibrarySymbol(handle, "Teardown"),
|
||||
Frame = OsFindLibrarySymbol(handle, "Frame"),
|
||||
|
||||
version = version,
|
||||
handle = handle,
|
||||
mod_time = mod,
|
||||
}, true;
|
||||
}
|
||||
|
||||
UnloadLibrary :: (library: *Library) {
|
||||
path := tprint(TempLibPathPattern, library.version, LibExtension);
|
||||
if library.handle && !OsUnloadLibrary(library.handle) {
|
||||
print("host: failed to unload temporary library: %\n", path);
|
||||
}
|
||||
|
||||
if !file_delete(path) {
|
||||
print("host: failed to delete temporary library: %\n", path);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS == .WINDOWS {
|
||||
#import "Windows";
|
||||
// @note(judah): Haven't tested this on my windows machine,
|
||||
// but I remember things working like this.
|
||||
|
||||
OsLoadLibrary :: (path: string) -> *void {
|
||||
handle := LoadLibraryA(temp_c_string(path));
|
||||
return handle;
|
||||
}
|
||||
|
||||
OsUnloadLibrary :: (handle: *void) -> bool {
|
||||
return FreeLibrary(handle);
|
||||
}
|
||||
|
||||
OsFindLibrarySymbol :: (handle: *void, symbol: string) -> *void {
|
||||
return GetProcAddress(handle, temp_c_string(symbol));
|
||||
}
|
||||
}
|
||||
else #if OS == .MACOS || OS == .LINUX {
|
||||
#import "POSIX";
|
||||
|
||||
OsLoadLibrary :: (path: string) -> *void {
|
||||
handle := dlopen(temp_c_string(path), RTLD_NOW);
|
||||
return handle;
|
||||
}
|
||||
|
||||
OsUnloadLibrary :: (handle: *void) -> bool {
|
||||
return dlclose(handle) == 0;
|
||||
}
|
||||
|
||||
OsFindLibrarySymbol :: (handle: *void, symbol: string) -> *void {
|
||||
return dlsym(handle, temp_c_string(symbol));
|
||||
}
|
||||
}
|
||||
else {
|
||||
#assert(false, "only windows, mac, and linux are supported for now");
|
||||
}
|
||||
}
|
||||
|
||||
#import,file "util.jai";
|
||||
|
||||
program :: #import,file "program.jai";
|
||||
HOT_RELOAD :: program.HOT_RELOAD;
|
||||
RELEASE_BUILD :: program.RELEASE_BUILD;
|
||||
|
||||
#import "Basic";
|
||||
#import "String";
|
||||
#import "File";
|
||||
#import "System";
|
||||
#import "File_Utilities";
|
||||
43
source/program.jai
Normal file
43
source/program.jai
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#scope_export;
|
||||
|
||||
HOT_RELOAD :: true;
|
||||
RELEASE_BUILD :: false;
|
||||
|
||||
G: *struct {
|
||||
allocator: Allocator;
|
||||
iterations: int;
|
||||
};
|
||||
|
||||
#program_export MaxMemory : u64 = 4 * Gigabyte;
|
||||
#program_export StateSize : u64 = size_of(type_of(G.*));
|
||||
|
||||
#program_export Init :: (state: *void, allocator: Allocator, reset: bool) {
|
||||
Remap_Context(,,allocator = allocator);
|
||||
|
||||
G = xx state;
|
||||
G.allocator = allocator;
|
||||
if !reset return;
|
||||
}
|
||||
|
||||
#program_export Startup :: () {
|
||||
print("in startup\n");
|
||||
}
|
||||
|
||||
#program_export Teardown :: () {
|
||||
print("in teardown\n");
|
||||
}
|
||||
|
||||
#program_export Frame :: () -> Status {
|
||||
G.iterations += 1; // change this line and rebuild the program library
|
||||
|
||||
print("in frame, count: %\n", G.iterations);
|
||||
sleep_milliseconds(1000);
|
||||
return .none;
|
||||
}
|
||||
|
||||
#scope_file;
|
||||
|
||||
#import,file "util.jai";
|
||||
|
||||
#import "Remap_Context"(!RELEASE_BUILD);
|
||||
#import "Basic";
|
||||
96
source/util.jai
Normal file
96
source/util.jai
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
Kilobyte :: 1024;
|
||||
Megabyte :: 1024 * Kilobyte;
|
||||
Gigabyte :: 1024 * Megabyte;
|
||||
|
||||
Status :: enum {
|
||||
none;
|
||||
quit;
|
||||
soft_reload;
|
||||
hard_reload;
|
||||
}
|
||||
|
||||
Arena :: struct {
|
||||
memory: *void;
|
||||
memory_size: u64;
|
||||
offset: u64;
|
||||
}
|
||||
|
||||
InitArena :: (a: *Arena, memory: *void, size: u64) {
|
||||
a.memory = memory;
|
||||
a.memory_size = size;
|
||||
a.offset = 0;
|
||||
}
|
||||
|
||||
ArenaAllocatorProc :: (mode: Allocator_Mode, size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
|
||||
arena := allocator_data.(*Arena);
|
||||
if mode == {
|
||||
case .ALLOCATE;
|
||||
return ArenaAlloc(arena, size);
|
||||
|
||||
case .RESIZE;
|
||||
if old_memory == null {
|
||||
return ArenaAlloc(arena, size);
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
return null;
|
||||
}
|
||||
|
||||
if size == old_size {
|
||||
return old_memory;
|
||||
}
|
||||
|
||||
new_memory := ArenaAlloc(arena, size);
|
||||
memcpy(new_memory, old_memory, old_size);
|
||||
return new_memory;
|
||||
case;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArenaAlloc :: (a: *Arena, count: int, alignment := DefaultAlignment, loc := #caller_location) -> *void {
|
||||
assert(a.memory != null, "arena: not initialized", loc = loc);
|
||||
assert(IsPowerOfTwo(alignment));
|
||||
|
||||
end := a.memory.(*u8) + a.offset;
|
||||
ptr := align_forward(end.(s64), xx alignment);
|
||||
total_size := (count + ptr.(*u8) - end.(*u8)).(u64);
|
||||
|
||||
assert(a.offset + total_size <= a.memory_size, "arena: out of memory", loc = loc);
|
||||
a.offset += total_size;
|
||||
|
||||
return ptr.(*void);
|
||||
}
|
||||
|
||||
CrashAllocatorProc :: (mode: Allocator_Mode, size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
|
||||
message: string;
|
||||
if mode == {
|
||||
case .ALLOCATE;
|
||||
message = tprint("Attempt to allocate % byte(s) using the crash allocator!", size);
|
||||
case .RESIZE;
|
||||
message = tprint("Attempt to resize (from % to % byte(s)) using the crash allocator!", old_size, size);
|
||||
case .FREE;
|
||||
message = tprint("Attempt to free % byte(s) using the crash allocator!", size);
|
||||
}
|
||||
|
||||
assert(false, message);
|
||||
debug_break();
|
||||
return null;
|
||||
}
|
||||
|
||||
DefaultAlignment :: 2 * #run align_of(*void);
|
||||
|
||||
IsPowerOfTwo :: (x: int) -> bool {
|
||||
if x <= 0 return false;
|
||||
return (x & (x - 1)) == 0;
|
||||
}
|
||||
|
||||
align_of :: ($T: Type) -> int #expand {
|
||||
return #run -> int {
|
||||
info := type_info(struct{ p: u8; t: T; });
|
||||
return info.members[1].offset_in_bytes;
|
||||
};
|
||||
}
|
||||
|
||||
#import "Basic";
|
||||
Loading…
Reference in a new issue