mostly tests, more reload module examples
This commit is contained in:
parent
5562037b1d
commit
3f2d499025
9 changed files with 224 additions and 45 deletions
37
README
37
README
|
|
@ -1 +1,36 @@
|
|||
My base library for Jai
|
||||
------
|
||||
jx.jai
|
||||
------
|
||||
|
||||
cd [jai install directory]/modules
|
||||
git clone https://git.brut.systems/judah/jx.jai.git jx
|
||||
|
||||
#import "jx";
|
||||
|
||||
What
|
||||
----
|
||||
|
||||
A set of modules for things I usually want in Jai, namely,
|
||||
utilities, bindings, and experiments.
|
||||
|
||||
How
|
||||
---
|
||||
|
||||
If you'd like to learn more about *what* a specific module
|
||||
does, take a look at its 'module.jai' file.
|
||||
|
||||
Why
|
||||
---
|
||||
|
||||
Because Jai is still in closed beta (as of May 15, 2025),
|
||||
updates to the compiler and "standard library" will break
|
||||
projects of mine; sometimes in a very annoying way.
|
||||
|
||||
jx.jai was made to 1) give myself an escape
|
||||
hatch/skin-suit to cause fewer breaking changes when
|
||||
updating the compiler, and 2) put all of my non-project
|
||||
code in a single place that's easier to manage.
|
||||
|
||||
While I do use many of the modules shipped with the
|
||||
compiler, my goal is to eventually replace them.
|
||||
|
||||
|
|
|
|||
10
_run_all_tests.jai
Normal file
10
_run_all_tests.jai
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#scope_file #run {
|
||||
compiler :: #import "Compiler";
|
||||
compiler.set_build_options_dc(.{ do_output = false });
|
||||
|
||||
#import,file "./module.jai"(true);
|
||||
#import,file "./encoding/module.jai"(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -2,7 +2,7 @@ Static_Array :: struct(capacity: int, T: Type) {
|
|||
items: [capacity]T;
|
||||
count: int;
|
||||
|
||||
Zero :: #run zero_value(T);
|
||||
Default :: #run default_of(T);
|
||||
}
|
||||
|
||||
operator [] :: inline (a: Static_Array, index: int, loc := #caller_location) -> a.T #no_abc {
|
||||
|
|
@ -55,7 +55,7 @@ reset :: inline (a: *Static_Array, $keep_memory := true) {
|
|||
a.count = 0;
|
||||
}
|
||||
else {
|
||||
for 0..a.count - 1 a.items[it] = a.Zero;
|
||||
for 0..a.count - 1 a.items[it] = a.Default;
|
||||
a.count = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#module_parameters(RUN_TESTS := false);
|
||||
|
||||
#load "base64.jai";
|
||||
#load "json.jai";
|
||||
|
||||
|
||||
#scope_module;
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ basic :: #import "Basic";
|
|||
compiler :: #import "Compiler";
|
||||
|
||||
#if RUN_TESTS {
|
||||
#run compiler.set_build_options_dc(.{ do_output = false });
|
||||
|
||||
// so files within this module can use 'test' without directly importing it.
|
||||
test :: #import,file "test/module.jai";
|
||||
#import,file "./encoding/module.jai"(RUN_TESTS);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,72 @@
|
|||
/*
|
||||
Building this program will result in two outputs:
|
||||
|
||||
1) an executable that controls memory,
|
||||
hot-reloading, and the main loop.
|
||||
|
||||
2) a dynamic library the executable will call into.
|
||||
|
||||
This program is the library. The "host" executable is
|
||||
automatically generated at build-time.
|
||||
|
||||
If you'd like to see code that isn't heavily annotated,
|
||||
see: 'quickstart.jai'
|
||||
*/
|
||||
|
||||
// The global state for this program.
|
||||
// It's a pointer because the memory is allocated by the
|
||||
// reload module and given to this program when init is called.
|
||||
G: *struct {
|
||||
allocator: Allocator;
|
||||
iterations: int;
|
||||
};
|
||||
|
||||
// The maximum amount of memory your program is allowed to use.
|
||||
// Its true size is max_memory - state_size bytes.
|
||||
#program_export max_memory: u64 = 4 * Gigabyte;
|
||||
|
||||
// The current size of the global state structure.
|
||||
#program_export state_size: u64 = size_of(type_of(G.*));
|
||||
|
||||
#program_export init :: (state: *void, allocator: Allocator, reset: bool) {
|
||||
// Called at program startup or on reload.
|
||||
//
|
||||
// 'allocator' should be stored in the global state to ensure
|
||||
// memory allocated by this program persists between reloads
|
||||
// and does not leak.
|
||||
//
|
||||
// 'full_reset' will be 'true' when the program is expected
|
||||
// to (re)initialize its global state, as if the program restarted.
|
||||
#program_export init :: (state: *void, allocator: Allocator, full_reset: bool) {
|
||||
print("in: init\n");
|
||||
|
||||
G = xx state;
|
||||
G = state.(type_of(G));
|
||||
G.allocator = allocator;
|
||||
if !full_reset return;
|
||||
|
||||
// initialize global state
|
||||
G.iterations = 0;
|
||||
}
|
||||
|
||||
// Called once at program startup (after init).
|
||||
// Your window/rendering context should be created here.
|
||||
#program_export setup :: () {
|
||||
print("in: setup\n");
|
||||
}
|
||||
|
||||
#program_export teardown :: () {
|
||||
print("in: teardown\n");
|
||||
}
|
||||
|
||||
// Called in a loop until this program exits.
|
||||
// This is your classic update/render procedure.
|
||||
//
|
||||
// The status this returns tells the reload module
|
||||
// what to do after this frame is done.
|
||||
//
|
||||
// .none: continue running
|
||||
// .quit: stop running
|
||||
// .soft_reload: reload without reinitializing state
|
||||
// .hard_reload: reload then reinitialize state
|
||||
//
|
||||
// Unless given explicitly, soft_reload or hard_reload
|
||||
// will be done automatically depending on how this
|
||||
// program has changed between compiles.
|
||||
#program_export frame :: () -> reload.Status {
|
||||
G.iterations += 1; // change this line and rebuild the program library
|
||||
|
||||
|
|
@ -29,7 +75,11 @@ G: *struct {
|
|||
return .none;
|
||||
}
|
||||
|
||||
#import "Basic";
|
||||
// Called once at program exit.
|
||||
// This will be called before any program memory has been deallocated.
|
||||
#program_export teardown :: () {
|
||||
print("in: teardown\n");
|
||||
}
|
||||
|
||||
#import "jx";
|
||||
|
||||
|
|
@ -39,3 +89,5 @@ G: *struct {
|
|||
#poke_name reload teardown;
|
||||
|
||||
reload :: #import "jx/reload";
|
||||
|
||||
#import "Basic";
|
||||
|
|
|
|||
59
reload/examples/quickstart.jai
Normal file
59
reload/examples/quickstart.jai
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
G: *struct {
|
||||
allocator: Allocator;
|
||||
};
|
||||
|
||||
#program_export max_memory: u64 = 4 * Gigabyte;
|
||||
#program_export state_size: u64 = size_of(type_of(G.*));
|
||||
|
||||
#program_export init :: (state: *void, allocator: Allocator, full_reset: bool) {
|
||||
G = state.(type_of(G));
|
||||
G.allocator = allocator;
|
||||
if !full_reset return;
|
||||
}
|
||||
|
||||
#program_export setup :: () {
|
||||
}
|
||||
|
||||
#program_export teardown :: () {
|
||||
}
|
||||
|
||||
#program_export frame :: () -> reload.Status {
|
||||
print("doing nothing for one second...\n");
|
||||
sleep_milliseconds(1000);
|
||||
return .none;
|
||||
}
|
||||
|
||||
|
||||
#import "jx";
|
||||
#import "Basic";
|
||||
|
||||
DISABLE_HOT_RELOADING :: false;
|
||||
|
||||
#if DISABLE_HOT_RELOADING {
|
||||
// @note(judah): dumb workaround because module parameters are weird
|
||||
reload :: struct { Status :: (#import "jx/reload"(disabled = true)).Status; };
|
||||
|
||||
main :: () {
|
||||
system_allocator := context.allocator;
|
||||
|
||||
state := alloc(xx state_size);
|
||||
init(state, system_allocator, true);
|
||||
|
||||
setup();
|
||||
|
||||
while true {
|
||||
status := frame();
|
||||
if status == .quit break;
|
||||
}
|
||||
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
else {
|
||||
reload :: #import "jx/reload";
|
||||
#poke_name reload frame;
|
||||
#poke_name reload init;
|
||||
#poke_name reload setup;
|
||||
#poke_name reload teardown;
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
#module_parameters(disable := false, loc := #caller_location);
|
||||
/*
|
||||
A module that hijacks the build process of your project
|
||||
to enable easy hot-reloading.
|
||||
|
||||
See: 'examples/everything-you-need.jai' for how this
|
||||
module is intended to be used.
|
||||
*/
|
||||
#module_parameters(disabled := false, loc := #caller_location);
|
||||
|
||||
Status :: enum {
|
||||
none;
|
||||
|
|
@ -7,7 +14,7 @@ Status :: enum {
|
|||
hard_reload;
|
||||
}
|
||||
|
||||
Init_Proc :: #type (*void, Allocator, bool);
|
||||
Init_Proc :: #type (state: *void, allocator: Allocator, full_reset: bool);
|
||||
Setup_Proc :: #type ();
|
||||
Teardown_Proc :: #type ();
|
||||
Frame_Proc :: #type () -> Status;
|
||||
|
|
@ -20,6 +27,10 @@ Frame_Proc :: #type () -> Status;
|
|||
#placeholder setup;
|
||||
#placeholder teardown;
|
||||
|
||||
#if !disabled {
|
||||
#load "reload_main.jai";
|
||||
}
|
||||
|
||||
// Ensure laptops use the higher performance GPU on Windows.
|
||||
#if OS == .WINDOWS {
|
||||
/*
|
||||
|
|
@ -37,25 +48,24 @@ Frame_Proc :: #type () -> Status;
|
|||
#program_export AmdPowerXpressRequestHighPerformance: u32 = 0x00000001;
|
||||
}
|
||||
|
||||
#if !disable {
|
||||
#load "reload_main.jai";
|
||||
|
||||
#scope_module;
|
||||
|
||||
#if !disabled {
|
||||
#assert (type_of(init) == Init_Proc) "init was the wrong type!";
|
||||
#assert (type_of(setup) == Setup_Proc) "setup was the wrong type!";
|
||||
#assert (type_of(teardown) == Teardown_Proc) "teardown was the wrong type!";
|
||||
#assert (type_of(frame) == Frame_Proc) "frame was the wrong type!";
|
||||
}
|
||||
|
||||
|
||||
#scope_file;
|
||||
|
||||
#assert (type_of(init) == Init_Proc) "init was the wrong type!";
|
||||
#assert (type_of(setup) == Setup_Proc) "setup was the wrong type!";
|
||||
#assert (type_of(teardown) == Teardown_Proc) "teardown was the wrong type!";
|
||||
#assert (type_of(frame) == Frame_Proc) "frame was the wrong type!";
|
||||
|
||||
file_path :: #run -> string { return loc.fully_pathed_filename; }
|
||||
proj_path :: #run -> string { return path_strip_filename(file_path); }
|
||||
temp_path :: #run -> string { return tprint("%/.build/tmp.jai", proj_path); };
|
||||
temp_exists :: #run -> bool { return file_exists(temp_path); }
|
||||
file_path :: #run loc.fully_pathed_filename;
|
||||
proj_path :: #run path_strip_filename(file_path);
|
||||
temp_path :: #run tprint("%/.build/tmp.jai", proj_path);
|
||||
temp_exists :: #run file_exists(temp_path);
|
||||
lib_name :: #run tprint("%_lib", path_basename(file_path));
|
||||
|
||||
// only run the build when we the module importing us is compiled
|
||||
#if !(disable || temp_exists) #run {
|
||||
#if !(disabled || temp_exists) #run {
|
||||
set_build_options_dc(.{ do_output = false });
|
||||
|
||||
make_directory_if_it_does_not_exist(".build");
|
||||
|
|
@ -63,7 +73,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
|
|||
out_path := path_strip_extension(proj_path);
|
||||
out_name := path_strip_extension(path_filename(file_path));
|
||||
|
||||
// build target executable
|
||||
// build host executable
|
||||
{
|
||||
ws := compiler_create_workspace(file_path);
|
||||
defer compiler_destroy_workspace(ws);
|
||||
|
|
@ -74,6 +84,10 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
|
|||
opts.output_type = .EXECUTABLE;
|
||||
opts.output_executable_name = out_name;
|
||||
|
||||
// create a temporary file that contains the hijacked main.
|
||||
// we need to #load our program library so the #placeholders
|
||||
// are filled.
|
||||
|
||||
tmp: String_Builder;
|
||||
print_to_builder(*tmp, #string END
|
||||
#load "%1";
|
||||
|
|
@ -96,7 +110,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
|
|||
compiler_end_intercept(ws);
|
||||
}
|
||||
|
||||
// build library
|
||||
// build program as dynamic library
|
||||
{
|
||||
ws := compiler_create_workspace(file_path);
|
||||
defer compiler_destroy_workspace(ws);
|
||||
|
|
@ -104,7 +118,7 @@ temp_exists :: #run -> bool { return file_exists(temp_path); }
|
|||
opts := get_build_options(ws);
|
||||
opts.output_path = out_path;
|
||||
opts.output_type = .DYNAMIC_LIBRARY;
|
||||
opts.output_executable_name = "lib";
|
||||
opts.output_executable_name = lib_name;
|
||||
|
||||
compiler_begin_intercept(ws);
|
||||
set_build_options(opts, ws);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
reload_main :: () {
|
||||
basic.set_working_directory(strings.path_strip_filename(system.get_path_of_running_executable()));
|
||||
|
||||
system_allocator := context.allocator;
|
||||
|
||||
// Ensure we're not allocating anywhere unexpected
|
||||
context.allocator = Crash_Allocator;
|
||||
|
||||
basic.set_working_directory(strings.path_strip_filename(system.get_path_of_running_executable()));
|
||||
|
||||
lib_api, ok := load_library(0);
|
||||
basic.assert(ok, "host: failed to load library");
|
||||
|
||||
lib_versions: [..]Library;
|
||||
lib_versions.allocator = system_allocator;
|
||||
|
||||
// H always points to the currently loaded dynamic library.
|
||||
H := lib_api;
|
||||
next_lib_version := 1;
|
||||
|
||||
defer {
|
||||
for * lib_versions {
|
||||
unload_library(it);
|
||||
cleanup_library(it);
|
||||
}
|
||||
|
||||
unload_library(*H);
|
||||
cleanup_library(*H);
|
||||
basic.free(lib_versions.data,, allocator = system_allocator);
|
||||
}
|
||||
|
||||
|
|
@ -72,17 +73,23 @@ reload_main :: () {
|
|||
old_msize := H.max_memory;
|
||||
new_ssize := new_api.state_size;
|
||||
new_msize := new_api.max_memory;
|
||||
|
||||
if old_ssize != new_ssize || old_msize != new_msize {
|
||||
status = .hard_reload;
|
||||
}
|
||||
|
||||
// hard reloads are when full initialization has to happen again,
|
||||
// as if we restarted the program. this is mostly due to memory/state
|
||||
// size changing between reloads. however, programs can explicitly request this.
|
||||
if status == .hard_reload {
|
||||
basic.print("host: performing hard reload...\n");
|
||||
|
||||
// the currently allocated memory block is only safe to reuse
|
||||
// if the memory size hasn't changed between reloads.
|
||||
if old_msize <= new_msize {
|
||||
lib_arena.offset = 0;
|
||||
}
|
||||
// otherwise we need to reallocate the program memory
|
||||
// and reinitialize.
|
||||
else {
|
||||
free(lib_memory,, allocator = system_allocator);
|
||||
|
||||
|
|
@ -90,17 +97,20 @@ reload_main :: () {
|
|||
basic.assert(lib_memory != null, "host: failed to allocator new memory");
|
||||
|
||||
init_arena(*lib_arena, lib_memory, new_msize);
|
||||
lib_state = basic.alloc(xx new_ssize,, allocator = lib_allocator);
|
||||
}
|
||||
|
||||
// in either case, we want ensure lib_state is at the start of our program's memory.
|
||||
lib_state = basic.alloc(xx new_ssize,, allocator = lib_allocator);
|
||||
}
|
||||
else {
|
||||
basic.print("host: performing soft reload...\n");
|
||||
basic.array_add(*lib_versions, H);
|
||||
// we don't need to do anything special for soft reloads, only transfer
|
||||
// the current state pointer and allocator to the library.
|
||||
basic.print("host: performing soft reload...\n");
|
||||
basic.array_add(*lib_versions, H);
|
||||
}
|
||||
|
||||
H = new_api;
|
||||
next_lib_version += 1;
|
||||
|
||||
H.init(lib_state, lib_allocator, status == .hard_reload);
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +134,8 @@ Library :: struct {
|
|||
mod_time: basic.Apollo_Time;
|
||||
}
|
||||
|
||||
Default_Lib_Path :: "lib.%";
|
||||
Temp_Lib_Path :: "__temp_lib_%.%";
|
||||
Default_Lib_Path :: #run tprint("%.\%", lib_name);
|
||||
Temp_Lib_Path :: #run tprint("__temp_%_\%.\%", lib_name);
|
||||
|
||||
#if OS == {
|
||||
case .WINDOWS;
|
||||
|
|
@ -171,7 +181,7 @@ load_library :: (version: int) -> Library, bool {
|
|||
}, true;
|
||||
}
|
||||
|
||||
unload_library :: (library: *Library) {
|
||||
cleanup_library :: (library: *Library) {
|
||||
path := basic.tprint(Temp_Lib_Path, library.version, Lib_Extension);
|
||||
if library.handle && !os_unload_library(library.handle) {
|
||||
basic.print("host: failed to unload temporary library: %\n", path);
|
||||
|
|
|
|||
Loading…
Reference in a new issue