Restructured the codebase yet again but this time with compiler support for monlithic packages
So no need to stage generate symbolic links in a flat directory for the compiler
This commit is contained in:
400
code/sectr/engine/client_api.odin
Normal file
400
code/sectr/engine/client_api.odin
Normal file
@ -0,0 +1,400 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import c "core:c/libc"
|
||||
import "core:dynlib"
|
||||
import "core:mem"
|
||||
import "core:mem/virtual"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
import "core:strings"
|
||||
import "core:time"
|
||||
import "core:prof/spall"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Path_Assets :: "../assets/"
|
||||
Path_Shaders :: "../shaders/"
|
||||
Path_Input_Replay :: "scratch.sectr_replay"
|
||||
|
||||
Persistent_Slab_DBG_Name := "Peristent Slab"
|
||||
Frame_Slab_DBG_Name := "Frame Slab"
|
||||
Transient_Slab_DBG_Name := "Transient Slab"
|
||||
|
||||
ModuleAPI :: struct {
|
||||
lib : dynlib.Library,
|
||||
write_time : FileTime,
|
||||
lib_version : i32,
|
||||
|
||||
startup : type_of( startup ),
|
||||
shutdown : type_of( sectr_shutdown ),
|
||||
reload : type_of( reload ),
|
||||
tick : type_of( tick ),
|
||||
clean_frame : type_of( clean_frame ),
|
||||
}
|
||||
|
||||
@export
|
||||
startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^VArena, host_logger : ^Logger )
|
||||
{
|
||||
spall.SCOPED_EVENT( & prof.ctx, & prof.buffer, #procedure )
|
||||
Memory_App.profiler = prof
|
||||
|
||||
startup_tick := time.tick_now()
|
||||
|
||||
logger_init( & Memory_App.logger, "Sectr", host_logger.file_path, host_logger.file )
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
// Setup memory for the first time
|
||||
{
|
||||
using Memory_App;
|
||||
persistent = persistent_mem
|
||||
frame = frame_mem
|
||||
transient = transient_mem
|
||||
files_buffer = files_buffer_mem
|
||||
|
||||
context.allocator = persistent_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
// TODO(Ed) : Put on the transient allocator a slab allocator (transient slab)
|
||||
}
|
||||
|
||||
state := new( State, persistent_allocator() )
|
||||
Memory_App.state = state
|
||||
using state
|
||||
|
||||
// Setup Persistent Slab
|
||||
{
|
||||
alignment := uint(mem.DEFAULT_ALIGNMENT)
|
||||
|
||||
policy_ptr := & default_slab_policy
|
||||
push( policy_ptr, SlabSizeClass { 128 * Kilobyte, 1 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 256 * Kilobyte, 2 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 512 * Kilobyte, 4 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 16 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 32 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 64 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 128 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 256 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 512 * Kilobyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 1 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 2 * Megabyte, 2 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 4 * Megabyte, 4 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 8 * Megabyte, 8 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 16 * Megabyte, 16 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 32 * Megabyte, 32 * Megabyte, alignment })
|
||||
push( policy_ptr, SlabSizeClass { 64 * Megabyte, 64 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 128 * Megabyte, 128 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 256 * Megabyte, 256 * Megabyte, alignment })
|
||||
// push( policy_ptr, SlabSizeClass { 512 * Megabyte, 512 * Megabyte, alignment })
|
||||
|
||||
alloc_error : AllocatorError
|
||||
persistent_slab, alloc_error = slab_init( policy_ptr, allocator = persistent_allocator(), dbg_name = Persistent_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate the persistent slab" )
|
||||
|
||||
transient_slab, alloc_error = slab_init( & default_slab_policy, allocator = transient_allocator(), dbg_name = Transient_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate transient slab" )
|
||||
|
||||
transient_clear_time = 120 // Seconds, 2 Minutes
|
||||
}
|
||||
|
||||
string_cache = str_cache_init()
|
||||
|
||||
input = & input_data[1]
|
||||
input_prev = & input_data[0]
|
||||
|
||||
// Configuration Load
|
||||
{
|
||||
using config
|
||||
resolution_width = 1000
|
||||
resolution_height = 600
|
||||
refresh_rate = 0
|
||||
|
||||
cam_min_zoom = 0.10
|
||||
cam_max_zoom = 30.0
|
||||
cam_zoom_mode = .Smooth
|
||||
cam_zoom_smooth_snappiness = 4.0
|
||||
cam_zoom_sensitivity_digital = 0.2
|
||||
cam_zoom_sensitivity_smooth = 4.0
|
||||
|
||||
engine_refresh_hz = 30
|
||||
|
||||
timing_fps_moving_avg_alpha = 0.9
|
||||
|
||||
ui_resize_border_width = 5
|
||||
}
|
||||
|
||||
Desired_OS_Scheduler_MS :: 1
|
||||
sleep_is_granular = set__scheduler_granularity( Desired_OS_Scheduler_MS )
|
||||
|
||||
// Rough setup of window with rl stuff
|
||||
{
|
||||
// rl.Odin_SetMalloc( RL_MALLOC )
|
||||
|
||||
rl.SetConfigFlags( {
|
||||
rl.ConfigFlag.WINDOW_RESIZABLE,
|
||||
// rl.ConfigFlag.WINDOW_TOPMOST,
|
||||
})
|
||||
|
||||
window_width : i32 = cast(i32) config.resolution_width
|
||||
window_height : i32 = cast(i32) config.resolution_height
|
||||
win_title : cstring = "Sectr Prototype"
|
||||
rl.InitWindow( window_width, window_height, win_title )
|
||||
log( "Raylib initialized and window opened" )
|
||||
|
||||
window := & state.app_window
|
||||
window.extent.x = f32(window_width) * 0.5
|
||||
window.extent.y = f32(window_height) * 0.5
|
||||
|
||||
// We do not support non-uniform DPI.
|
||||
window.dpi_scale = rl.GetWindowScaleDPI().x
|
||||
window.ppcm = os_default_ppcm * window.dpi_scale
|
||||
|
||||
// Determining current monitor and setting the target frametime based on it..
|
||||
monitor_id = rl.GetCurrentMonitor()
|
||||
monitor_refresh_hz = rl.GetMonitorRefreshRate( monitor_id )
|
||||
log( str_fmt_tmp( "Set target FPS to: %v", monitor_refresh_hz ) )
|
||||
}
|
||||
|
||||
// Basic Font Setup
|
||||
{
|
||||
font_provider_startup()
|
||||
// path_rec_mono_semicasual_reg := strings.concatenate( { Path_Assets, "RecMonoSemicasual-Regular-1.084.ttf" })
|
||||
// font_rec_mono_semicasual_reg = font_load( path_rec_mono_semicasual_reg, 24.0, "RecMonoSemiCasual_Regular" )
|
||||
|
||||
// path_squidgy_slimes := strings.concatenate( { Path_Assets, "Squidgy Slimes.ttf" } )
|
||||
// font_squidgy_slimes = font_load( path_squidgy_slimes, 24.0, "Squidgy_Slime" )
|
||||
|
||||
path_firacode := strings.concatenate( { Path_Assets, "FiraCode-Regular.ttf" }, transient_allocator() )
|
||||
font_firacode = font_load( path_firacode, 24.0, "FiraCode" )
|
||||
default_font = font_firacode
|
||||
log( "Default font loaded" )
|
||||
}
|
||||
|
||||
// Setup the screen ui state
|
||||
{
|
||||
ui_startup( & screen_ui.base, cache_allocator = persistent_slab_allocator() )
|
||||
ui_floating_startup( & screen_ui.floating, persistent_slab_allocator(), 1 * Kilobyte, 1 * Kilobyte, "screen ui floating manager" )
|
||||
|
||||
using screen_ui
|
||||
menu_bar.pos = { -60, 0 }
|
||||
// menu_bar.pos = Vec2(app_window.extent) * { -1, 1 }
|
||||
menu_bar.size = {200, 40}
|
||||
|
||||
settings_menu.min_size = {250, 200}
|
||||
}
|
||||
|
||||
// Demo project setup
|
||||
{
|
||||
using project
|
||||
path = str_intern("./")
|
||||
name = str_intern( "First Project" )
|
||||
workspace.name = str_intern( "First Workspace" )
|
||||
{
|
||||
using project.workspace
|
||||
cam = {
|
||||
target = { 0, 0 },
|
||||
offset = transmute(Vec2) app_window.extent,
|
||||
rotation = 0,
|
||||
zoom = 1.0,
|
||||
}
|
||||
// cam = {
|
||||
// position = { 0, 0, -100 },
|
||||
// target = { 0, 0, 0 },
|
||||
// up = { 0, 1, 0 },
|
||||
// fovy = 90,
|
||||
// projection = rl.CameraProjection.ORTHOGRAPHIC,
|
||||
// }
|
||||
|
||||
// Setup workspace UI state
|
||||
ui_startup( & workspace.ui, cache_allocator = persistent_slab_allocator() )
|
||||
}
|
||||
|
||||
debug.path_lorem = str_fmt_alloc("C:/projects/SectrPrototype/examples/Lorem Ipsum.txt", allocator = persistent_slab_allocator())
|
||||
|
||||
alloc_error : AllocatorError; success : bool
|
||||
debug.lorem_content, success = os.read_entire_file( debug.path_lorem, persistent_slab_allocator() )
|
||||
|
||||
debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, persistent_slab_allocator() )
|
||||
verify( alloc_error == .None, "Faield to parse due to allocation failure" )
|
||||
|
||||
// Render texture test
|
||||
// debug.viewport_rt = rl.LoadRenderTexture( 1280, 720 )
|
||||
|
||||
// debug.proto_text_shader = rl.LoadShader( "C:/projects/SectrPrototype/code/shaders/text_shader.vs", "C:/projects/SectrPrototype/code/shaders/text_shader.fs" )
|
||||
}
|
||||
|
||||
startup_ms := duration_ms( time.tick_lap_time( & startup_tick))
|
||||
log( str_fmt_tmp("Startup time: %v ms", startup_ms) )
|
||||
|
||||
// Make sure to cleanup transient before continuing...
|
||||
// From here on, tarnsinet usage has to be done with care.
|
||||
// For most cases, the frame allocator should be more than enough.
|
||||
}
|
||||
|
||||
// For some reason odin's symbols conflict with native foreign symbols...
|
||||
@export
|
||||
sectr_shutdown :: proc()
|
||||
{
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
if Memory_App.persistent == nil {
|
||||
return
|
||||
}
|
||||
state := get_state()
|
||||
|
||||
// Replay
|
||||
{
|
||||
file_close( Memory_App.replay.active_file )
|
||||
}
|
||||
|
||||
font_provider_shutdown()
|
||||
|
||||
log("Module shutdown complete")
|
||||
}
|
||||
|
||||
@export
|
||||
reload :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^VArena, host_logger : ^ Logger )
|
||||
{
|
||||
spall.SCOPED_EVENT( & prof.ctx, & prof.buffer, #procedure )
|
||||
Memory_App.profiler = prof
|
||||
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
using Memory_App;
|
||||
|
||||
persistent = persistent_mem
|
||||
frame = frame_mem
|
||||
transient = transient_mem
|
||||
files_buffer = files_buffer_mem
|
||||
|
||||
context.allocator = persistent_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
|
||||
Memory_App.state = get_state()
|
||||
using state
|
||||
|
||||
// Procedure Addresses are not preserved on hot-reload. They must be restored for persistent data.
|
||||
// The only way to alleviate this is to either do custom handles to allocators
|
||||
// Or as done below, correct containers using allocators on reload.
|
||||
// Thankfully persistent dynamic allocations are rare, and thus we know exactly which ones they are.
|
||||
|
||||
slab_reload( persistent_slab, persistent_allocator() )
|
||||
|
||||
hmap_chained_reload( font_provider_data.font_cache, persistent_allocator())
|
||||
|
||||
slab_reload( string_cache.slab, persistent_allocator() )
|
||||
zpl_hmap_reload( & string_cache.table, persistent_slab_allocator())
|
||||
|
||||
slab_reload( frame_slab, frame_allocator())
|
||||
slab_reload( transient_slab, transient_allocator())
|
||||
|
||||
ui_reload( & get_state().project.workspace.ui, cache_allocator = persistent_slab_allocator() )
|
||||
|
||||
log("Module reloaded")
|
||||
}
|
||||
|
||||
@export
|
||||
tick :: proc( host_delta_time : f64, host_delta_ns : Duration ) -> b32
|
||||
{
|
||||
profile( "Client Tick" )
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
state := get_state(); using state
|
||||
|
||||
should_close : b32
|
||||
|
||||
client_tick := time.tick_now()
|
||||
{
|
||||
profile("Work frame")
|
||||
|
||||
// Setup Frame Slab
|
||||
{
|
||||
alloc_error : AllocatorError
|
||||
frame_slab, alloc_error = slab_init( & default_slab_policy, bucket_reserve_num = 0,
|
||||
allocator = frame_allocator(),
|
||||
dbg_name = Frame_Slab_DBG_Name,
|
||||
should_zero_buckets = true )
|
||||
verify( alloc_error == .None, "Failed to allocate frame slab" )
|
||||
}
|
||||
|
||||
context.allocator = frame_allocator()
|
||||
context.temp_allocator = transient_allocator()
|
||||
|
||||
rl.PollInputEvents()
|
||||
|
||||
debug.draw_ui_box_bounds_points = true
|
||||
debug.draw_UI_padding_bounds = false
|
||||
debug.draw_ui_content_bounds = true
|
||||
|
||||
should_close = update( host_delta_time )
|
||||
render()
|
||||
|
||||
rl.SwapScreenBuffer()
|
||||
}
|
||||
|
||||
// Timing
|
||||
{
|
||||
// profile("Client tick timing processing")
|
||||
config.engine_refresh_hz = uint(monitor_refresh_hz)
|
||||
// config.engine_refresh_hz = 6
|
||||
frametime_target_ms = 1.0 / f64(config.engine_refresh_hz) * S_To_MS
|
||||
sub_ms_granularity_required := frametime_target_ms <= Frametime_High_Perf_Threshold_MS
|
||||
|
||||
frametime_delta_ns = time.tick_lap_time( & client_tick )
|
||||
frametime_delta_ms = duration_ms( frametime_delta_ns )
|
||||
frametime_delta_seconds = duration_seconds( frametime_delta_ns )
|
||||
frametime_elapsed_ms = frametime_delta_ms + host_delta_time
|
||||
|
||||
if frametime_elapsed_ms < frametime_target_ms
|
||||
{
|
||||
sleep_ms := frametime_target_ms - frametime_elapsed_ms
|
||||
pre_sleep_tick := time.tick_now()
|
||||
|
||||
if sleep_ms > 0 {
|
||||
thread_sleep( cast(Duration) sleep_ms * MS_To_NS )
|
||||
// thread__highres_wait( sleep_ms )
|
||||
}
|
||||
|
||||
sleep_delta_ns := time.tick_lap_time( & pre_sleep_tick)
|
||||
sleep_delta_ms := duration_ms( sleep_delta_ns )
|
||||
|
||||
if sleep_delta_ms < sleep_ms {
|
||||
// log( str_fmt_tmp("frametime sleep was off by: %v ms", sleep_delta_ms - sleep_ms ))
|
||||
}
|
||||
|
||||
frametime_elapsed_ms += sleep_delta_ms
|
||||
for ; frametime_elapsed_ms < frametime_target_ms; {
|
||||
sleep_delta_ns = time.tick_lap_time( & pre_sleep_tick)
|
||||
sleep_delta_ms = duration_ms( sleep_delta_ns )
|
||||
|
||||
frametime_elapsed_ms += sleep_delta_ms
|
||||
}
|
||||
}
|
||||
|
||||
config.timing_fps_moving_avg_alpha = 0.99
|
||||
frametime_avg_ms = mov_avg_exp( f64(config.timing_fps_moving_avg_alpha), frametime_elapsed_ms, frametime_avg_ms )
|
||||
fps_avg = 1 / (frametime_avg_ms * MS_To_S)
|
||||
|
||||
if frametime_elapsed_ms > 60.0 {
|
||||
log( str_fmt_tmp("Big tick! %v ms", frametime_elapsed_ms), LogLevel.Warning )
|
||||
}
|
||||
}
|
||||
return should_close
|
||||
}
|
||||
|
||||
@export
|
||||
clean_frame :: proc()
|
||||
{
|
||||
// profile( #procedure)
|
||||
state := get_state(); using state
|
||||
context.logger = to_odin_logger( & Memory_App.logger )
|
||||
|
||||
free_all( frame_allocator() )
|
||||
|
||||
transient_clear_elapsed += frametime_delta32()
|
||||
if transient_clear_elapsed >= transient_clear_time && ! transinet_clear_lock
|
||||
{
|
||||
transient_clear_elapsed = 0
|
||||
free_all( transient_allocator() )
|
||||
|
||||
alloc_error : AllocatorError
|
||||
transient_slab, alloc_error = slab_init( & default_slab_policy, allocator = transient_allocator(), dbg_name = Transient_Slab_DBG_Name )
|
||||
verify( alloc_error == .None, "Failed to allocate transient slab" )
|
||||
}
|
||||
}
|
127
code/sectr/engine/logger.odin
Normal file
127
code/sectr/engine/logger.odin
Normal file
@ -0,0 +1,127 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import str "core:strings"
|
||||
import "core:time"
|
||||
import core_log "core:log"
|
||||
|
||||
Max_Logger_Message_Width :: 300
|
||||
|
||||
LogLevel :: core_log.Level
|
||||
|
||||
Logger :: struct {
|
||||
file_path : string,
|
||||
file : os.Handle,
|
||||
id : string,
|
||||
}
|
||||
|
||||
to_odin_logger :: proc( logger : ^ Logger ) -> core_log.Logger {
|
||||
return { logger_interface, logger, core_log.Level.Debug, core_log.Default_File_Logger_Opts }
|
||||
}
|
||||
|
||||
logger_init :: proc( logger : ^ Logger, id : string, file_path : string, file := os.INVALID_HANDLE )
|
||||
{
|
||||
if file == os.INVALID_HANDLE
|
||||
{
|
||||
logger_file, result_code := file_open( file_path, os.O_RDWR | os.O_CREATE )
|
||||
if result_code != os.ERROR_NONE {
|
||||
// Log failures are fatal and must never occur at runtime (there is no logging)
|
||||
runtime.debug_trap()
|
||||
os.exit( -1 )
|
||||
// TODO(Ed) : Figure out the error code enums..
|
||||
}
|
||||
logger.file = logger_file
|
||||
}
|
||||
else {
|
||||
logger.file = file
|
||||
}
|
||||
logger.file_path = file_path
|
||||
logger.id = id
|
||||
|
||||
context.logger = { logger_interface, logger, core_log.Level.Debug, core_log.Default_File_Logger_Opts }
|
||||
log("Initialized Logger")
|
||||
when false {
|
||||
log("This sentence is over 80 characters long on purpose to test the ability of this logger to properfly wrap long as logs with a new line and then at the end of that pad it with the appropraite signature.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger_interface :: proc(
|
||||
logger_data : rawptr,
|
||||
level : core_log.Level,
|
||||
text : string,
|
||||
options : core_log.Options,
|
||||
location := #caller_location )
|
||||
{
|
||||
logger := cast(^ Logger) logger_data
|
||||
|
||||
@static builder_backing : [16 * Kilobyte] byte; {
|
||||
mem.set( raw_data( builder_backing[:] ), 0, len(builder_backing) )
|
||||
}
|
||||
builder := str.builder_from_bytes( builder_backing[:] )
|
||||
|
||||
first_line_length := len(text) > Max_Logger_Message_Width ? Max_Logger_Message_Width : len(text)
|
||||
first_line := transmute(string) text[ 0 : first_line_length ]
|
||||
str_fmt_builder( & builder, "%-*s ", Max_Logger_Message_Width, first_line )
|
||||
|
||||
// Signature
|
||||
{
|
||||
when time.IS_SUPPORTED
|
||||
{
|
||||
if core_log.Full_Timestamp_Opts & options != nil {
|
||||
str_fmt_builder( & builder, "[")
|
||||
|
||||
t := time.now()
|
||||
year, month, day := time.date(t)
|
||||
hour, minute, second := time.clock(t)
|
||||
|
||||
if .Date in options {
|
||||
str_fmt_builder( & builder, "%d-%02d-%02d ", year, month, day )
|
||||
}
|
||||
if .Time in options {
|
||||
str_fmt_builder( & builder, "%02d:%02d:%02d", hour, minute, second)
|
||||
}
|
||||
|
||||
str_fmt_builder( & builder, "] ")
|
||||
}
|
||||
}
|
||||
core_log.do_level_header( options, level, & builder )
|
||||
|
||||
if logger.id != "" {
|
||||
str_fmt_builder( & builder, "[%s] ", logger.id )
|
||||
}
|
||||
core_log.do_location_header( options, & builder, location )
|
||||
}
|
||||
|
||||
// Oversized message handling
|
||||
if len(text) > Max_Logger_Message_Width
|
||||
{
|
||||
offset := Max_Logger_Message_Width
|
||||
bytes := transmute( []u8 ) text
|
||||
for left := len(bytes) - Max_Logger_Message_Width; left > 0; left -= Max_Logger_Message_Width
|
||||
{
|
||||
str_fmt_builder( & builder, "\n" )
|
||||
subset_length := len(text) - offset
|
||||
if subset_length > Max_Logger_Message_Width {
|
||||
subset_length = Max_Logger_Message_Width
|
||||
}
|
||||
subset := slice_ptr( ptr_offset( raw_data(bytes), offset), subset_length )
|
||||
str_fmt_builder( & builder, "%s", transmute(string) subset )
|
||||
offset += Max_Logger_Message_Width
|
||||
}
|
||||
}
|
||||
|
||||
str_to_file_ln( logger.file, to_string(builder) )
|
||||
}
|
||||
|
||||
log :: proc( msg : string, level := LogLevel.Info, loc := #caller_location ) {
|
||||
core_log.log( level, msg, location = loc )
|
||||
}
|
||||
|
||||
logf :: proc( fmt : string, args : ..any, level := LogLevel.Info, loc := #caller_location ) {
|
||||
// context.allocator = transient_allocator()
|
||||
core_log.logf( level, fmt, args, location = loc )
|
||||
}
|
2
code/sectr/engine/render_gl.odin
Normal file
2
code/sectr/engine/render_gl.odin
Normal file
@ -0,0 +1,2 @@
|
||||
package sectr
|
||||
|
406
code/sectr/engine/render_raylib.odin
Normal file
406
code/sectr/engine/render_raylib.odin
Normal file
@ -0,0 +1,406 @@
|
||||
package sectr
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
range2_to_rl_rect :: #force_inline proc "contextless"( range : Range2 ) -> rl.Rectangle
|
||||
{
|
||||
rect := rl.Rectangle {
|
||||
range.min.x,
|
||||
range.max.y,
|
||||
abs(range.max.x - range.min.x),
|
||||
abs(range.max.y - range.min.y),
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
draw_rectangle :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_RenderBoxInfo ) {
|
||||
using box
|
||||
if style.corner_radii[0] > 0 {
|
||||
rl.DrawRectangleRounded( rect, style.corner_radii[0], 9, style.bg_color )
|
||||
}
|
||||
else {
|
||||
rl.DrawRectangleRec( rect, style.bg_color )
|
||||
}
|
||||
}
|
||||
|
||||
draw_rectangle_lines :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_RenderBoxInfo, color : Color, thickness : f32 ) {
|
||||
using box
|
||||
if style.corner_radii[0] > 0 {
|
||||
rl.DrawRectangleRoundedLines( rect, style.corner_radii[0], 9, thickness, color )
|
||||
}
|
||||
else {
|
||||
rl.DrawRectangleLinesEx( rect, thickness, color )
|
||||
}
|
||||
}
|
||||
|
||||
render :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
render_mode_3d()
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground( Color_ThmDark_BG )
|
||||
|
||||
render_mode_2d_workspace()
|
||||
render_mode_screenspace()
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
// Experimental 3d viewport, not really the focus of this prototype
|
||||
// Until we can have a native or interpreted program render to it its not very useful.
|
||||
// Note(Ed): Other usecase could be 3d vis notes & math/graphical debug
|
||||
render_mode_3d :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
state := get_state(); using state
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.BeginTextureMode( debug.viewport_rt )
|
||||
rl.BeginMode3D( debug.cam_vp )
|
||||
rl.ClearBackground( Color_3D_BG )
|
||||
|
||||
rl.EndMode3D()
|
||||
rl.EndTextureMode()
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
// TODO(Ed): Eventually this needs to become a 'viewport within a UI'
|
||||
// This would allow the user to have more than one workspace open at the same time
|
||||
render_mode_2d_workspace :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
cam := & project.workspace.cam
|
||||
|
||||
win_extent := state.app_window.extent
|
||||
|
||||
rl.BeginMode2D( project.workspace.cam )
|
||||
|
||||
// Draw 3D Viewport
|
||||
when false
|
||||
{
|
||||
viewport_size := Vec2 { 1280.0, 720.0 }
|
||||
vp_half_size := viewport_size * 0.5
|
||||
viewport_box := range2( -vp_half_size, vp_half_size )
|
||||
viewport_render := range2(
|
||||
ws_view_to_render_pos( viewport_box.min),
|
||||
ws_view_to_render_pos( viewport_box.max),
|
||||
)
|
||||
viewport_rect := range2_to_rl_rect( viewport_render )
|
||||
rl.DrawTextureRec( debug.viewport_rt.texture, viewport_rect, -vp_half_size, Color_White )
|
||||
}
|
||||
|
||||
// draw_text( "This is text in world space", { 0, 200 }, 16.0 )
|
||||
|
||||
cam_zoom_ratio := 1.0 / cam.zoom
|
||||
|
||||
view_bounds := view_get_bounds()
|
||||
when false
|
||||
{
|
||||
render_view := Range2 { pts = {
|
||||
ws_view_to_render_pos( view_bounds.min),
|
||||
ws_view_to_render_pos( view_bounds.max),
|
||||
}}
|
||||
view_rect := rl.Rectangle {
|
||||
render_view.min.x,
|
||||
render_view.max.y,
|
||||
abs(render_view.max.x - render_view.min.x),
|
||||
abs(render_view.max.y - render_view.min.y),
|
||||
}
|
||||
rl.DrawRectangleRounded( view_rect, 0.3, 9, { 255, 0, 0, 20 } )
|
||||
}
|
||||
|
||||
ImguiRender:
|
||||
{
|
||||
profile("Imgui Render")
|
||||
ui := & state.project.workspace.ui
|
||||
root := ui.root
|
||||
if root == nil || root.num_children == 0 {
|
||||
break ImguiRender
|
||||
}
|
||||
state.ui_context = ui
|
||||
|
||||
current := root.first
|
||||
for & current in array_to_slice(ui.render_queue)
|
||||
{
|
||||
profile("Box")
|
||||
style := current.style
|
||||
computed := current.computed
|
||||
|
||||
if ! intersects_range2( view_bounds, computed.bounds ) {
|
||||
continue
|
||||
}
|
||||
|
||||
profile_begin("render space calc")
|
||||
render_bounds := range2(
|
||||
ws_view_to_render_pos(computed.bounds.min),
|
||||
ws_view_to_render_pos(computed.bounds.max),
|
||||
)
|
||||
render_padding := range2(
|
||||
ws_view_to_render_pos(computed.padding.min),
|
||||
ws_view_to_render_pos(computed.padding.max),
|
||||
)
|
||||
render_content := range2(
|
||||
ws_view_to_render_pos(computed.content.min),
|
||||
ws_view_to_render_pos(computed.content.max),
|
||||
)
|
||||
|
||||
rect_bounds := range2_to_rl_rect( render_bounds )
|
||||
rect_padding := range2_to_rl_rect( render_padding )
|
||||
rect_content := range2_to_rl_rect( render_content )
|
||||
profile_end()
|
||||
|
||||
profile_begin("raylib drawing")
|
||||
if style.bg_color.a != 0
|
||||
{
|
||||
draw_rectangle( rect_bounds, & current )
|
||||
}
|
||||
if current.border_width > 0 {
|
||||
draw_rectangle_lines( rect_bounds, & current, style.border_color, current.border_width )
|
||||
}
|
||||
|
||||
line_thickness := 1 * cam_zoom_ratio
|
||||
if debug.draw_UI_padding_bounds && equal_range2(computed.content, computed.padding) {
|
||||
draw_rectangle_lines( rect_padding, & current, Color_Debug_UI_Padding_Bounds, line_thickness )
|
||||
}
|
||||
else if debug.draw_ui_content_bounds {
|
||||
draw_rectangle_lines( rect_content, & current, Color_Debug_UI_Content_Bounds, line_thickness )
|
||||
}
|
||||
|
||||
point_radius := 3 * cam_zoom_ratio
|
||||
|
||||
// profile_begin("circles")
|
||||
if debug.draw_ui_box_bounds_points
|
||||
{
|
||||
computed_size := computed.bounds.p1 - computed.bounds.p0
|
||||
// center := Vec2 {
|
||||
// render_bounds.p0.x + computed_size.x * 0.5,
|
||||
// render_bounds.p0.y - computed_size.y * 0.5,
|
||||
// }
|
||||
// rl.DrawCircleV( center, point_radius, Color_White )
|
||||
|
||||
rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red )
|
||||
rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue )
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
profile_end()
|
||||
|
||||
if len(current.text.str) > 0 {
|
||||
ws_view_draw_text( current.text, ws_view_to_render_pos(computed.text_pos * {1, -1}), current.font_size, style.text_color )
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion Imgui Render
|
||||
|
||||
|
||||
if debug.mouse_vis {
|
||||
cursor_world_pos := screen_to_ws_view_pos(input.mouse.pos)
|
||||
rl.DrawCircleV( ws_view_to_render_pos(cursor_world_pos), 5, Color_GreyRed )
|
||||
}
|
||||
|
||||
rl.DrawCircleV( { 0, 0 }, 1 * cam_zoom_ratio, Color_White )
|
||||
|
||||
rl.EndMode2D()
|
||||
}
|
||||
|
||||
render_mode_screenspace :: proc ()
|
||||
{
|
||||
profile("Render Screenspace")
|
||||
|
||||
state := get_state(); using state
|
||||
replay := & Memory_App.replay
|
||||
cam := & project.workspace.cam
|
||||
win_extent := state.app_window.extent
|
||||
|
||||
render_screen_ui()
|
||||
|
||||
fps_msg := str_fmt_tmp( "FPS: %f", fps_avg)
|
||||
fps_msg_width := measure_text_size( fps_msg, default_font, 12.0, 0.0 ).x
|
||||
fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 } - { 5, 5 }
|
||||
debug_draw_text( fps_msg, fps_msg_pos, 12.0, color = rl.GREEN )
|
||||
|
||||
debug_text :: proc( format : string, args : ..any )
|
||||
{
|
||||
@static draw_text_scratch : [Kilobyte * 64]u8
|
||||
|
||||
state := get_state(); using state
|
||||
if debug.draw_debug_text_y > 800 {
|
||||
debug.draw_debug_text_y = 0
|
||||
}
|
||||
|
||||
cam := & project.workspace.cam
|
||||
screen_corners := screen_get_corners()
|
||||
|
||||
position := screen_corners.top_right
|
||||
position.x -= app_window.extent.x
|
||||
position.y -= debug.draw_debug_text_y
|
||||
|
||||
content := str_fmt_buffer( draw_text_scratch[:], format, ..args )
|
||||
debug_draw_text( content, position, 12.0 )
|
||||
|
||||
debug.draw_debug_text_y += 14
|
||||
}
|
||||
|
||||
// Debug Text
|
||||
{
|
||||
// debug_text( "Screen Width : %v", rl.GetScreenWidth () )
|
||||
// debug_text( "Screen Height: %v", rl.GetScreenHeight() )
|
||||
debug_text( "frametime_target_ms : %f ms", frametime_target_ms )
|
||||
debug_text( "frametime : %f ms", frametime_delta_ms )
|
||||
// debug_text( "frametime_last_elapsed_ms : %f ms", frametime_elapsed_ms )
|
||||
if replay.mode == ReplayMode.Record {
|
||||
debug_text( "Recording Input")
|
||||
}
|
||||
if replay.mode == ReplayMode.Playback {
|
||||
debug_text( "Replaying Input")
|
||||
}
|
||||
}
|
||||
|
||||
debug_text("Zoom Target: %v", project.workspace.zoom_target)
|
||||
|
||||
if debug.mouse_vis {
|
||||
debug_text("Mouse Vertical Wheel: %v", input.mouse.vertical_wheel )
|
||||
debug_text("Mouse Delta : %v", input.mouse.delta )
|
||||
debug_text("Mouse Position (Render) : %v", input.mouse.raw_pos )
|
||||
debug_text("Mouse Position (Screen) : %v", input.mouse.pos )
|
||||
debug_text("Mouse Position (Workspace View): %v", screen_to_ws_view_pos(input.mouse.pos) )
|
||||
rl.DrawCircleV( input.mouse.raw_pos, 10, Color_White_A125 )
|
||||
rl.DrawCircleV( screen_to_render_pos(input.mouse.pos), 2, Color_BG )
|
||||
}
|
||||
|
||||
ui := & project.workspace.ui
|
||||
|
||||
debug_text("Box Count (Workspace): %v", ui.built_box_count )
|
||||
|
||||
hot_box := ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
active_box := ui_box_from_key( ui.curr_cache, ui.active )
|
||||
if hot_box != nil {
|
||||
debug_text("Worksapce Hot Box : %v", hot_box.label.str )
|
||||
debug_text("Workspace Hot Range2: %v", hot_box.computed.bounds.pts)
|
||||
}
|
||||
if active_box != nil{
|
||||
debug_text("Workspace Active Box: %v", active_box.label.str )
|
||||
}
|
||||
|
||||
ui = & screen_ui
|
||||
|
||||
debug_text("Box Count: %v", ui.built_box_count )
|
||||
|
||||
hot_box = ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
active_box = ui_box_from_key( ui.curr_cache, ui.active )
|
||||
if hot_box != nil {
|
||||
debug_text("Hot Box : %v", hot_box.label.str )
|
||||
debug_text("Hot Range2: %v", hot_box.computed.bounds.pts)
|
||||
}
|
||||
if active_box != nil{
|
||||
debug_text("Active Box: %v", active_box.label.str )
|
||||
}
|
||||
|
||||
view := view_get_bounds()
|
||||
|
||||
debug.draw_debug_text_y = 14
|
||||
|
||||
// Define the triangle vertices and colors
|
||||
vertices := []f32{
|
||||
// Positions // Colors (RGBA)
|
||||
-0.5, -0.5, 0.0, 1.0, 0.0, 0.0, 1.0, // Vertex 1: Red
|
||||
0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, // Vertex 2: Green
|
||||
0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 1.0 // Vertex 3: Blue
|
||||
}
|
||||
}
|
||||
|
||||
// A non-zoomable static-view for ui
|
||||
// Only a scalar factor may be applied to the size of widgets & fonts
|
||||
// 'Window tiled' panels reside here
|
||||
render_screen_ui :: proc()
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
using state := get_state()
|
||||
|
||||
//region App UI
|
||||
Render_App_UI:
|
||||
{
|
||||
profile("App UI")
|
||||
ui := & state.screen_ui
|
||||
state.ui_context = ui
|
||||
root := ui.root
|
||||
if root.num_children == 0 {
|
||||
break Render_App_UI
|
||||
}
|
||||
|
||||
for & current in array_to_slice(ui.render_queue)
|
||||
{
|
||||
profile("Box")
|
||||
|
||||
style := current.style
|
||||
computed := & current.computed
|
||||
|
||||
profile_begin("Coordinate space conversion")
|
||||
render_bounds := range2(
|
||||
screen_to_render_pos(computed.bounds.min),
|
||||
screen_to_render_pos(computed.bounds.max),
|
||||
)
|
||||
render_padding := range2(
|
||||
screen_to_render_pos(computed.padding.min),
|
||||
screen_to_render_pos(computed.padding.max),
|
||||
)
|
||||
render_content := range2(
|
||||
screen_to_render_pos(computed.content.min),
|
||||
screen_to_render_pos(computed.content.max),
|
||||
)
|
||||
rect_bounds := range2_to_rl_rect( render_bounds )
|
||||
rect_padding := range2_to_rl_rect( render_padding )
|
||||
rect_content := range2_to_rl_rect( render_content )
|
||||
profile_end()
|
||||
|
||||
profile_begin("raylib drawing")
|
||||
if style.bg_color.a != 0
|
||||
{
|
||||
draw_rectangle( rect_bounds, & current )
|
||||
}
|
||||
if current.border_width > 0 {
|
||||
draw_rectangle_lines( rect_bounds, & current, style.border_color, current.border_width )
|
||||
}
|
||||
|
||||
line_thickness : f32 = 1
|
||||
|
||||
if debug.draw_UI_padding_bounds && equal_range2(computed.content, computed.padding) {
|
||||
draw_rectangle_lines( rect_padding, & current, Color_Debug_UI_Padding_Bounds, line_thickness )
|
||||
}
|
||||
else if debug.draw_ui_content_bounds {
|
||||
draw_rectangle_lines( rect_content, & current, Color_Debug_UI_Content_Bounds, line_thickness )
|
||||
}
|
||||
|
||||
point_radius : f32 = 3
|
||||
|
||||
if debug.draw_ui_box_bounds_points
|
||||
{
|
||||
computed_size := computed.bounds.p1 - computed.bounds.p0
|
||||
if false
|
||||
{
|
||||
center := Vec2 {
|
||||
render_bounds.p0.x + computed_size.x * 0.5,
|
||||
render_bounds.p0.y - computed_size.y * 0.5,
|
||||
}
|
||||
rl.DrawCircleV( center, point_radius, Color_White )
|
||||
}
|
||||
rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red )
|
||||
rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue )
|
||||
}
|
||||
|
||||
if len(current.text.str) > 0 && style.font.key != 0 {
|
||||
draw_text_screenspace( current.text, screen_to_render_pos(computed.text_pos), current.font_size, style.text_color )
|
||||
}
|
||||
profile_end()
|
||||
}
|
||||
}
|
||||
//endregion App UI
|
||||
}
|
224
code/sectr/engine/render_text_raylib.odin
Normal file
224
code/sectr/engine/render_text_raylib.odin
Normal file
@ -0,0 +1,224 @@
|
||||
package sectr
|
||||
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import "core:unicode/utf8"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
debug_draw_text :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content ) == 0 {
|
||||
return
|
||||
}
|
||||
runes, alloc_error := to_runes( content, frame_allocator() )
|
||||
// runes, alloc_error := to_runes( content, context.temp_allocator )
|
||||
// verify( alloc_error == AllocatorError.None, "Failed to temp allocate runes" )
|
||||
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
// if ( len(font) == 0 ) {
|
||||
font = default_font
|
||||
}
|
||||
pos := screen_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
|
||||
rl_font := to_rl_Font(font, px_size )
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
draw_text_screenspace :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content.str ) == 0 {
|
||||
return
|
||||
}
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
font = default_font
|
||||
}
|
||||
pos := pos
|
||||
|
||||
rl_font := to_rl_Font(font, size )
|
||||
runes := content.runes
|
||||
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
ws_view_draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
// profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content ) == 0 {
|
||||
return
|
||||
}
|
||||
runes, alloc_error := to_runes( content, frame_allocator() )
|
||||
verify( alloc_error == AllocatorError.None, "Failed to temp allocate runes" )
|
||||
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
// if len(font) == 0 {
|
||||
font = default_font
|
||||
}
|
||||
pos := ws_view_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
zoom_adjust := px_size * project.workspace.cam.zoom
|
||||
|
||||
rl_font := to_rl_Font(font, zoom_adjust )
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
rl.DrawTextCodepoints( rl_font,
|
||||
raw_data(runes), cast(i32) len(runes),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color );
|
||||
rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT)
|
||||
}
|
||||
|
||||
when true
|
||||
{
|
||||
ws_view_draw_text_StrRunesPair :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
if len( content.str ) == 0 {
|
||||
return
|
||||
}
|
||||
font := font
|
||||
if font.key == Font_Default.key {
|
||||
font = default_font
|
||||
}
|
||||
pos := ws_view_to_render_pos(pos)
|
||||
|
||||
px_size := size
|
||||
zoom_adjust := px_size * project.workspace.cam.zoom
|
||||
rl_font := to_rl_Font(font, zoom_adjust )
|
||||
runes := content.runes
|
||||
|
||||
profile_begin("raylib draw codepoints related")
|
||||
// rl.DrawTextCodepoints( rl_font,
|
||||
// raw_data(runes), cast(i32) len(runes),
|
||||
// position = transmute(rl.Vector2) pos,
|
||||
// fontSize = px_size,
|
||||
// spacing = 0.0,
|
||||
// tint = color );
|
||||
rl.DrawTextEx(rl_font,
|
||||
strings.clone_to_cstring(content.str),
|
||||
position = transmute(rl.Vector2) pos,
|
||||
fontSize = px_size,
|
||||
spacing = 0.0,
|
||||
tint = color
|
||||
)
|
||||
profile_end()
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ws_view_draw_text_StrRunesPair :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
|
||||
// We need an alternative way to draw text to the screen (the above is way to expensive)
|
||||
// Possibly need to watch handmade hero...
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Raylib's equivalent doesn't take a length for the string (making it a pain in the ass)
|
||||
// So this is a 1:1 copy except it takes Odin strings
|
||||
measure_text_size_raylib :: proc( text : string, font : FontID, font_size := Font_Use_Default_Size, spacing : f32 ) -> Vec2
|
||||
{
|
||||
// profile(#procedure)
|
||||
px_size := math.round( points_to_pixels( font_size ) )
|
||||
rl_font := to_rl_Font( font, font_size )
|
||||
|
||||
// This is a static var within raylib. We don't have getter access to it.
|
||||
// Note(Ed) : raylib font size is in pixels so this is also.
|
||||
@static text_line_spacing : f32 = 15
|
||||
|
||||
text_size : Vec2
|
||||
|
||||
if rl_font.texture.id == 0 || len(text) == 0 {
|
||||
return text_size
|
||||
}
|
||||
|
||||
temp_byte_counter : i32 = 0 // Used to count longer text line num chars
|
||||
byte_counter : i32 = 0
|
||||
|
||||
text_width : f32 = 0.0
|
||||
temp_text_width : f32 = 0.0 // Used to counter longer text line width
|
||||
|
||||
text_height := cast(f32) rl_font.baseSize
|
||||
scale_factor := px_size / text_height
|
||||
|
||||
letter : rune
|
||||
index : i32 = 0
|
||||
|
||||
for id : i32 = 0; id < i32(len(text));
|
||||
{
|
||||
byte_counter += 1
|
||||
|
||||
next : i32 = 0
|
||||
|
||||
ctext := cast(cstring) ( & raw_data( text )[id] )
|
||||
letter = rl.GetCodepointNext( ctext, & next )
|
||||
index = rl.GetGlyphIndex( rl_font, letter )
|
||||
|
||||
id += 1
|
||||
|
||||
if letter != rune('\n')
|
||||
{
|
||||
if rl_font.glyphs[index].advanceX != 0 {
|
||||
text_width += f32(rl_font.glyphs[index].advanceX)
|
||||
}
|
||||
else {
|
||||
text_width += rl_font.recs[index].width + f32(rl_font.glyphs[index].offsetX)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if temp_text_width < text_width {
|
||||
temp_text_width = text_width
|
||||
}
|
||||
byte_counter = 0
|
||||
text_width = 0
|
||||
|
||||
text_height += text_line_spacing
|
||||
|
||||
if temp_byte_counter < byte_counter {
|
||||
temp_byte_counter = byte_counter
|
||||
}
|
||||
}
|
||||
}
|
||||
if temp_text_width < text_width {
|
||||
temp_text_width = text_width
|
||||
}
|
||||
text_size.x = temp_text_width * scale_factor + f32(temp_byte_counter - 1) * spacing
|
||||
text_size.y = text_height * scale_factor
|
||||
|
||||
return text_size
|
||||
}
|
62
code/sectr/engine/replay.odin
Normal file
62
code/sectr/engine/replay.odin
Normal file
@ -0,0 +1,62 @@
|
||||
package sectr
|
||||
|
||||
import "core:os"
|
||||
|
||||
ReplayMode :: enum {
|
||||
Off,
|
||||
Record,
|
||||
Playback,
|
||||
}
|
||||
|
||||
ReplayState :: struct {
|
||||
loop_active : b32,
|
||||
mode : ReplayMode,
|
||||
active_file : os.Handle
|
||||
}
|
||||
|
||||
replay_recording_begin :: proc( path : string )
|
||||
{
|
||||
if file_exists( path ) {
|
||||
result := file_remove( path )
|
||||
verify( result != os.ERROR_NONE, "Failed to delete replay file before beginning a new one" )
|
||||
}
|
||||
|
||||
replay_file, open_error := file_open( path, FileFlag_ReadWrite | FileFlag_Create )
|
||||
verify( open_error != os.ERROR_NONE, "Failed to create or open the replay file" )
|
||||
|
||||
file_seek( replay_file, 0, 0 )
|
||||
|
||||
replay := & Memory_App.replay
|
||||
replay.active_file = replay_file
|
||||
replay.mode = ReplayMode.Record
|
||||
}
|
||||
|
||||
replay_recording_end :: proc() {
|
||||
replay := & Memory_App.replay
|
||||
replay.mode = ReplayMode.Off
|
||||
|
||||
file_seek( replay.active_file, 0, 0 )
|
||||
file_close( replay.active_file )
|
||||
}
|
||||
|
||||
replay_playback_begin :: proc( path : string )
|
||||
{
|
||||
verify( ! file_exists( path ), "Failed to find replay file" )
|
||||
|
||||
replay_file, open_error := file_open( path, FileFlag_ReadWrite | FileFlag_Create )
|
||||
verify( open_error != os.ERROR_NONE, "Failed to create or open the replay file" )
|
||||
|
||||
file_seek( replay_file, 0, 0 )
|
||||
|
||||
replay := & Memory_App.replay
|
||||
replay.active_file = replay_file
|
||||
replay.mode = ReplayMode.Playback
|
||||
}
|
||||
|
||||
replay_playback_end :: proc() {
|
||||
input := get_state().input
|
||||
replay := & Memory_App.replay
|
||||
replay.mode = ReplayMode.Off
|
||||
file_seek( replay.active_file, 0, 0 )
|
||||
file_close( replay.active_file )
|
||||
}
|
5
code/sectr/engine/startup.odin
Normal file
5
code/sectr/engine/startup.odin
Normal file
@ -0,0 +1,5 @@
|
||||
package sectr
|
||||
|
||||
|
||||
|
||||
|
251
code/sectr/engine/update.odin
Normal file
251
code/sectr/engine/update.odin
Normal file
@ -0,0 +1,251 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import "core:math"
|
||||
import "core:math/linalg"
|
||||
import "core:os"
|
||||
import str "core:strings"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
DebugActions :: struct {
|
||||
load_project : b32,
|
||||
save_project : b32,
|
||||
pause_renderer : b32,
|
||||
|
||||
load_auto_snapshot : b32,
|
||||
record_replay : b32,
|
||||
play_replay : b32,
|
||||
|
||||
show_mouse_pos : b32,
|
||||
|
||||
mouse_select : b32,
|
||||
|
||||
cam_move_up : b32,
|
||||
cam_move_left : b32,
|
||||
cam_move_down : b32,
|
||||
cam_move_right : b32,
|
||||
cam_mouse_pan : b32,
|
||||
}
|
||||
|
||||
poll_debug_actions :: proc( actions : ^ DebugActions, input : ^ InputState )
|
||||
{
|
||||
// profile(#procedure)
|
||||
using actions
|
||||
using input
|
||||
|
||||
modifier_active := keyboard.right_alt.ended_down ||
|
||||
keyboard.right_control.ended_down ||
|
||||
keyboard.right_shift.ended_down ||
|
||||
keyboard.left_alt.ended_down ||
|
||||
keyboard.left_control.ended_down ||
|
||||
keyboard.left_shift.ended_down
|
||||
|
||||
load_project = keyboard.left_control.ended_down && pressed( keyboard.O )
|
||||
save_project = keyboard.left_control.ended_down && pressed( keyboard.S )
|
||||
|
||||
base_replay_bind := keyboard.right_alt.ended_down && pressed( keyboard.L)
|
||||
record_replay = base_replay_bind && keyboard.right_shift.ended_down
|
||||
play_replay = base_replay_bind && ! keyboard.right_shift.ended_down
|
||||
|
||||
show_mouse_pos = keyboard.right_alt.ended_down && pressed(keyboard.M)
|
||||
|
||||
mouse_select = pressed(mouse.left)
|
||||
|
||||
cam_move_up = keyboard.W.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_left = keyboard.A.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_down = keyboard.S.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
cam_move_right = keyboard.D.ended_down && ( ! modifier_active || keyboard.left_shift.ended_down )
|
||||
|
||||
cam_mouse_pan = mouse.right.ended_down && ! pressed(mouse.right)
|
||||
}
|
||||
|
||||
frametime_delta32 :: #force_inline proc "contextless" () -> f32 {
|
||||
return cast(f32) get_state().frametime_delta_seconds
|
||||
}
|
||||
|
||||
update :: proc( delta_time : f64 ) -> b32
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state(); using state
|
||||
replay := & Memory_App.replay
|
||||
workspace := & project.workspace
|
||||
cam := & workspace.cam
|
||||
|
||||
if rl.IsWindowResized() {
|
||||
window := & state.app_window
|
||||
window.extent.x = f32(rl.GetScreenWidth()) * 0.5
|
||||
window.extent.y = f32(rl.GetScreenHeight()) * 0.5
|
||||
|
||||
project.workspace.cam.offset = transmute(Vec2) window.extent
|
||||
}
|
||||
|
||||
state.input, state.input_prev = swap( state.input, state.input_prev )
|
||||
poll_input( state.input_prev, state.input )
|
||||
|
||||
debug_actions : DebugActions = {}
|
||||
poll_debug_actions( & debug_actions, state.input )
|
||||
|
||||
// Saving & Loading
|
||||
{
|
||||
if debug_actions.save_project {
|
||||
project_save( & project )
|
||||
}
|
||||
if debug_actions.load_project {
|
||||
project_load( str_tmp_from_any( project.path, project.name, ".sectr_proj", sep = "" ), & project )
|
||||
}
|
||||
}
|
||||
|
||||
//region Input Replay
|
||||
// TODO(Ed) : Implment host memory mapping api
|
||||
when false
|
||||
{
|
||||
if debug_actions.record_replay { #partial switch replay.mode
|
||||
{
|
||||
case ReplayMode.Off : {
|
||||
save_snapshot( & Memory_App.snapshot )
|
||||
replay_recording_begin( Path_Input_Replay )
|
||||
}
|
||||
case ReplayMode.Record : {
|
||||
replay_recording_end()
|
||||
}
|
||||
}}
|
||||
|
||||
if debug_actions.play_replay { switch replay.mode
|
||||
{
|
||||
case ReplayMode.Off : {
|
||||
if ! file_exists( Path_Input_Replay ) {
|
||||
save_snapshot( & Memory_App.snapshot )
|
||||
replay_recording_begin( Path_Input_Replay )
|
||||
}
|
||||
else {
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
replay_playback_begin( Path_Input_Replay )
|
||||
}
|
||||
}
|
||||
case ReplayMode.Playback : {
|
||||
replay_playback_end()
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
}
|
||||
case ReplayMode.Record : {
|
||||
replay_recording_end()
|
||||
load_snapshot( & Memory_App.snapshot )
|
||||
replay_playback_begin( Path_Input_Replay )
|
||||
}
|
||||
}}
|
||||
|
||||
if replay.mode == ReplayMode.Record {
|
||||
record_input( replay.active_file, input )
|
||||
}
|
||||
else if replay.mode == ReplayMode.Playback {
|
||||
play_input( replay.active_file, input )
|
||||
}
|
||||
}
|
||||
//endregion Input Replay
|
||||
|
||||
if debug_actions.show_mouse_pos {
|
||||
debug.mouse_vis = !debug.mouse_vis
|
||||
}
|
||||
|
||||
//region 2D Camera Manual Nav
|
||||
// TODO(Ed): This should be per workspace view
|
||||
{
|
||||
// profile("Camera Manual Nav")
|
||||
digital_move_speed : f32 = 1000.0
|
||||
|
||||
if workspace.zoom_target == 0.0 {
|
||||
workspace.zoom_target = cam.zoom
|
||||
}
|
||||
|
||||
config.cam_max_zoom = 30
|
||||
config.cam_zoom_sensitivity_digital = 0.04
|
||||
config.cam_min_zoom = 0.04
|
||||
config.cam_zoom_mode = .Digital
|
||||
switch config.cam_zoom_mode
|
||||
{
|
||||
case .Smooth:
|
||||
zoom_delta := input.mouse.vertical_wheel * config.cam_zoom_sensitivity_smooth
|
||||
workspace.zoom_target *= 1 + zoom_delta * f32(delta_time)
|
||||
workspace.zoom_target = clamp(workspace.zoom_target, config.cam_min_zoom, config.cam_max_zoom)
|
||||
|
||||
// Linearly interpolate cam.zoom towards zoom_target
|
||||
lerp_factor := config.cam_zoom_smooth_snappiness // Adjust this value to control the interpolation speed
|
||||
cam.zoom += (workspace.zoom_target - cam.zoom) * lerp_factor * f32(delta_time)
|
||||
cam.zoom = clamp(cam.zoom, config.cam_min_zoom, config.cam_max_zoom) // Ensure cam.zoom stays within bounds
|
||||
case .Digital:
|
||||
zoom_delta := input.mouse.vertical_wheel * config.cam_zoom_sensitivity_digital
|
||||
workspace.zoom_target = clamp(workspace.zoom_target + zoom_delta, config.cam_min_zoom, config.cam_max_zoom)
|
||||
cam.zoom = workspace.zoom_target
|
||||
}
|
||||
|
||||
move_velocity : Vec2 = {
|
||||
- cast(f32) i32(debug_actions.cam_move_left) + cast(f32) i32(debug_actions.cam_move_right),
|
||||
- cast(f32) i32(debug_actions.cam_move_up) + cast(f32) i32(debug_actions.cam_move_down),
|
||||
}
|
||||
move_velocity *= digital_move_speed * f32(delta_time)
|
||||
cam.target += move_velocity
|
||||
|
||||
if debug_actions.cam_mouse_pan
|
||||
{
|
||||
if is_within_screenspace(input.mouse.pos) {
|
||||
pan_velocity := input.mouse.delta * vec2(1, -1) * ( 1 / cam.zoom )
|
||||
cam.target -= pan_velocity
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion 2D Camera Manual Nav
|
||||
|
||||
// TODO(Ed): We need input buffer so that we can consume input actions based on the UI with priority
|
||||
|
||||
ui_screen_tick()
|
||||
|
||||
//region WorkspaceImgui Tick
|
||||
{
|
||||
profile("Workspace Imgui")
|
||||
|
||||
// Creates the root box node, set its as the first parent.
|
||||
ui_graph_build( & state.project.workspace.ui )
|
||||
ui := ui_context
|
||||
|
||||
frame_style_flags : UI_LayoutFlags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center,
|
||||
}
|
||||
default_layout := UI_Layout {
|
||||
flags = frame_style_flags,
|
||||
anchor = {},
|
||||
// alignment = { 0.5, 0.5 },
|
||||
font_size = 30,
|
||||
text_alignment = { 0.0, 0.0 },
|
||||
// corner_radii = { 0.2, 0.2, 0.2, 0.2 },
|
||||
pos = { 0, 0 },
|
||||
// size = range2( { 1000, 1000 }, {}),
|
||||
// padding = { 20, 20, 20, 20 }
|
||||
}
|
||||
ui_layout( default_layout )
|
||||
frame_style_default := UI_Style {
|
||||
bg_color = Color_BG_TextBox,
|
||||
font = default_font,
|
||||
text_color = Color_White,
|
||||
}
|
||||
frame_theme := to_ui_style_combo(frame_style_default)
|
||||
frame_theme.disabled.bg_color = Color_Frame_Disabled
|
||||
frame_theme.hot. bg_color = Color_Frame_Hover
|
||||
frame_theme.active. bg_color = Color_Frame_Select
|
||||
ui_style( frame_theme )
|
||||
|
||||
config.ui_resize_border_width = 2.5
|
||||
// test_hover_n_click()
|
||||
// test_draggable()
|
||||
// test_text_box()
|
||||
test_parenting( & default_layout, & frame_style_default )
|
||||
// test_whitespace_ast( & default_layout, & frame_style_default )
|
||||
}
|
||||
//endregion Workspace Imgui Tick
|
||||
|
||||
debug.last_mouse_pos = input.mouse.pos
|
||||
|
||||
should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose()
|
||||
return should_shutdown
|
||||
}
|
Reference in New Issue
Block a user