/* Sectr Host Executable Manages the client module (sectr) application & loads its required memory to operate. Reserves the virtual memory spaces for the following: * Persistent * Frame * Transient * FilesBuffer Currently the prototype has hot-reload always enabled, eventually there will be conditional compliation to omit if when desired. */ package sectr_host //region Grime & Dependencies import "base:runtime" Byte :: runtime.Byte Kilobyte :: runtime.Kilobyte Megabyte :: runtime.Megabyte Gigabyte :: runtime.Gigabyte Terabyte :: runtime.Terabyte Petabyte :: runtime.Petabyte Exabyte :: runtime.Exabyte import "core:dynlib" os_lib_load :: dynlib.load_library os_lib_unload :: dynlib.unload_library os_lib_get_proc :: dynlib.symbol_address import "core:io" import fmt_io "core:fmt" str_fmt :: fmt_io.printf str_fmt_alloc :: fmt_io.aprintf str_fmt_tmp :: fmt_io.tprintf str_fmt_buffer :: fmt_io.bprintf str_fmt_builder :: fmt_io.sbprintf import "core:log" import "core:mem" Allocator :: mem.Allocator AllocatorError :: mem.Allocator_Error Arena :: mem.Arena arena_allocator :: mem.arena_allocator import "core:mem/virtual" MapFileError :: virtual.Map_File_Error MapFileFlag :: virtual.Map_File_Flag MapFileFlags :: virtual.Map_File_Flags import "core:os" FileFlag_Create :: os.O_CREATE FileFlag_ReadWrite :: os.O_RDWR file_open :: os.open file_close :: os.close file_rename :: os.rename file_remove :: os.remove file_resize :: os.ftruncate file_status_via_handle :: os.fstat file_status_via_path :: os.stat import "core:strings" builder_to_string :: strings.to_string str_clone :: strings.clone str_builder_from_bytes :: strings.builder_from_bytes import "core:time" Millisecond :: time.Millisecond Second :: time.Second Duration :: time.Duration duration_seconds :: time.duration_seconds thread_sleep :: time.sleep import "core:prof/spall" import rl "vendor:raylib" import "codebase:grime" file_copy_sync :: grime.file_copy_sync file_is_locked :: grime.file_is_locked varena_init :: grime.varena_init import "codebase:sectr" VArena :: sectr.VArena fatal :: sectr.fatal Logger :: sectr.Logger logger_init :: sectr.logger_init LogLevel :: sectr.LogLevel log :: sectr.log SpallProfiler :: sectr.SpallProfiler to_odin_logger :: sectr.to_odin_logger verify :: sectr.verify file_status :: proc { file_status_via_handle, file_status_via_path, } to_str :: proc { builder_to_string, } //endregion Grime & Dependencies Path_Snapshot :: "VMemChunk_1.snapshot" Path_Logs :: "../logs" when ODIN_OS == runtime.Odin_OS_Type.Windows { Path_Sectr_Module :: "sectr.dll" Path_Sectr_Live_Module :: "sectr_live.dll" Path_Sectr_Debug_Symbols :: "sectr.pdb" } // TODO(Ed): Disable the default allocators for the host, we'll be handling it instead. RuntimeState :: struct { persistent : Arena, transient : Arena, running : b32, client_memory : ClientMemory, sectr_api : sectr.ModuleAPI, } ClientMemory :: struct { persistent : VArena, frame : VArena, transient : VArena, files_buffer : VArena, } setup_memory :: proc( profiler : ^SpallProfiler ) -> ClientMemory { spall.SCOPED_EVENT( & profiler.ctx, & profiler.buffer, #procedure ) memory : ClientMemory; using memory // Setup the static arena for the entire application { alloc_error : AllocatorError persistent, alloc_error = varena_init( sectr.Memory_Base_Address_Persistent, sectr.Memory_Reserve_Persistent, sectr.Memory_Commit_Initial_Persistent, growth_policy = nil, allow_any_resize = true, dbg_name = "persistent", enable_mem_tracking = false ) verify( alloc_error == .None, "Failed to allocate persistent virtual arena for the sectr module") frame, alloc_error = varena_init( sectr.Memory_Base_Address_Frame, sectr.Memory_Reserve_Frame, sectr.Memory_Commit_Initial_Frame, growth_policy = nil, allow_any_resize = true, dbg_name = "frame" ) verify( alloc_error == .None, "Failed to allocate frame virtual arena for the sectr module") transient, alloc_error = varena_init( sectr.Memory_Base_Address_Transient, sectr.Memory_Reserve_Transient, sectr.Memory_Commit_Initial_Transient, growth_policy = nil, allow_any_resize = true, dbg_name = "transient" ) verify( alloc_error == .None, "Failed to allocate transient virtual arena for the sectr module") files_buffer, alloc_error = varena_init( sectr.Memory_Base_Address_Files_Buffer, sectr.Memory_Reserve_FilesBuffer, sectr.Memory_Commit_Initial_Filebuffer, growth_policy = nil, allow_any_resize = true, dbg_name = "files_buffer" ) verify( alloc_error == .None, "Failed to allocate files buffer virtual arena for the sectr module") } // Setup memory mapped io for snapshots // TODO(Ed) : We cannot do this with our growing arenas. Instead we need to map on demand for saving and loading when false { snapshot_file, open_error := file_open( Path_Snapshot, FileFlag_ReadWrite | FileFlag_Create ) verify( open_error == os.ERROR_NONE, "Failed to open snapshot file for the sectr module" ) file_info, stat_code := file_status( snapshot_file ) { if file_info.size != sectr.Memory_Chunk_Size { file_resize( snapshot_file, sectr.Memory_Chunk_Size ) } } map_error : MapFileError map_flags : MapFileFlags = { MapFileFlag.Read, MapFileFlag.Write } sectr_snapshot, map_error = virtual.map_file_from_file_descriptor( uintptr(snapshot_file), map_flags ) verify( map_error == MapFileError.None, "Failed to allocate snapshot memory for the sectr module" ) file_close(snapshot_file) } log("Memory setup") return memory; } load_sectr_api :: proc( version_id : i32 ) -> (loaded_module : sectr.ModuleAPI) { write_time, result := os.last_write_time_by_name("sectr.dll") if result != os.ERROR_NONE { log( "Could not resolve the last write time for sectr.dll", LogLevel.Warning ) runtime.debug_trap() return } thread_sleep( Millisecond * 100 ) live_file := Path_Sectr_Live_Module file_copy_sync( Path_Sectr_Module, live_file, allocator = context.temp_allocator ) lib, load_result := os_lib_load( live_file ) if ! load_result { log( "Failed to load the sectr module.", LogLevel.Warning ) runtime.debug_trap() return } startup := cast( type_of( sectr.startup )) os_lib_get_proc( lib, "startup" ) shutdown := cast( type_of( sectr.sectr_shutdown )) os_lib_get_proc( lib, "sectr_shutdown" ) reload := cast( type_of( sectr.hot_reload )) os_lib_get_proc( lib, "hot_reload" ) tick := cast( type_of( sectr.tick )) os_lib_get_proc( lib, "tick" ) clean_frame := cast( type_of( sectr.clean_frame )) os_lib_get_proc( lib, "clean_frame" ) missing_symbol : b32 = false if startup == nil do log("Failed to load sectr.startup symbol", LogLevel.Warning ) if shutdown == nil do log("Failed to load sectr.shutdown symbol", LogLevel.Warning ) if reload == nil do log("Failed to load sectr.reload symbol", LogLevel.Warning ) if tick == nil do log("Failed to load sectr.tick symbol", LogLevel.Warning ) if clean_frame == nil do log("Failed to load sector.clean_frame symbol", LogLevel.Warning ) if missing_symbol { runtime.debug_trap() return } log("Loaded sectr API") loaded_module = { lib = lib, write_time = write_time, lib_version = version_id, startup = startup, shutdown = shutdown, reload = reload, tick = tick, clean_frame = clean_frame, } return } unload_sectr_api :: proc( module : ^ sectr.ModuleAPI ) { os_lib_unload( module.lib ) file_remove( Path_Sectr_Live_Module ) module^ = {} log("Unloaded sectr API") } sync_sectr_api :: proc( sectr_api : ^sectr.ModuleAPI, memory : ^ClientMemory, logger : ^Logger, profiler : ^SpallProfiler ) { spall.SCOPED_EVENT( & profiler.ctx, & profiler.buffer, #procedure ) if write_time, result := os.last_write_time_by_name( Path_Sectr_Module ); result == os.ERROR_NONE && sectr_api.write_time != write_time { version_id := sectr_api.lib_version + 1 unload_sectr_api( sectr_api ) // Wait for pdb to unlock (linker may still be writting) for ; file_is_locked( Path_Sectr_Debug_Symbols ) && file_is_locked( Path_Sectr_Live_Module ); {} thread_sleep( Millisecond * 100 ) (sectr_api ^) = load_sectr_api( version_id ) verify( sectr_api.lib_version != 0, "Failed to hot-reload the sectr module" ) sectr_api.reload( profiler, & memory.persistent, & memory.frame, & memory.transient, & memory.files_buffer, logger ) } } fmt_backing : [16 * Kilobyte] u8 persistent_backing : [2 * Megabyte] byte transient_backing : [32 * Megabyte] byte main :: proc() { state : RuntimeState using state mem.arena_init( & state.persistent, persistent_backing[:] ) mem.arena_init( & state.transient, transient_backing[:] ) context.allocator = arena_allocator( & state.persistent) context.temp_allocator = arena_allocator( & state.transient) // Setup profiling profiler : SpallProfiler { buffer_backing := make([]u8, spall.BUFFER_DEFAULT_SIZE) profiler.ctx = spall.context_create("sectr.spall") profiler.buffer = spall.buffer_create(buffer_backing) } spall.SCOPED_EVENT( & profiler.ctx, & profiler.buffer, #procedure ) // Generating the logger's name, it will be used when the app is shutting down. path_logger_finalized : string { startup_time := time.now() year, month, day := time.date( startup_time) hour, min, sec := time.clock_from_time( startup_time) if ! os.is_dir( Path_Logs ) { os.make_directory( Path_Logs ) } timestamp := str_fmt_buffer( fmt_backing[:], "%04d-%02d-%02d_%02d-%02d-%02d", year, month, day, hour, min, sec) path_logger_finalized = str_fmt_buffer( fmt_backing[:], "%s/sectr_%v.log", Path_Logs, timestamp) } logger : sectr.Logger logger_init( & logger, "Sectr Host", str_fmt_buffer( fmt_backing[:], "%s/sectr.log", Path_Logs ) ) context.logger = to_odin_logger( & logger ) { // Log System Context backing_builder : [1 * Kilobyte] u8 builder := str_builder_from_bytes( backing_builder[:] ) str_fmt_builder( & builder, "Core Count: %v, ", os.processor_core_count() ) str_fmt_builder( & builder, "Page Size: %v", os.get_page_size() ) log( to_str(builder) ) } memory := setup_memory( & profiler ) // Load the Enviornment API for the first-time { sectr_api = load_sectr_api( 1 ) verify( sectr_api.lib_version != 0, "Failed to initially load the sectr module" ) } // free_all( context.temp_allocator ) running = true; sectr_api = sectr_api sectr_api.startup( & profiler, & memory.persistent, & memory.frame, & memory.transient, & memory.files_buffer, & logger ) delta_ns : Duration host_tick := time.tick_now() // TODO(Ed) : This should have an end status so that we know the reason the engine stopped. for ; running ; { spall.SCOPED_EVENT( & profiler.ctx, & profiler.buffer, "Host Tick" ) // Hot-Reload sync_sectr_api( & sectr_api, & memory, & logger, & profiler ) running = sectr_api.tick( duration_seconds( delta_ns ), delta_ns ) sectr_api.clean_frame() delta_ns = time.tick_lap_time( & host_tick ) host_tick = time.tick_now() free_all( arena_allocator( & state.transient)) } // Determine how the run_cyle completed, if it failed due to an error, // fallback the env to a failsafe state and reload the run_cycle. { // TODO(Ed): Implement this. } sectr_api.shutdown() unload_sectr_api( & sectr_api ) spall.buffer_destroy( & profiler.ctx, & profiler.buffer ) spall.context_destroy( & profiler.ctx ) log("Succesfuly closed") file_close( logger.file ) file_rename( str_fmt_buffer( fmt_backing[:], "%s/sectr.log", Path_Logs), path_logger_finalized ) }