From 3f2d499025e8ef105c54e0ca23effe4b0e8b64a9 Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Thu, 15 May 2025 15:42:38 -0600 Subject: [PATCH] mostly tests, more reload module examples --- README | 37 +++++++++++++- _run_all_tests.jai | 10 ++++ array.jai | 4 +- encoding/module.jai | 1 + module.jai | 4 +- reload/examples/everything-you-need.jai | 66 ++++++++++++++++++++++--- reload/examples/quickstart.jai | 59 ++++++++++++++++++++++ reload/module.jai | 54 ++++++++++++-------- reload/reload_main.jai | 34 ++++++++----- 9 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 _run_all_tests.jai create mode 100644 reload/examples/quickstart.jai diff --git a/README b/README index 690d1af..9a3e4c6 100644 --- a/README +++ b/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. + diff --git a/_run_all_tests.jai b/_run_all_tests.jai new file mode 100644 index 0000000..6bb1237 --- /dev/null +++ b/_run_all_tests.jai @@ -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); +} + + + diff --git a/array.jai b/array.jai index 00f1c51..1643101 100644 --- a/array.jai +++ b/array.jai @@ -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; } } diff --git a/encoding/module.jai b/encoding/module.jai index a878300..cf22d9f 100644 --- a/encoding/module.jai +++ b/encoding/module.jai @@ -1,6 +1,7 @@ #module_parameters(RUN_TESTS := false); #load "base64.jai"; +#load "json.jai"; #scope_module; diff --git a/module.jai b/module.jai index a38d7f4..0fd0c08 100644 --- a/module.jai +++ b/module.jai @@ -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); } diff --git a/reload/examples/everything-you-need.jai b/reload/examples/everything-you-need.jai index 4de298e..4653c73 100644 --- a/reload/examples/everything-you-need.jai +++ b/reload/examples/everything-you-need.jai @@ -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"; diff --git a/reload/examples/quickstart.jai b/reload/examples/quickstart.jai new file mode 100644 index 0000000..abc98fb --- /dev/null +++ b/reload/examples/quickstart.jai @@ -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; +} + diff --git a/reload/module.jai b/reload/module.jai index 7751b36..4cb1369 100644 --- a/reload/module.jai +++ b/reload/module.jai @@ -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); diff --git a/reload/reload_main.jai b/reload/reload_main.jai index 7081906..7925dcf 100644 --- a/reload/reload_main.jai +++ b/reload/reload_main.jai @@ -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);