Files
SectrPrototype/code/host/host.odin
Ed_ 6d780482c7 Mostly still reviewing and planning... (see description)
Anything considered static can be aggregated into a single VArena. We don't have to worry about ever releasing its memory or it growing "too large".  All memory here must be fixed sized.
Conservative persistent memory can grow on demand but we would perfer if it could be trimmed or released when no longer dealing with heavy scenarios. Persistent memory should use a slab allocator that is backed by a virtual address space pool allocator instead of pools allocating from a single varena. Chained Arenas can source thier chunks of vmem from the slab which can be utilized for scratch memory. Fonts should be loaded from VSlab. The string cache should use a dedicated varena with 16-byte alignment. All conservative memory should be trimmable by a wipe command which should free all unused blocks. Each block should be a single OS aware reserve of vmem.

The Frame can possilby stay as a single varena with scratch allocation utilized on demand. Although it may be more viable for chained varenas to be derived from the main varena via a slab or pool interface. Frame memory should be trimmable on command which should release its committed vmem to its initial value. A dedicated transient varena should not exist. It should be removed when possible. File mappings for now can use a dedicated varena made on demand with a capped reserve size of 4 meg. Any file exceeding this needs the host to support virtual memory mapped I/O for files. The codebase db will use sqlite for the file I/O abstraction.

Host might only need to track the first persistent block of vmem, and the rest can be handled by the client (including wrapping that vmem up in a varena). Hot-reload only needs persistent vmem's ref restored on the client module's side. All other references can be resolved from there.
2025-07-07 02:00:57 -04:00

389 lines
12 KiB
Odin

/* 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
JobSystemContext :: sectr.JobSystemContext
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,
job_system: JobSystemContext,
}
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 : [32 * 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 * 4)
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 )
}