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:
2024-05-16 17:27:15 -04:00
parent 0527a033c8
commit b30f3c3466
75 changed files with 24 additions and 97 deletions

View 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" )
}
}

View 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 )
}

View File

@ -0,0 +1,2 @@
package sectr

View 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
}

View 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
}

View 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 )
}

View File

@ -0,0 +1,5 @@
package sectr

View 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
}