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

View File

@ -0,0 +1,41 @@
package sectr
// Scratch space
import rl "vendor:raylib"
DebugData :: struct {
square_size : i32,
square_pos : rl.Vector2,
draw_debug_text_y : f32,
cursor_locked : b32,
cursor_unlock_pos : Vec2, // Raylib changes the mose position on lock, we want restore the position the user would be in on screen
mouse_vis : b32,
last_mouse_pos : Vec2,
// UI Vis
draw_ui_box_bounds_points : bool,
draw_UI_padding_bounds : bool,
draw_ui_content_bounds : bool,
// Test First
frame_2_created : b32,
// Test Draggable
draggable_box_pos : Vec2,
draggable_box_size : Vec2,
box_original_size : Vec2,
// Test parsing
path_lorem : string,
lorem_content : []byte,
lorem_parse : PWS_ParseResult,
// Test 3d Viewport
cam_vp : rl.Camera3D,
viewport_rt : rl.RenderTexture,
proto_text_shader : rl.Shader
}

320
code/sectr/app/screen.odin Normal file
View File

@ -0,0 +1,320 @@
package sectr
UI_ScreenState :: struct
{
using base : UI_State,
floating : UI_FloatingManager,
// TODO(Ed): The docked should be the base, floating is should be nested within as a 'veiwport' to a 'desktop' or 'canvas'
// docked : UI_Docking,
menu_bar : struct
{
pos, size : Vec2,
container : UI_HBox,
settings_btn : struct
{
using widget : UI_Widget,
}
},
settings_menu : struct
{
pos, size, min_size : Vec2,
container : UI_Widget,
is_open : b32,
is_maximized : b32,
},
}
ui_screen_tick :: proc() {
profile("Screenspace Imgui")
using state := get_state()
ui_graph_build( & screen_ui )
ui := ui_context
ui_floating_manager_begin( & screen_ui.floating )
{
ui_floating("Menu Bar", ui_screen_menu_bar)
ui_floating("Settings Menu", ui_screen_settings_menu)
}
ui_floating_manager_end()
}
ui_screen_menu_bar :: proc( captures : rawptr = nil ) -> (should_raise : b32 = false )
{
profile("App Menu Bar")
fmt :: str_fmt_alloc
@(deferred_none = ui_theme_pop)
ui_theme_app_menu_bar_default :: proc()
{
@static theme : UI_Theme
@static loaded : b32 = false
if true && ! loaded
{
layout := UI_Layout {
flags = {},
anchor = range2({},{}),
alignment = {0.5, 0.5},
text_alignment = {0.0, 1.5},
font_size = 12,
margins = {0, 0, 0, 0},
padding = {0, 0, 0, 0},
border_width = 0.6,
pos = {0, 0},
size = range2({},{})
}
style := UI_Style {
bg_color = Color_ThmDark_BG,
border_color = Color_ThmDark_Border_Default,
corner_radii = {},
blur_size = 0,
font = get_state().default_font,
text_color = Color_ThmDark_Text_Default,
cursor = {},
}
// loaded = true
layout_combo := to_ui_layout_combo(layout)
style_combo := to_ui_style_combo(style)
{
using layout_combo.hot
using style_combo.hot
bg_color = Color_ThmDark_Btn_BG_Hot
text_color = Color_ThmDark_Text_Hot
}
{
using layout_combo.active
using style_combo.active
bg_color = Color_ThmDark_Btn_BG_Active
text_color = Color_ThmDark_Text_Active
}
theme = UI_Theme {
layout_combo, style_combo
}
}
ui_layout_push(theme.layout)
ui_style_push(theme.style)
}
using state := get_state()
using screen_ui
{
using screen_ui.menu_bar
ui_theme_app_menu_bar_default()
container = ui_hbox( .Left_To_Right, "Menu Bar" )
{
using container
layout.flags = {.Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, .Origin_At_Anchor_Center}
layout.pos = pos
layout.size = range2( size, {})
text = str_intern("menu_bar")
}
ui_theme_btn_default()
move_box := ui_button("Move Box");
{
using move_box
if active {
pos += input.mouse.delta
should_raise = true
}
layout.anchor.ratio.x = 0.4
}
spacer := ui_spacer("Menu Bar: Move Spacer")
spacer.layout.flags |= {.Fixed_Width}
spacer.layout.size.min.x = 30
// TODO(Ed): Implement an external composition for theme interpolation using the settings btn
settings_btn.widget = ui_button("Menu Bar: Settings Btn")
{
using settings_btn
text = str_intern("Settings")
layout.flags = {
// .Scale_Width_By_Height_Ratio,
.Fixed_Width
}
layout.size.min.x = 100
if pressed {
screen_ui.settings_menu.is_open = true
}
}
spacer = ui_spacer("Menu Bar: End Spacer")
spacer.layout.anchor.ratio.x = 1.0
}
return
}
ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b32 = false)
{
profile("Settings Menu")
using state := get_state()
using state.screen_ui
if ! settings_menu.is_open do return
using settings_menu
if size.x < min_size.x do size.x = min_size.x
if size.y < min_size.y do size.y = min_size.y
ui_theme_transparent()
container = ui_widget("Settings Menu", {})
{
using container
layout.flags = { .Fixed_Width, .Fixed_Height, .Fixed_Position_X, .Fixed_Position_Y, .Origin_At_Anchor_Center }
style.bg_color = Color_ThmDark_Translucent_Panel
style.border_color = { 0, 0, 0, 200 }
layout.alignment = {0.0, 0.0}
layout.border_width = 1.0
layout.pos = pos
layout.size = range2( size, {})
}
ui_parent(container)
if settings_menu.is_maximized {
using container
layout.flags = {.Origin_At_Anchor_Center }
layout.pos = {}
}
should_raise |= ui_resizable_handles( & container, & pos, & size/*, compute_layout = true*/)
// ui_box_compute_layout(container)
vbox := ui_vbox_begin( .Top_To_Bottom, "Settings Menu: VBox", {.Mouse_Clickable}, compute_layout = true)
{
should_raise |= b32(vbox.active)
ui_parent(vbox)
ui_layout( UI_Layout {
// font_size = 16,
// alignment = {0, 1},
})
ui_style( UI_Style {
// bg_color = Color_Transparent,
font = default_font,
text_color = Color_White,
})
ui_style_ref().hot.bg_color = Color_Blue
frame_bar := ui_hbox_begin(.Left_To_Right, "Settings Menu: Frame Bar", { .Mouse_Clickable, .Focusable, .Click_To_Focus })
{
frame_bar.layout.flags = {.Fixed_Height}
frame_bar.layout.size.min.y = 50
ui_parent(frame_bar)
ui_layout( UI_Layout {
font_size = 18,
})
title := ui_text("Settings Menu: Title", str_intern("Settings Menu"), {.Disabled})
{
using title
layout.margins = { 0, 0, 15, 0}
layout.text_alignment = {0 , 0.5}
layout.anchor.ratio.x = 1.0
}
ui_layout( UI_Layout {
font_size = 16,
})
ui_style(ui_style_peek())
style := ui_style_ref()
maximize_btn := ui_button("Settings Menu: Maximize Btn")
{
using maximize_btn
layout.flags = {.Fixed_Width}
layout.size.min = {50, 50}
layout.text_alignment = {0.5, 0.5}
layout.anchor.ratio.x = 1.0
if maximize_btn.pressed {
settings_menu.is_maximized = ~settings_menu.is_maximized
should_raise = true
}
if settings_menu.is_maximized do text = str_intern("min")
else do text = str_intern("max")
}
close_btn := ui_button("Settings Menu: Close Btn")
{
using close_btn
text = str_intern("close")
layout.flags = {.Fixed_Width}
layout.size.min = {50, 0}
layout.text_alignment = {0.5, 0.5}
layout.anchor.ratio.x = 1.0
if close_btn.pressed {
settings_menu.is_open = false
}
}
ui_hbox_end(frame_bar, compute_layout = true)
}
if frame_bar.active {
pos += input.mouse.delta
should_raise = true
}
// Populate settings with values from config (hardcoded for now)
ui_layout(UI_Layout {
flags = {
// .Origin_At_Anchor_Center,
// .Fixed_Height,
},
// pos = {0, 50},
// size = range2({100, 100},{}),
// alignment = {0,0},
})
ui_style( UI_Style {
// bg_color = Color_GreyRed
})
drop_down_bar := ui_hbox_begin(.Left_To_Right, "settings_menu.vbox: config drop_down_bar", {.Mouse_Clickable})
btn : UI_Widget
{
drop_down_bar.layout.anchor.ratio.y = 0.1
{
using drop_down_bar
text = str_intern("drop_down_bar")
// style.bg_color = { 55, 55, 55, 100 }
style.font = default_font
style.text_color = Color_White
layout.flags = {.Fixed_Height}
layout.font_size = 12
layout.text_alignment = {1, 0}
layout.size.min.y = 35
}
ui_parent(drop_down_bar)
btn = ui_text("pls", str_intern("Lets figure this out..."))
{
using btn
text = str_intern("Config")
style.font = default_font
style.text_color = Color_White
// layout.flags = {.Origin_At_Anchor_Center}
layout.alignment = {0.0, 0.0}
layout.anchor.ratio.x = 1.0
layout.font_size = 12
layout.margins = {0,0, 15, 0}
layout.size.min.y = 35
}
um := ui_spacer("um...")
um.layout.anchor.ratio.x = 1.0
ui_hbox_end(drop_down_bar, compute_layout = true)
}
// ui_layout(UI_Layout {
// })
// ui_style( UI_Style {
// })
// res_width_hbox := ui_hbox_begin(.Left_To_Right, "settings_menu.vbox: config.resolution_width: hbox", {})
// ui_parent(res_width_hbox)
spacer := ui_spacer("Settings Menu: Spacer")
spacer.layout.anchor.ratio.y = 1.0
ui_vbox_end(vbox, compute_layout = false )
}
return
}

View File

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

260
code/sectr/app/state.odin Normal file
View File

@ -0,0 +1,260 @@
package sectr
import "base:runtime"
import "core:fmt"
import "core:mem"
import "core:mem/virtual"
import "core:os"
import rl "vendor:raylib"
Str_App_State := "App State"
#region("Memory")
Memory_App : Memory
Memory_Base_Address_Persistent :: Terabyte * 1
Memory_Base_Address_Frame :: Memory_Base_Address_Persistent + Memory_Reserve_Persistent * 2
Memory_Base_Address_Transient :: Memory_Base_Address_Frame + Memory_Reserve_Frame * 2
Memory_Base_Address_Files_Buffer :: Memory_Base_Address_Transient + Memory_Reserve_Transient * 2
// This reserve goes beyond the typical amount of ram the user has,
// TODO(Ed): Setup warnings when the amount is heading toward half the ram size
Memory_Reserve_Persistent :: 32 * Gigabyte
Memory_Reserve_Frame :: 16 * Gigabyte
Memory_Reserve_Transient :: 16 * Gigabyte
Memory_Reserve_FilesBuffer :: 64 * Gigabyte
Memory_Commit_Initial_Persistent :: 4 * Kilobyte
Memory_Commit_Initial_Frame :: 4 * Kilobyte
Memory_Commit_Initial_Transient :: 4 * Kilobyte
Memory_Commit_Initial_Filebuffer :: 4 * Kilobyte
MemorySnapshot :: struct {
persistent : []u8,
frame : []u8,
transient : []u8,
// files_buffer cannot be restored from snapshot
}
Memory :: struct {
persistent : ^VArena,
frame : ^VArena,
transient : ^VArena,
files_buffer : ^VArena,
state : ^State,
// Should only be used for small memory allocation iterations
// Not for large memory env states
snapshot : MemorySnapshot,
replay : ReplayState,
logger : Logger,
profiler : ^SpallProfiler
}
persistent_allocator :: proc() -> Allocator {
result := varena_allocator( Memory_App.persistent )
return result
}
frame_allocator :: proc() -> Allocator {
result := varena_allocator( Memory_App.frame )
return result
}
transient_allocator :: proc() -> Allocator {
result := varena_allocator( Memory_App.transient )
return result
}
files_buffer_allocator :: proc() -> Allocator {
result := varena_allocator( Memory_App.files_buffer )
return result
}
persistent_slab_allocator :: proc() -> Allocator {
state := get_state()
result := slab_allocator( state.persistent_slab )
return result
}
frame_slab_allocator :: proc() -> Allocator {
result := slab_allocator( get_state().frame_slab )
return result
}
transient_slab_allocator :: proc() -> Allocator {
result := slab_allocator( get_state().transient_slab )
return result
}
// TODO(Ed) : Implment host memory mapping api
save_snapshot :: proc( snapshot : ^MemorySnapshot )
{
// Make sure the snapshot size is able to hold the current size of the arenas
// Grow the files & mapping otherwise
{
// TODO(Ed) : Implement eventually
}
persistent := Memory_App.persistent
mem.copy_non_overlapping( & snapshot.persistent[0], persistent.reserve_start, int(persistent.commit_used) )
frame := Memory_App.frame
mem.copy_non_overlapping( & snapshot.frame[0], frame.reserve_start, int(frame.commit_used) )
transient := Memory_App.transient
mem.copy_non_overlapping( & snapshot.transient[0], transient.reserve_start, int(transient.commit_used) )
}
// TODO(Ed) : Implment host memory mapping api
load_snapshot :: proc( snapshot : ^MemorySnapshot ) {
persistent := Memory_App.persistent
mem.copy_non_overlapping( persistent.reserve_start, & snapshot.persistent[0], int(persistent.commit_used) )
frame := Memory_App.frame
mem.copy_non_overlapping( frame.reserve_start, & snapshot.frame[0], int(frame.commit_used) )
transient := Memory_App.transient
mem.copy_non_overlapping( transient.reserve_start, & snapshot.transient[0], int(transient.commit_used) )
}
// TODO(Ed) : Implement usage of this
MemoryConfig :: struct {
reserve_persistent : uint,
reserve_frame : uint,
reserve_transient : uint,
reserve_filebuffer : uint,
commit_initial_persistent : uint,
commit_initial_frame : uint,
commit_initial_transient : uint,
commit_initial_filebuffer : uint,
}
#endregion("Memory")
#region("State")
// ALl nobs available for this application
AppConfig :: struct {
using memory : MemoryConfig,
resolution_width : uint,
resolution_height : uint,
refresh_rate : uint,
cam_min_zoom : f32,
cam_max_zoom : f32,
cam_zoom_mode : CameraZoomMode,
cam_zoom_smooth_snappiness : f32,
cam_zoom_sensitivity_smooth : f32,
cam_zoom_sensitivity_digital : f32,
engine_refresh_hz : uint,
timing_fps_moving_avg_alpha : f32,
ui_resize_border_width : f32,
}
AppWindow :: struct {
extent : Extents2, // Window half-size
dpi_scale : f32, // Dots per inch scale (provided by raylib via glfw)
ppcm : f32, // Dots per centimetre
}
FontData :: struct {
provider : FontProviderData,
// TODO(Ed): We can have font constants here I guess but eventually
// I rather have fonts configurable for a 'theme' combo
// So that way which IDs are picked depends on runtime
firacode : FontID,
squidgy_slimes : FontID,
rec_mono_semicasual_reg : FontID,
default_font : FontID,
}
FrameTime :: struct {
sleep_is_granular : b32,
delta_seconds : f64,
delta_ms : f64,
delta_ns : Duration,
target_ms : f64,
elapsed_ms : f64,
avg_ms : f64,
fps_avg : f64,
}
// Global Singleton stored in the persistent virtual arena, the first allocated data.
// Use get_state() to conviently retrieve at any point for the program's lifetime
State :: struct {
default_slab_policy : SlabPolicy,
persistent_slab : Slab,
frame_slab : Slab,
transient_slab : Slab, // TODO(Ed): This needs to be recreated per transient wipe
transinet_clear_lock : b32, // Pravents auto-free of transient at designated intervals
transient_clear_time : f32, // Time in seconds for the usual period to clear transient
transient_clear_elapsed : f32, // Time since last clear
string_cache : StringCache,
input_data : [2]InputState,
input_prev : ^InputState,
input : ^InputState,
debug : DebugData,
project : Project,
config : AppConfig,
app_window : AppWindow,
screen_ui : UI_ScreenState,
monitor_id : i32,
monitor_refresh_hz : i32,
// using frametime : FrameTime,
sleep_is_granular : b32,
frametime_delta_seconds : f64,
frametime_delta_ms : f64,
frametime_delta_ns : Duration,
frametime_target_ms : f64,
frametime_elapsed_ms : f64,
frametime_avg_ms : f64,
fps_avg : f64,
// fonts : FontData,
font_provider_data : FontProviderData,
font_firacode : FontID,
font_squidgy_slimes : FontID,
font_rec_mono_semicasual_reg : FontID,
default_font : FontID,
// There are two potential UI contextes for this prototype so far,
// the screen-space UI and the current workspace UI.
// This is used so that the ui api doesn't need to have the user pass the context every single time.
ui_context : ^UI_State,
ui_floating_context : ^UI_FloatingManager,
// The camera is considered the "context" for coodrinate space operations in rendering
cam_context : Camera,
}
get_state :: #force_inline proc "contextless" () -> ^ State {
return cast( ^ State ) Memory_App.persistent.reserve_start
}
// get_frametime :: #force_inline proc "contextless" () -> FrameTime {
// return get_state().frametime
// }
#endregion("State")

View File

@ -0,0 +1,114 @@
package sectr
/*
UI Themes: Comprise of UI_Box's layout & style
Provides presets for themes and their interface for manipulating the combo stacks in UI_State in pairs
*/
// TODO(Ed): Eventually this will have a configuration wizard, and we'll save the presets
@(deferred_none = ui_theme_pop)
ui_theme_btn_default :: proc()
{
@static theme : UI_Theme
@static loaded : b32 = false
if ! loaded
{
layout := UI_Layout {
flags = {},
anchor = range2({},{}),
alignment = {0, 0},
text_alignment = {0.5, 0.5},
font_size = 16,
margins = {0, 0, 0, 0},
padding = {0, 0, 0, 0},
border_width = 1,
pos = {0, 0},
size = range2({},{})
}
style := UI_Style {
bg_color = Color_ThmDark_Btn_BG_Default,
border_color = Color_ThmDark_Border_Default,
corner_radii = {},
blur_size = 0,
font = get_state().default_font,
text_color = Color_ThmDark_Text_Default,
cursor = {},
}
layout_combo := to_ui_layout_combo(layout)
style_combo := to_ui_style_combo(style)
{
using layout_combo.hot
using style_combo.hot
bg_color = Color_ThmDark_Btn_BG_Hot
text_color = Color_ThmDark_Text_Hot
margins = {2, 2, 2, 2}
}
{
using layout_combo.active
using style_combo.active
bg_color = Color_ThmDark_Btn_BG_Active
text_color = Color_ThmDark_Text_Active
margins = {2, 2, 2, 2}
}
theme = UI_Theme {
layout_combo, style_combo
}
loaded = true
}
ui_layout_push(theme.layout)
ui_style_push(theme.style)
}
@(deferred_none = ui_theme_pop)
ui_theme_transparent :: proc()
{
@static theme : UI_Theme
@static loaded : b32 = false
if ! loaded || true
{
layout := UI_Layout {
flags = {},
anchor = range2({},{}),
alignment = {0, 0},
text_alignment = {0.0, 0.0},
font_size = 16,
margins = {0, 0, 0, 0},
padding = {0, 0, 0, 0},
border_width = 0,
pos = {0, 0},
size = range2({},{})
}
style := UI_Style {
bg_color = Color_Transparent,
border_color = Color_Transparent,
corner_radii = {},
blur_size = 0,
font = get_state().default_font,
text_color = Color_ThmDark_Text_Default,
cursor = {},
}
layout_combo := to_ui_layout_combo(layout)
style_combo := to_ui_style_combo(style)
{
using layout_combo.disabled
using style_combo.disabled
}
{
using layout_combo.hot
using style_combo.hot
}
{
using layout_combo.active
using style_combo.active
}
theme = UI_Theme {
layout_combo, style_combo
}
loaded = true
}
ui_layout_push(theme.layout)
ui_style_push(theme.style)
}

35
code/sectr/chrono.odin Normal file
View File

@ -0,0 +1,35 @@
package sectr
Nanosecond_To_Microsecond :: 1.0 / (1000.0)
Nanosecond_To_Millisecond :: 1.0 / (1000.0 * 1000.0)
Nanosecond_To_Second :: 1.0 / (1000.0 * 1000.0 * 1000.0)
Microsecond_To_Nanosecond :: 1000.0
Microsecond_To_Millisecond :: 1.0 / 1000.0
Microsecond_To_Second :: 1.0 / (1000.0 * 1000.0)
Millisecond_To_Nanosecond :: 1000.0 * 1000.0
Millisecond_To_Microsecond :: 1000.0
Millisecond_To_Second :: 1.0 / 1000.0
Second_To_Nanosecond :: 1000.0 * 1000.0 * 1000.0
Second_To_Microsecnd :: 1000.0 * 1000.0
Second_To_Millisecond :: 1000.0
NS_To_MS :: Nanosecond_To_Millisecond
NS_To_US :: Nanosecond_To_Microsecond
NS_To_S :: Nanosecond_To_Second
US_To_NS :: Microsecond_To_Nanosecond
US_To_MS :: Microsecond_To_Millisecond
US_To_S :: Microsecond_To_Second
MS_To_NS :: Millisecond_To_Nanosecond
MS_To_US :: Millisecond_To_Microsecond
MS_To_S :: Millisecond_To_Second
S_To_NS :: Second_To_Nanosecond
S_To_US :: Second_To_Microsecnd
S_To_MS :: Second_To_Millisecond
Frametime_High_Perf_Threshold_MS :: 1 / 240.0

42
code/sectr/collision.odin Normal file
View File

@ -0,0 +1,42 @@
// Goal is for any Position or 'Shape' intersections used by the prototype to be defined here for centeralization
package sectr
import "core:math/linalg"
// AABB: Separating Axis Theorem
intersects_range2 :: #force_inline proc "contextless" ( a, b: Range2 ) -> bool
{
// Check if there's no overlap on the x-axis
if a.max.x < b.min.x || b.max.x < a.min.x {
return false; // No overlap on x-axis means no intersection
}
// Check if there's no overlap on the y-axis
if a.max.y < b.min.y || b.max.y < a.min.y {
return false; // No overlap on y-axis means no intersection
}
// If neither of the above conditions are true, there's at least a partial overlap
return true;
}
// TODO(Ed): Do we need this? Also does it even work (looks unfinished)?
is_within_screenspace :: #force_inline proc "contextless" ( pos : Vec2 ) -> b32 {
state := get_state(); using state
screen_extent := state.app_window.extent
cam := & project.workspace.cam
within_x_bounds : b32 = pos.x >= -screen_extent.x && pos.x <= screen_extent.x
within_y_bounds : b32 = pos.y >= -screen_extent.y && pos.y <= screen_extent.y
return within_x_bounds && within_y_bounds
}
within_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> bool {
within_x := b.min.x >= a.min.x && b.max.x <= a.max.x
within_y := b.min.y >= a.min.y && b.max.y <= a.max.y
return within_x && within_y
}
pos_within_range2 :: #force_inline proc "contextless" ( pos : Vec2, range : Range2 ) -> b32 {
within_x := pos.x > range.min.x && pos.x < range.max.x
within_y := pos.y > range.min.y && pos.y < range.max.y
return b32(within_x && within_y)
}

62
code/sectr/colors.odin Normal file
View File

@ -0,0 +1,62 @@
package sectr
import rl "vendor:raylib"
Color :: rl.Color
Color_Blue :: rl.BLUE
// Color_Green :: rl.GREEN
Color_Red :: rl.RED
Color_White :: rl.WHITE
Color_Transparent :: Color { 0, 0, 0, 0 }
Color_BG :: Color { 55, 55, 60, 255 }
Color_BG_TextBox :: Color { 32, 32, 32, 180 }
Color_BG_Panel :: Color { 32, 32, 32, 255 }
Color_BG_Panel_Translucent :: Color { 32, 32, 32, 220 }
Color_BG_TextBox_Green :: Color { 102, 102, 110, 255 }
Color_Frame_Disabled :: Color { 22, 22, 22, 120 }
Color_Frame_Hover :: Color { 122, 122, 125, 200 }
Color_Frame_Select :: Color { 188, 188, 188, 220 }
Color_GreyRed :: Color { 220, 100, 100, 50 }
Color_White_A125 :: Color { 255, 255, 255, 165 }
Color_Black :: Color { 0, 0, 0, 255 }
Color_Green :: Color { 0, 180, 0, 255 }
Color_ResizeHandle :: Color { 80, 80, 90, 180 }
Color_3D_BG :: Color { 188, 182 , 170, 255 }
Color_Debug_UI_Padding_Bounds :: Color { 40, 195, 170, 160 }
Color_Debug_UI_Content_Bounds :: Color { 170, 120, 240, 160 }
// TODO(Ed): The entire rendering pass should be post-processed by a tone curve configurable for the user
// This is how you properly support any tonality of light or dark themes and not have it be base don the monitors raw output.
// Dark Theme
// Brightest value limited to (text is the only exception):
Color_ThmDark_BrightLimit :: Color {230, 230, 230, 255}
// Darkness value limited to (text is the only exception):
Color_ThmDark_DarkLimit :: Color {10, 10, 10, 255}
Color_ThmDark_BG :: Color {33, 33, 33, 255}
Color_ThmDark_Translucent_Panel :: Color { 0, 0, 0, 60}
Color_ThmDark_ResizeHandle_Default :: Color_Transparent
Color_ThmDark_ResizeHandle_Hot :: Color { 72, 72, 72, 90}
Color_ThmDark_ResizeHandle_Active :: Color { 88, 88, 88, 90}
Color_ThmDark_Border_Default :: Color { 64, 64, 64, 255}
Color_ThmDark_Btn_BG_Default :: Color { 40, 40, 40, 255}
Color_ThmDark_Btn_BG_Hot :: Color { 60, 60, 70, 255}
Color_ThmDark_Btn_BG_Active :: Color { 90, 100, 130, 255}
Color_ThmDark_Text_Default :: Color {120, 117, 115, 255}
Color_ThmDark_Text_Hot :: Color {180, 180, 180, 255}
Color_ThmDark_Text_Active :: Color {240, 240, 240, 255}
// Light Theme
// LightTheme_BG :: Color { 120, 120, 120, 255 }

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
}

View File

@ -0,0 +1,193 @@
package sectr
import "core:fmt"
import "core:math"
import "core:mem"
import "core:path/filepath"
import "core:os"
import rl "vendor:raylib"
Font_Largest_Px_Size :: 32
Font_Size_Interval :: 2
// Font_Default :: ""
Font_Default :: FontID { 0, "" }
Font_Default_Point_Size :: 18.0
Font_TTF_Default_Chars_Padding :: 4
Font_Load_Use_Default_Size :: -1
Font_Load_Gen_ID :: ""
Font_Atlas_Packing_Method :: enum u32 {
Raylib_Basic = 0, // Basic packing algo
Skyeline_Rect = 1, // stb_pack_rect
}
FontID :: struct {
key : u64,
label : string,
}
FontTag :: struct {
key : FontID,
point_size : f32
}
FontGlyphsRender :: struct {
size : i32,
count : i32,
padding : i32,
texture : rl.Texture2D,
recs : [^]rl.Rectangle, // Characters rectangles in texture
glyphs : [^]rl.GlyphInfo, // Characters info data
}
FontDef :: struct {
path_file : string,
// TODO(Ed) : you may have to store font data in the future if we render on demand
// data : []u8,
default_size : i32,
size_table : [Font_Largest_Px_Size / Font_Size_Interval] FontGlyphsRender,
}
FontProviderData :: struct {
// font_cache : HMapZPL(FontDef),
font_cache : HMapChainedPtr(FontDef),
}
font_provider_startup :: proc()
{
profile(#procedure)
state := get_state()
font_provider_data := & get_state().font_provider_data; using font_provider_data
font_cache_alloc_error : AllocatorError
font_cache, font_cache_alloc_error = hmap_chained_init(FontDef, hmap_closest_prime(1 * Kilo), persistent_allocator(), dbg_name = "font_cache" )
verify( font_cache_alloc_error == AllocatorError.None, "Failed to allocate font_cache" )
log("font_cache created")
log("font_provider initialized")
}
font_provider_shutdown :: proc()
{
font_provider_data := & get_state().font_provider_data; using font_provider_data
for & entry in font_cache.lookup
{
if entry == nil do continue
def := entry.value
for & px_render in def.size_table {
using px_render
rl.UnloadFontData( glyphs, count )
rl.UnloadTexture ( texture )
rl.MemFree( recs )
}
}
}
font_load :: proc( path_file : string,
default_size : f32 = Font_Load_Use_Default_Size,
desired_id : string = Font_Load_Gen_ID
) -> FontID
{
profile(#procedure)
log( str_fmt_tmp("Loading font: %v", path_file))
font_provider_data := & get_state().font_provider_data; using font_provider_data
font_data, read_succeded : = os.read_entire_file( path_file, context.temp_allocator )
verify( b32(read_succeded), str_fmt_tmp("Failed to read font file for: %v", path_file) )
font_data_size := cast(i32) len(font_data)
desired_id := desired_id
// Use file name as key
if len(desired_id) == 0 {
// NOTE(Ed): This should never be used except for laziness so I'll be throwing a warning everytime.
log("desired_key not provided, using file name. Give it a proper name!", LogLevel.Warning)
// desired_id = cast(FontID) file_name_from_path(path_file)
desired_id = file_name_from_path(path_file)
}
default_size := default_size
if default_size == Font_Load_Use_Default_Size {
default_size = Font_Default_Point_Size
}
key := cast(u64) crc32( transmute([]byte) desired_id )
def, set_error := hmap_chained_set(font_cache, key, FontDef{})
verify( set_error == AllocatorError.None, "Failed to add new font entry to cache" )
def.path_file = path_file
def.default_size = i32(points_to_pixels(default_size))
// TODO(Ed): this is slow & eats quite a bit of memory early on. Setup a more on demand load for a specific size.
// Also, we need to eventually switch to a SDF shader for rendering
// Render all sizes at once
// Note(Ed) : We only generate textures for even multiples of the font.
for font_size : i32 = Font_Size_Interval; font_size <= Font_Largest_Px_Size; font_size += Font_Size_Interval
{
profile("font size render")
id := (font_size / Font_Size_Interval) + (font_size % Font_Size_Interval)
px_render := & def.size_table[id - 1]
using px_render
size = font_size
count = 95 // This is the default codepoint count from raylib when loading a font.
padding = Font_TTF_Default_Chars_Padding
glyphs = rl.LoadFontData( raw_data(font_data), font_data_size,
fontSize = size,
codepoints = nil,
codepointCount = count,
type = rl.FontType.DEFAULT )
verify( glyphs != nil, str_fmt_tmp("Failed to load glyphs for font: %v at desired size: %v", desired_id, size ) )
atlas := rl.GenImageFontAtlas( glyphs, & recs, count, size, padding, i32(Font_Atlas_Packing_Method.Raylib_Basic) )
texture = rl.LoadTextureFromImage( atlas )
// glyphs_slice := slice_ptr( glyphs, count )
// for glyph in glyphs_slice {
// TODO(Ed) : See if above can properly reference
// NOTE(raylib): Update glyphs[i].image to use alpha, required to be used on image_draw_text()
for glyph_id : i32 = 0; glyph_id < count; glyph_id += 1 {
glyph := & glyphs[glyph_id]
rl.UnloadImage( glyph.image )
glyph.image = rl.ImageFromImage( atlas, recs[glyph_id] )
}
rl.UnloadImage( atlas )
}
free_all( context.temp_allocator )
return { key, desired_id }
}
Font_Use_Default_Size :: f32(0.0)
to_rl_Font :: proc( id : FontID, size := Font_Use_Default_Size ) -> rl.Font
{
font_provider_data := & get_state().font_provider_data; using font_provider_data
even_size := math.round(size * (1.0/f32(Font_Size_Interval))) * f32(Font_Size_Interval)
size := clamp( i32( even_size), 4, Font_Largest_Px_Size )
def := hmap_chained_get( font_cache, id.key )
size = size if size != i32(Font_Use_Default_Size) else def.default_size
id := (size / Font_Size_Interval) + (size % Font_Size_Interval)
px_render := & def.size_table[ id - 1 ]
rl_font : rl.Font
rl_font.baseSize = px_render.size
rl_font.glyphCount = px_render.count
rl_font.glyphPadding = px_render.padding
rl_font.glyphs = px_render.glyphs
rl_font.recs = px_render.recs
rl_font.texture = px_render.texture
return rl_font
}

View File

@ -0,0 +1,62 @@
/*
The default arena allocator Odin provides does fragmented resizes even for the last most allocated block getting resized.
This is an alternative to alleviates that.
TODO(Ed): Implement?
*/
package sectr
import "core:mem"
// Initialize a sub-section of our virtual memory as a sub-arena
sub_arena_init :: proc( address : ^byte, size : int ) -> ( ^ Arena) {
Arena :: mem.Arena
arena_size :: size_of( Arena)
sub_arena := cast( ^ Arena ) address
mem_slice := slice_ptr( ptr_offset( address, arena_size), size )
arena_init( sub_arena, mem_slice )
return sub_arena
}
// TODO(Ed) : Once this is done (ArenaFixed), rename to just Arena as we're not going to use the core implementation
ArenaFixedHeader :: struct {
data : []byte,
offset : uint,
peak_used : uint,
}
ArenaFixed :: struct {
using header : ^ArenaFixedHeader,
}
arena_fixed_init :: proc( backing : []byte ) -> (arena : ArenaFixed) {
header_size := size_of(ArenaFixedHeader)
verify(len(backing) >= (header_size + Kilobyte), "Attempted to init an arena with less than kilobyte of memory...")
arena.header = cast(^ArenaFixedHeader) raw_data(backing)
using arena.header
data_ptr := cast([^]byte) (cast( [^]ArenaFixedHeader) arena.header)[ 1:]
data = slice_ptr( data_ptr, len(backing) - header_size )
offset = 0
peak_used = 0
return
}
arena_fixed_allocator_proc :: proc(
allocator_data : rawptr,
mode : AllocatorMode,
size : int,
alignment : int,
old_memory : rawptr,
old_size : int,
location := #caller_location
) -> ([]byte, AllocatorError)
{
return nil, .Out_Of_Memory
}

308
code/sectr/grime/array.odin Normal file
View File

@ -0,0 +1,308 @@
// Based on gencpp's and thus zpl's Array implementation
// Made becasue of the map issue with fonts during hot-reload.
// I didn't want to make the HMapZPL impl with the [dynamic] array for now to isolate the hot-reload issue (when I was diagnoising)
package sectr
import "core:c/libc"
import "core:mem"
import "core:slice"
// Array :: struct ( $ Type : typeid ) {
// bakcing : Allocator,
// capacity : u64,
// num : u64,
// data : [^]Type,
// }
ArrayHeader :: struct ( $ Type : typeid ) {
backing : Allocator,
dbg_name : string,
fixed_cap : b32,
capacity : u64,
num : u64,
data : [^]Type,
}
Array :: struct ( $ Type : typeid ) {
using header : ^ArrayHeader(Type),
}
array_underlying_slice :: proc(slice: []($ Type)) -> Array(Type)
{
if len(slice) == 0 {
return nil
}
array_size := size_of( Array(Type))
raw_data := & slice[0]
array_ptr := cast( ^Array(Type)) ( uintptr(first_element_ptr) - uintptr(array_size))
return array_ptr ^
}
array_to_slice :: proc( using self : Array($ Type) ) -> []Type {
return slice_ptr( data, int(num) )
}
array_to_slice_capacity :: proc( using self : Array($ Type) ) -> []Type {
return slice_ptr( data, int(capacity))
}
array_grow_formula :: proc( value : u64 ) -> u64 {
result := (2 * value) + 8
return result
}
array_init :: proc( $ Type : typeid, allocator : Allocator ) -> ( Array(Type), AllocatorError ) {
return array_init_reserve( Type, allocator, array_grow_formula(0) )
}
array_init_reserve :: proc
( $ Type : typeid, allocator : Allocator, capacity : u64, fixed_cap : b32 = false, dbg_name : string = "" ) -> ( result : Array(Type), alloc_error : AllocatorError )
{
header_size := size_of(ArrayHeader(Type))
array_size := header_size + int(capacity) * size_of(Type)
raw_mem : rawptr
raw_mem, alloc_error = alloc( array_size, allocator = allocator )
// log( str_fmt_tmp("array reserved: %d", header_size + int(capacity) * size_of(Type) ))
if alloc_error != AllocatorError.None do return
result.header = cast( ^ArrayHeader(Type)) raw_mem
result.backing = allocator
// result.dbg_name = dbg_name
result.fixed_cap = fixed_cap
result.capacity = capacity
result.data = cast( [^]Type ) (cast( [^]ArrayHeader(Type)) result.header)[ 1:]
return
}
array_append :: proc( self : ^Array( $ Type), value : Type ) -> AllocatorError
{
// profile(#procedure)
if self.header.num == self.header.capacity
{
grow_result := array_grow( self, self.header.capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
self.header.data[ self.header.num ] = value
self.header.num += 1
return AllocatorError.None
}
array_append_slice :: proc( using self : ^Array( $ Type ), items : []Type ) -> AllocatorError
{
if num + len(items) > capacity
{
grow_result := array_grow( self, capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
// Note(Ed) : Original code from gencpp
// libc.memcpy( ptr_offset(data, num), raw_data(items), len(items) * size_of(Type) )
// TODO(Ed) : VERIFY VIA DEBUG THIS COPY IS FINE.
target := ptr_offset( data, num )
copy( slice_ptr(target, capacity - num), items )
num += len(items)
return AllocatorError.None
}
array_append_at :: proc( using self : ^Array( $ Type ), item : Type, id : u64 ) -> AllocatorError
{
id := id
if id >= num {
id = num - 1
}
if id < 0 {
id = 0
}
if capacity < num + 1
{
grow_result := array_grow( self, capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
target := & data[id]
libc.memmove( ptr_offset(target, 1), target, uint(num - id) * size_of(Type) )
data[id] = item
num += 1
return AllocatorError.None
}
array_append_at_slice :: proc( using self : ^Array( $ Type ), items : []Type, id : u64 ) -> AllocatorError
{
id := id
if id >= num {
return array_append_slice( items )
}
if len(items) > capacity
{
grow_result := array_grow( self, capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
// Note(Ed) : Original code from gencpp
// target := ptr_offset( data, id + len(items) )
// src := ptr_offset( data, id )
// libc.memmove( target, src, num - id * size_of(Type) )
// libc.memcpy ( src, raw_data(items), len(items) * size_of(Type) )
// TODO(Ed) : VERIFY VIA DEBUG THIS COPY IS FINE
target := & data[id + len(items)]
dst := slice_ptr( target, num - id - len(items) )
src := slice_ptr( & data[id], num - id )
copy( dst, src )
copy( src, items )
num += len(items)
return AllocatorError.None
}
// array_back :: proc( )
array_push_back :: proc( using self : Array( $ Type)) -> b32 {
if num == capacity {
return false
}
data[ num ] = value
num += 1
return true
}
array_clear :: proc "contextless" ( using self : Array( $ Type ), zero_data : b32 = false ) {
if zero_data {
mem.set( data, 0, int(num * size_of(Type)) )
}
header.num = 0
}
array_fill :: proc( using self : Array( $ Type ), begin, end : u64, value : Type ) -> b32
{
if begin < 0 || end >= num {
return false
}
// data_slice := slice_ptr( ptr_offset( data, begin ), end - begin )
// slice.fill( data_slice, cast(int) value )
for id := begin; id < end; id += 1 {
data[ id ] = value
}
return true
}
array_free :: proc( using self : Array( $ Type ) ) {
free( self.header, backing )
self.data = nil
}
array_grow :: proc( using self : ^Array( $ Type ), min_capacity : u64 ) -> AllocatorError
{
// profile(#procedure)
new_capacity := array_grow_formula( capacity )
if new_capacity < min_capacity {
new_capacity = min_capacity
}
return array_set_capacity( self, new_capacity )
}
array_pop :: proc( using self : Array( $ Type ) ) {
verify( num != 0, "Attempted to pop an array with no elements" )
num -= 1
}
array_remove_at :: proc( using self : Array( $ Type ), id : u64 )
{
verify( id < header.num, "Attempted to remove from an index larger than the array" )
left := & data[id]
right := & data[id + 1]
libc.memmove( left, right, uint(num - id) * size_of(Type) )
header.num -= 1
}
array_reserve :: proc( using self : ^Array( $ Type ), new_capacity : u64 ) -> AllocatorError
{
if capacity < new_capacity {
return array_set_capacity( self, new_capacity )
}
return AllocatorError.None
}
array_resize :: proc( array : ^Array( $ Type ), num : u64 ) -> AllocatorError
{
if array.capacity < num
{
grow_result := array_grow( array, array.capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
array.num = num
return AllocatorError.None
}
array_set_capacity :: proc( self : ^Array( $ Type ), new_capacity : u64 ) -> AllocatorError
{
if new_capacity == self.capacity {
return AllocatorError.None
}
if new_capacity < self.num {
self.num = new_capacity
return AllocatorError.None
}
header_size :: size_of(ArrayHeader(Type))
new_size := header_size + (cast(int) new_capacity ) * size_of(Type)
old_size := header_size + (cast(int) self.capacity) * size_of(Type)
new_mem, result_code := resize_non_zeroed( self.header, old_size, new_size, mem.DEFAULT_ALIGNMENT, allocator = self.backing )
if result_code != AllocatorError.None {
ensure( false, "Failed to allocate for new array capacity" )
return result_code
}
if new_mem == nil {
ensure(false, "new_mem is nil but no allocation error")
return result_code
}
self.header = cast( ^ArrayHeader(Type)) raw_data(new_mem);
self.header.data = cast( [^]Type ) (cast( [^]ArrayHeader(Type)) self.header)[ 1:]
self.header.capacity = new_capacity
self.header.num = self.num
return result_code
}
array_block_size :: proc "contextless" ( self : Array( $Type ) ) -> u64 {
header_size :: size_of(ArrayHeader(Type))
block_size := cast(u64) (header_size + self.capacity * size_of(Type))
return block_size
}
array_memtracker_entry :: proc( self : Array( $Type ), name : string ) -> MemoryTrackerEntry {
header_size :: size_of(ArrayHeader(Type))
block_size := cast(uintptr) (header_size + (cast(uintptr) self.capacity) * size_of(Type))
block_start := transmute(^u8) self.header
block_end := ptr_offset( block_start, block_size )
tracker_entry := MemoryTrackerEntry { name, block_start, block_end }
return tracker_entry
}

View File

@ -0,0 +1,62 @@
package sectr
import "base:runtime"
import "core:io"
import "core:os"
import "core:text/table"
dump_stacktrace :: proc( allocator := context.temp_allocator ) -> string
{
trace_result := stacktrace()
lines, error := stacktrace_lines( trace_result )
padding := " "
log_table := table.init( & table.Table{}, context.temp_allocator, context.temp_allocator )
for line in lines {
table.row( log_table, padding, line.symbol, " - ", line.location )
}
table.build(log_table)
// writer_builder_backing : [Kilobyte * 16] u8
// writer_builder := from_bytes( writer_builder_backing[:] )
writer_builder : StringBuilder
str_builder_init( & writer_builder, allocator = allocator )
writer := to_writer( & writer_builder )
for row in 2 ..< log_table.nr_rows {
for col in 0 ..< log_table.nr_cols {
table.write_table_cell( writer, log_table, row, col )
}
io.write_byte( writer, '\n' )
}
return to_string( writer_builder )
}
ensure :: proc( condition : b32, msg : string, location := #caller_location )
{
if condition {
return
}
log( msg, LogLevel.Warning, location )
runtime.debug_trap()
}
// TODO(Ed) : Setup exit codes!
fatal :: proc( msg : string, exit_code : int = -1, location := #caller_location )
{
log( msg, LogLevel.Fatal, location )
runtime.debug_trap()
os.exit( exit_code )
}
verify :: proc( condition : b32, msg : string, exit_code : int = -1, location := #caller_location )
{
if condition {
return
}
log( msg, LogLevel.Fatal, location )
runtime.debug_trap()
os.exit( exit_code )
}

View File

@ -0,0 +1,6 @@
package sectr
context_ext :: proc( $ Type : typeid ) -> (^Type) {
return cast(^Type) context.user_ptr
}

View File

@ -0,0 +1,67 @@
// TODO(Ed) : Move this to a grime package
package sectr
import "core:fmt"
import "core:os"
import "base:runtime"
// Test
file_copy_sync :: proc( path_src, path_dst: string, allocator := context.temp_allocator ) -> b32
{
file_size : i64
{
path_info, result := file_status( path_src, allocator )
if result != os.ERROR_NONE {
logf("Could not get file info: %v", result, LogLevel.Error )
return false
}
file_size = path_info.size
}
src_content, result := os.read_entire_file( path_src, allocator )
if ! result {
logf( "Failed to read file to copy: %v", path_src, LogLevel.Error )
runtime.debug_trap()
return false
}
result = os.write_entire_file( path_dst, src_content, false )
if ! result {
logf( "Failed to copy file: %v", path_dst, LogLevel.Error )
runtime.debug_trap()
return false
}
return true
}
file_exists :: proc( file_path : string ) -> b32 {
path_info, result := file_status( file_path, frame_allocator() )
if result != os.ERROR_NONE {
return false
}
return true;
}
file_is_locked :: proc( file_path : string ) -> b32 {
handle, err := file_open(file_path, os.O_RDONLY)
if err != os.ERROR_NONE {
// If the error indicates the file is in use, return true.
return true
}
// If the file opens successfully, close it and return false.
file_close(handle)
return false
}
file_rewind :: proc( file : os.Handle ) {
file_seek( file, 0, 0 )
}
file_read_looped :: proc( file : os.Handle, data : []byte ) {
total_read, result_code := file_read( file, data )
if result_code == os.ERROR_HANDLE_EOF {
file_rewind( file )
}
}

324
code/sectr/grime/grime.odin Normal file
View File

@ -0,0 +1,324 @@
package sectr
#region("Import Aliases")
import "base:builtin"
copy :: builtin.copy
import "base:intrinsics"
mem_zero :: intrinsics.mem_zero
ptr_sub :: intrinsics.ptr_sub
type_has_field :: intrinsics.type_has_field
type_elem_type :: intrinsics.type_elem_type
import "base:runtime"
Byte :: runtime.Byte
Kilobyte :: runtime.Kilobyte
Megabyte :: runtime.Megabyte
Gigabyte :: runtime.Gigabyte
Terabyte :: runtime.Terabyte
Petabyte :: runtime.Petabyte
Exabyte :: runtime.Exabyte
resize_non_zeroed :: runtime.non_zero_mem_resize
SourceCodeLocation :: runtime.Source_Code_Location
import c "core:c/libc"
import "core:dynlib"
import "core:hash"
crc32 :: hash.crc32
import "core:hash/xxhash"
xxh32 :: xxhash.XXH32
import fmt_io "core:fmt"
str_fmt :: fmt_io.printf
str_fmt_tmp :: fmt_io.tprintf
str_fmt_alloc :: fmt_io.aprintf
str_fmt_builder :: fmt_io.sbprintf
str_fmt_buffer :: fmt_io.bprintf
str_to_file_ln :: fmt_io.fprintln
str_tmp_from_any :: fmt_io.tprint
import "core:math"
import "core:mem"
align_forward_int :: mem.align_forward_int
align_forward_uint :: mem.align_forward_uint
align_forward_uintptr :: mem.align_forward_uintptr
Allocator :: mem.Allocator
AllocatorError :: mem.Allocator_Error
AllocatorMode :: mem.Allocator_Mode
AllocatorModeSet :: mem.Allocator_Mode_Set
alloc :: mem.alloc
alloc_bytes :: mem.alloc_bytes
alloc_bytes_non_zeroed :: mem.alloc_bytes_non_zeroed
Arena :: mem.Arena
arena_allocator :: mem.arena_allocator
arena_init :: mem.arena_init
byte_slice :: mem.byte_slice
copy_non_overlapping :: mem.copy_non_overlapping
free :: mem.free
is_power_of_two_uintptr :: mem.is_power_of_two
ptr_offset :: mem.ptr_offset
resize :: mem.resize
slice_ptr :: mem.slice_ptr
TrackingAllocator :: mem.Tracking_Allocator
tracking_allocator :: mem.tracking_allocator
tracking_allocator_init :: mem.tracking_allocator_init
import "core:mem/virtual"
VirtualProtectFlags :: virtual.Protect_Flags
// import "core:odin"
import "core:os"
FileFlag_Create :: os.O_CREATE
FileFlag_ReadWrite :: os.O_RDWR
FileTime :: os.File_Time
file_close :: os.close
file_open :: os.open
file_read :: os.read
file_remove :: os.remove
file_seek :: os.seek
file_status :: os.stat
file_write :: os.write
import "core:path/filepath"
file_name_from_path :: filepath.short_stem
import str "core:strings"
StringBuilder :: str.Builder
str_builder_from_bytes :: str.builder_from_bytes
str_builder_init :: str.builder_init
str_builder_to_writer :: str.to_writer
str_builder_to_string :: str.to_string
import "core:time"
Duration :: time.Duration
duration_seconds :: time.duration_seconds
duration_ms :: time.duration_milliseconds
thread_sleep :: time.sleep
import "core:unicode"
is_white_space :: unicode.is_white_space
import "core:unicode/utf8"
str_rune_count :: utf8.rune_count_in_string
runes_to_string :: utf8.runes_to_string
// string_to_runes :: utf8.string_to_runes
import "thirdparty:backtrace"
StackTraceData :: backtrace.Trace_Const
stacktrace :: backtrace.trace
stacktrace_lines :: backtrace.lines
#endregion("Import Aliases")
#region("Proc overload mappings")
// This has to be done on a per-module basis.
add :: proc {
add_range2,
}
bivec3 :: proc {
bivec3_via_f32s,
vec3_to_bivec3,
}
cm_to_pixels :: proc {
f32_cm_to_pixels,
vec2_cm_to_pixels,
range2_cm_to_pixels,
}
regress :: proc {
regress_bivec3,
}
cross :: proc {
cross_vec3,
}
dot :: proc {
dot_vec2,
dot_vec3,
dot_v3_unitv3,
dot_unitv3_vs,
}
ws_view_draw_text :: proc {
ws_view_draw_text_string,
ws_view_draw_text_StrRunesPair,
}
from_bytes :: proc {
str_builder_from_bytes,
}
get_bounds :: proc {
view_get_bounds,
}
inverse_mag :: proc {
inverse_mag_vec3,
// inverse_mag_rotor3,
}
is_power_of_two :: proc {
is_power_of_two_u32,
is_power_of_two_uintptr,
}
measure_text_size :: proc {
measure_text_size_raylib,
}
mov_avg_exp :: proc {
mov_avg_exp_f32,
mov_avg_exp_f64,
}
pixels_to_cm :: proc {
f32_pixels_to_cm,
vec2_pixels_to_cm,
range2_pixels_to_cm,
}
points_to_pixels :: proc {
f32_points_to_pixels,
vec2_points_to_pixels,
}
pop :: proc {
stack_pop,
stack_allocator_pop,
}
pow :: proc{
math.pow_f16,
math.pow_f16le,
math.pow_f16be,
math.pow_f32,
math.pow_f32le,
math.pow_f32be,
math.pow_f64,
math.pow_f64le,
math.pow_f64be,
}
pow2 :: proc {
pow2_vec3,
}
pressed :: proc {
btn_pressed,
}
push :: proc {
stack_push,
stack_allocator_push,
}
rotor3 :: proc {
rotor3_via_comps,
rotor3_via_bv_s,
rotor3_via_from_to,
}
released :: proc {
btn_released,
}
sqrt :: proc{
math.sqrt_f16,
math.sqrt_f16le,
math.sqrt_f16be,
math.sqrt_f32,
math.sqrt_f32le,
math.sqrt_f32be,
math.sqrt_f64,
math.sqrt_f64le,
math.sqrt_f64be,
}
inverse_sqrt :: proc {
inverse_sqrt_f32,
}
sub :: proc {
sub_point3,
sub_range2,
sub_bivec3,
}
to_quat128 :: proc {
rotor3_to_quat128,
}
to_rl_rect :: proc {
range2_to_rl_rect,
}
to_runes :: proc {
string_to_runes,
}
to_string :: proc {
runes_to_string,
str_builder_to_string,
}
vec3 :: proc {
vec3_via_f32s,
bivec3_to_vec3,
point3_to_vec3,
pointflat3_to_vec3,
unitvec3_to_vec3,
}
vec4 :: proc {
unitvec4_to_vec4,
}
to_writer :: proc {
str_builder_to_writer,
}
to_ui_layout_side :: proc {
to_ui_layout_side_f32,
to_ui_layout_side_vec2,
}
ui_compute_layout :: proc {
ui_core_compute_layout,
ui_box_compute_layout,
}
ui_floating :: proc {
ui_floating_just_builder,
ui_floating_with_capture,
}
ui_layout_push :: proc {
ui_layout_push_layout,
ui_layout_push_theme,
}
ui_layout :: proc {
ui_layout_via_layout,
ui_layout_via_combo,
}
ui_style_push :: proc {
ui_style_push_style,
ui_style_push_combo,
}
ui_style :: proc {
ui_style_via_style,
ui_style_via_combo,
}
ui_theme :: proc {
ui_theme_via_layout_style,
ui_theme_via_combos,
ui_theme_via_theme,
}
wedge :: proc {
wedge_vec3,
wedge_bivec3,
}
#endregion("Proc overload mappings")
OS_Type :: type_of(ODIN_OS)
swap :: #force_inline proc( a, b : ^ $Type ) -> ( ^ Type, ^ Type ) { return b, a }

View File

@ -0,0 +1,225 @@
/*
Separate chaining hashtable with tombstone (vacancy aware)
This is an alternative to odin's map and the zpl hashtable I first used for this codebase.
I haven't felt the need to go back to dealing with odin's map for my edge case hot reload/memory replay failure.
So this is a hahstable loosely based at what I saw in the raddbg codebase.
It uses a fixed-size lookup table for the base layer of entries that can be chained.
Each slot keeps track of its vacancy (tombstone, is occupied).
If its occupied a new slot is chained using the fixed bucket-size pool allocator which will have its blocks sized to the type of the table.
This is ideal for tables have an indeterminate scope for how entires are added,
and direct pointers are kept across the codebase instead of a key to the slot.
*/
package sectr
import "core:mem"
HTable_Minimum_Capacity :: 4 * Kilobyte
HMapChainedSlot :: struct( $Type : typeid ) {
using links : DLL_NodePN(HMapChainedSlot(Type)),
value : Type,
key : u64,
occupied : b32,
}
HMapChained :: struct( $ Type : typeid ) {
pool : Pool,
lookup : [] ^HMapChainedSlot(Type),
}
HMapChainedPtr :: struct( $ Type : typeid) {
using header : ^HMapChained(Type),
}
// Provides the nearest prime number value for the given capacity
hmap_closest_prime :: proc( capacity : uint ) -> uint
{
prime_table : []uint = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319,
201326611, 402653189, 805306457, 1610612741, 3221225473, 6442450941
};
for slot in prime_table {
if slot >= capacity {
return slot
}
}
return prime_table[len(prime_table) - 1]
}
hmap_chained_init :: proc( $Type : typeid, lookup_capacity : uint, allocator : Allocator,
pool_bucket_cap : uint = 1 * Kilo,
pool_bucket_reserve_num : uint = 0,
pool_alignment : uint = mem.DEFAULT_ALIGNMENT,
dbg_name : string = ""
) -> (table : HMapChainedPtr(Type), error : AllocatorError)
{
header_size := size_of(HMapChained(Type))
size := header_size + int(lookup_capacity) * size_of( ^HMapChainedSlot(Type)) + size_of(int)
raw_mem : rawptr
raw_mem, error = alloc( size, allocator = allocator )
if error != AllocatorError.None do return
table.header = cast( ^HMapChained(Type)) raw_mem
table.pool, error = pool_init(
should_zero_buckets = false,
block_size = size_of(HMapChainedSlot(Type)),
bucket_capacity = pool_bucket_cap,
bucket_reserve_num = pool_bucket_reserve_num,
alignment = pool_alignment,
allocator = allocator,
dbg_name = str_intern(str_fmt_tmp("%v: pool", dbg_name)).str
)
data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChained(Type)) table.header)[1:]
table.lookup = slice_ptr( data, int(lookup_capacity) )
return
}
hmap_chained_clear :: proc( using self : HMapChainedPtr($Type))
{
for slot in lookup
{
if slot == nil {
continue
}
for probe_slot = slot.next; probe_slot != nil; probe_slot = probe_slot.next {
slot.occupied = false
}
slot.occupied = false
}
}
hmap_chained_destroy :: proc( using self : ^HMapChainedPtr($Type)) {
pool_destroy( pool )
free( self.header, backing)
self = nil
}
hmap_chained_lookup_id :: #force_inline proc( using self : HMapChainedPtr($Type), key : u64 ) -> u64
{
hash_index := key % u64( len(lookup) )
return hash_index
}
hmap_chained_get :: proc( using self : HMapChainedPtr($Type), key : u64) -> ^Type
{
// profile(#procedure)
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
if surface_slot == nil {
return nil
}
if surface_slot.occupied && surface_slot.key == key {
return & surface_slot.value
}
for slot := surface_slot.next; slot != nil; slot = slot.next {
if slot.occupied && slot.key == key {
return & surface_slot.value
}
}
return nil
}
hmap_chained_reload :: proc( self : HMapChainedPtr($Type), allocator : Allocator )
{
pool_reload(self.pool, allocator)
}
// Returns true if an slot was actually found and marked as vacant
// Entries already found to be vacant will not return true
hmap_chained_remove :: proc( self : HMapChainedPtr($Type), key : u64 ) -> b32
{
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
if surface_slot == nil {
return false
}
if surface_slot.occupied && surface_slot.key == key {
surface_slot.occupied = false
return true
}
for slot := surface_slot.next; slot != nil; slot.next
{
if slot.occupied && slot.key == key {
slot.occupied = false
return true
}
}
return false
}
// Sets the value to a vacant slot
// Will preemptively allocate the next slot in the hashtable if its null for the slot.
hmap_chained_set :: proc( using self : HMapChainedPtr($Type), key : u64, value : Type ) -> (^ Type, AllocatorError)
{
// profile(#procedure)
hash_index := hmap_chained_lookup_id(self, key)
surface_slot := lookup[hash_index]
set_slot :: #force_inline proc( using self : HMapChainedPtr(Type),
slot : ^HMapChainedSlot(Type),
key : u64,
value : Type
) -> (^ Type, AllocatorError )
{
error := AllocatorError.None
if slot.next == nil {
block : []byte
block, error = pool_grab(pool)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
slot.next = next
next.prev = slot
}
slot.key = key
slot.value = value
slot.occupied = true
return & slot.value, error
}
if surface_slot == nil {
block, error := pool_grab(pool)
surface_slot := transmute( ^HMapChainedSlot(Type)) & block[0]
surface_slot.key = key
surface_slot.value = value
surface_slot.occupied = true
if error != AllocatorError.None {
ensure(error != AllocatorError.None, "Allocation failure for chained slot in hash table")
return nil, error
}
lookup[hash_index] = surface_slot
block, error = pool_grab(pool)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
surface_slot.next = next
next.prev = surface_slot
return & surface_slot.value, error
}
if ! surface_slot.occupied
{
result, error := set_slot( self, surface_slot, key, value)
return result, error
}
slot := surface_slot.next
for ; slot != nil; slot = slot.next
{
if !slot.occupied
{
result, error := set_slot( self, surface_slot, key, value)
return result, error
}
}
ensure(false, "Somehow got to a null slot that wasn't preemptively allocated from a previus set")
return nil, AllocatorError.None
}

View File

@ -0,0 +1,274 @@
/*
This is an alternative to Odin's default map type.
The only reason I may need this is due to issues with allocator callbacks or something else going on
with hot-reloads...
This implementation uses two ZPL-Based Arrays to hold entires and the actual hash table.
Instead of using separate chains, it maintains linked entries within the array.
Each entry contains a next field, which is an index pointing to the next entry in the same array.
Growing this hashtable is destructive, so it should usually be kept to a fixed-size unless
the populating operations only occur in one place and from then on its read-only.
*/
package sectr
import "core:slice"
// Note(Ed) : See core:hash for hasing procs.
HMapZPL_MapProc :: #type proc( $ Type : typeid, key : u64, value : Type )
HMapZPL_MapMutProc :: #type proc( $ Type : typeid, key : u64, value : ^ Type )
HMapZPL_CritialLoadScale :: 0.70
HMapZPL_HashToEntryRatio :: 1.50
HMapZPL_FindResult :: struct {
hash_index : i64,
prev_index : i64,
entry_index : i64,
}
HMapZPL_Entry :: struct ( $ Type : typeid) {
key : u64,
next : i64,
value : Type,
}
HMapZPL :: struct ( $ Type : typeid ) {
table : Array( i64 ),
entries : Array( HMapZPL_Entry(Type) ),
}
zpl_hmap_init :: proc( $ Type : typeid, allocator : Allocator ) -> ( HMapZPL( Type), AllocatorError ) {
return zpl_hmap_init_reserve( Type, allocator )
}
zpl_hmap_init_reserve :: proc
( $ Type : typeid, allocator : Allocator, num : u64, dbg_name : string = "" ) -> ( HMapZPL( Type), AllocatorError )
{
result : HMapZPL(Type)
table_result, entries_result : AllocatorError
result.table, table_result = array_init_reserve( i64, allocator, num, dbg_name = dbg_name )
if table_result != AllocatorError.None {
ensure( false, "Failed to allocate table array" )
return result, table_result
}
array_resize( & result.table, num )
slice.fill( slice_ptr( result.table.data, cast(int) result.table.num), -1 )
result.entries, entries_result = array_init_reserve( HMapZPL_Entry(Type), allocator, num, dbg_name = dbg_name )
if entries_result != AllocatorError.None {
ensure( false, "Failed to allocate entries array" )
return result, entries_result
}
return result, AllocatorError.None
}
zpl_hmap_clear :: proc( using self : ^ HMapZPL( $ Type ) ) {
for id := 0; id < table.num; id += 1 {
table[id] = -1
}
array_clear( table )
array_clear( entries )
}
zpl_hmap_destroy :: proc( using self : ^ HMapZPL( $ Type ) ) {
if table.data != nil && table.capacity > 0 {
array_free( table )
array_free( entries )
}
}
zpl_hmap_get :: proc ( using self : ^ HMapZPL( $ Type ), key : u64 ) -> ^ Type
{
// profile(#procedure)
id := zpl_hmap_find( self, key ).entry_index
if id >= 0 {
return & entries.data[id].value
}
return nil
}
zpl_hmap_map :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapProc ) {
ensure( map_proc != nil, "Mapping procedure must not be null" )
for id := 0; id < entries.num; id += 1 {
map_proc( Type, entries[id].key, entries[id].value )
}
}
zpl_hmap_map_mut :: proc( using self : ^ HMapZPL( $ Type), map_proc : HMapZPL_MapMutProc ) {
ensure( map_proc != nil, "Mapping procedure must not be null" )
for id := 0; id < entries.num; id += 1 {
map_proc( Type, entries[id].key, & entries[id].value )
}
}
zpl_hmap_grow :: proc( using self : ^ HMapZPL( $ Type ) ) -> AllocatorError {
new_num := array_grow_formula( entries.num )
return zpl_hmap_rehash( self, new_num )
}
zpl_hmap_rehash :: proc( ht : ^ HMapZPL( $ Type ), new_num : u64 ) -> AllocatorError
{
profile(#procedure)
// For now the prototype should never allow this to happen.
ensure( false, "ZPL HMAP IS REHASHING" )
last_added_index : i64
new_ht, init_result := zpl_hmap_init_reserve( Type, ht.table.backing, new_num, ht.table.dbg_name )
if init_result != AllocatorError.None {
ensure( false, "New zpl_hmap failed to allocate" )
return init_result
}
for id : u64 = 0; id < ht.entries.num; id += 1 {
find_result : HMapZPL_FindResult
entry := & ht.entries.data[id]
find_result = zpl_hmap_find( & new_ht, entry.key )
last_added_index = zpl_hmap_add_entry( & new_ht, entry.key )
if find_result.prev_index < 0 {
new_ht.table.data[ find_result.hash_index ] = last_added_index
}
else {
new_ht.entries.data[ find_result.prev_index ].next = last_added_index
}
new_ht.entries.data[ last_added_index ].next = find_result.entry_index
new_ht.entries.data[ last_added_index ].value = entry.value
}
zpl_hmap_destroy( ht )
(ht ^) = new_ht
return AllocatorError.None
}
zpl_hmap_rehash_fast :: proc( using self : ^ HMapZPL( $ Type ) )
{
for id := 0; id < entries.num; id += 1 {
entries[id].Next = -1;
}
for id := 0; id < table.num; id += 1 {
table[id] = -1
}
for id := 0; id < entries.num; id += 1 {
entry := & entries[id]
find_result := zpl_hmap_find( entry.key )
if find_result.prev_index < 0 {
table[ find_result.hash_index ] = id
}
else {
entries[ find_result.prev_index ].next = id
}
}
}
// Used when the address space of the allocator changes and the backing reference must be updated
zpl_hmap_reload :: proc( using self : ^HMapZPL($Type), new_backing : Allocator ) {
table.backing = new_backing
entries.backing = new_backing
}
zpl_hmap_remove :: proc( self : ^ HMapZPL( $ Type ), key : u64 ) {
find_result := zpl_hmap_find( key )
if find_result.entry_index >= 0 {
array_remove_at( & entries, find_result.entry_index )
zpl_hmap_rehash_fast( self )
}
}
zpl_hmap_remove_entry :: proc( using self : ^ HMapZPL( $ Type ), id : i64 ) {
array_remove_at( & entries, id )
}
zpl_hmap_set :: proc( using self : ^ HMapZPL( $ Type), key : u64, value : Type ) -> (^ Type, AllocatorError)
{
// profile(#procedure)
id : i64 = 0
find_result : HMapZPL_FindResult
if zpl_hmap_full( self )
{
grow_result := zpl_hmap_grow( self )
if grow_result != AllocatorError.None {
return nil, grow_result
}
}
find_result = zpl_hmap_find( self, key )
if find_result.entry_index >= 0 {
id = find_result.entry_index
}
else
{
id = zpl_hmap_add_entry( self, key )
if find_result.prev_index >= 0 {
entries.data[ find_result.prev_index ].next = id
}
else {
table.data[ find_result.hash_index ] = id
}
}
entries.data[id].value = value
if zpl_hmap_full( self ) {
alloc_error := zpl_hmap_grow( self )
return & entries.data[id].value, alloc_error
}
return & entries.data[id].value, AllocatorError.None
}
zpl_hmap_slot :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 {
for id : i64 = 0; id < table.num; id += 1 {
if table.data[id] == key {
return id
}
}
return -1
}
zpl_hmap_add_entry :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> i64 {
entry : HMapZPL_Entry(Type) = { key, -1, {} }
id := cast(i64) entries.num
array_append( & entries, entry )
return id
}
zpl_hmap_find :: proc( using self : ^ HMapZPL( $ Type), key : u64 ) -> HMapZPL_FindResult
{
// profile(#procedure)
result : HMapZPL_FindResult = { -1, -1, -1 }
if table.num > 0 {
result.hash_index = cast(i64)( key % table.num )
result.entry_index = table.data[ result.hash_index ]
verify( result.entry_index < i64(entries.num), "Entry index is larger than the number of entries" )
for ; result.entry_index >= 0; {
entry := & entries.data[ result.entry_index ]
if entry.key == key {
break
}
result.prev_index = result.entry_index
result.entry_index = entry.next
}
}
return result
}
zpl_hmap_full :: proc( using self : ^ HMapZPL( $ Type) ) -> b32 {
critical_load := u64(HMapZPL_CritialLoadScale * cast(f64) table.num)
result : b32 = entries.num > critical_load
return result
}

View File

@ -0,0 +1,190 @@
package sectr
LL_Node :: struct ( $ Type : typeid ) {
next : ^Type,
}
// ll_push :: proc( list_ptr : ^(^ ($ Type)), node : ^Type ) {
ll_push :: #force_inline proc "contextless" ( list_ptr : ^(^ ($ Type)), node : ^Type ) {
list : ^Type = (list_ptr^)
node.next = list
(list_ptr^) = node
}
ll_pop :: #force_inline proc "contextless" ( list_ptr : ^(^ ($ Type)) ) -> ( node : ^Type ) {
list : ^Type = (list_ptr^)
(list_ptr^) = list.next
return list
}
//region Intrusive Doubly-Linked-List
DLL_Node :: struct ( $ Type : typeid ) #raw_union {
using _ : struct {
left, right : ^Type,
},
using _ : struct {
prev, next : ^Type,
},
using _ : struct {
first, last : ^Type,
},
using _ : struct {
bottom, top : ^Type,
}
}
DLL_NodeFull :: struct ( $ Type : typeid ) {
// using _ : DLL_NodeFL(Type),
first, last : ^Type,
prev, next : ^Type,
}
DLL_NodePN :: struct ( $ Type : typeid ) {
// using _ : struct {
prev, next : ^Type,
// },
// using _ : struct {
// left, right : ^Type,
// },
}
DLL_NodeFL :: struct ( $ Type : typeid ) {
// using _ : struct {
first, last : ^Type,
// },
// TODO(Ed): Review this
// using _ : struct {
// bottom, top: ^Type,
// },
}
type_is_node :: #force_inline proc "contextless" ( $ Type : typeid ) -> bool
{
// elem_type := type_elem_type(Type)
return type_has_field( type_elem_type(Type), "prev" ) && type_has_field( type_elem_type(Type), "next" )
}
// First/Last append
dll_fl_append :: proc ( list : ^( $TypeList), node : ^( $TypeNode) )
{
if list.first == nil {
list.first = node
list.last = node
}
else {
list.last = node
}
}
dll_push_back :: proc "contextless" ( current_ptr : ^(^ ($ TypeCurr)), node : ^$TypeNode )
{
current : ^TypeCurr = (current_ptr ^)
if current == nil
{
(current_ptr ^) = node
node.prev = nil
}
else
{
node.prev = current
(current_ptr^) = node
current.next = node
}
node.next = nil
}
dll_pn_pop :: proc "contextless" ( node : ^$Type )
{
if node == nil {
return
}
if node.prev != nil {
node.prev.next = nil
node.prev = nil
}
if node.next != nil {
node.next.prev = nil
node.next = nil
}
}
dll_pop_back :: #force_inline proc "contextless" ( current_ptr : ^(^ ($ Type)) )
{
to_remove : ^Type = (current_ptr ^)
if to_remove == nil {
return
}
if to_remove.prev == nil {
(current_ptr ^) = nil
}
else {
(current_ptr ^) = to_remove.prev
(current_ptr ^).next = nil
}
}
dll_full_insert_raw :: proc "contextless" ( null : ^($ Type), parent : ^$ParentType, pos, node : ^Type )
{
if parent.first == null {
parent.first = node
parent.last = node
node.next = null
node.prev = null
}
else if pos == null {
// Position is not set, insert at beginning
node.next = parent.first
parent.first.prev = node
parent.first = node
node.prev = null
}
else if pos == parent.last {
// Positin is set to last, insert at end
parent.last.next = node
node.prev = parent.last
parent.last = node
node.next = null
}
else
{
if pos.next != null {
pos.next.prev = node
}
node.next = pos.next
pos.next = node
node.prev = pos
}
}
dll_full_pop :: proc "contextless" ( node : ^$NodeType, parent : ^$ParentType ) {
if node == nil {
return
}
if parent.first == node {
parent.first = node.next
}
if parent.last == node {
parent.last = node.prev
}
prev := node.prev
next := node.next
if prev != nil {
prev.next = next
node.prev = nil
}
if next != nil {
next.prev = prev
node.next = nil
}
}
dll_full_push_back :: proc "contextless" ( parent : ^$ParentType, node : ^$Type, null : ^Type ) {
dll_full_insert_raw( null, parent, parent.last, node )
}
//endregion Intrusive Doubly-Linked-List

View File

@ -0,0 +1,91 @@
// TODO(Ed) : Move this to a grime package problably
package sectr
import "core:fmt"
import "core:mem"
import "core:mem/virtual"
import "base:runtime"
import "core:os"
kilobytes :: #force_inline proc "contextless" ( kb : $ integer_type ) -> integer_type {
return kb * Kilobyte
}
megabytes :: #force_inline proc "contextless" ( mb : $ integer_type ) -> integer_type {
return mb * Megabyte
}
gigabytes :: #force_inline proc "contextless" ( gb : $ integer_type ) -> integer_type {
return gb * Gigabyte
}
terabytes :: #force_inline proc "contextless" ( tb : $ integer_type ) -> integer_type {
return tb * Terabyte
}
//region Memory Math
// See: core/mem.odin, I wanted to study it an didn't like the naming.
@(require_results)
calc_padding_with_header :: proc "contextless" (pointer: uintptr, alignment: uintptr, header_size: int) -> int
{
alignment_offset := pointer & (alignment - 1)
initial_padding := uintptr(0)
if alignment_offset != 0 {
initial_padding = alignment - alignment_offset
}
header_space_adjustment := uintptr(header_size)
if initial_padding < header_space_adjustment
{
additional_space_needed := header_space_adjustment - initial_padding
unaligned_extra_space := additional_space_needed & (alignment - 1)
if unaligned_extra_space > 0 {
initial_padding += alignment * (1 + (additional_space_needed / alignment))
}
else {
initial_padding += alignment * (additional_space_needed / alignment)
}
}
return int(initial_padding)
}
// Helper to get the the beginning of memory after a slice
memory_after :: #force_inline proc "contextless" ( slice : []byte ) -> ( ^ byte) {
return ptr_offset( & slice[0], len(slice) )
}
memory_after_header :: #force_inline proc "contextless" ( header : ^($ Type) ) -> ( [^]byte) {
result := cast( [^]byte) ptr_offset( header, 1 )
// result := cast( [^]byte) (cast( [^]Type) header)[ 1:]
return result
}
@(require_results)
memory_align_formula :: #force_inline proc "contextless" ( size, align : uint) -> uint {
result := size + align - 1
return result - result % align
}
// This is here just for docs
memory_misalignment :: #force_inline proc ( address, alignment : uintptr) -> uint {
// address % alignment
assert(is_power_of_two(alignment))
return uint( address & (alignment - 1) )
}
// This is here just for docs
@(require_results)
memory_aign_forward :: #force_inline proc( address, alignment : uintptr) -> uintptr
{
assert(is_power_of_two(alignment))
aligned_address := address
misalignment := cast(uintptr) memory_misalignment( address, alignment )
if misalignment != 0 {
aligned_address += alignment - misalignment
}
return aligned_address
}
//endregion Memory Math

View File

@ -0,0 +1,172 @@
/*
This was a tracking allocator made to kill off various bugs left with grime's pool & slab allocators
It doesn't perform that well on a per-frame basis and should be avoided for general memory debugging
It only makes sure that memory allocations don't collide in the allocator and deallocations don't occur for memory never allocated.
I'm keeping it around as an artifact & for future allocators I may make.
*/
package sectr
MemoryTrackerEntry :: struct {
start, end : rawptr,
}
MemoryTracker :: struct {
name : string,
entries : Array(MemoryTrackerEntry),
}
Track_Memory :: false
tracker_msg_buffer : [Kilobyte * 16]u8
memtracker_clear :: proc ( tracker : MemoryTracker ) {
when ! Track_Memory {
return
}
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
logf("Clearing tracker: %v", tracker.name)
memtracker_dump_entries(tracker);
array_clear(tracker.entries)
}
memtracker_init :: proc ( tracker : ^MemoryTracker, allocator : Allocator, num_entries : u64, name : string )
{
when ! Track_Memory {
return
}
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
tracker.name = name
error : AllocatorError
tracker.entries, error = array_init_reserve( MemoryTrackerEntry, allocator, num_entries, dbg_name = name )
if error != AllocatorError.None {
fatal("Failed to allocate memory tracker's hashmap");
}
}
memtracker_register :: proc( tracker : ^MemoryTracker, new_entry : MemoryTrackerEntry )
{
when ! Track_Memory {
return
}
profile(#procedure)
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
if tracker.entries.num == tracker.entries.capacity {
ensure(false, "Memory tracker entries array full, can no longer register any more allocations")
return
}
for idx in 0..< tracker.entries.num
{
entry := & tracker.entries.data[idx]
if new_entry.start > entry.start {
continue
}
if (entry.end < new_entry.start)
{
msg := str_fmt_tmp("Memory tracker(%v) detected a collision:\nold_entry: %v\nnew_entry: %v", tracker.name, entry, new_entry)
ensure( false, msg )
memtracker_dump_entries(tracker ^)
}
array_append_at( & tracker.entries, new_entry, idx )
log(str_fmt_tmp("%v : Registered: %v", tracker.name, new_entry) )
return
}
array_append( & tracker.entries, new_entry )
log(str_fmt_tmp("%v : Registered: %v", tracker.name, new_entry) )
}
memtracker_register_auto_name :: proc( tracker : ^MemoryTracker, start, end : rawptr )
{
when ! Track_Memory {
return
}
memtracker_register( tracker, {start, end})
}
memtracker_register_auto_name_slice :: proc( tracker : ^MemoryTracker, slice : []byte )
{
when ! Track_Memory {
return
}
start := raw_data(slice)
end := & slice[ len(slice) - 1 ]
memtracker_register( tracker, {start, end})
}
memtracker_unregister :: proc( tracker : MemoryTracker, to_remove : MemoryTrackerEntry )
{
when ! Track_Memory {
return
}
profile(#procedure)
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
entries := array_to_slice(tracker.entries)
for idx in 0..< tracker.entries.num
{
entry := & entries[idx]
if entry.start == to_remove.start {
if (entry.end == to_remove.end || to_remove.end == nil) {
log(str_fmt_tmp("%v: Unregistered: %v", tracker.name, to_remove));
array_remove_at(tracker.entries, idx)
return
}
ensure(false, str_fmt_tmp("%v: Found an entry with the same start address but end address was different:\nentry : %v\nto_remove: %v", tracker.name, entry, to_remove))
memtracker_dump_entries(tracker)
}
}
ensure(false, str_fmt_tmp("%v: Attempted to unregister an entry that was not tracked: %v", tracker.name, to_remove))
memtracker_dump_entries(tracker)
}
memtracker_check_for_collisions :: proc ( tracker : MemoryTracker )
{
when ! Track_Memory {
return
}
profile(#procedure)
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
entries := array_to_slice(tracker.entries)
for idx in 1 ..< tracker.entries.num {
// Check to make sure each allocations adjacent entries do not intersect
left := & entries[idx - 1]
right := & entries[idx]
collided := left.start > right.start || left.end > right.end
if collided {
msg := str_fmt_tmp("%v: Memory tracker detected a collision:\nleft: %v\nright: %v", tracker.name, left, right)
memtracker_dump_entries(tracker)
}
}
}
memtracker_dump_entries :: proc( tracker : MemoryTracker )
{
when ! Track_Memory {
return
}
temp_arena : Arena; arena_init(& temp_arena, tracker_msg_buffer[:])
context.temp_allocator = arena_allocator(& temp_arena)
log( "Dumping Memory Tracker:")
for idx in 0 ..< tracker.entries.num {
entry := & tracker.entries.data[idx]
log( str_fmt_tmp("%v", entry) )
}
}

View File

@ -0,0 +1,361 @@
/*
This is a pool allocator setup to grow incrementally via buckets.
Buckets are stored in singly-linked lists so that allocations aren't necessrily contiguous.
The pool is setup with the intention to only grab single entires from the bucket,
not for a contiguous array of them.
Thus the free-list only tracks the last free entries thrown out by the user,
irrespective of the bucket the originated from.
This means if there is a heavy recyling of entires in a pool
there can be a large discrepancy of memory localicty if buckets are small.
The pool doesn't allocate any buckets on initialization unless the user specifies.
*/
package sectr
import "base:intrinsics"
import "base:runtime"
import "core:mem"
import "core:slice"
Pool :: struct {
using header : ^PoolHeader,
}
PoolHeader :: struct {
backing : Allocator,
dbg_name : string,
tracker : MemoryTracker,
zero_bucket : b32,
block_size : uint,
bucket_capacity : uint,
alignment : uint,
free_list_head : ^Pool_FreeBlock,
current_bucket : ^PoolBucket,
bucket_list : DLL_NodeFL( PoolBucket),
}
PoolBucket :: struct {
using nodes : DLL_NodePN( PoolBucket),
next_block : uint,
blocks : [^]byte,
}
Pool_FreeBlock :: struct {
next : ^Pool_FreeBlock,
}
Pool_Check_Release_Object_Validity :: true
pool_init :: proc (
should_zero_buckets : b32,
block_size : uint,
bucket_capacity : uint,
bucket_reserve_num : uint = 0,
alignment : uint = mem.DEFAULT_ALIGNMENT,
allocator : Allocator = context.allocator,
dbg_name : string,
) -> ( pool : Pool, alloc_error : AllocatorError )
{
header_size := align_forward_int( size_of(PoolHeader), int(alignment) )
raw_mem : rawptr
raw_mem, alloc_error = alloc( header_size, int(alignment), allocator )
if alloc_error != .None do return
ensure(block_size > 0, "Bad block size provided")
ensure(bucket_capacity > 0, "Bad bucket capacity provided")
pool.header = cast( ^PoolHeader) raw_mem
pool.zero_bucket = should_zero_buckets
pool.backing = allocator
pool.dbg_name = dbg_name
pool.block_size = align_forward_uint(block_size, alignment)
pool.bucket_capacity = bucket_capacity
pool.alignment = alignment
when ODIN_DEBUG {
memtracker_init( & pool.tracker, allocator, Kilobyte * 96, dbg_name )
}
if bucket_reserve_num > 0 {
alloc_error = pool_allocate_buckets( pool, bucket_reserve_num )
}
pool.current_bucket = pool.bucket_list.first
return
}
pool_reload :: proc( pool : Pool, allocator : Allocator ) {
pool.backing = allocator
}
pool_destroy :: proc ( using self : Pool )
{
if bucket_list.first != nil
{
bucket := bucket_list.first
for ; bucket != nil; bucket = bucket.next {
free( bucket, backing )
}
}
free( self.header, backing )
when ODIN_DEBUG {
memtracker_clear( self.tracker )
}
}
pool_allocate_buckets :: proc( pool : Pool, num_buckets : uint ) -> AllocatorError
{
profile(#procedure)
if num_buckets == 0 {
return .Invalid_Argument
}
header_size := cast(uint) align_forward_int( size_of(PoolBucket), int(pool.alignment))
bucket_size := header_size + pool.bucket_capacity
to_allocate := cast(int) (bucket_size * num_buckets)
// log(str_fmt_tmp("Allocating %d bytes for %d buckets with header_size %d bytes & bucket_size %d", to_allocate, num_buckets, header_size, bucket_size ))
bucket_memory : []byte
alloc_error : AllocatorError
pool_validate( pool )
if pool.zero_bucket {
bucket_memory, alloc_error = alloc_bytes( to_allocate, int(pool.alignment), pool.backing )
}
else {
bucket_memory, alloc_error = alloc_bytes_non_zeroed( to_allocate, int(pool.alignment), pool.backing )
}
pool_validate( pool )
// log(str_fmt_tmp("Bucket memory size: %d bytes, without header: %d", len(bucket_memory), len(bucket_memory) - int(header_size)))
if alloc_error != .None {
return alloc_error
}
verify( bucket_memory != nil, "Bucket memory is null")
next_bucket_ptr := cast( [^]byte) raw_data(bucket_memory)
for index in 0 ..< num_buckets
{
bucket := cast( ^PoolBucket) next_bucket_ptr
bucket.blocks = memory_after_header(bucket)
bucket.next_block = 0
// log( str_fmt_tmp("\tPool (%d) allocated bucket: %p start %p capacity: %d (raw: %d)",
// pool.block_size,
// raw_data(bucket_memory),
// bucket.blocks,
// pool.bucket_capacity / pool.block_size,
// pool.bucket_capacity ))
if pool.bucket_list.first == nil {
pool.bucket_list.first = bucket
pool.bucket_list.last = bucket
}
else {
dll_push_back( & pool.bucket_list.last, bucket )
}
// log( str_fmt_tmp("Bucket List First: %p", self.bucket_list.first))
next_bucket_ptr = next_bucket_ptr[ bucket_size: ]
}
return alloc_error
}
pool_grab :: proc( pool : Pool, zero_memory := false ) -> ( block : []byte, alloc_error : AllocatorError )
{
pool := pool
if pool.current_bucket != nil {
if ( pool.current_bucket.blocks == nil ) {
ensure( false, str_fmt_tmp("(corruption) current_bucket was wiped %p", pool.current_bucket) )
}
// verify( pool.current_bucket.blocks != nil, str_fmt_tmp("(corruption) current_bucket was wiped %p", pool.current_bucket) )
}
// profile(#procedure)
alloc_error = .None
// Check the free-list first for a block
if pool.free_list_head != nil
{
head := & pool.free_list_head
// Compiler Bug? Fails to compile
// last_free := ll_pop( & pool.free_list_head )
last_free : ^Pool_FreeBlock = pool.free_list_head
pool.free_list_head = pool.free_list_head.next
block = byte_slice( cast([^]byte) last_free, int(pool.block_size) )
// log( str_fmt_tmp("\tReturning free block: %p %d", raw_data(block), pool.block_size))
if zero_memory {
slice.zero(block)
}
when ODIN_DEBUG {
memtracker_register_auto_name_slice( & pool.tracker, block)
}
return
}
if pool.current_bucket == nil
{
alloc_error = pool_allocate_buckets( pool, 1 )
if alloc_error != .None {
ensure(false, "Failed to allocate bucket")
return
}
pool.current_bucket = pool.bucket_list.first
// log( "First bucket allocation")
}
next := uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
end := uintptr(pool.current_bucket.blocks) + uintptr(pool.bucket_capacity)
blocks_left, overflow_signal := intrinsics.overflow_sub( end, next )
if blocks_left == 0 || overflow_signal
{
// Compiler Bug
// if current_bucket.next != nil {
if pool.current_bucket.next != nil {
// current_bucket = current_bucket.next
// log( str_fmt_tmp("\tBucket %p exhausted using %p", pool.current_bucket, pool.current_bucket.next))
pool.current_bucket = pool.current_bucket.next
verify( pool.current_bucket.blocks != nil, "New current_bucket's blocks are null (new current_bucket is corrupted)" )
}
else
{
// log( "\tAll previous buckets exhausted, allocating new bucket")
alloc_error := pool_allocate_buckets( pool, 1 )
if alloc_error != .None {
ensure(false, "Failed to allocate bucket")
return
}
pool.current_bucket = pool.current_bucket.next
verify( pool.current_bucket.blocks != nil, "Next's blocks are null (Post new bucket alloc)" )
}
}
verify( pool.current_bucket != nil, "Attempted to grab a block from a null bucket reference" )
// Compiler Bug
// block = slice_ptr( current_bucket.blocks[ current_bucket.next_block:], int(block_size) )
// self.current_bucket.next_block += block_size
block_ptr := cast(rawptr) (uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block))
block = byte_slice( block_ptr, int(pool.block_size) )
pool.current_bucket.next_block += pool.block_size
next = uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
// log( str_fmt_tmp("\tgrabbing block: %p from %p blocks left: %d", raw_data(block), pool.current_bucket.blocks, (end - next) / uintptr(pool.block_size) ))
if zero_memory {
slice.zero(block)
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", block_ptr, cast(rawptr) (uintptr(block_ptr) + uintptr(pool.block_size))))
}
when ODIN_DEBUG {
memtracker_register_auto_name_slice( & pool.tracker, block)
}
return
}
pool_release :: proc( self : Pool, block : []byte, loc := #caller_location )
{
// profile(#procedure)
if Pool_Check_Release_Object_Validity {
within_bucket := pool_validate_ownership( self, block )
verify( within_bucket, "Attempted to release data that is not within a bucket of this pool", location = loc )
}
// Compiler bug
// ll_push( & self.free_list_head, cast(^Pool_FreeBlock) raw_data(block) )
pool_watch := self
head_watch := & self.free_list_head
// ll_push:
new_free_block := cast(^Pool_FreeBlock) raw_data(block)
(new_free_block ^) = {}
new_free_block.next = self.free_list_head
self.free_list_head = new_free_block
// new_free_block = new_free_block
// log( str_fmt_tmp("Released block: %p %d", new_free_block, self.block_size))
start := new_free_block
end := transmute(rawptr) (uintptr(new_free_block) + uintptr(self.block_size) - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
}
pool_reset :: proc( using pool : Pool )
{
bucket : ^PoolBucket = bucket_list.first // TODO(Ed): Compiler bug? Build fails unless ^PoolBucket is explcitly specified.
for ; bucket != nil; {
bucket.next_block = 0
}
pool.free_list_head = nil
pool.current_bucket = bucket_list.first
}
pool_validate :: proc( pool : Pool )
{
when !ODIN_DEBUG do return
pool := pool
// Make sure all buckets don't show any indication of corruption
bucket : ^PoolBucket = pool.bucket_list.first
if bucket != nil && uintptr(bucket) < 0x10000000000 {
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
}
// Compiler bug ^^ same as pool_reset
for ; bucket != nil; bucket = bucket.next
{
if bucket != nil && uintptr(bucket) < 0x10000000000 {
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
}
if ( bucket.blocks == nil ) {
ensure(false, str_fmt_tmp("Found a corrupted bucket %p", bucket ))
}
}
}
pool_validate_ownership :: proc( using self : Pool, block : [] byte ) -> b32
{
profile(#procedure)
within_bucket := b32(false)
// Compiler Bug : Same as pool_reset
bucket : ^PoolBucket = bucket_list.first
for ; bucket != nil; bucket = bucket.next
{
start := uintptr( bucket.blocks )
end := start + uintptr(bucket_capacity)
block_address := uintptr(raw_data(block))
if start <= block_address && block_address < end
{
misalignment := (block_address - start) % uintptr(block_size)
if misalignment != 0 {
ensure(false, "pool_validate_ownership: This data is within this pool's buckets, however its not aligned to the start of a block")
log(str_fmt_tmp("Block address: %p Misalignment: %p closest: %p",
transmute(rawptr)block_address,
transmute(rawptr)misalignment,
rawptr(block_address - misalignment)))
}
within_bucket = true
break
}
}
return within_bucket
}

View File

@ -0,0 +1,22 @@
package sectr
import "base:runtime"
import "core:prof/spall"
SpallProfiler :: struct {
ctx : spall.Context,
buffer : spall.Buffer,
}
@(deferred_none=profile_end)
profile :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
spall._buffer_begin( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer, name, "", loc )
}
profile_begin :: #force_inline proc "contextless" ( name : string, loc := #caller_location ) {
spall._buffer_begin( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer, name, "", loc )
}
profile_end :: #force_inline proc "contextless" () {
spall._buffer_end( & Memory_App.profiler.ctx, & Memory_App.profiler.buffer)
}

30
code/sectr/grime/ptr.odin Normal file
View File

@ -0,0 +1,30 @@
package sectr
// Provides an alternative syntax for pointers
Ptr :: struct( $ Type : typeid ) {
v : Type,
}
exmaple_ptr :: proc()
{
a, b : int
var : ^Ptr(int)
reg : ^int
a = 1
b = 1
var = &{a}
var.v = 2
var = &{b}
var.v = 3
a = 1
b = 1
reg = (& a)
(reg^) = 2
reg = (& b)
(reg^) = 3
}

View File

@ -0,0 +1,335 @@
/* Slab Allocator
These are a collection of pool allocators serving as a general way
to allocate a large amount of dynamic sized data.
The usual use case for this is an arena, stack,
or dedicated pool allocator fail to be enough to handle a data structure
that either is too random with its size (ex: strings)
or is intended to grow an abitrary degree with an unknown upper bound (dynamic arrays, and hashtables).
The protototype will use slab allocators for two purposes:
* String interning
* General purpose set for handling large arrays & hash tables within some underlying arena or stack.
Technically speaking the general purpose situations can instead be grown on demand
with a dedicated segement of vmem, however this might be overkill
if the worst case buckets allocated are < 500 mb for most app usage.
The slab allocators are expected to hold growable pool allocators,
where each pool stores a 'bucket' of fixed-sized blocks of memory.
When a pools bucket is full it will request another bucket from its arena
for permanent usage within the arena's lifetime.
A freelist is tracked for free-blocks for each pool (provided by the underlying pool allocator)
A slab starts out with pools initialized with no buckets and grows as needed.
When a slab is initialized the slab policy is provided to know how many size-classes there should be
which each contain the ratio of bucket to block size.
*/
package sectr
import "base:runtime"
import "core:mem"
import "core:slice"
SlabSizeClass :: struct {
bucket_capacity : uint,
block_size : uint,
block_alignment : uint,
}
Slab_Max_Size_Classes :: 64
SlabPolicy :: StackFixed(SlabSizeClass, Slab_Max_Size_Classes)
SlabHeader :: struct {
dbg_name : string,
tracker : MemoryTracker,
backing : Allocator,
pools : StackFixed(Pool, Slab_Max_Size_Classes),
}
Slab :: struct {
using header : ^SlabHeader,
}
slab_allocator :: proc( slab : Slab ) -> ( allocator : Allocator ) {
allocator.procedure = slab_allocator_proc
allocator.data = slab.header
return
}
slab_init :: proc( policy : ^SlabPolicy, bucket_reserve_num : uint = 0, allocator : Allocator, dbg_name : string = "", should_zero_buckets : b32 = false ) -> ( slab : Slab, alloc_error : AllocatorError )
{
header_size :: size_of( SlabHeader )
raw_mem : rawptr
raw_mem, alloc_error = alloc( header_size, mem.DEFAULT_ALIGNMENT, allocator )
if alloc_error != .None do return
slab.header = cast( ^SlabHeader) raw_mem
slab.backing = allocator
slab.dbg_name = dbg_name
when ODIN_DEBUG {
memtracker_init( & slab.tracker, allocator, Kilobyte * 256, dbg_name )
}
alloc_error = slab_init_pools( slab, policy, bucket_reserve_num, should_zero_buckets )
return
}
slab_init_pools :: proc ( using self : Slab, policy : ^SlabPolicy, bucket_reserve_num : uint = 0, should_zero_buckets : b32 ) -> AllocatorError
{
profile(#procedure)
for id in 0 ..< policy.idx {
using size_class := policy.items[id]
pool_dbg_name := str_fmt_alloc("%v pool[%v]", dbg_name, block_size, allocator = backing)
pool, alloc_error := pool_init( should_zero_buckets, block_size, bucket_capacity, bucket_reserve_num, block_alignment, backing, pool_dbg_name )
if alloc_error != .None do return alloc_error
push( & self.pools, pool )
}
return .None
}
slab_reload :: proc ( slab : Slab, allocator : Allocator )
{
slab.backing = allocator
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_reload( pool, slab.backing )
}
}
slab_destroy :: proc( using self : Slab )
{
for id in 0 ..< pools.idx {
pool := pools.items[id]
pool_destroy( pool )
}
free( self.header, backing )
when ODIN_DEBUG {
memtracker_clear(tracker)
}
}
slab_alloc :: proc( self : Slab,
size : uint,
alignment : uint,
zero_memory := true,
loc := #caller_location
) -> ( data : []byte, alloc_error : AllocatorError )
{
// profile(#procedure)
pool : Pool
id : u32 = 0
for ; id < self.pools.idx; id += 1 {
pool = self.pools.items[id]
if pool.block_size >= size && pool.alignment >= alignment {
break
}
}
verify( id < self.pools.idx, "There is not a size class in the slab's policy to satisfy the requested allocation", location = loc )
verify( pool.header != nil, "Requested alloc not supported by the slab allocator", location = loc )
block : []byte
slab_validate_pools( self )
block, alloc_error = pool_grab(pool)
slab_validate_pools( self )
if block == nil || alloc_error != .None {
ensure(false, "Bad block from pool")
return nil, alloc_error
}
// log( str_fmt_tmp("%v: Retrieved block: %p %d", self.dbg_name, raw_data(block), len(block) ))
data = byte_slice(raw_data(block), size)
if zero_memory {
slice.zero(data)
}
when ODIN_DEBUG {
memtracker_register_auto_name( & self.tracker, raw_data(block), & block[ len(block) - 1 ] )
}
return
}
slab_free :: proc( using self : Slab, data : []byte, loc := #caller_location )
{
// profile(#procedure)
pool : Pool
for id in 0 ..< pools.idx
{
pool = pools.items[id]
if pool_validate_ownership( pool, data ) {
start := raw_data(data)
end := ptr_offset(start, pool.block_size - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
pool_release( pool, data, loc )
return
}
}
verify(false, "Attempted to free a block not within a pool of this slab", location = loc)
}
slab_resize :: proc( using self : Slab,
data : []byte,
new_size : uint,
alignment : uint,
zero_memory := true,
loc := #caller_location
) -> ( new_data : []byte, alloc_error : AllocatorError )
{
// profile(#procedure)
old_size := uint( len(data))
pool_resize, pool_old : Pool
for id in 0 ..< pools.idx
{
pool := pools.items[id]
if pool.block_size >= new_size && pool.alignment >= alignment {
pool_resize = pool
}
if pool_validate_ownership( pool, data ) {
pool_old = pool
}
if pool_resize.header != nil && pool_old.header != nil {
break
}
}
verify( pool_resize.header != nil, "Requested resize not supported by the slab allocator", location = loc )
// Resize will keep block in the same size_class, just give it more of its already allocated block
if pool_old.block_size == pool_resize.block_size
{
new_data_ptr := memory_after(data)
new_data = byte_slice( raw_data(data), new_size )
// log( dump_stacktrace() )
// log( str_fmt_tmp("%v: Resize via expanding block space allocation %p %d", dbg_name, new_data_ptr, int(new_size - old_size)))
if zero_memory && new_size > old_size {
to_zero := byte_slice( new_data_ptr, int(new_size - old_size) )
slab_validate_pools( self )
slice.zero( to_zero )
slab_validate_pools( self )
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", new_data_ptr, cast(rawptr) (uintptr(new_data_ptr) + uintptr(new_size - old_size))))
}
return
}
// We'll need to provide an entirely new block, so the data will need to be copied over.
new_block : []byte
slab_validate_pools( self )
new_block, alloc_error = pool_grab( pool_resize )
slab_validate_pools( self )
if new_block == nil {
ensure(false, "Retreived a null block")
return
}
if alloc_error != .None do return
// TODO(Ed): Reapply this when safe.
if zero_memory {
slice.zero( new_block )
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", raw_data(new_block), cast(rawptr) (uintptr(raw_data(new_block)) + uintptr(new_size))))
}
// log( str_fmt_tmp("Resize via new block: %p %d (old : %p $d )", raw_data(new_block), len(new_block), raw_data(data), old_size ))
if raw_data(data) != raw_data(new_block) {
// log( str_fmt_tmp("%v: Resize via new block, copying from old data block to new block: (%p %d), (%p %d)", dbg_name, raw_data(data), len(data), raw_data(new_block), len(new_block)))
copy_non_overlapping( raw_data(new_block), raw_data(data), int(old_size) )
pool_release( pool_old, data )
start := raw_data( data )
end := rawptr(uintptr(start) + uintptr(pool_old.block_size) - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
}
new_data = new_block[ :new_size]
when ODIN_DEBUG {
memtracker_register_auto_name( & self.tracker, raw_data(new_block), & new_block[ len(new_block) - 1 ] )
}
return
}
slab_reset :: proc( slab : Slab )
{
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_reset( pool )
}
when ODIN_DEBUG {
memtracker_clear(slab.tracker)
}
}
slab_validate_pools :: proc( slab : Slab )
{
slab := slab
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_validate( pool )
}
}
slab_allocator_proc :: proc(
allocator_data : rawptr,
mode : AllocatorMode,
size : int,
alignment : int,
old_memory : rawptr,
old_size : int,
loc := #caller_location
) -> ( data : []byte, alloc_error : AllocatorError)
{
slab : Slab
slab.header = cast( ^SlabHeader) allocator_data
size := uint(size)
alignment := uint(alignment)
old_size := uint(old_size)
switch mode
{
case .Alloc, .Alloc_Non_Zeroed:
return slab_alloc( slab, size, alignment, (mode != .Alloc_Non_Zeroed), loc)
case .Free:
slab_free( slab, byte_slice( old_memory, int(old_size)), loc )
case .Free_All:
slab_reset( slab )
case .Resize, .Resize_Non_Zeroed:
return slab_resize( slab, byte_slice(old_memory, int(old_size)), size, alignment, (mode != .Resize_Non_Zeroed), loc)
case .Query_Features:
set := cast( ^AllocatorModeSet) old_memory
if set != nil {
(set ^) = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
}
case .Query_Info:
alloc_error = .Mode_Not_Implemented
}
return
}

280
code/sectr/grime/stack.odin Normal file
View File

@ -0,0 +1,280 @@
package sectr
import "core:mem"
import "core:slice"
//region Fixed Stack
StackFixed :: struct ( $ Type : typeid, $ Size : u32 ) {
idx : u32,
items : [ Size ] Type,
}
stack_clear :: #force_inline proc ( using stack : ^StackFixed( $Type, $Size)) {
idx = 0
}
stack_push :: #force_inline proc( using stack : ^ StackFixed( $ Type, $ Size ), value : Type ) {
verify( idx < len( items ), "Attempted to push on a full stack" )
items[ idx ] = value
idx += 1
}
stack_pop :: #force_inline proc( using stack : ^StackFixed( $ Type, $ Size ) ) {
verify( idx > 0, "Attempted to pop an empty stack" )
idx -= 1
if idx == 0 {
items[idx] = {}
}
}
stack_peek_ref :: #force_inline proc "contextless" ( using stack : ^StackFixed( $ Type, $ Size ) ) -> ( ^Type) {
last_idx := max( 0, idx - 1 ) if idx > 0 else 0
last := & items[last_idx]
return last
}
stack_peek :: #force_inline proc "contextless" ( using stack : ^StackFixed( $ Type, $ Size ) ) -> Type {
last := max( 0, idx - 1 ) if idx > 0 else 0
return items[last]
}
//endregion Fixed Stack
//region Stack Allocator
// TODO(Ed) : This is untested and problably filled with bugs.
/* Growing Stack allocator
This implementation can support growing if the backing allocator supports
it without fragmenting the backing allocator.
Each block in the stack is tracked with a doubly-linked list to have debug stats.
(It could be removed for non-debug builds)
*/
StackAllocatorBase :: struct {
backing : Allocator,
using links : DLL_NodeFL(StackAllocatorHeader),
peak_used : int,
size : int,
data : [^]byte,
}
StackAllocator :: struct {
using base : ^StackAllocatorBase,
}
StackAllocatorHeader :: struct {
using links : DLL_NodePN(StackAllocatorHeader),
block_size : int,
padding : int,
}
stack_allocator :: proc( using self : StackAllocator ) -> ( allocator : Allocator ) {
allocator.procedure = stack_allocator_proc
allocator.data = self.base
return
}
stack_allocator_init :: proc( size : int, allocator := context.allocator ) -> ( stack : StackAllocator, alloc_error : AllocatorError )
{
header_size := size_of(StackAllocatorBase)
raw_mem : rawptr
raw_mem, alloc_error = alloc( header_size + size, mem.DEFAULT_ALIGNMENT )
if alloc_error != AllocatorError.None do return
stack.base = cast( ^StackAllocatorBase) raw_mem
stack.size = size
stack.data = cast( [^]byte) (cast( [^]StackAllocatorBase) stack.base)[ 1:]
stack.last = cast(^StackAllocatorHeader) stack.data
stack.first = stack.last
return
}
stack_allocator_destroy :: proc( using self : StackAllocator )
{
free( self.base, backing )
}
stack_allocator_init_via_memory :: proc( memory : []byte ) -> ( stack : StackAllocator )
{
header_size := size_of(StackAllocatorBase)
if len(memory) < (header_size + Kilobyte) {
verify(false, "Assigning a stack allocator less than a kilobyte of space")
return
}
stack.base = cast( ^StackAllocatorBase) & memory[0]
stack.size = len(memory) - header_size
stack.data = cast( [^]byte ) (cast( [^]StackAllocatorBase) stack.base)[ 1:]
stack.last = cast( ^StackAllocatorHeader) stack.data
stack.first = stack.last
return
}
stack_allocator_push :: proc( using self : StackAllocator, block_size, alignment : int, zero_memory : bool ) -> ( []byte, AllocatorError )
{
// TODO(Ed): Make sure first push is fine.
verify( block_size > Kilobyte, "Attempted to push onto the stack less than a Kilobyte")
top_block_ptr := memory_after_header( last )
theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(block_size) - uintptr(first))
if theoretical_size > size {
// TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow.
return nil, .Out_Of_Memory
}
top_block_slice := slice_ptr( top_block_ptr, last.block_size )
next_spot := uintptr( top_block_ptr) + uintptr(last.block_size)
header_offset_pad := calc_padding_with_header( uintptr(next_spot), uintptr(alignment), size_of(StackAllocatorHeader) )
header := cast( ^StackAllocatorHeader) (next_spot + uintptr(header_offset_pad) - uintptr(size_of( StackAllocatorHeader)))
header.padding = header_offset_pad
header.prev = last
header.block_size = block_size
curr_block_ptr := memory_after_header( header )
curr_block := slice_ptr( curr_block_ptr, block_size )
curr_used := cast(int) (uintptr(curr_block_ptr) + uintptr(block_size) - uintptr(self.last))
self.peak_used += max( peak_used, curr_used )
dll_push_back( & base.links.last, header )
if zero_memory {
slice.zero( curr_block )
}
return curr_block, .None
}
stack_allocator_resize_top :: proc( using self : StackAllocator, new_block_size, alignment : int, zero_memory : bool ) -> AllocatorError
{
verify( new_block_size > Kilobyte, "Attempted to resize the last pushed on the stack to less than a Kilobyte")
top_block_ptr := memory_after_header( last )
theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(last.block_size) - uintptr(first))
if theoretical_size > size {
// TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow.
return .Out_Of_Memory
}
if zero_memory && new_block_size > last.block_size {
added_ptr := top_block_ptr[ last.block_size:]
added_slice := slice_ptr( added_ptr, new_block_size - last.block_size )
slice.zero( added_slice )
}
last.block_size = new_block_size
return .None
}
stack_allocator_pop :: proc( using self : StackAllocator ) {
base.links.last = last.prev
base.links.last.next = nil
}
stack_allocator_proc :: proc(
allocator_data : rawptr,
mode : AllocatorMode,
block_size : int,
alignment : int,
old_memory : rawptr,
old_size : int,
location : SourceCodeLocation = #caller_location
) -> ([]byte, AllocatorError)
{
stack := StackAllocator { cast( ^StackAllocatorBase) allocator_data }
if stack.data == nil {
return nil, AllocatorError.Invalid_Argument
}
switch mode
{
case .Alloc, .Alloc_Non_Zeroed:
{
return stack_allocator_push( stack, block_size, alignment, mode == .Alloc )
}
case .Free:
{
if old_memory == nil {
return nil, .None
}
start := uintptr(stack.data)
end := start + uintptr(block_size)
curr_addr := uintptr(old_memory)
verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (free)" )
block_ptr := memory_after_header( stack.last )
if curr_addr >= start + uintptr(block_ptr) {
return nil, .None
}
dll_pop_back( & stack.last )
}
case .Free_All:
// TODO(Ed) : Review that we don't have any header issues with the reset.
stack.first = stack.last
stack.last.next = nil
stack.last.block_size = 0
case .Resize, .Resize_Non_Zeroed:
{
// Check if old_memory is at the first on the stack, if it is, just grow its size
// Otherwise, log that the user cannot resize stack items that are not at the top of the stack allocated.
if old_memory == nil {
return stack_allocator_push(stack, block_size, alignment, mode == .Resize )
}
if block_size == 0 {
return nil, .None
}
start := uintptr(stack.data)
end := start + uintptr(block_size)
curr_addr := uintptr(old_memory)
verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (resize)" )
block_ptr := memory_after_header( stack.last )
if block_ptr != old_memory {
ensure( false, "Attempted to reszie a block of memory on the stack other than top most" )
return nil, .None
}
if old_size == block_size {
return byte_slice( old_memory, block_size ), .None
}
stack_allocator_resize_top( stack, block_size, alignment, mode == .Resize )
return byte_slice( block_ptr, block_size ), .None
}
case .Query_Features:
{
feature_flags := ( ^AllocatorModeSet)(old_memory)
if feature_flags != nil {
(feature_flags ^) = {.Alloc, .Alloc_Non_Zeroed, .Free, .Free_All, .Resize, .Resize_Non_Zeroed, .Query_Features}
}
return nil, .None
}
case .Query_Info:
{
return nil, .Mode_Not_Implemented
}
}
return nil, .None
}
//endregion Stack Allocator

View File

@ -0,0 +1,11 @@
// This provides a string generator using a token replacement approach instead of a %<id> verb-syntax to parse.
// This was done just for preference as I personally don't like the c-printf-like syntax.
package sectr
// str_format :: proc ( format : string, tokens : ..args ) {
// }

View File

@ -0,0 +1,117 @@
/*
This is a quick and dirty string table.
IT uses the HMapZPL for the hashtable of strings, and the string's content is stored in a dedicated slab.
Future Plans (IF needed for performance):
The goal is to eventually swap out the slab with possilby a dedicated growing vmem arena for the strings.
The table would be swapped with a table stored in the general slab and uses either linear probing or open addressing
If linear probing, the hash node list per table bucket is store with the strigns in the same arena.
If open addressing, we just keep the open addressed array of node slots in the general slab (but hopefully better perf)
*/
package sectr
import "base:runtime"
import "core:mem"
import "core:slice"
import "core:strings"
StringKey :: distinct u64
RunesCached :: []rune
StrRunesPair :: struct {
str : string,
runes : []rune,
}
to_str_runes_pair :: proc ( content : string ) -> StrRunesPair {
return { content, to_runes(content) }
}
StringCache :: struct {
slab : Slab,
table : HMapZPL(StrRunesPair),
}
str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) {
alignment := uint(mem.DEFAULT_ALIGNMENT)
policy : SlabPolicy
policy_ptr := & policy
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 16, alignment })
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 32, alignment })
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 64, alignment })
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 128, alignment })
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 256, alignment })
push( policy_ptr, SlabSizeClass { 64 * Kilobyte, 512, alignment })
push( policy_ptr, SlabSizeClass { 1 * Megabyte, 1 * Kilobyte, alignment })
push( policy_ptr, SlabSizeClass { 4 * Megabyte, 4 * Kilobyte, alignment })
push( policy_ptr, SlabSizeClass { 16 * Megabyte, 16 * Kilobyte, alignment })
push( policy_ptr, SlabSizeClass { 32 * Megabyte, 32 * Kilobyte, alignment })
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 64 * Kilobyte, alignment })
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 128 * Kilobyte, alignment })
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 256 * Kilobyte, alignment })
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 512 * Kilobyte, alignment })
// push( policy_ptr, SlabSizeClass { 64 * Megabyte, 1 * Megabyte, alignment })
header_size :: size_of( Slab )
@static dbg_name := "StringCache slab"
state := get_state()
alloc_error : AllocatorError
cache.slab, alloc_error = slab_init( & policy, allocator = persistent_allocator(), dbg_name = dbg_name )
verify(alloc_error == .None, "Failed to initialize the string cache" )
cache.table, alloc_error = zpl_hmap_init_reserve( StrRunesPair, persistent_allocator(), 4 * Megabyte, dbg_name )
return
}
str_intern_key :: #force_inline proc( content : string ) -> StringKey { return cast(StringKey) crc32( transmute([]byte) content ) }
str_intern_lookup :: #force_inline proc( key : StringKey ) -> (^StrRunesPair) { return zpl_hmap_get( & get_state().string_cache.table, transmute(u64) key ) }
str_intern :: proc(
content : string
) -> StrRunesPair
{
// profile(#procedure)
cache := & get_state().string_cache
key := str_intern_key(content)
result := zpl_hmap_get( & cache.table, transmute(u64) key )
if result != nil {
return (result ^)
}
// profile_begin("new entry")
{
length := len(content)
// str_mem, alloc_error := alloc( length, mem.DEFAULT_ALIGNMENT )
str_mem, alloc_error := slab_alloc( cache.slab, uint(length), uint(mem.DEFAULT_ALIGNMENT), zero_memory = false )
verify( alloc_error == .None, "String cache had a backing allocator error" )
// copy_non_overlapping( str_mem, raw_data(content), length )
copy_non_overlapping( raw_data(str_mem), raw_data(content), length )
runes : []rune
// runes, alloc_error = to_runes( content, persistent_allocator() )
runes, alloc_error = to_runes( content, slab_allocator(cache.slab) )
verify( alloc_error == .None, "String cache had a backing allocator error" )
slab_validate_pools( get_state().persistent_slab )
// result, alloc_error = zpl_hmap_set( & cache.table, key, StrRunesPair { transmute(string) byte_slice(str_mem, length), runes } )
result, alloc_error = zpl_hmap_set( & cache.table, transmute(u64) key, StrRunesPair { transmute(string) str_mem, runes } )
verify( alloc_error == .None, "String cache had a backing allocator error" )
slab_validate_pools( get_state().persistent_slab )
}
// profile_end()
return (result ^)
}
// runes_intern :: proc( content : []rune ) -> StrRunesPair
// {
// cache := get_state().string_cache
// }

View File

@ -0,0 +1,43 @@
package sectr
rune16 :: distinct u16
// Exposing the alloc_error
@(require_results)
string_to_runes :: proc ( content : string, allocator := context.allocator) -> (runes : []rune, alloc_error : AllocatorError) #optional_allocator_error {
num := str_rune_count(content)
runes, alloc_error = make([]rune, num, allocator)
if runes == nil || alloc_error != AllocatorError.None {
return
}
idx := 0
for codepoint in content {
runes[idx] = codepoint
idx += 1
}
return
}
string_to_runes_array :: proc( content : string, allocator := context.allocator ) -> ( []rune, AllocatorError )
{
num := cast(u64) str_rune_count(content)
runes_array, alloc_error := array_init_reserve( rune, allocator, num )
if alloc_error != AllocatorError.None {
return nil, alloc_error
}
runes := array_to_slice_capacity(runes_array)
idx := 0
for codepoint in content {
runes[idx] = codepoint
idx += 1
}
return runes, alloc_error
}

View File

@ -0,0 +1,312 @@
/*
Odin's virtual arena allocator doesn't do what I ideally want for allocation resizing.
(It was also a nice exercise along with making the other allocators)
So this is a virtual memory backed arena allocator designed
to take advantage of one large contigous reserve of memory.
With the expectation that resizes with its interface will only occur using the last allocated block.
All virtual address space memory for this application is managed by a virtual arena.
No other part of the program will directly touch the vitual memory interface direclty other than it.
Thus for the scope of this prototype the Virtual Arena are the only interfaces to dynamic address spaces for the runtime of the client app.
The host application as well ideally (although this may not be the case for a while)
*/
package sectr
import "base:intrinsics"
import "base:runtime"
import "core:mem"
import "core:os"
import "core:slice"
import "core:sync"
VArena_GrowthPolicyProc :: #type proc( commit_used, committed, reserved, requested_size : uint ) -> uint
VArena :: struct {
using vmem : VirtualMemoryRegion,
dbg_name : string,
tracker : MemoryTracker,
commit_used : uint,
growth_policy : VArena_GrowthPolicyProc,
allow_any_reize : b32,
mutex : sync.Mutex,
}
varena_default_growth_policy :: proc( commit_used, committed, reserved, requested_size : uint ) -> uint
{
@static commit_limit := uint(1 * Megabyte)
@static increment := uint(16 * Kilobyte)
page_size := uint(virtual_get_page_size())
if increment < Gigabyte && committed > commit_limit {
commit_limit *= 2
increment *= 2
increment = clamp( increment, Megabyte, Gigabyte )
}
remaining_reserve := reserved - committed
growth_increment := max( increment, requested_size )
growth_increment = clamp( growth_increment, page_size, remaining_reserve )
next_commit_size := memory_align_formula( committed + growth_increment, page_size )
return next_commit_size
}
varena_allocator :: proc( arena : ^VArena ) -> ( allocator : Allocator ) {
allocator.procedure = varena_allocator_proc
allocator.data = arena
return
}
// Default growth_policy is nil
varena_init :: proc( base_address : uintptr, to_reserve, to_commit : uint,
growth_policy : VArena_GrowthPolicyProc, allow_any_reize : b32 = false, dbg_name : string
) -> ( arena : VArena, alloc_error : AllocatorError)
{
page_size := uint(virtual_get_page_size())
verify( page_size > size_of(VirtualMemoryRegion), "Make sure page size is not smaller than a VirtualMemoryRegion?")
verify( to_reserve >= page_size, "Attempted to reserve less than a page size" )
verify( to_commit >= page_size, "Attempted to commit less than a page size")
verify( to_reserve >= to_commit, "Attempted to commit more than there is to reserve" )
vmem : VirtualMemoryRegion
vmem, alloc_error = virtual_reserve_and_commit( base_address, to_reserve, to_commit )
if vmem.base_address == nil || alloc_error != .None {
ensure(false, "Failed to allocate requested virtual memory for virtual arena")
return
}
arena.vmem = vmem
arena.commit_used = 0
if growth_policy == nil {
arena.growth_policy = varena_default_growth_policy
}
else {
arena.growth_policy = growth_policy
}
arena.allow_any_reize = allow_any_reize
when ODIN_DEBUG {
memtracker_init( & arena.tracker, runtime.heap_allocator(), Kilobyte * 128, dbg_name )
}
return
}
varena_alloc :: proc( using self : ^VArena,
size : uint,
alignment : uint = mem.DEFAULT_ALIGNMENT,
zero_memory := true,
location := #caller_location
) -> ( data : []byte, alloc_error : AllocatorError )
{
verify( alignment & (alignment - 1) == 0, "Non-power of two alignment", location = location )
page_size := uint(virtual_get_page_size())
requested_size := size
if requested_size == 0 {
ensure(false, "Requested 0 size")
return nil, .Invalid_Argument
}
// ensure( requested_size > page_size, "Requested less than a page size, going to allocate a page size")
// requested_size = max(requested_size, page_size)
sync.mutex_guard( & mutex )
alignment_offset := uint(0)
current_offset := uintptr(self.reserve_start) + uintptr(commit_used)
mask := uintptr(alignment - 1)
if current_offset & mask != 0 {
alignment_offset = alignment - uint(current_offset & mask)
}
size_to_allocate, overflow_signal := intrinsics.overflow_add( requested_size, alignment_offset )
if overflow_signal {
alloc_error = .Out_Of_Memory
return
}
to_be_used : uint
to_be_used, overflow_signal = intrinsics.overflow_add( commit_used, size_to_allocate )
if overflow_signal || to_be_used > reserved {
alloc_error = .Out_Of_Memory
return
}
header_offset := uint( uintptr(reserve_start) - uintptr(base_address) )
commit_left := committed - commit_used - header_offset
needs_more_committed := commit_left < size_to_allocate
if needs_more_committed
{
profile("VArena Growing")
next_commit_size := growth_policy( commit_used, committed, reserved, size_to_allocate )
alloc_error = virtual_commit( vmem, next_commit_size )
if alloc_error != .None {
return
}
}
data_ptr := rawptr(current_offset + uintptr(alignment_offset))
data = byte_slice( data_ptr, int(requested_size) )
self.commit_used += size_to_allocate
alloc_error = .None
// log_backing : [Kilobyte * 16]byte
// backing_slice := byte_slice( & log_backing[0], len(log_backing))
// log( str_fmt_buffer( backing_slice, "varena alloc - BASE: %p PTR: %X, SIZE: %d", cast(rawptr) self.base_address, & data[0], requested_size) )
if zero_memory
{
// log( str_fmt_buffer( backing_slice, "Zeroring data (Range: %p to %p)", raw_data(data), cast(rawptr) (uintptr(raw_data(data)) + uintptr(requested_size))))
// slice.zero( data )
mem_zero( data_ptr, int(requested_size) )
}
when ODIN_DEBUG {
memtracker_register_auto_name( & tracker, & data[0], & data[len(data) - 1] )
}
return
}
varena_free_all :: proc( using self : ^VArena )
{
sync.mutex_guard( & mutex )
commit_used = 0
when ODIN_DEBUG && Track_Memory {
array_clear(tracker.entries)
}
}
varena_release :: proc( using self : ^VArena )
{
sync.mutex_guard( & mutex )
virtual_release( vmem )
commit_used = 0
}
varena_allocator_proc :: proc(
allocator_data : rawptr,
mode : AllocatorMode,
size : int,
alignment : int,
old_memory : rawptr,
old_size : int,
location : SourceCodeLocation = #caller_location
) -> ( data : []byte, alloc_error : AllocatorError)
{
arena := cast( ^VArena) allocator_data
size := uint(size)
alignment := uint(alignment)
old_size := uint(old_size)
page_size := uint(virtual_get_page_size())
switch mode
{
case .Alloc, .Alloc_Non_Zeroed:
data, alloc_error = varena_alloc( arena, size, alignment, (mode != .Alloc_Non_Zeroed), location )
return
case .Free:
alloc_error = .Mode_Not_Implemented
case .Free_All:
varena_free_all( arena )
case .Resize, .Resize_Non_Zeroed:
if old_memory == nil {
ensure(false, "Resizing without old_memory?")
data, alloc_error = varena_alloc( arena, size, alignment, (mode != .Resize_Non_Zeroed), location )
return
}
if size == old_size {
ensure(false, "Requested resize when none needed")
data = byte_slice( old_memory, old_size )
return
}
alignment_offset := uintptr(old_memory) & uintptr(alignment - 1)
if alignment_offset == 0 && size < old_size {
ensure(false, "Requested a shrink from a virtual arena")
data = byte_slice( old_memory, size )
return
}
old_memory_offset := uintptr(old_memory) + uintptr(old_size)
current_offset := uintptr(arena.reserve_start) + uintptr(arena.commit_used)
// if old_size < page_size {
// // We're dealing with an allocation that requested less than the minimum allocated on vmem.
// // Provide them more of their actual memory
// data = byte_slice( old_memory, size )
// return
// }
verify( old_memory_offset == current_offset || arena.allow_any_reize,
"Cannot resize existing allocation in vitual arena to a larger size unless it was the last allocated" )
log_backing : [Kilobyte * 16]byte
backing_slice := byte_slice( & log_backing[0], len(log_backing))
if old_memory_offset != current_offset && arena.allow_any_reize
{
// Give it new memory and copy the old over. Old memory is unrecoverable until clear.
new_region : []byte
new_region, alloc_error = varena_alloc( arena, size, alignment, (mode != .Resize_Non_Zeroed), location )
if new_region == nil || alloc_error != .None {
ensure(false, "Failed to grab new region")
data = byte_slice( old_memory, old_size )
when ODIN_DEBUG {
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
}
return
}
copy_non_overlapping( raw_data(new_region), old_memory, int(old_size) )
data = new_region
// log( str_fmt_tmp("varena resize (new): old: %p %v new: %p %v", old_memory, old_size, (& data[0]), size))
when ODIN_DEBUG {
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
}
return
}
new_region : []byte
new_region, alloc_error = varena_alloc( arena, size - old_size, alignment, (mode != .Resize_Non_Zeroed), location )
if new_region == nil || alloc_error != .None {
ensure(false, "Failed to grab new region")
data = byte_slice( old_memory, old_size )
return
}
data = byte_slice( old_memory, size )
// log( str_fmt_tmp("varena resize (expanded): old: %p %v new: %p %v", old_memory, old_size, (& data[0]), size))
when ODIN_DEBUG {
memtracker_register_auto_name( & arena.tracker, & data[0], & data[len(data) - 1] )
}
return
case .Query_Features:
{
set := cast( ^AllocatorModeSet) old_memory
if set != nil {
(set ^) = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
}
}
case .Query_Info:
{
alloc_error = .Mode_Not_Implemented
}
}
return
}

View File

@ -0,0 +1,116 @@
/* Virtual Memory OS Interface
This is an alternative to the virtual core library provided by odin, suppport setting the base address among other things.
*/
package sectr
import core_virtual "core:mem/virtual"
import "core:os"
VirtualMemoryRegionHeader :: struct {
committed : uint,
reserved : uint,
reserve_start : [^]byte,
}
VirtualMemoryRegion :: struct {
using base_address : ^VirtualMemoryRegionHeader
}
virtual_get_page_size :: proc "contextless" () -> int {
@static page_size := 0
if page_size == 0 {
page_size = os.get_page_size()
}
return page_size
}
virtual_reserve_remaining :: proc "contextless" ( using vmem : VirtualMemoryRegion ) -> uint {
header_offset := cast(uint) (uintptr(reserve_start) - uintptr(vmem.base_address))
return reserved - header_offset
}
@(require_results)
virtual_commit :: proc "contextless" ( using vmem : VirtualMemoryRegion, size : uint ) -> ( alloc_error : AllocatorError )
{
if size < committed {
return .None
}
header_size := size_of(VirtualMemoryRegionHeader)
page_size := uint(virtual_get_page_size())
to_commit := memory_align_formula( size, page_size )
alloc_error = core_virtual.commit( base_address, to_commit )
if alloc_error != .None {
return alloc_error
}
base_address.committed = size
return alloc_error
}
virtual_decommit :: proc "contextless" ( vmem : VirtualMemoryRegion, size : uint ) {
core_virtual.decommit( vmem.base_address, size )
}
virtual_protect :: proc "contextless" ( vmem : VirtualMemoryRegion, region : []byte, flags : VirtualProtectFlags ) -> b32
{
page_size := virtual_get_page_size()
if len(region) % page_size != 0 {
return false
}
return cast(b32) core_virtual.protect( raw_data(region), len(region), flags )
}
@(require_results)
virtual_reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( VirtualMemoryRegion, AllocatorError ) {
page_size := uint(virtual_get_page_size())
to_reserve := memory_align_formula( size, page_size )
return virtual__reserve( base_address, to_reserve )
}
@(require_results)
virtual_reserve_and_commit :: proc "contextless" (
base_address : uintptr, reserve_size, commit_size : uint
) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
{
if reserve_size < commit_size {
alloc_error = .Invalid_Argument
return
}
vmem, alloc_error = virtual_reserve( base_address, reserve_size )
if alloc_error != .None {
return
}
alloc_error = virtual_commit( vmem, commit_size )
return
}
virtual_release :: proc "contextless" ( vmem : VirtualMemoryRegion ) {
core_virtual.release( vmem.base_address, vmem.reserved )
}
// If the OS is not windows, we just use the library's interface which does not support base_address.
when ODIN_OS != OS_Type.Windows {
virtual__reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
{
header_size := memory_align_formula(size_of(VirtualMemoryRegionHeader), mem.DEFAULT_ALIGNMENT)
// Ignoring the base address, add an os specific impl if you want it.
data : []byte
data, alloc_error := core_virtual.reserve( header_size + size ) or_return
alloc_error := core_virtual.commit( header_size )
vmem.base_address := cast( ^VirtualMemoryRegionHeader ) raw_data(data)
vmem.reserve_start = cast([^]byte) (uintptr(vmem.base_address) + uintptr(header_size))
vmem.reserved = len(data)
vmem.committed = header_size
return
}
} // END: ODIN_OS != runtime.Odin_OS_Type.Windows

View File

@ -0,0 +1,112 @@
package sectr
import "core:c"
import "core:c/libc"
import "core:fmt"
import "core:mem"
import core_virtual "core:mem/virtual"
import "core:strings"
import win32 "core:sys/windows"
when ODIN_OS == OS_Type.Windows {
thread__highres_wait :: proc( desired_ms : f64, loc := #caller_location ) -> b32
{
// label_backing : [1 * Megabyte]u8
// label_arena : Arena
// arena_init( & label_arena, slice_ptr( & label_backing[0], len(label_backing)) )
// label_u8 := str_fmt_tmp( "SECTR: WAIT TIMER")//, allocator = arena_allocator( &label_arena) )
// label_u16 := win32.utf8_to_utf16( label_u8, context.temp_allocator) //arena_allocator( & label_arena) )
timer := win32.CreateWaitableTimerExW( nil, nil, win32.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, win32.TIMER_ALL_ACCESS )
if timer == nil {
msg := str_fmt_tmp("Failed to create win32 timer - ErrorCode: %v", win32.GetLastError() )
log( msg, LogLevel.Warning, loc)
return false
}
due_time := win32.LARGE_INTEGER(desired_ms * MS_To_NS)
result := win32.SetWaitableTimerEx( timer, & due_time, 0, nil, nil, nil, 0 )
if ! result {
msg := str_fmt_tmp("Failed to set win32 timer - ErrorCode: %v", win32.GetLastError() )
log( msg, LogLevel.Warning, loc)
return false
}
WAIT_ABANDONED : win32.DWORD : 0x00000080
WAIT_IO_COMPLETION : win32.DWORD : 0x000000C0
WAIT_OBJECT_0 : win32.DWORD : 0x00000000
WAIT_TIMEOUT : win32.DWORD : 0x00000102
WAIT_FAILED : win32.DWORD : 0xFFFFFFFF
wait_result := win32.WaitForSingleObjectEx( timer, win32.INFINITE, win32.BOOL(true) )
switch wait_result
{
case WAIT_ABANDONED:
msg := str_fmt_tmp("Failed to wait for win32 timer - Error: WAIT_ABANDONED" )
log( msg, LogLevel.Error, loc)
return false
case WAIT_IO_COMPLETION:
msg := str_fmt_tmp("Waited for win32 timer: Ended by APC queued to the thread" )
log( msg, LogLevel.Error, loc)
return false
case WAIT_OBJECT_0:
msg := str_fmt_tmp("Waited for win32 timer- Reason : WAIT_OBJECT_0" )
log( msg, loc = loc)
return false
case WAIT_FAILED:
msg := str_fmt_tmp("Waited for win32 timer failed - ErrorCode: $v", win32.GetLastError() )
log( msg, LogLevel.Error, loc)
return false
}
return true
}
set__scheduler_granularity :: proc "contextless" ( desired_ms : u32 ) -> b32 {
return win32.timeBeginPeriod( desired_ms ) == win32.TIMERR_NOERROR
}
WIN32_ERROR_INVALID_ADDRESS :: 487
WIN32_ERROR_COMMITMENT_LIMIT :: 1455
@(require_results)
virtual__reserve :: proc "contextless" ( base_address : uintptr, size : uint ) -> ( vmem : VirtualMemoryRegion, alloc_error : AllocatorError )
{
header_size := cast(uint) memory_align_formula(size_of(VirtualMemoryRegionHeader), mem.DEFAULT_ALIGNMENT)
result := win32.VirtualAlloc( rawptr(base_address), header_size + size, win32.MEM_RESERVE, win32.PAGE_READWRITE )
if result == nil {
alloc_error = .Out_Of_Memory
return
}
result = win32.VirtualAlloc( rawptr(base_address), header_size, win32.MEM_COMMIT, win32.PAGE_READWRITE )
if result == nil
{
switch err := win32.GetLastError(); err
{
case 0:
alloc_error = .Invalid_Argument
return
case WIN32_ERROR_INVALID_ADDRESS, WIN32_ERROR_COMMITMENT_LIMIT:
alloc_error = .Out_Of_Memory
return
}
alloc_error = .Out_Of_Memory
return
}
vmem.base_address = cast(^VirtualMemoryRegionHeader) result
vmem.reserve_start = cast([^]byte) (uintptr(vmem.base_address) + uintptr(header_size))
vmem.reserved = size
vmem.committed = header_size
alloc_error = .None
return
}
} // END: ODIN_OS == runtime.Odin_OS_Type.Windows

View File

@ -0,0 +1 @@
package sectr

View File

@ -0,0 +1 @@
package sectr

501
code/sectr/input/input.odin Normal file
View File

@ -0,0 +1,501 @@
// TODO(Ed) : This if its gets larget can be moved to its own package
package sectr
import "base:runtime"
AnalogAxis :: f32
AnalogStick :: struct {
X, Y : f32
}
DigitalBtn :: struct {
half_transitions : i32,
ended_down : b32
}
btn_pressed :: proc( btn : DigitalBtn ) -> b32 {
return btn.ended_down && btn.half_transitions > 0
}
btn_released :: proc ( btn : DigitalBtn ) -> b32 {
return btn.ended_down == false && btn.half_transitions > 0
}
MaxKeyboardKeys :: 256
KeyboardKey :: enum u32 {
null = 0x00,
enter = 0x01,
tab = 0x02,
space = 0x03,
bracket_open = 0x04,
bracket_close = 0x05,
semicolon = 0x06,
apostrophe = 0x07,
comma = 0x08,
period = 0x09,
// 0x0A
// 0x0B
// 0x0C
// 0x0D
// 0x0E
// 0x0F
caps_lock = 0x10,
scroll_lock = 0x11,
num_lock = 0x12,
left_alt = 0x13,
left_shit = 0x14,
left_control = 0x15,
right_alt = 0x16,
right_shift = 0x17,
right_control = 0x18,
// 0x19
// 0x1A
// 0x1B
// 0x1C
// 0x1D
// 0x1C
// 0x1D
escape = 0x1F,
F1 = 0x20,
F2 = 0x21,
F3 = 0x22,
F4 = 0x23,
F5 = 0x24,
F6 = 0x25,
F7 = 0x26,
F8 = 0x27,
F9 = 0x28,
F10 = 0x29,
F11 = 0x2A,
F12 = 0x2B,
print_screen = 0x2C,
pause = 0x2D,
// = 0x2E,
backtick = 0x2F,
nrow_0 = 0x30,
nrow_1 = 0x31,
nrow_2 = 0x32,
nrow_3 = 0x33,
nrow_4 = 0x34,
nrow_5 = 0x35,
nrow_6 = 0x36,
nrow_7 = 0x37,
nrow_8 = 0x38,
nrow_9 = 0x39,
hyphen = 0x3A,
equals = 0x3B,
backspace = 0x3C,
backslash = 0x3D,
slash = 0x3E,
// = 0x3F,
// = 0x40,
A = 0x41,
B = 0x42,
C = 0x43,
D = 0x44,
E = 0x45,
F = 0x46,
G = 0x47,
H = 0x48,
I = 0x49,
J = 0x4A,
K = 0x4B,
L = 0x4C,
M = 0x4D,
N = 0x4E,
O = 0x4F,
P = 0x50,
Q = 0x51,
R = 0x52,
S = 0x53,
T = 0x54,
U = 0x55,
V = 0x56,
W = 0x57,
X = 0x58,
Y = 0x59,
Z = 0x5A,
insert = 0x5B,
delete = 0x5C,
home = 0x5D,
end = 0x5E,
page_up = 0x5F,
page_down = 0x60,
npad_0 = 0x61,
npad_1 = 0x62,
npad_2 = 0x63,
npad_3 = 0x64,
npad_4 = 0x65,
npad_5 = 0x66,
npad_6 = 0x67,
npad_7 = 0x68,
npad_8 = 0x69,
npad_9 = 0x6A,
npad_decimal = 0x6B,
npad_equals = 0x6C,
npad_plus = 0x6D,
npad_minus = 0x6E,
npad_multiply = 0x6F,
npad_divide = 0x70,
npad_enter = 0x71,
count = 0x72
}
KeyboardState :: struct #raw_union {
keys : [MaxKeyboardKeys] DigitalBtn,
using individual : struct {
enter,
tab,
space,
bracket_open,
bracket_close,
semicolon,
apostrophe,
comma,
period : DigitalBtn,
__0x0A_0x0F_Unassigned__ : [ 6 * size_of( DigitalBtn )] u8,
caps_lock,
scroll_lock,
num_lock,
left_alt,
left_shift,
left_control,
right_alt,
right_shift,
right_control : DigitalBtn,
__0x19_0x1D_Unassigned__ : [ 6 * size_of( DigitalBtn )] u8,
escape,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12 : DigitalBtn,
print_screen,
pause : DigitalBtn,
__0x2E_Unassigned__ : [size_of(DigitalBtn)] u8,
backtick,
nrow_0,
nrow_1,
nrow_2,
nrow_3,
nrow_4,
nrow_5,
nrow_6,
nrow_7,
nrow_8,
nrow_9,
hyphen,
equals,
backspace : DigitalBtn,
backslash,
slash : DigitalBtn,
__0x3F_0x40_Unassigned__ : [ 2 * size_of(DigitalBtn)] u8,
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z : DigitalBtn,
insert,
delete,
home,
end,
page_up,
page_down : DigitalBtn,
npad_0,
npad_1,
npad_2,
npad_3,
npad_4,
npad_5,
npad_6,
npad_7,
npad_8,
npad_9,
npad_decimal,
npad_equals,
npad_plus,
npad_minus,
npad_multiply,
npad_divide,
npad_enter : DigitalBtn
}
}
MaxMouseBtns :: 16
MouseBtn :: enum u32 {
Left = 0x0,
Middle = 0x1,
Right = 0x2,
Side = 0x3,
Forward = 0x4,
Back = 0x5,
Extra = 0x6,
count
}
MouseState :: struct {
using _ : struct #raw_union {
btns : [16] DigitalBtn,
using individual : struct {
left, middle, right : DigitalBtn,
side, forward, back, extra : DigitalBtn
}
},
raw_pos, pos, delta : Vec2,
vertical_wheel, horizontal_wheel : AnalogAxis
}
mouse_world_delta :: #force_inline proc "contextless" () -> Vec2 {
using state := get_state()
cam := & state.project.workspace.cam
return input.mouse.delta * ( 1 / cam.zoom )
}
InputState :: struct {
keyboard : KeyboardState,
mouse : MouseState
}
import "core:os"
import c "core:c/libc"
import rl "vendor:raylib"
poll_input :: proc( old, new : ^ InputState )
{
profile(#procedure)
input_process_digital_btn :: proc( old_state, new_state : ^ DigitalBtn, is_down : b32 )
{
new_state.ended_down = is_down
had_transition := old_state.ended_down != new_state.ended_down
if had_transition {
new_state.half_transitions += 1
}
else {
new_state.half_transitions = 0
}
}
// Keyboard
{
// profile("Keyboard")
check_range :: proc( old, new : ^ InputState, start, end : i32 )
{
for id := start; id < end; id += 1
{
// TODO(Ed) : LOOK OVER THIS...
entry_old := & old.keyboard.keys[id - 1]
entry_new := & new.keyboard.keys[id - 1]
key_id := cast(KeyboardKey) id
is_down := cast(b32) rl.IsKeyDown( to_raylib_key(id) )
input_process_digital_btn( entry_old, entry_new, is_down )
}
}
DeadBound_1 :: 0x0A
DeadBound_2 :: 0x2E
DeadBound_3 :: 0x19
DeadBound_4 :: 0x3F
check_range( old, new, cast(i32) KeyboardKey.enter, DeadBound_1 )
check_range( old, new, cast(i32) KeyboardKey.caps_lock, DeadBound_2 )
check_range( old, new, cast(i32) KeyboardKey.escape, DeadBound_3 )
check_range( old, new, cast(i32) KeyboardKey.backtick, DeadBound_4 )
check_range( old, new, cast(i32) KeyboardKey.A, cast(i32) KeyboardKey.count )
}
// Mouse
{
// profile("Mouse")
// Process Buttons
for id : i32 = 0; id < i32(MouseBtn.count); id += 1
{
old_btn := & old.mouse.btns[id]
new_btn := & new.mouse.btns[id]
mouse_id := cast(MouseBtn) id
is_down := cast(b32) rl.IsMouseButtonDown( to_raylib_mouse_btn(id) )
input_process_digital_btn( old_btn, new_btn, is_down )
}
new.mouse.raw_pos = rl.GetMousePosition()
new.mouse.pos = render_to_screen_pos(new.mouse.raw_pos)
new.mouse.delta = rl.GetMouseDelta() * {1, -1}
new.mouse.vertical_wheel = rl.GetMouseWheelMove()
}
}
record_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
raw_data := slice_ptr( transmute(^ byte) input, size_of(InputState) )
file_write( replay_file, raw_data )
}
play_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
raw_data := slice_ptr( transmute(^ byte) input, size_of(InputState) )
total_read, result_code := file_read( replay_file, raw_data )
if result_code == os.ERROR_HANDLE_EOF {
file_rewind( replay_file )
load_snapshot( & Memory_App.snapshot )
}
}
to_raylib_key :: proc( key : i32 ) -> rl.KeyboardKey {
@static raylib_key_lookup_table := [?] rl.KeyboardKey {
rl.KeyboardKey.KEY_NULL,
rl.KeyboardKey.ENTER,
rl.KeyboardKey.TAB,
rl.KeyboardKey.SPACE,
rl.KeyboardKey.LEFT_BRACKET,
rl.KeyboardKey.RIGHT_BRACKET,
rl.KeyboardKey.SEMICOLON,
rl.KeyboardKey.APOSTROPHE,
rl.KeyboardKey.COMMA,
rl.KeyboardKey.PERIOD,
cast(rl.KeyboardKey) 0, // 0x0A
cast(rl.KeyboardKey) 0, // 0x0B
cast(rl.KeyboardKey) 0, // 0x0C
cast(rl.KeyboardKey) 0, // 0x0D
cast(rl.KeyboardKey) 0, // 0x0E
cast(rl.KeyboardKey) 0, // 0x0F
rl.KeyboardKey.CAPS_LOCK,
rl.KeyboardKey.SCROLL_LOCK,
rl.KeyboardKey.NUM_LOCK,
rl.KeyboardKey.LEFT_ALT,
rl.KeyboardKey.LEFT_SHIFT,
rl.KeyboardKey.LEFT_CONTROL,
rl.KeyboardKey.RIGHT_ALT,
rl.KeyboardKey.RIGHT_SHIFT,
rl.KeyboardKey.RIGHT_CONTROL,
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
cast(rl.KeyboardKey) 0, // 0x0F
rl.KeyboardKey.ESCAPE,
rl.KeyboardKey.F1,
rl.KeyboardKey.F2,
rl.KeyboardKey.F3,
rl.KeyboardKey.F4,
rl.KeyboardKey.F5,
rl.KeyboardKey.F7,
rl.KeyboardKey.F8,
rl.KeyboardKey.F9,
rl.KeyboardKey.F10,
rl.KeyboardKey.F11,
rl.KeyboardKey.F12,
rl.KeyboardKey.PRINT_SCREEN,
rl.KeyboardKey.PAUSE,
cast(rl.KeyboardKey) 0, // 0x2E
rl.KeyboardKey.GRAVE,
cast(rl.KeyboardKey) '0',
cast(rl.KeyboardKey) '1',
cast(rl.KeyboardKey) '2',
cast(rl.KeyboardKey) '3',
cast(rl.KeyboardKey) '4',
cast(rl.KeyboardKey) '5',
cast(rl.KeyboardKey) '6',
cast(rl.KeyboardKey) '7',
cast(rl.KeyboardKey) '8',
cast(rl.KeyboardKey) '9',
rl.KeyboardKey.MINUS,
rl.KeyboardKey.EQUAL,
rl.KeyboardKey.BACKSPACE,
rl.KeyboardKey.BACKSLASH,
rl.KeyboardKey.SLASH,
cast(rl.KeyboardKey) 0, // 0x3F
cast(rl.KeyboardKey) 0, // 0x40
rl.KeyboardKey.A,
rl.KeyboardKey.B,
rl.KeyboardKey.C,
rl.KeyboardKey.D,
rl.KeyboardKey.E,
rl.KeyboardKey.F,
rl.KeyboardKey.G,
rl.KeyboardKey.H,
rl.KeyboardKey.I,
rl.KeyboardKey.J,
rl.KeyboardKey.K,
rl.KeyboardKey.L,
rl.KeyboardKey.M,
rl.KeyboardKey.N,
rl.KeyboardKey.O,
rl.KeyboardKey.P,
rl.KeyboardKey.Q,
rl.KeyboardKey.R,
rl.KeyboardKey.S,
rl.KeyboardKey.T,
rl.KeyboardKey.U,
rl.KeyboardKey.V,
rl.KeyboardKey.W,
rl.KeyboardKey.X,
rl.KeyboardKey.Y,
rl.KeyboardKey.Z,
rl.KeyboardKey.INSERT,
rl.KeyboardKey.DELETE,
rl.KeyboardKey.HOME,
rl.KeyboardKey.END,
rl.KeyboardKey.PAGE_UP,
rl.KeyboardKey.PAGE_DOWN,
rl.KeyboardKey.KP_0,
rl.KeyboardKey.KP_1,
rl.KeyboardKey.KP_2,
rl.KeyboardKey.KP_3,
rl.KeyboardKey.KP_4,
rl.KeyboardKey.KP_5,
rl.KeyboardKey.KP_6,
rl.KeyboardKey.KP_7,
rl.KeyboardKey.KP_8,
rl.KeyboardKey.KP_9,
rl.KeyboardKey.KP_DECIMAL,
rl.KeyboardKey.KP_EQUAL,
rl.KeyboardKey.KP_ADD,
rl.KeyboardKey.KP_SUBTRACT,
rl.KeyboardKey.KP_MULTIPLY,
rl.KeyboardKey.KP_DIVIDE,
rl.KeyboardKey.KP_ENTER }
return raylib_key_lookup_table[ key ]
}
to_raylib_mouse_btn :: proc( btn : i32 ) -> rl.MouseButton {
@static raylib_mouse_btn_lookup_table := [?] rl.MouseButton {
rl.MouseButton.LEFT,
rl.MouseButton.MIDDLE,
rl.MouseButton.RIGHT,
rl.MouseButton.SIDE,
rl.MouseButton.FORWARD,
rl.MouseButton.BACK,
rl.MouseButton.EXTRA,
}
return raylib_mouse_btn_lookup_table[ btn ]
}

132
code/sectr/math/math.odin Normal file
View File

@ -0,0 +1,132 @@
// General mathematical constructions used for the prototype
package sectr
import "core:math"
// These are the same as the runtime constants for memory units just using a more general name when not refering to bytes
Kilo :: Kilobyte
Mega :: Megabyte
Giga :: Gigabyte
Tera :: Terabyte
Peta :: Petabyte
Exa :: Exabyte
Axis2 :: enum i32 {
Invalid = -1,
X = 0,
Y = 1,
Count,
}
f32_Infinity :: 0x7F800000
f32_Min :: 0x00800000
// Note(Ed) : I don't see an intrinsict available anywhere for this. So I'll be using the Terathon non-sse impl
// Inverse Square Root
// C++ Source https://github.com/EricLengyel/Terathon-Math-Library/blob/main/TSMath.cpp#L191
inverse_sqrt_f32 :: proc "contextless" ( value : f32 ) -> f32
{
if ( value < f32_Min) {
return f32_Infinity
}
value_u32 := transmute(u32) value
initial_approx := 0x5F375A86 - (value_u32 >> 1)
refined_approx := transmute(f32) initial_approx
// NewtonRaphson method for getting better approximations of square roots
// Done twice for greater accuracy.
refined_approx = refined_approx * (1.5 - value * 0.5 * refined_approx * refined_approx )
refined_approx = refined_approx * (1.5 - value * 0.5 * refined_approx * refined_approx )
// refined_approx = (0.5 * refined_approx) * (3.0 - value * refined_approx * refined_approx)
// refined_approx = (0.5 * refined_approx) * (3.0 - value * refined_approx * refined_approx)
return refined_approx
}
is_power_of_two_u32 :: #force_inline proc "contextless" ( value : u32 ) -> b32
{
return value != 0 && ( value & ( value - 1 )) == 0
}
mov_avg_exp_f32 := #force_inline proc "contextless" ( alpha, delta_interval, last_value : f32 ) -> f32
{
result := (delta_interval * alpha) + (delta_interval * (1.0 - alpha))
return result
}
mov_avg_exp_f64 := #force_inline proc "contextless" ( alpha, delta_interval, last_value : f64 ) -> f64
{
result := (delta_interval * alpha) + (delta_interval * (1.0 - alpha))
return result
}
import "core:math/linalg"
Quat128 :: quaternion128
Matrix2 :: matrix [2, 2] f32
Vec2i :: [2]i32
Vec3i :: [3]i32
vec2i_to_vec2 :: #force_inline proc "contextless" (v : Vec2i) -> Vec2 {return transmute(Vec2) v}
vec3i_to_vec3 :: #force_inline proc "contextless" (v : Vec3i) -> Vec3 {return transmute(Vec3) v}
#region("Range2")
Range2 :: struct #raw_union {
using min_max : struct {
min, max : Vec2
},
using pts : struct {
p0, p1 : Vec2
},
using xy : struct {
x0, y0 : f32,
x1, y1 : f32,
},
using side : struct {
left, bottom : f32,
right, top : f32,
},
ratio : struct {
x, y : f32,
},
// TODO(Ed) : Test these
array : [4]f32,
mat : matrix[2, 2] f32,
}
UnitRange2 :: distinct Range2
range2 :: #force_inline proc "contextless" ( a, b : Vec2 ) -> Range2 {
result := Range2 { pts = { a, b } }
return result
}
add_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> Range2 {
result := Range2 { pts = {
a.p0 + b.p0,
a.p1 + b.p1,
}}
return result
}
sub_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> Range2 {
// result := Range2 { array = a.array - b.array }
result := Range2 { mat = a.mat - b.mat }
return result
}
equal_range2 :: #force_inline proc "contextless" ( a, b : Range2 ) -> b32 {
result := a.p0 == b.p0 && a.p1 == b.p1
return b32(result)
}
size_range2 :: #force_inline proc "contextless" ( value : Range2 ) -> Vec2 {
return { value.p1.x - value.p0.x, value.p0.y - value.p1.y }
}
#endregion("Range2")

36
code/sectr/math/pga2.odin Normal file
View File

@ -0,0 +1,36 @@
package sectr
/*
Vec2 : 2D Vector 4D Extension (x, y, z : 0, w : 0)
Bivec2 : 2D Bivector
Transform2 : 3x3 Matrix where 3rd row is always (0, 0, 1)
*/
Vec2 :: [2]f32
Bivec2 :: distinct f32
Tansform2 :: matrix [3, 3] f32
UnitVec2 :: distinct Vec2
Rotor2 :: struct {
bv : Bivec2,
s : f32, // Scalar
}
rotor2_to_complex64 :: #force_inline proc( rotor : Rotor2 ) -> complex64 { return transmute(complex64) rotor; }
vec2 :: #force_inline proc "contextless" ( x, y : f32 ) -> Vec2 { return {x, y} }
dot_vec2 :: proc "contextless" ( a, b : Vec2 ) -> (s : f32) {
x := a.x * b.x
y := a.y + b.y
s = x + y
return
}
/*
PointFlat2 : CGA: 2D flat point (x, y, z)
Line : PGA: 2D line (x, y, z)
*/
Point2 :: distinct Vec2
PointFlat2 :: distinct Vec3
Line2 :: distinct Vec3

239
code/sectr/math/pga3.odin Normal file
View File

@ -0,0 +1,239 @@
package sectr
/*
Vec3 : 3D Vector (x, y, z) (3x1) 4D Expression : (x, y, z, 0)
Bivec3 : 3D Bivector (yz, zx, xy) (3x1)
Trivec3 : 3D Trivector (xyz) (1x1)
Rotor3 : 3D Rotation Versor-Transform (4x1)
Motor3 : 3D Rotation & Translation Transform (4x2)
*/
Vec3 :: [3]f32
Vec4 :: [4]f32
Bivec3 :: struct #raw_union {
using _ : struct { yz, zx, xy : f32 },
using xyz : Vec3,
}
Trivec3 :: distinct f32
Rotor3 :: struct {
using bv : Bivec3,
s : f32, // Scalar
}
Shifter3 :: struct {
using bv : Bivec3,
s : f32, // Scalar
}
Motor3 :: struct {
rotor : Rotor3,
md : Shifter3,
}
UnitVec3 :: distinct Vec3
UnitVec4 :: distinct Vec4
UnitBivec3 :: distinct Bivec3
//region Vec3
vec3_via_f32s :: #force_inline proc "contextless" (x, y, z : f32) -> Vec3 { return {x, y, z} }
// complement_vec3 :: #force_inline proc "contextless" ( v : Vec3 ) -> Bivec3 {return transmute(Bivec3) v}
cross_vec3 :: proc "contextless" (a, b : Vec3) -> (v : Vec3) {
v = vec3( wedge(a, b))
return
}
dot_vec3 :: proc "contextless" ( a, b : Vec3 ) -> (s : f32) {
mult := a * b // array multiply
s = mult.x + mult.y + mult.z
return
}
inverse_mag_vec3 :: proc "contextless" (v : Vec3) -> (result : f32) {
square := pow2(v)
result = inverse_sqrt( square )
return
}
magnitude_vec3 :: proc "contextless" (v : Vec3) -> (mag : f32) {
square := pow2(v)
mag = sqrt(square)
return
}
normalize_vec3 :: proc "contextless" (v : Vec3) -> (unit_v : UnitVec3) {
unit_v = transmute(UnitVec3) (v * inverse_mag(v))
return
}
pow2_vec3 :: #force_inline proc "contextless" ( v : Vec3 ) -> (s : f32) { return dot(v, v) }
project_vec3 :: proc "contextless" ( a, b : Vec3 ) -> ( a_to_b : Vec3 ) {
return
}
reject_vec3 :: proc "contextless" ( a, b : Vec3 ) -> ( a_from_b : Vec3 ) {
return
}
project_v3_unitv3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> (v_to_u : Vec3) {
inner := dot(v, u)
v_to_u = (transmute(Vec3) u) * inner
return
}
project_unitv3_v3 :: #force_inline proc "contextless" (u : UnitVec3, v : Vec3) -> (u_to_v : Vec3) {
inner := dot(u, v)
u_to_v = v * inner
return
}
// Anti-wedge of vectors
regress_vec3 :: proc "contextless" ( a, b : Vec3 ) -> f32 {
return a.x * b.y - a.y * b.x
}
reject_v3_unitv3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> ( v_from_u : Vec3) {
inner := dot(v, u)
v_from_u = (v - (transmute(Vec3) u)) * inner
return
}
reject_unitv3_v3 :: proc "contextless" ( v : Vec3, u : UnitVec3 ) -> ( u_from_v : Vec3) {
inner := dot(u, v)
u_from_v = ((transmute(Vec3) u) - v) * inner
return
}
// Combines the deimensions that are present in a & b
wedge_vec3 :: proc "contextless" (a, b : Vec3) -> (bv : Bivec3) {
yzx_zxy := a.yzx * b.zxy
zxy_yzx := a.zxy * b.yzx
bv = transmute(Bivec3) (yzx_zxy - zxy_yzx)
return
}
//endregion Vec3
//region Bivec3
bivec3_via_f32s :: #force_inline proc "contextless" (yz, zx, xy : f32) -> Bivec3 {return { xyz = {yz, zx, xy} }}
complement_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> Bivec3 {return transmute(Bivec3) b.xyz}
//region Operations isomoprhic to vectors
negate_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> Bivec3 {return transmute(Bivec3) -b.xyz}
add_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz + b.xyz)}
sub_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz - b.xyz)}
mul_bivec3 :: #force_inline proc "contextless" (a, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (a.xyz * b.xyz)}
mul_bivec3_f32 :: #force_inline proc "contextless" (b : Bivec3, s : f32) -> Bivec3 {return transmute(Bivec3) (b.xyz * s)}
mul_f32_bivec3 :: #force_inline proc "contextless" (s : f32, b : Bivec3) -> Bivec3 {return transmute(Bivec3) (s * b.xyz)}
div_bivec3_f32 :: #force_inline proc "contextless" (b : Bivec3, s : f32) -> Bivec3 {return transmute(Bivec3) (b.xyz / s)}
inverse_mag_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return inverse_mag_vec3(b.xyz)}
magnitude_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return magnitude_vec3 (b.xyz)}
normalize_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> UnitBivec3 {return transmute(UnitBivec3) normalize_vec3(b.xyz)}
squared_mag_bivec3 :: #force_inline proc "contextless" (b : Bivec3) -> f32 {return pow2_vec3(b.xyz)}
//endregion Operations isomoprhic to vectors
// The wedge of a bi-vector in 3D vector space results in a Trivector represented as a scalar.
// This scalar usually resolves to zero with six possible exceptions that lead to the negative volume element.
wedge_bivec3 :: proc ( a, b : Bivec3 ) -> f32 {
s := a.yz + b.yz + a.zx + b.zx + a.xy + b.xy
return s
}
// anti-wedge (Combines dimensions that are absent from a & b)
regress_bivec3 :: #force_inline proc "contextless" ( a, b : Bivec3 ) -> Vec3 {return wedge_vec3(vec3(a), vec3(b))}
regress_bivec3_v :: #force_inline proc "contextless" (b : Bivec3, v : Vec3) -> f32 {return regress_vec3(b.xyz, v)}
regress_v3_bivec3 :: #force_inline proc "contextless" (v : Vec3, b : Bivec3) -> f32 {return regress_vec3(b.xyz, v)}
//endregion Bivec3
//region Rotor3
rotor3_via_comps :: proc "contextless" (yz, zx, xy, scalar : f32) -> (rotor : Rotor3) {
rotor = Rotor3 {bivec3_via_f32s(yz, zx, xy), scalar}
return
}
rotor3_via_bv_s :: proc "contextless" (bv : Bivec3, scalar : f32) -> (rotor : Rotor3) {
rotor = Rotor3 {bv, scalar}
return
}
rotor3_via_from_to :: proc "contextless" ( from, to : Vec3 ) -> (rotor : Rotor3) {
scalar := 1 + dot( from, to )
return
}
inverse_mag_rotor3 :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
return
}
magnitude_rotor3 :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
return
}
squared_mag :: proc "contextless" (rotor : Rotor3) -> (s : f32) {
return
}
reverse_rotor3 :: proc "contextless" (rotor : Rotor3) -> (reversed : Rotor3) {
reversed = { negate_bivec3(rotor.bv), rotor.s }
return
}
//endregion Rotor3
//region Flat Projective Geometry
Point3 :: distinct Vec3
PointFlat3 :: distinct Vec4
Line3 :: struct {
weight : Vec3,
bulk : Bivec3,
}
Plane3 :: distinct Vec4 // 4D Anti-vector
// aka: wedge operation for points
join_point3 :: proc "contextless" (p, q : Point3) -> (l : Line3) {
weight := sub(q, p)
bulk := wedge(vec3(p), vec3(q))
l = {weight, bulk}
return
}
join_pointflat3 :: proc "contextless" (p, q : PointFlat3) -> (l : Line3) {
weight := vec3(
p.w * q.x - p.x * q.w,
p.w * q.y - p.y * q.w,
p.w * q.z - p.z * q.w
)
bulk := wedge(vec3(p), vec3(q))
l = { weight, bulk}
return
}
sub_point3 :: proc "contextless" (a, b : Point3) -> (v : Vec3) {
v = vec3(a) - vec3(b)
return
}
//endregion Flat Projective Geometry
//region Rational Trig
quadrance :: proc "contextless" (a, b : Point3) -> (q : f32) {
q = pow2( sub(a, b))
return
}
// Assumes the weight component is normalized.
spread :: proc "contextless" (l, m : Line3) -> (s : f32) {
s = dot(l.weight, m.weight)
return
}
//endregion Rational Trig

View File

@ -0,0 +1,24 @@
package sectr
// A dump of equivalent symbol generatioon (because the toolchain can't do it yet)
// Symbol alias tables are in grim.odin
vec3_to_bivec3 :: #force_inline proc "contextless" (v : Vec3) -> Bivec3 {return transmute(Bivec3) v }
bivec3_to_vec3 :: #force_inline proc "contextless" (bv : Bivec3) -> Vec3 {return transmute(Vec3) bv }
rotor3_to_quat128 :: #force_inline proc "contextless" (rotor : Rotor3) -> Quat128 {return transmute(Quat128) rotor}
unitvec3_to_vec3 :: #force_inline proc "contextless" (v : UnitVec3) -> Vec3 {return transmute(Vec3) v }
unitvec4_to_vec4 :: #force_inline proc "contextless" (v : UnitVec4) -> Vec4 {return transmute(Vec4) v }
// plane_to_vec4 :: #force_inline proc "contextless" (p : Plane3) -> Vec4 {return transmute(Vec4) p}
point3_to_vec3 :: #force_inline proc "contextless" (p : Point3) -> Vec3 {return transmute(Vec3) p}
pointflat3_to_vec3 :: #force_inline proc "contextless" (p : PointFlat3) -> Vec3 {return { p.x, p.y, p.z }}
vec3_to_point3 :: #force_inline proc "contextless" (v : Vec3) -> Point3 {return transmute(Point3) v}
cross_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, u : UnitVec3) -> Vec3 {return cross_vec3(v, transmute(Vec3) u)}
cross_unitv3_vs :: #force_inline proc "contextless" (u : UnitVec3, v : Vec3) -> Vec3 {return cross_vec3(transmute(Vec3) u, v)}
dot_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, unit_v : UnitVec3) -> f32 {return dot_vec3(v, transmute(Vec3) unit_v)}
dot_unitv3_vs :: #force_inline proc "contextless" (unit_v : UnitVec3, v : Vec3) -> f32 {return dot_vec3(v, transmute(Vec3) unit_v)}
wedge_v3_unitv3 :: #force_inline proc "contextless" (v : Vec3, unit_v : UnitVec3) -> Bivec3 {return wedge_vec3(v, transmute(Vec3) unit_v)}
wedge_unitv3_vs :: #force_inline proc "contextless" (unit_v : UnitVec3, v : Vec3) -> Bivec3 {return wedge_vec3(transmute(Vec3) unit_v, v)}

View File

@ -0,0 +1,244 @@
/* Parser: Code Agnostic
This is a 'coding langauge agnostic' parser.
Its not meant to parse regular textual formats used in natural langauges (paragraphs, sentences, etc).
It instead is meant to encode constructs significant to most programming languages.
AST Types:
* Word
* Operator
* BracketsScope
This parser supports parsing whitepsace asts or raw text content.
Operator tokens are not parsed into expressions (binary or polish) Thats beyond the scope of this parser.
*/
package sectr
PA_TokenType :: enum u32 {
Invalid,
B_Literal_Begin,
Integer, // 12345
Deciaml, // 123.45
Word, // Any string of visible characters that doesn't use an operator symbol.
B_Literal_End,
B_Operator_Begin,
Ampersand, // &
Ampersand_Double, // &&
Ampersand_Double_Equal, // &&=
Ampersand_Equal, // &=
And_Not, // &~
And_Not_Equal, // &~=
Arrow_Left, // <-
Arrow_Right, // ->
Asterisk, // *
Asterisk_Equal, // *=
At, // @
Backslash, // \
Backslash_Double, // \\
Brace_Open, // {
Brace_Close, // }
Bracket_Open, // [
Bracket_Close, // ]
Caret, // ^
Caret_Equal, // ^=
Colon, // :
Comma, // ,
Dash_Triple, // ---
Dollar, // $
Ellispis_Dobule, // ..
Ellipsis_Triple, // ...
Equal, // =
Equal_Double, // ==
Exclamation, // !
Exclamation_Equal, // !=
Greater, // >
Greater_Double, // >>
Greater_Double_Equal, // >>=
Greater_Equal, // >=
Hash, // #
Lesser, // <
Lesser_Double, // <<
Lesser_Double_Equal, // <<=
Lesser_Equal, // <=
Minus, // -
Minus_Double, // --
Minus_Equal, // -=
Parenthesis_Open, // (
Parenthesis_Close, // )
Percent, // %
Percent_Equal, // %=
Percent_Double, // %%
Percent_Dboule_Equal, // %%=
Period, // .
Plus, // +
Plus_Dobule, // ++
Plus_Equal, // +=
Question, // ?
Semi_Colon, // ;
Slash, // /
Slash_Equal, // /=
Slash_Double, //
Tilde, // ~
Tilde_Equal, // ~=
Vert_Bar, // |
Vert_Bar_Double, // ||
Vert_Bar_Equal, // |=
Vert_Bar_Double_Equal, // |==
B_Operator_End,
Count,
}
PA_Token_Str_Table := [PA_TokenType.Count] string {
"____Invalid____", // Invalid,
"____B_Literal_Begin____", // B_Literal_Begin,
"____Integer____", // Integer, // 12345
"____Deciaml____", // 123.45
"____Word____", // Any string of visible characters that doesn't use an operator symbol.
"____B_Literal_Begin____", // B_Literal_End,
"____B_Operator_Begin____", // B_Operator_Begin,
"&", // Ampersand, // &
"&&", // Ampersand_Double, // &&
"&&=", // Ampersand_Double_Equal, // &&=
"&=", // Ampersand_Equal, // &=
"&~", // And_Not, // &~
"&~=", // And_Not_Equal, // &~=
"<-", // Arrow_Left, // <-
"->", // Arrow_Right, // ->
"*", // Asterisk, // *
"*=", // Asterisk_Equal, // *=
"@", // At, // @
"\\", // Backslash, // \
"\\\\", // Backslash_Double, // \\
"{", // Brace_Open, // {
"}", // Brace_Close, // }
"[", // Bracket_Open, // [
"]", // Bracket_Close, // ]
"^", // Caret, // ^
"^=", // Caret_Equal, // ^=
":", // Colon, // :
",", // Comma, // ,
"---", // Dash_Triple, // ---
"$", // Dollar, // $
"..", // Ellispis_Dobule, // ..
"...", // Ellipsis_Triple, // ...
"=", // Equal, // =
"==", // Equal_Double, // ==
"!", // Exclamation, // !
"!=", // Exclamation_Equal, // !=
">", // Greater, // >
">>", // Greater_Double, // >>
">>=", // Greater_Double_Equal, // >>=
">=", // Greater_Equal, // >=
"#", // Hash, // #
"<", // Lesser, // <
"<<", // Lesser_Double, // <<
"<<=", // Lesser_Double_Equal, // <<=
"<=", // Lesser_Equal, // <=
"-", // Minus, // -
"--", // Minus_Double, // --
"-=", // Minus_Equal, // -=
"(", // Parenthesis_Open, // (
")", // Parenthesis_Close, // )
"%", // Percent, // %
"%=", // Percent_Equal, // %=
"%%", // Percent_Double, // %%
"%%=", // Percent_Dboule_Equal, // %%=
".", // Period, // .
"+", // Plus, // +
"++", // Plus_Dobule, // ++
"+=", // Plus_Equal, // +=
"?", // Question, // ?
";", // Semi_Colon, // ;
"/", // Slash, // /
"/=", // Slash_Equal, // /=
"//", // Slash_Double, //
"~", // Tilde, // ~
"~=", // Tilde_Equal, // ~=
"|", // Vert_Bar, // |
"||", // Vert_Bar_Double, // ||
"|=", // Vert_Bar_Equal, // |=
"//=", // Vert_Bar_Double_Equal, //=
"____B_Operator_End____", // B_Operator_End,
}
PA_Token :: struct {
type : PA_TokenType,
line, column : u32,
ptr : ^rune,
}
PA_LiteralType :: enum u32 {
Integer,
Decimal,
Word,
}
PA_Literal :: struct {
type : PA_LiteralType,
token : ^PA_Token,
}
PA_OperatorType :: enum u32 {
}
PA_Operator :: struct {
type : PA_OperatorType,
token : ^PA_Token,
}
PA_BracketScopeType :: enum u32 {
Angled,
Curly,
Square,
Round,
}
PA_BracketScope :: struct {
type : PA_BracketScopeType,
token : ^PA_Token,
body : ^PA_AST,
}
PA_AST :: union {
}
// Changes parse behavior for specific tokens.
PA_ParsePolicy :: struct {
scope_detect_angled : b8,
scope_detect_curly : b8,
scope_detect_square : b8,
scope_detect_round : b8,
}
PA_ParseError :: struct {
token : ^ PA_Token,
msg : string,
}
PA_ParseError_Max :: 32
PA_NodeArray_ReserveSize :: 4 * Kilobyte
PA_ParseResult :: struct {
content : string,
runes : []rune,
tokens : Array(PA_Token),
pws_ast : ^PWS_AST,
nodes : Array(PA_AST), // Switch this to a pool?
errors : [PA_ParseError_Max] PA_ParseError
}
pa_parse_text :: proc( content : string, allocator : Allocator ) -> ( PA_ParseResult, AllocatorError )
{
return {}, AllocatorError.None
}
pa_parse_ws_ast :: proc( ast : ^PWS_AST, allocator : Allocator ) -> ( PA_ParseResult, AllocatorError )
{
return {}, AllocatorError.None
}

View File

@ -0,0 +1,14 @@
/* Parser : Code Formatting
This is a prototype parser meant to parse whitespace formatting constructs used in text based languages.
These include indentation of a block, spacial alignment of similar statement components, etc.
This would be used to have awareness of constructs having associating with each other via formatting.
AST Types:
* Statement
* Block-Indent Group
* Aligned-Statements
*/
package sectr

View File

@ -0,0 +1,17 @@
/* Parser: Odin Frontend AST (WYSIWYG)
This is a parser to generate and manage a WYSIWYG variant of an Odin AST.
The AST is naturally meant to be used for frontend interface, not backend.
*/
package sectr
PF_Odin_TokenType :: enum u32 {
placeholder,
}
POdin_Token :: struct {
placeholder : int,
}
POdin_AST :: struct {
placeholder : int,
}

View File

@ -0,0 +1,374 @@
/* Parser: Whitespace
This is a prototype parser meant to only parse whitespace from visible blocks of code.
Its meant to be the most minimal useful AST with coupling to traditional text file formatting.
All symbols related directly to the parser are prefixed with the PWS_ namespace.
The AST is composed of the following node types:
* Visible
* Spaces
* Tabs
* Line
AST_Visible tracks a slice of visible codepoints.
It tracks a neighboring ASTs (left or right) which should always be Spaces, or Tabs.
AST_Spaces tracks a slice of singluar or consecutive Spaces.
Neighboring ASTS should either be Visible, Tabs.
AST_Tabs tracks a slice of singlar or consectuive Tabs.
Neighboring ASTS should be either Visible or Spaces.
AST_Line tracks a slice of AST nodes of Visible, Spaces, or Tabs that terminate with a New-Line token.
Neighboring ASTS are only Lines.
The ParseData struct will contain an Array of AST_Line. This represents the entire AST where the root is the first entry.
ASTs keep track of neighboring ASTs in double-linked list pattern for ease of use.
This may be removed in the future for perforamance reasons,
since this is a prototype it will only be removed if there is a performance issue.
Because this parser is so primtive, it can only be
manually constructed via an AST editor or from parsed text.
So there is only a parser directly dealing with text.
If its constructed from an AST-Editor. There will not be a content string referencable or runes derived fromt hat content string.
Instead the AST's content will directly contain the runes associated.
*/
package sectr
import "core:os"
Rune_Space :: ' '
Rune_Tab :: '\t'
Rune_Carriage_Return :: '\r'
Rune_Line_Feed :: '\n'
// Rune_Tab_Vertical :: '\v'
PWS_TokenType :: enum u32 {
Invalid,
Visible,
Spaces,
Tabs,
New_Line,
End_Of_File,
Count,
}
// TODO(Ed) : The runes and token arrays should be handled by a slab allocator
// This can grow in undeterministic ways, persistent will get very polluted otherwise.
PWS_LexResult :: struct {
tokens : Array(PWS_Token),
}
PWS_Token :: struct {
type : PWS_TokenType,
line, column : u32,
content : StrRunesPair,
}
PWS_AST_Type :: enum u32 {
Invalid,
Visible,
Spaces,
Tabs,
Line,
Count,
}
PWS_AST :: struct {
using links : DLL_NodeFull(PWS_AST),
type : PWS_AST_Type,
line, column : u32,
content : StrRunesPair,
}
PWS_ParseError :: struct {
token : ^PWS_Token,
msg : string,
}
PWS_ParseError_Max :: 32
PWS_TokenArray_ReserveSize :: 128
PWS_NodeArray_ReserveSize :: 32 * Kilobyte
PWS_LineArray_ReserveSize :: 32
// TODO(Ed) : The ast arrays should be handled by a slab allocator dedicated to PWS_ASTs
// This can grow in undeterministic ways, persistent will get very polluted otherwise.
PWS_ParseResult :: struct {
content : string,
tokens : Array(PWS_Token),
nodes : Array(PWS_AST), // Nodes should be dumped in a pool.
lines : Array( ^PWS_AST),
errors : [PWS_ParseError_Max] PWS_ParseError,
}
PWS_LexerData :: struct {
using result : PWS_LexResult,
content : string,
previous_rune : rune,
current_rune : rune,
previous : PWS_TokenType,
line : u32,
column : u32,
start : int,
length : int,
current : PWS_Token,
}
pws_parser_lex :: proc ( text : string, allocator : Allocator ) -> ( PWS_LexResult, AllocatorError )
{
bytes := transmute([]byte) text
log( str_fmt_tmp( "lexing: %v ...", (len(text) > 30 ? transmute(string) bytes[ :30] : text) ))
profile(#procedure)
using lexer : PWS_LexerData
context.user_ptr = & lexer
content = text
if len(text) == 0 {
ensure( false, "Attempted to lex nothing")
return result, .None
}
rune_type :: proc( codepoint : rune ) -> PWS_TokenType
{
using self := context_ext( PWS_LexerData)
switch codepoint
{
case Rune_Space:
return PWS_TokenType.Spaces
case Rune_Tab:
return PWS_TokenType.Tabs
case Rune_Line_Feed:
return PWS_TokenType.New_Line
// Support for CRLF format
case Rune_Carriage_Return:
{
if previous_rune == 0 {
return PWS_TokenType.Invalid
}
// Assume for now its a new line
return PWS_TokenType.New_Line
}
}
// Everything that isn't the supported whitespace code points is considered 'visible'
// Eventually we should support other types of whitespace
return PWS_TokenType.Visible
}
alloc_error : AllocatorError
// tokens, alloc_error = array_init_reserve( PWS_Token, allocator, Kilobyte * 4 )
tokens, alloc_error = array_init_reserve( PWS_Token, allocator, PWS_TokenArray_ReserveSize )
if alloc_error != AllocatorError.None {
ensure(false, "Failed to allocate token's array")
return result, alloc_error
}
line = 0
column = 0
make_token :: proc ( byte_offset : int ) -> AllocatorError
{
self := context_ext( PWS_LexerData); using self
if previous_rune == Rune_Carriage_Return && current_rune != Rune_Line_Feed {
ensure(false, "Rouge Carriage Return")
}
start_ptr := uintptr( raw_data(content)) + uintptr(start)
token_slice := transmute(string) byte_slice( rawptr(start_ptr), length )
current.content = str_intern( token_slice )
start = byte_offset
length = 0
line += cast(u32) (current.type == .New_Line)
column = 0
return array_append( & tokens, current )
}
last_rune : rune
last_byte_offset : int
for codepoint, byte_offset in text
{
type := rune_type( codepoint )
current_rune = codepoint
if (current.type != type && previous != .Invalid) ||
( previous_rune != Rune_Carriage_Return && current.type == .New_Line )
{
alloc_error = make_token( byte_offset )
if alloc_error != AllocatorError.None {
ensure(false, "Failed to append token to token array")
return lexer, alloc_error
}
}
current.type = type
current.line = line
current.column = column
column += 1
length += 1
previous = current.type
previous_rune = codepoint
last_byte_offset = byte_offset
}
make_token( last_byte_offset )
return result, alloc_error
}
PWS_ParseData :: struct {
using result : PWS_ParseResult,
left : u32,
head : [^]PWS_Token,
line : PWS_AST,
prev_line : ^PWS_AST,
}
pws_parser_parse :: proc( text : string, allocator : Allocator ) -> ( PWS_ParseResult, AllocatorError )
{
bytes := transmute([]byte) text
profile(#procedure)
using parser : PWS_ParseData
context.user_ptr = & result
if len(text) == 0 {
ensure( false, "Attempted to lex nothing")
return result, .None
}
lex, alloc_error := pws_parser_lex( text, allocator = allocator )
verify( alloc_error == nil, "Allocation faiure in lex")
tokens = lex.tokens
log( str_fmt_tmp( "parsing: %v ...", (len(text) > 30 ? transmute(string) bytes[ :30] : text) ))
// TODO(Ed): Change this to use a node pool
nodes, alloc_error = array_init_reserve( PWS_AST, allocator, PWS_NodeArray_ReserveSize )
verify( alloc_error == nil, "Allocation failure creating nodes array")
parser.lines, alloc_error = array_init_reserve( ^PWS_AST, allocator, PWS_LineArray_ReserveSize )
verify( alloc_error == nil, "Allocation failure creating line array")
//region Helper procs
eat_line :: #force_inline proc()
{
self := context_ext( PWS_ParseData); using self
tok := cast( ^PWS_Token) head
line.type = .Line
line.line = tok.line
line.column = tok.column
line.content = tok.content
alloc_error := array_append( & nodes, line )
verify( alloc_error == nil, "Allocation failure appending node")
node := & nodes.data[ nodes.num - 1 ]
// TODO(Ed): Review this with multiple line test
dll_push_back( & prev_line, node )
prev_line = node
// Debug build compile error
// alloc_error = array_append( & lines, prev_line )
// verify( alloc_error == nil, "Allocation failure appending node")
line = {}
}
//endregion
head = & tokens.data[0]
left = u32(tokens.num)
// Parse Line
for ; left > 0;
{
type : PWS_AST_Type
#partial switch head[0].type
{
case .Tabs:
type = .Tabs
case .Spaces:
type = .Spaces
case .Visible:
type = .Visible
case .New_Line:
eat_line()
alloc_error = array_append( & parser.lines, prev_line )
verify( alloc_error == nil, "Allocation failure appending node")
case PWS_TokenType.End_Of_File:
}
if type != .Line
{
tok := cast( ^PWS_Token) head
ast : PWS_AST
ast.type = type
ast.line = tok.line
ast.column = tok.column
ast.content = tok.content
// Compiler Error (-Debug)
// prev_node = array_back( nodes )
prev_node : ^PWS_AST = nil
if nodes.num > 0 {
prev_node = & nodes.data[ nodes.num - 1 ]
}
alloc_error := array_append( & nodes, ast )
verify( alloc_error == nil, "Allocation failure appending node")
node := & nodes.data[ nodes.num - 1 ]
// dll_push_back( & prev_node, last_node )
{
if prev_node != nil
{
node.prev = prev_node
prev_node.next = node
}
}
// dll_fl_append( & line, last_node )
if line.first == nil {
line.first = node
line.last = node
}
else {
line.last = node
}
}
head = head[ 1:]
left -= 1
}
if line.first != nil {
eat_line()
alloc_error = array_append( & parser.lines, prev_line )
verify( alloc_error == nil, "Allocation failure appending node")
}
return result, alloc_error
}

View File

@ -0,0 +1,23 @@
package sectr
import "core:encoding/json"
import "core:reflect"
// TODO(Ed) : Generic Unmarshling of json objects (There should be a way I believe todo this generically but the reflect library is not well documented)
vec2_json_unmarshal :: proc( value : ^ json.Value ) -> Vec2 {
json_v := value.(json.Array)
return {
f32(json_v[0].(json.Float)),
f32(json_v[1].(json.Float)),
}
}
color_json_unmarshal :: proc( value : ^ json.Value ) -> Color {
json_color := value.(json.Array)
r := u8(json_color[0].(json.Float))
g := u8(json_color[1].(json.Float))
b := u8(json_color[2].(json.Float))
a := u8(json_color[3].(json.Float))
return { r, g, b, a }
}

View File

@ -0,0 +1,26 @@
package sectr
/*
Project: Encapsulation of all things a user can do separate from the core app behavior
that is managed independetly of it.
*/
// PMDB
CodeBase :: struct {
placeholder : int,
}
ProjectConfig :: struct {
placeholder : int,
}
Project :: struct {
path : StrRunesPair,
name : StrRunesPair,
config : ProjectConfig,
codebase : CodeBase,
// TODO(Ed) : Support multiple workspaces
workspace : Workspace,
}

View File

@ -0,0 +1,186 @@
package sectr
import "core:encoding/json"
import "core:fmt"
import "core:os"
import "core:reflect"
import "base:runtime"
import "core:strings"
@(private="file")
assign_int :: proc(val: any, i: $T) -> bool {
v := reflect.any_core(val)
switch &dst in v {
case i8: dst = i8 (i)
case i16: dst = i16 (i)
case i16le: dst = i16le (i)
case i16be: dst = i16be (i)
case i32: dst = i32 (i)
case i32le: dst = i32le (i)
case i32be: dst = i32be (i)
case i64: dst = i64 (i)
case i64le: dst = i64le (i)
case i64be: dst = i64be (i)
case i128: dst = i128 (i)
case i128le: dst = i128le (i)
case i128be: dst = i128be (i)
case u8: dst = u8 (i)
case u16: dst = u16 (i)
case u16le: dst = u16le (i)
case u16be: dst = u16be (i)
case u32: dst = u32 (i)
case u32le: dst = u32le (i)
case u32be: dst = u32be (i)
case u64: dst = u64 (i)
case u64le: dst = u64le (i)
case u64be: dst = u64be (i)
case u128: dst = u128 (i)
case u128le: dst = u128le (i)
case u128be: dst = u128be (i)
case int: dst = int (i)
case uint: dst = uint (i)
case uintptr: dst = uintptr(i)
case: return false
}
return true
}
when false {
unmarshal_from_object :: proc( $Type: typeid, object : json.Object ) -> Type
{
result : Type
type_info := type_info_of(Type)
#partial switch type in type_info.variant {
case runtime.Type_Info_Union:
ensure( false, "This proc doesn't support raw unions" )
}
base_ptr := uintptr( & result )
field_infos := reflect.struct_fields_zipped(Type)
for field_info in field_infos
{
field_type := field_info.type.id
field_ptr := cast(field_type) rawptr( base_ptr + field_info.offset )
#partial switch type in field_info.type.variant {
case runtime.Type_Info_Integer:
field_ptr = object[filed_info.name].(json.Integer)
}
}
return result
}
}
Serializer_Version :: 1
Serializer_Loading :: false
ArchiveData :: struct {
data : [] byte,
version : i32,
}
archive_init_temp :: proc() -> ^ ArchiveData {
archive := new( ArchiveData, context.temp_allocator )
archive.version = Serializer_Version
return archive
}
state_serialize :: proc( archive : ^ ArchiveData = nil ) {
// TODO(Ed): We'll need this for a better save/load snapshot setup.
}
project_serialize :: proc( project : ^ Project, archive : ^ ArchiveData, is_writting : b32 = true )
{
options : json.Marshal_Options
options.spec = json.Specification.MJSON
options.indentation = 2
options.pretty = true
options.use_spaces = false
MarshalArchive :: struct {
version : i32,
project : Project
}
if is_writting
{
marshal_archive := new(MarshalArchive, frame_slab_allocator())
marshal_archive.version = archive.version
marshal_archive.project = project^
// TODO(Ed): In the future this will be more complicated, as serialization of workspaces and the code database won't be trivial
json_data, marshal_code := json.marshal( marshal_archive, options, allocator = context.temp_allocator )
verify( marshal_code == json.Marshal_Data_Error.None, "Failed to marshal the project to JSON" )
archive.data = json_data
}
else
{
parsed_json, parse_code := json.parse( archive.data, json.Specification.MJSON, allocator = context.temp_allocator )
verify( parse_code == json.Error.None, "Failed to parse project JSON")
archive_json := parsed_json.(json.Object)
archive_version : i32 = cast(i32) archive_json["version"].(json.Float)
verify( Serializer_Version == archive_version, "Version mismatch on archive!" )
// Note(Ed) : This works fine for now, but eventually it will most likely break with pointers...
// We'll most likely set things up so that all refs in the project & workspace are handles.
marshal_archive := new(MarshalArchive, frame_slab_allocator())
json.unmarshal( archive.data, & marshal_archive, spec = json.Specification.MJSON, allocator = context.temp_allocator )
if marshal_archive.version == Serializer_Version {
project^ = marshal_archive.project
}
// Manual unmarshal
when false
{
project_json := archive_json["project"].(json.Object)
project.name = project_json["name"].(json.String)
// TODO(Ed) : Make this a separate proc
workspace_json := project_json["workspace"].(json.Object)
{
using project.workspace
name = workspace_json["name"].(json.String)
// cam = unmarshal_from_object(Camera, workspace_json["camera"].(json.Object) )
frame_1 = frame_json_unmarshal( & workspace_json["frame_1"] )
}
}
// DEBUG DUD
options.use_spaces = false
}
}
project_save :: proc( project : ^ Project, archive : ^ ArchiveData = nil )
{
archive := archive
if archive == nil {
archive = archive_init_temp()
}
project_serialize( project, archive )
if ! os.is_dir( project.path.str ) {
os.make_directory( project.path.str )
verify( cast(b32) os.is_dir( project.path.str ), "Failed to create project path for saving" )
}
os.write_entire_file( str_tmp_from_any( project.path.str, project.name.str, ".sectr_proj", sep = ""), archive.data )
}
project_load :: proc( path : string, project : ^ Project, archive : ^ ArchiveData = nil )
{
archive := archive
if archive == nil {
archive = archive_init_temp()
}
data, read_code := os.read_entire_file( path, context.temp_allocator )
verify( b32(read_code), "Failed to read from project file" )
archive.data = data
project_serialize( project, archive, Serializer_Loading )
}

View File

@ -0,0 +1,38 @@
/*
Workspace : A canvas for compositoning a view for the codebase along with notes.
Each workspace viewport supports both a canvas composition of code frames
or frame tiling towards the application's screenspace.
*/
package sectr
Workspace :: struct {
name : StrRunesPair,
cam : Camera,
zoom_target : f32,
frames : Array(Frame),
test_frame : Frame,
// TODO(Ed) : The workspace is mainly a 'UI' conceptually...
ui : UI_State,
}
// Top level widgets for the workspace
Frame :: struct {
pos : Vec2,
size : Vec2,
ui : UI_Widget,
}
CodeFrame :: struct {
readonly : b32, // Should this frame allow editing?
}
NoteFrame :: struct {
}

243
code/sectr/space.odin Normal file
View File

@ -0,0 +1,243 @@
/* Space
Provides various definitions for converting from one standard of measurement to another.
Ultimately the user's window ppcm (pixels-per-centimeter) determins how all virtual metric conventions are handled.
*/
package sectr
import rl "vendor:raylib"
// The points to pixels and pixels to points are our only reference to accurately converting
// an object from world space to screen-space.
// This prototype engine will have all its spacial unit base for distances in virtual pixels.
Inches_To_CM :: cast(f32) 2.54
Points_Per_CM :: cast(f32) 28.3465
CM_Per_Point :: cast(f32) 1.0 / DPT_DPCM
CM_Per_Pixel :: cast(f32) 1.0 / DPT_PPCM
DPT_DPCM :: cast(f32) 72.0 * Inches_To_CM // 182.88 points/dots per cm
DPT_PPCM :: cast(f32) 96.0 * Inches_To_CM // 243.84 pixels per cm
when ODIN_OS == OS_Type.Windows {
op_default_dpcm :: 72.0 * Inches_To_CM
os_default_ppcm :: 96.0 * Inches_To_CM
// 1 inch = 2.54 cm, 96 inch * 2.54 = 243.84 DPCM
}
//region Unit Conversion Impl
// cm_to_points :: proc( cm : f32 ) -> f32 {
// }
// points_to_cm :: proc( points : f32 ) -> f32 {
// screen_dpc := get_state().app_window.dpc
// cm_per_pixel := 1.0 / screen_dpc
// pixels := points * DPT_DPC * cm_per_pixel
// return points *
// }
f32_cm_to_pixels :: #force_inline proc "contextless"(cm: f32) -> f32 {
screen_ppcm := get_state().app_window.ppcm
return cm * screen_ppcm
}
f32_pixels_to_cm :: #force_inline proc "contextless"(pixels: f32) -> f32 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
return pixels * cm_per_pixel
}
f32_points_to_pixels :: #force_inline proc "contextless"(points: f32) -> f32 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
return points * DPT_PPCM * cm_per_pixel
}
f32_pixels_to_points :: #force_inline proc "contextless"(pixels: f32) -> f32 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
return pixels * cm_per_pixel * Points_Per_CM
}
vec2_cm_to_pixels :: #force_inline proc "contextless"(v: Vec2) -> Vec2 {
screen_ppcm := get_state().app_window.ppcm
return v * screen_ppcm
}
vec2_pixels_to_cm :: #force_inline proc "contextless"(v: Vec2) -> Vec2 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
return v * cm_per_pixel
}
vec2_points_to_pixels :: #force_inline proc "contextless"(vpoints: Vec2) -> Vec2 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
return vpoints * DPT_PPCM * cm_per_pixel
}
range2_cm_to_pixels :: #force_inline proc "contextless"( range : Range2 ) -> Range2 {
screen_ppcm := get_state().app_window.ppcm
result := Range2 { pts = { range.min * screen_ppcm, range.max * screen_ppcm }}
return result
}
range2_pixels_to_cm :: #force_inline proc "contextless"( range : Range2 ) -> Range2 {
screen_ppcm := get_state().app_window.ppcm
cm_per_pixel := 1.0 / screen_ppcm
result := Range2 { pts = { range.min * cm_per_pixel, range.max * cm_per_pixel }}
return result
}
// vec2_points_to_cm :: proc( vpoints : Vec2 ) -> Vec2 {
// }
//endregion
Camera :: rl.Camera2D
CameraZoomMode :: enum u32 {
Digital,
Smooth,
}
// TODO(Ed) : I'm not sure making the size and extent types distinct has made things easier or more difficult in Odin..
// The lack of operator overloads is going to make any sort of nice typesystem
// for doing lots of math or phyiscs more error prone or filled with proc wrappers
AreaSize :: distinct Vec2
Bounds2 :: struct {
top_left, bottom_right: Vec2,
}
BoundsCorners2 :: struct {
top_left, top_right, bottom_left, bottom_right: Vec2,
}
Extents2 :: distinct Vec2
Extents2i :: distinct Vec2i
WS_Pos :: struct {
tile_id : Vec2i,
rel : Vec2,
}
bounds2_radius :: proc(bounds: Bounds2) -> f32 {
return max( bounds.bottom_right.x, bounds.top_left.y )
}
extent_from_size :: proc(size: AreaSize) -> Extents2 {
return transmute(Extents2) size * 2.0
}
screen_size :: proc "contextless" () -> AreaSize {
extent := get_state().app_window.extent
return transmute(AreaSize) ( extent * 2.0 )
}
screen_get_bounds :: #force_inline proc "contextless" () -> Range2 {
state := get_state(); using state
screen_extent := state.app_window.extent
bottom_left := Vec2 { -screen_extent.x, -screen_extent.y}
top_right := Vec2 { screen_extent.x, screen_extent.y}
return range2( bottom_left, top_right )
}
screen_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 {
state := get_state(); using state
screen_extent := state.app_window.extent
top_left := Vec2 { -screen_extent.x, screen_extent.y }
top_right := Vec2 { screen_extent.x, screen_extent.y }
bottom_left := Vec2 { -screen_extent.x, -screen_extent.y }
bottom_right := Vec2 { screen_extent.x, -screen_extent.y }
return { top_left, top_right, bottom_left, bottom_right }
}
// TODO(Ed): Use a cam/workspace context instead (when multiple workspaces viewproting supported)
view_get_bounds :: #force_inline proc "contextless"() -> Range2 {
state := get_state(); using state
cam := & project.workspace.cam
screen_extent := state.app_window.extent
cam_zoom_ratio := 1.0 / cam.zoom
bottom_left := Vec2 { cam.target.x, -cam.target.y } + Vec2 { -screen_extent.x, -screen_extent.y} * cam_zoom_ratio
top_right := Vec2 { cam.target.x, -cam.target.y } + Vec2 { screen_extent.x, screen_extent.y} * cam_zoom_ratio
return range2( bottom_left, top_right )
}
// TODO(Ed): Use a cam/workspace context instead (when multiple workspace viewproting)
view_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 {
state := get_state(); using state
cam := & project.workspace.cam
cam_zoom_ratio := 1.0 / cam.zoom
screen_extent := state.app_window.extent * cam_zoom_ratio
top_left := cam.target + Vec2 { -screen_extent.x, screen_extent.y }
top_right := cam.target + Vec2 { screen_extent.x, screen_extent.y }
bottom_left := cam.target + Vec2 { -screen_extent.x, -screen_extent.y }
bottom_right := cam.target + Vec2 { screen_extent.x, -screen_extent.y }
return { top_left, top_right, bottom_left, bottom_right }
}
render_to_screen_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
extent := & get_state().app_window.extent
result := Vec2 {
pos.x - extent.x,
pos.y * -1 + extent.y
}
return result
}
render_to_ws_view_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
return {}
}
screen_to_ws_view_pos :: #force_inline proc "contextless" (pos: Vec2) -> Vec2 {
state := get_state(); using state
cam := & project.workspace.cam
result := Vec2 { cam.target.x, -cam.target.y} + Vec2 { pos.x, pos.y } * (1 / cam.zoom)
return result
}
// Centered screen space to conventional screen space used for rendering
screen_to_render_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 {
screen_extent := transmute(Vec2) get_state().app_window.extent
return pos * {1, -1} + { screen_extent.x, screen_extent.y }
}
// TODO(Ed): These should assume a cam_context or have the ability to provide it in params
// Extent of workspace view (currently hardcoded to the app window's extent, eventually will be based on a viewport object's extent field)
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
ws_view_extent :: #force_inline proc "contextless"() -> Extents2 {
state := get_state(); using state
cam_zoom_ratio := 1.0 / project.workspace.cam.zoom
return app_window.extent * cam_zoom_ratio
}
// Workspace view to screen space position
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
ws_view_to_screen_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
return position
}
ws_view_to_render_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
return { position.x, position.y * -1 }
}
// Workspace view to screen space position (zoom agnostic)
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
ws_view_to_screen_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
state := get_state(); using state
cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom
return { position.x, position.y } * cam_zoom_ratio
}
// Workspace view to render space position (zoom agnostic)
// TODO(Ed): Support a position which would not be centered on the screen if in a viewport
ws_view_to_render_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 {
state := get_state(); using state
cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom
return { position.x, position.y } * cam_zoom_ratio
}

View File

@ -0,0 +1,6 @@
package sectr
// Specialization of floating that allows panning across the viewport in the space (space isn't clampped to a specific size))
UI_Canvas :: struct {
using floating : UI_Floating,
}

262
code/sectr/ui/core.odin Normal file
View File

@ -0,0 +1,262 @@
package sectr
import "base:runtime"
// TODO(Ed) : This is in Raddbg base_types.h, consider moving outside of UI.
Corner :: enum i32 {
Invalid = -1,
_00,
_01,
_10,
_11,
TopLeft = _00,
TopRight = _01,
BottomLeft = _10,
BottomRight = _11,
Count = 4,
}
Side :: enum i32 {
Invalid = -1,
Min = 0,
Max = 1,
Count
}
// Side2 :: enum u32 {
// Top,
// Bottom,
// Left,
// Right,
// Count,
// }
// UI_AnchorPresets :: enum u32 {
// Top_Left,
// Top_Right,
// Bottom_Right,
// Bottom_Left,
// Center_Left,
// Center_Top,
// Center_Right,
// Center_Bottom,
// Center,
// Left_Wide,
// Top_Wide,
// Right_Wide,
// Bottom_Wide,
// VCenter_Wide,
// HCenter_Wide,
// Full,
// Count,
// }
UI_Cursor :: struct {
placeholder : int,
}
UI_FramePassKind :: enum {
Generate,
Compute,
Logical,
}
UI_InteractState :: struct {
hot_time : f32,
active_time : f32,
disabled_time : f32,
}
UI_Key :: distinct u64
UI_Scalar :: f32
UI_ScalarConstraint :: struct {
min, max : UI_Scalar,
}
UI_Scalar2 :: [Axis2.Count]UI_Scalar
// UI_BoxFlags_Stack_Size :: 512
UI_Layout_Stack_Size :: 512
UI_Style_Stack_Size :: 512
UI_Parent_Stack_Size :: 512
// UI_Built_Boxes_Array_Size :: 8
UI_Built_Boxes_Array_Size :: 128 * Kilobyte
UI_State :: struct {
// TODO(Ed) : Use these
// build_arenas : [2]Arena,
// build_arena : ^ Arena,
built_box_count : i32,
caches : [2] HMapZPL( UI_Box ),
prev_cache : ^HMapZPL( UI_Box ),
curr_cache : ^HMapZPL( UI_Box ),
render_queue : Array(UI_RenderBoxInfo),
null_box : ^UI_Box, // This was used with the Linked list interface...
// TODO(Ed): Should we change our convention for null boxes to use the above and nil as an invalid state?
root : ^UI_Box,
// Children of the root node are unique in that they have their order preserved per frame
// This is to support overlapping frames
// So long as their parent-index is non-negative they'll be rendered
// Do we need to recompute the layout?
// layout_dirty : b32,
// TODO(Ed) : Look into using a build arena like Ryan does for these possibly (and thus have a linked-list stack)
layout_combo_stack : StackFixed( UI_LayoutCombo, UI_Style_Stack_Size ),
style_combo_stack : StackFixed( UI_StyleCombo, UI_Style_Stack_Size ),
parent_stack : StackFixed( ^UI_Box, UI_Parent_Stack_Size ),
// flag_stack : Stack( UI_BoxFlags, UI_BoxFlags_Stack_Size ),
hot : UI_Key,
hot_start_style : UI_Style,
active_mouse : [MouseBtn.count] UI_Key,
active : UI_Key,
active_start_signal : UI_Signal,
clipboard_copy : UI_Key,
last_clicked : UI_Key,
active_start_style : UI_Style,
last_pressed_key : [MouseBtn.count] UI_Key,
last_pressed_key_us : [MouseBtn.count] f32,
}
ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator /* , cache_reserve_size : u64 */ )
{
ui := ui
ui^ = {}
for & cache in ui.caches {
box_cache, allocation_error := zpl_hmap_init_reserve( UI_Box, cache_allocator, UI_Built_Boxes_Array_Size )
verify( allocation_error == AllocatorError.None, "Failed to allocate box cache" )
cache = box_cache
}
ui.curr_cache = (& ui.caches[1])
ui.prev_cache = (& ui.caches[0])
allocation_error : AllocatorError
ui.render_queue, allocation_error = array_init_reserve( UI_RenderBoxInfo, cache_allocator, UI_Built_Boxes_Array_Size, fixed_cap = true )
verify( allocation_error == AllocatorError.None, "Failed to allocate render queue" )
log("ui_startup completed")
}
ui_reload :: proc( ui : ^ UI_State, cache_allocator : Allocator )
{
// We need to repopulate Allocator references
for & cache in ui.caches {
zpl_hmap_reload( & cache, cache_allocator)
}
ui.render_queue.backing = cache_allocator
}
// TODO(Ed) : Is this even needed?
ui_shutdown :: proc() {
}
ui_cursor_pos :: #force_inline proc "contextless" () -> Vec2 {
using state := get_state()
if ui_context == & state.project.workspace.ui {
return screen_to_ws_view_pos( input.mouse.pos )
}
else {
return input.mouse.pos
}
}
ui_drag_delta :: #force_inline proc "contextless" () -> Vec2 {
using state := get_state()
return ui_cursor_pos() - state.ui_context.active_start_signal.cursor_pos
}
ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} )
{
profile(#procedure)
state := get_state()
get_state().ui_context = ui
using get_state().ui_context
stack_clear( & layout_combo_stack )
stack_clear( & style_combo_stack )
array_clear( render_queue )
curr_cache, prev_cache = swap( curr_cache, prev_cache )
if ui.active == UI_Key(0) {
//ui.hot = UI_Key(0)
ui.active_start_signal = {}
}
ui.built_box_count = 0
root = ui_box_make( {}, str_intern(str_fmt_tmp("%s: root#001", ui == & state.screen_ui ? "Screen" : "Workspace" )).str)
if ui == & state.screen_ui {
root.layout.size = range2(Vec2(state.app_window.extent) * 2, {})
}
ui_parent_push(root)
}
ui_graph_build_end :: proc( ui : ^UI_State )
{
profile(#procedure)
ui_parent_pop() // Should be ui_context.root
// Regenerate the computed layout if dirty
ui_compute_layout( ui )
get_state().ui_context = nil
}
@(deferred_in = ui_graph_build_end)
ui_graph_build :: #force_inline proc( ui : ^ UI_State ) { ui_graph_build_begin( ui ) }
ui_key_from_string :: #force_inline proc "contextless" ( value : string ) -> UI_Key
{
// profile(#procedure)
USE_RAD_DEBUGGERS_METHOD :: true
key : UI_Key
when USE_RAD_DEBUGGERS_METHOD {
hash : u64
for str_byte in transmute([]byte) value {
hash = ((hash << 5) + hash) + u64(str_byte)
}
key = cast(UI_Key) hash
}
when ! USE_RAD_DEBUGGERS_METHOD {
key = cast(UI_Key) crc32( transmute([]byte) value )
}
return key
}
ui_parent_push :: #force_inline proc( ui : ^ UI_Box ) { stack_push( & ui_context().parent_stack, ui ) }
ui_parent_pop :: #force_inline proc() { stack_pop( & get_state().ui_context.parent_stack ) }
@(deferred_none = ui_parent_pop)
ui_parent :: #force_inline proc( ui : ^UI_Box) { ui_parent_push( ui ) }
ui_prev_cached_box :: #force_inline proc( box : ^UI_Box ) -> ^UI_Box { return zpl_hmap_get( ui_context().prev_cache, cast(u64) box.key ) }
// Topmost ancestor that is not the root
ui_top_ancestor :: #force_inline proc "contextless" ( box : ^UI_Box ) -> (^UI_Box) {
using ui := get_state().ui_context
ancestor := box
for ; ancestor.parent != root; ancestor = ancestor.parent {}
return ancestor
}
ui_context :: #force_inline proc() -> ^UI_State { return get_state().ui_context }

153
code/sectr/ui/core_box.odin Normal file
View File

@ -0,0 +1,153 @@
package sectr
UI_BoxFlag :: enum u64
{
Disabled,
Focusable,
Click_To_Focus,
Mouse_Clickable,
Keyboard_Clickable,
Count,
}
UI_BoxFlags :: bit_set[UI_BoxFlag; u64]
// UI_BoxFlag_Scroll :: UI_BoxFlags { .Scroll_X, .Scroll_Y }
UI_RenderBoxInfo :: struct {
using computed : UI_Computed,
using style : UI_Style,
text : StrRunesPair,
font_size : UI_Scalar,
border_width : UI_Scalar,
}
UI_Box :: struct {
// Cache ID
key : UI_Key,
// label : string,
label : StrRunesPair,
text : StrRunesPair,
// Regenerated per frame.
// first, last : The first and last child of this box
// prev, next : The adjacent neighboring boxes who are children of to the same parent
using links : DLL_NodeFull( UI_Box ),
parent : ^UI_Box,
num_children : i32,
ancestors : i32, // This value for rooted widgets gets set to -1 after rendering see ui_box_make() for the reason.
parent_index : i32,
flags : UI_BoxFlags,
computed : UI_Computed,
layout : UI_Layout,
style : UI_Style,
// Persistent Data
hot_delta : f32,
active_delta : f32,
disabled_delta : f32,
style_delta : f32,
first_frame : b8,
// root_order_id : i16,
// mouse : UI_InteractState,
// keyboard : UI_InteractState,
}
ui_box_equal :: #force_inline proc "contextless" ( a, b : ^ UI_Box ) -> b32 {
BoxSize :: size_of(UI_Box)
result : b32 = true
result &= a.key == b.key // We assume for now the label is the same as the key, if not something is terribly wrong.
result &= a.flags == b.flags
return result
}
ui_box_from_key :: #force_inline proc ( cache : ^HMapZPL(UI_Box), key : UI_Key ) -> (^UI_Box) {
return zpl_hmap_get( cache, cast(u64) key )
}
ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
{
// profile(#procedure)
using ui := get_state().ui_context
key := ui_key_from_string( label )
curr_box : (^ UI_Box)
prev_box := zpl_hmap_get( prev_cache, cast(u64) key )
{
// profile("Assigning current box")
set_result : ^ UI_Box
set_error : AllocatorError
if prev_box != nil
{
// Previous history was found, copy over previous state.
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, (prev_box ^) )
}
else {
box : UI_Box
box.key = key
box.label = str_intern( label )
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, box )
}
verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" )
curr_box = set_result
curr_box.first_frame = prev_box == nil
curr_box.flags = flags
}
// Clear non-persistent data
curr_box.computed.fresh = false
curr_box.links = {}
curr_box.num_children = 0
// If there is a parent, setup the relevant references
parent := stack_peek( & parent_stack )
if parent != nil
{
dll_full_push_back( parent, curr_box, nil )
curr_box.parent_index = parent.num_children
parent.num_children += 1
curr_box.parent = parent
curr_box.ancestors = parent.ancestors + 1
}
ui.built_box_count += 1
return curr_box
}
ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box)
{
using state := get_state()
// If current has children, do them first
if box.first != nil
{
// Check to make sure parent is present on the screen, if its not don't bother.
is_app_ui := ui_context == & screen_ui
if intersects_range2( view_get_bounds(), box.computed.bounds)
{
return box.first
}
}
if box.next != nil do return box.next
// There is no more adjacent nodes
parent := box.parent
// Attempt to find a parent with a next, otherwise we just return a parent with nil
for ; parent.parent != nil;
{
if parent.next != nil {
break
}
parent = parent.parent
}
// Lift back up to parent, and set it to its next.
return parent.next
}

View File

@ -0,0 +1,165 @@
package sectr
import "core:math"
import "core:math/linalg"
// Anchor_
// Alignment presets
LayoutAlign_OriginTL_Top :: Vec2{0.5, 0}
LayoutAlign_OriginTL_TopLeft :: Vec2{ 0, 0}
LayoutAlign_OriginTL_TopRight :: Vec2{ 1, 0}
LayoutAlign_OriginTL_Centered :: Vec2{0.5, 0.5}
LayoutAlign_OriginTL_Bottom :: Vec2{0.5, 1}
LayoutAlign_OriginTL_BottomLeft :: Vec2{ 0, 1}
LayoutAlign_OriginTL_BottomRight :: Vec2{ 1, 1}
// LayoutAlign_OriginTL_
Layout_OriginCenter_Centered :: Vec2{0.5, 0.5}
// The UI_Box's actual positioning and sizing
// There is an excess of rectangles here for debug puproses.
UI_Computed :: struct {
// anchors : Range2, // Bounds for anchors within parent
// margins : Range2, // Bounds for margins within parent
padding : Range2, // Bounds for padding's starting bounds (will be offset by border if there is one), only here for debug vis
bounds : Range2, // Bounds for box itself
content : Range2, // Bounds for content (text or children)
text_pos : Vec2, // Position of text within content
text_size : Vec2, // Size of text within content
fresh : b32, // If the auto-layout has been computed for the current frame
}
UI_LayoutDirectionX :: enum(i32) {
Left_To_Right,
Right_To_Left,
}
UI_LayoutDirectionY :: enum(i32) {
Top_To_Bottom,
Bottom_To_Top,
}
UI_LayoutSide :: struct {
// using _ : struct {
top, bottom : UI_Scalar,
left, right : UI_Scalar,
// }
}
UI_LayoutFlag :: enum u32 {
// Will perform scissor pass on children to their parent's bounds
// (Specified in the parent)
Clip_Children_To_Bounds,
// Enforces the box will always remain in a specific position relative to the parent.
// Overriding the anchors and margins.
Fixed_Position_X,
Fixed_Position_Y,
// Enforces box will always be within the bounds of the parent box.
Clamp_Position_X,
Clamp_Position_Y,
// Enroces the widget will maintain its size reguardless of any constraints
// Will override parent constraints (use the size.min.xy to specify the width & height)
Fixed_Width,
Fixed_Height,
// Enforces the widget will have a width specified as a ratio of its height (use the size.min/max.x to specify the scalar)
// If you wish for the width to stay fixed couple with the Fixed_Width flag
Scale_Width_By_Height_Ratio,
// Enforces the widget will have a height specified as a ratio of its width (use the size.min/max.y to specify the scalar)
// If you wish for the height to stay fixed couple with the Fixed_Height flag
Scale_Height_By_Width_Ratio,
// Sets the (0, 0) position of the child box to the parents anchor's center (post-margins bounds)
// By Default, the origin is at the top left of the anchor's bounds
Origin_At_Anchor_Center,
// TODO(Ed): Implement this!
// For this to work, the children must have a minimum size set & their size overall must be greater than the parent's minimum size
Size_To_Content,
// Will size the box to its text.
Size_To_Text,
// TODO(Ed): Implement this!
// ?Note(Ed): This can get pretty complicated... Maybe its better to leave this to composition of boxes.
// ?A text wrapping panel can organize text and wrap it via procedrually generated lines in a hbox/vbox.
// ?It would be a non-issue so long as the text rendering bottleneck is resolved.
// Wrap text around the box, text_alignment specifies the justification for its compostion when wrapping.
Text_Wrap,
Count,
}
UI_LayoutFlags :: bit_set[UI_LayoutFlag; u32]
// Used within UI_Box, provides the layout (spacial constraints & specification) of the widget and
UI_Layout :: struct {
flags : UI_LayoutFlags,
anchor : Range2,
alignment : Vec2,
text_alignment : Vec2,
font_size : UI_Scalar,
margins : UI_LayoutSide,
padding : UI_LayoutSide,
border_width : UI_Scalar,
// Position in relative coordinate space.
// If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space
pos : Vec2,
size : Range2,
// TODO(Ed) : Should thsi just always be WS_Pos for workspace UI?
// (We can union either varient and just know based on checking if its the screenspace UI)
// If the box is a child of the root parent, its automatically in world space and thus will use the tile_pos.
// tile_pos : WS_Pos,
}
UI_LayoutCombo :: struct #raw_union {
array : [UI_StylePreset.Count] UI_Layout,
using layouts : struct {
default, disabled, hot, active : UI_Layout,
}
}
to_ui_layout_side_f32 :: #force_inline proc( pixels : f32 ) -> UI_LayoutSide { return { pixels, pixels, pixels, pixels } }
to_ui_layout_side_vec2 :: #force_inline proc( v : Vec2) -> UI_LayoutSide { return { v.x, v.x, v.y, v.y} }
to_ui_layout_combo :: #force_inline proc( layout : UI_Layout ) -> UI_LayoutCombo { return { layouts = {layout, layout, layout, layout} } }
/*
Layout Interface
Layout for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called layout_combo_stack.
The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context
The following procedure overloads are available from grime.odin:
* ui_layout
* ui_layout_push
*/
ui_layout_peek :: #force_inline proc() -> UI_LayoutCombo { return stack_peek( & get_state().ui_context.layout_combo_stack) }
ui_layout_ref :: #force_inline proc() -> ^UI_LayoutCombo { return stack_peek_ref( & get_state().ui_context.layout_combo_stack) }
ui_layout_push_layout :: #force_inline proc( layout : UI_Layout ) { push( & get_state().ui_context.layout_combo_stack, to_ui_layout_combo(layout)) }
ui_layout_push_theme :: #force_inline proc( combo : UI_LayoutCombo ) { push( & get_state().ui_context.layout_combo_stack, combo ) }
ui_layout_pop :: #force_inline proc() { pop( & get_state().ui_context.layout_combo_stack ) }
@(deferred_none = ui_layout_pop) ui_layout_via_layout :: #force_inline proc( layout : UI_Layout ) { ui_layout_push( layout) }
@(deferred_none = ui_layout_pop) ui_layout_via_combo :: #force_inline proc( combo : UI_LayoutCombo ) { ui_layout_push( combo) }
ui_set_layout :: #force_inline proc( layout : UI_Layout, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.layout_combo_stack).array[preset] = layout }

View File

@ -0,0 +1,219 @@
package sectr
ui_box_compute_layout :: proc( box : ^UI_Box,
dont_mark_fresh : b32 = false,
ancestors_layout_required : b32 = false,
root_layout_required : b32 = false )
{
// profile("Layout Box")
state := get_state()
ui := state.ui_context
using box
size_to_text : bool = .Size_To_Text in layout.flags
parent_content := parent.computed.content
parent_content_size := parent_content.max - parent_content.min
parent_center := parent_content.min + parent_content_size * 0.5
/*
If fixed position (X or Y):
* Ignore Margins
* Ignore Anchors
If clampped position (X or Y):
* Positon cannot exceed the anchors/margins bounds.
If fixed size (X or Y):
* Ignore Parent constraints (can only be clipped)
If an axis is auto-sized by a ratio of the other axis
* Using the referenced axis, set the size of the ratio'd axis by that ratio.
If auto-sized:
* Enforce parent size constraint of bounds relative to
where the adjusted content bounds are after applying margins & anchors.
The 'side' conflicting with the bounds will end at that bound side instead of clipping.
If size.min is not 0:
* Ignore parent constraints if the bounds go below that value.
If size.max is 0:
* Allow the child box to spread to entire adjusted content bounds, otherwise clampped to max size.
*/
// 1. Anchors
anchor := & layout.anchor
anchored_bounds := range2(
parent_content.min + parent_content_size * anchor.min,
parent_content.max - parent_content_size * anchor.max,
)
// anchored_bounds_origin := (anchored_bounds.min + anchored_bounds.max) * 0.5
// 2. Apply Margins
margins := range2(
{ layout.margins.left, layout.margins.bottom },
{ layout.margins.right, layout.margins.top },
)
margined_bounds := range2(
anchored_bounds.min + margins.min,
anchored_bounds.max - margins.max,
)
margined_bounds_origin := (margined_bounds.min + margined_bounds.max) * 0.5
margined_size := margined_bounds.max - margined_bounds.min
// 3. Enforce Min/Max Size Constraints
adjusted_max_size_x := layout.size.max.x > 0 ? min( margined_size.x, layout.size.max.x ) : margined_size.x
adjusted_max_size_y := layout.size.max.y > 0 ? min( margined_size.y, layout.size.max.y ) : margined_size.y
adjusted_size : Vec2
adjusted_size.x = max( adjusted_max_size_x, layout.size.min.x)
adjusted_size.y = max( adjusted_max_size_y, layout.size.min.y)
text_size : Vec2
if layout.font_size == computed.text_size.y {
text_size = computed.text_size
}
else {
text_size = cast(Vec2) measure_text_size( box.text.str, style.font, layout.font_size, 0 )
}
if size_to_text {
adjusted_size = text_size
}
if .Scale_Width_By_Height_Ratio in layout.flags {
adjusted_size.x = adjusted_size.y * layout.size.min.x
}
if .Scale_Height_By_Width_Ratio in layout.flags {
adjusted_size.y = adjusted_size.x * layout.size.min.y
}
if .Size_To_Content in layout.flags {
// Preemtively traverse the children of this parent and have them compute their layout.
// This parent will just set its size to the max bounding area of those children.
// This will recursively occur if child also depends on their content size from their children, etc.
ui_box_compute_layout_children(box)
//ui_compute_children_bounding_area(box)
}
// TODO(Ed): Should this force override all of the previous auto-sizing possible?
if .Fixed_Width in layout.flags {
adjusted_size.x = layout.size.min.x
}
if .Fixed_Height in layout.flags {
adjusted_size.y = layout.size.min.y
}
// 5. Determine relative position
origin_center := margined_bounds_origin
origin_top_left := Vec2 { margined_bounds.min.x, margined_bounds.max.y }
origin := .Origin_At_Anchor_Center in layout.flags ? origin_center : origin_top_left
rel_pos := origin + layout.pos
if .Fixed_Position_X in layout.flags {
rel_pos.x = origin.x + layout.pos.x
}
if .Fixed_Position_Y in layout.flags {
rel_pos.y = origin.y + layout.pos.y
}
vec2_one := Vec2 { 1, 1 }
// 6. Determine the box bounds
// Adjust Alignment of pivot position
alignment := layout.alignment
bounds : Range2
if ! (.Origin_At_Anchor_Center in layout.flags) {
// The convention offset adjust the box so that the top-left point is at the top left of the anchor's bounds
tl_convention_offset := adjusted_size * {0, -1}
bounds = range2(
rel_pos - adjusted_size * alignment + tl_convention_offset,
rel_pos + adjusted_size * (vec2_one - alignment) + tl_convention_offset,
)
}
else {
centered_convention_offset := adjusted_size * -0.5
bounds = range2(
(rel_pos + centered_convention_offset) - adjusted_size * -alignment ,
(rel_pos + centered_convention_offset) + adjusted_size * (alignment + vec2_one),
)
}
// 7. Padding & Content
// Determine Padding's outer bounds
border_offset := Vec2 { layout.border_width, layout.border_width }
padding_bounds := range2(
bounds.min + border_offset,
bounds.min - border_offset,
)
// Determine Content Bounds
content_bounds := range2(
bounds.min + { layout.padding.left, layout.padding.bottom } + border_offset,
bounds.max - { layout.padding.right, layout.padding.top } - border_offset,
)
computed.bounds = bounds
computed.padding = padding_bounds
computed.content = content_bounds
// 8. Text position & size
if len(box.text.str) > 0
{
content_size := content_bounds.max - content_bounds.min
text_pos : Vec2
text_pos = content_bounds.min + { 0, text_size.y }
text_pos += (content_size - text_size) * layout.text_alignment
computed.text_size = text_size
computed.text_pos = text_pos
}
computed.fresh = true && !dont_mark_fresh
}
ui_box_compute_layout_children :: proc( box : ^UI_Box )
{
for current := box.first; current != nil && current.prev != box; current = ui_box_tranverse_next( current )
{
if current == box do return
if current.computed.fresh do continue
ui_box_compute_layout( current )
}
}
ui_core_compute_layout :: proc( ui : ^UI_State )
{
profile(#procedure)
state := get_state()
root := ui.root
{
computed := & root.computed
style := root.style
layout := & root.layout
if ui == & state.screen_ui {
computed.bounds.min = transmute(Vec2) state.app_window.extent * -1
computed.bounds.max = transmute(Vec2) state.app_window.extent
}
computed.content = computed.bounds
}
for current := root.first; current != nil; current = ui_box_tranverse_next( current )
{
if ! current.computed.fresh {
ui_box_compute_layout( current )
}
array_append( & ui.render_queue, UI_RenderBoxInfo {
current.computed,
current.style,
current.text,
current.layout.font_size,
current.layout.border_width,
})
}
}

View File

@ -0,0 +1,256 @@
package sectr
import "base:runtime"
UI_Signal :: struct {
cursor_pos : Vec2,
drag_delta : Vec2,
scroll : Vec2,
left_clicked : b8,
right_clicked : b8,
double_clicked : b8,
keyboard_clicked : b8,
left_shift_held : b8,
left_ctrl_held : b8,
active : b8,
hot : b8,
disabled : b8,
was_active : b8,
was_hot : b8,
was_disabled : b8,
pressed : b8,
released : b8,
cursor_over : b8,
commit : b8,
}
ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas := true ) -> UI_Signal
{
// profile(#procedure)
ui := get_state().ui_context
input := get_state().input
frame_delta := frametime_delta32()
signal := UI_Signal {}
// Cursor Collision
// profile_begin( "Cursor collision")
signal.cursor_pos = ui_cursor_pos()
signal.cursor_over = cast(b8) pos_within_range2( signal.cursor_pos, box.computed.bounds )
UnderCheck:
{
if ! signal.cursor_over do break UnderCheck
last_root := ui_box_from_key( ui.prev_cache, ui.root.key )
if last_root == nil do break UnderCheck
top_ancestor := ui_top_ancestor(box)
if top_ancestor.parent_index < last_root.parent_index
{
for curr := last_root.last; curr != nil && curr.key != box.key; curr = curr.prev {
if pos_within_range2( signal.cursor_pos, curr.computed.bounds ) {
signal.cursor_over = false
}
}
}
}
// profile_end()
// profile_begin("misc")
left_pressed := pressed( input.mouse.left )
left_released := released( input.mouse.left )
signal.left_shift_held = b8(input.keyboard.left_shift.ended_down)
mouse_clickable := UI_BoxFlag.Mouse_Clickable in box.flags
keyboard_clickable := UI_BoxFlag.Keyboard_Clickable in box.flags
was_hot := (box.hot_delta > 0)
was_active := (ui.active == box.key) && (box.active_delta > 0)
was_disabled := box.disabled_delta > 0
// if was_hot {
// runtime.debug_trap()
// }
// Check to see if this box is active
if mouse_clickable && signal.cursor_over && left_pressed && was_hot
{
// ui.hot = box.key
ui.active = box.key
ui.active_mouse[MouseBtn.Left] = box.key
ui.last_pressed_key = box.key
ui.active_start_style = box.style
signal.pressed = true
signal.left_clicked = b8(left_pressed)
// TODO(Ed) : Support double-click detection
}
if mouse_clickable && ! signal.cursor_over && left_released
{
box.active_delta = 0
ui.active = UI_Key(0)
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
signal.released = true
}
if keyboard_clickable
{
// TODO(Ed) : Add keyboard interaction support
}
// TODO(Ed): Should panning and scrolling get supported here? (problably not...)
// TODO(Ed) : Add scrolling support
// if UI_BoxFlag.Scroll_X in box.flags {
// }
// if UI_BoxFlag.Scroll_Y in box.flags {
// }
// TODO(Ed) : Add panning support
// if UI_BoxFlag.Pan_X in box.flags {
// }
// if UI_BoxFlag.Pan_Y in box.flags {
// }
is_disabled := UI_BoxFlag.Disabled in box.flags
is_hot := ui.hot == box.key
is_active := ui.active == box.key
// TODO(Ed): It should be able to enter hot without mouse_clickable
if mouse_clickable && signal.cursor_over && ! is_disabled
{
hot_vacant := ui.hot == UI_Key(0)
active_vacant := ui.active == UI_Key(0)
// (active_vacant is_active)
if signal.cursor_over && active_vacant
{
if ! hot_vacant {
prev := ui_box_from_key( ui.curr_cache, ui.hot )
prev.hot_delta = 0
}
// prev_hot := zpl_hmap_get( ui.prev_cache, u64(ui.hot) )
// prev_hot_label := prev_hot != nil ? prev_hot.label.str : ""
// log( str_fmt_tmp("Detected HOT via CURSOR OVER: %v is_hot: %v is_active: %v prev_hot: %v", box.label.str, is_hot, is_active, prev_hot_label ))
ui.hot = box.key
is_hot = true
ui.hot_start_style = box.style
}
}
else if ! signal.cursor_over && was_hot
{
ui.hot = UI_Key(0)
is_hot = false
box.hot_delta = 0
}
if mouse_clickable && signal.cursor_over && left_released
{
box.active_delta = 0
ui.active = UI_Key(0)
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
signal.released = true
if was_active {
signal.left_clicked = true
ui.last_clicked = box.key
}
}
// profile_end()
// State Deltas update
// profile_begin( "state deltas upate")
if is_hot
{
box.hot_delta += frame_delta
if was_hot {
box.style_delta += frame_delta
}
}
if is_active
{
box.active_delta += frame_delta
if was_active {
box.style_delta += frame_delta
}
}
else {
box.active_delta = 0
}
if is_disabled
{
box.disabled_delta += frame_delta
if was_hot {
box.style_delta += frame_delta
}
}
else {
box.disabled_delta = 0
}
// profile_end()
signal.active = cast(b8) is_active
signal.was_active = cast(b8) was_active
// logf("was_active: %v", was_active)
// Update style if not in default state
if update_style
{
// profile("Update style")
if is_hot
{
if ! was_hot {
box.style_delta = 0
}
box.layout = ui_layout_peek().hot
box.style = ui_style_peek().hot
}
if is_active
{
if ! was_active {
box.style_delta = 0
}
box.layout = ui_layout_peek().active
box.style = ui_style_peek().active
}
if is_disabled
{
if ! was_disabled {
box.style_delta = 0
}
box.layout = ui_layout_peek().disabled
box.style = ui_style_peek().disabled
}
if ! is_disabled && ! is_active && ! is_hot {
if was_disabled || was_active || was_hot {
box.style_delta = 0
}
else {
box.style_delta += frame_delta
}
box.layout = ui_layout_peek().default
box.style = ui_style_peek().default
}
}
if is_active && ! was_active {
ui.active_start_signal = signal
}
return signal
}

View File

@ -0,0 +1,72 @@
package sectr
// TODO(Ed): We problably can embedd this info into the UI_Layout with the regular text_alignment
UI_TextAlign :: enum u32 {
Left,
Center,
Right,
Count
}
UI_StylePreset :: enum u32 {
Default,
Disabled,
Hot,
Active,
Count,
}
UI_Style :: struct {
bg_color : Color,
border_color : Color,
// TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend)
corner_radii : [Corner.Count]f32,
// TODO(Ed) : Add support for this eventually
blur_size : f32,
// TODO(Ed): Add support for textures
// texture : Texture2,
// TODO(Ed): Add support for custom shader
// shader : UI_Shader,
font : FontID,
text_color : Color,
// TODO(Ed) : Support setting the cursor state
cursor : UI_Cursor,
}
UI_StyleCombo :: struct #raw_union {
array : [UI_StylePreset.Count] UI_Style,
using styles : struct {
default, disabled, hot, active : UI_Style,
}
}
to_ui_style_combo :: #force_inline proc( style : UI_Style ) -> UI_StyleCombo { return { styles = {style, style, style, style} } }
/*
Style Interface
Style for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called style_combo_stack.
The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context
The following procedure overloads are available from grime.odin :
* ui_style
* ui_style_push
*/
ui_style_peek :: #force_inline proc() -> UI_StyleCombo { return stack_peek( & get_state().ui_context.style_combo_stack ) }
ui_style_ref :: #force_inline proc() -> (^ UI_StyleCombo) { return stack_peek_ref( & get_state().ui_context.style_combo_stack ) }
ui_style_push_style :: #force_inline proc( style : UI_Style ) { push( & get_state().ui_context.style_combo_stack, to_ui_style_combo(style)) }
ui_style_push_combo :: #force_inline proc( combo : UI_StyleCombo ) { push( & get_state().ui_context.style_combo_stack, combo ) }
ui_style_pop :: #force_inline proc() { pop( & get_state().ui_context.style_combo_stack ) }
@(deferred_none = ui_style_pop) ui_style_via_style :: #force_inline proc( style : UI_Style ) { ui_style_push( style) }
@(deferred_none = ui_style_pop) ui_style_via_combo :: #force_inline proc( combo : UI_StyleCombo ) { ui_style_push( combo) }
ui_style_set :: #force_inline proc ( style : UI_Style, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.style_combo_stack ).array[preset] = style }

View File

@ -0,0 +1,12 @@
package sectr
UI_DockedEntry :: struct {
placeholder : int,
}
// Non-overlapping, Tiled, window/frame manager
// Has support for tabbing
// AKA: Tiled Window Manager
UI_Docking :: struct {
placeholder : int,
}

178
code/sectr/ui/floating.odin Normal file
View File

@ -0,0 +1,178 @@
package sectr
UI_Floating :: struct {
label : string,
using links : DLL_NodePN(UI_Floating),
captures : rawptr,
builder : UI_FloatingBuilder,
queued : b32,
}
UI_FloatingBuilder :: #type proc( captures : rawptr ) -> (became_active : b32)
// Overlapping/Stacking window/frame manager
// AKA: Stacking or Floating Window Manager
UI_FloatingManager :: struct {
using links : DLL_NodeFL(UI_Floating),
build_queue : Array(UI_Floating),
tracked : HMapChainedPtr(UI_Floating),
}
ui_floating_startup :: proc( self : ^UI_FloatingManager, allocator : Allocator, build_queue_cap, tracked_cap : u64, dbg_name : string = "" ) -> AllocatorError
{
error : AllocatorError
queue_dbg_name := str_intern(str_fmt_tmp("%s: build_queue", dbg_name))
self.build_queue, error = array_init_reserve( UI_Floating, allocator, build_queue_cap, dbg_name = queue_dbg_name.str )
if error != AllocatorError.None
{
ensure(false, "Failed to allocate the build_queue")
return error
}
tracked_dbg_name := str_intern(str_fmt_tmp("%s: tracked", dbg_name))
self.tracked, error = hmap_chained_init(UI_Floating, uint(tracked_cap), allocator, dbg_name = tracked_dbg_name.str )
if error != AllocatorError.None
{
ensure(false, "Failed to allocate tracking table")
return error
}
return error
}
ui_floating_reload :: proc( self : ^UI_FloatingManager, allocator : Allocator )
{
using self
build_queue.backing = allocator
hmap_chained_reload(tracked, allocator)
}
ui_floating_just_builder :: #force_inline proc( label : string, builder : UI_FloatingBuilder ) -> ^UI_Floating
{
No_Captures : rawptr = nil
return ui_floating_with_capture(label, No_Captures, builder)
}
ui_floating_with_capture :: proc( label : string, captures : rawptr = nil, builder : UI_FloatingBuilder ) -> ^UI_Floating
{
entry := UI_Floating {
label = label,
captures = captures,
builder = builder,
}
floating := get_state().ui_floating_context
array_append( & floating.build_queue, entry )
return nil
}
@(deferred_none = ui_floating_manager_end)
ui_floating_manager :: proc ( manager : ^UI_FloatingManager )
{
ui_floating_manager_begin(manager)
}
ui_floating_manager_begin :: proc ( manager : ^UI_FloatingManager )
{
state := get_state()
state.ui_floating_context = manager
}
ui_floating_manager_end :: proc()
{
state := get_state()
floating := & state.ui_floating_context
ui_floating_build()
floating = nil
}
ui_floating_build :: proc()
{
ui := ui_context()
using floating := get_state().ui_floating_context
for to_enqueue in array_to_slice( build_queue)
{
key := ui_key_from_string(to_enqueue.label)
lookup := hmap_chained_get( tracked, transmute(u64) key )
// Check if entry is already present
if lookup != nil && (lookup.next != nil || lookup == last) {
lookup.captures = to_enqueue.captures
lookup.builder = to_enqueue.builder
lookup.queued = true
continue
}
if lookup == nil {
error : AllocatorError
lookup, error = hmap_chained_set( tracked, transmute(u64) key, to_enqueue )
if error != AllocatorError.None {
ensure(false, "Failed to allocate entry to hashtable")
continue
}
}
else {
lookup.captures = to_enqueue.captures
lookup.builder = to_enqueue.builder
}
lookup.queued = true
dll_full_push_back(floating, lookup, nil )
// if first == nil {
// first = lookup
// last = lookup
// continue
// }
// if first == last {
// last = lookup
// last.prev = first
// first.next = last
// continue
// }
// last.next = lookup
// lookup.prev = last
// last = lookup
}
array_clear(build_queue)
to_raise : ^UI_Floating
for entry := first; entry != nil; entry = entry.next
{
if ! entry.queued
{
ensure(false, "There should be no queue failures yet")
if entry == first
{
first = entry.next
entry.next = nil
continue
}
if entry == last
{
last = last.prev
last.prev = nil
entry.prev = nil
continue
}
left := entry.prev
right := entry.next
left.next = right
right.prev = left
entry.prev = nil
entry.next = nil
}
if entry.builder( entry.captures ) && entry != last && to_raise == nil
{
to_raise = entry
}
entry.queued = false
}
if to_raise != nil
{
dll_full_pop( to_raise, floating )
dll_full_push_back( floating, to_raise, nil )
}
}

View File

@ -0,0 +1,142 @@
package sectr
/*
Widget Layout Ops
*/
ui_layout_children_horizontally :: proc( container : ^UI_Box, direction : UI_LayoutDirectionX, width_ref : ^f32 )
{
container_width : f32
if width_ref != nil {
container_width = width_ref ^
}
else {
container_width = container.computed.content.max.x - container.computed.content.min.x
}
// do layout calculations for the children
total_stretch_ratio : f32 = 0.0
size_req_children : f32 = 0
for child := container.first; child != nil; child = child.next
{
using child.layout
scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags)
if .Fixed_Width in flags
{
if scaled_width_by_height {
height := size.max.y != 0 ? size.max.y : container_width
width := height * size.min.x
size_req_children += width
continue
}
size_req_children += size.min.x
continue
}
total_stretch_ratio += anchor.ratio.x
}
avail_flex_space := container_width - size_req_children
allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 )
{
using child.layout
if ! (.Fixed_Width in flags) {
size.min.x = anchor.ratio.x * (1 / total_stretch_ratio) * avail_flex_space - child.layout.margins.left - child.layout.margins.right
}
flags |= {.Fixed_Width}
}
space_used : f32 = 0.0
switch direction{
case .Right_To_Left:
for child := container.last; child != nil; child = child.prev {
allocate_space(child, total_stretch_ratio, avail_flex_space)
using child.layout
anchor = range2({0, 0}, {0, 0})
pos.x = space_used
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
}
case .Left_To_Right:
for child := container.first; child != nil; child = child.next {
allocate_space(child, total_stretch_ratio, avail_flex_space)
using child.layout
anchor = range2({0, 0}, {0, 0})
pos.x = space_used
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
}
}
}
ui_layout_children_vertically :: proc( container : ^UI_Box, direction : UI_LayoutDirectionY, height_ref : ^f32 )
{
container_height : f32
if height_ref != nil {
container_height = height_ref ^
}
else {
container_height = container.computed.content.max.y - container.computed.content.min.y
}
// do layout calculations for the children
total_stretch_ratio : f32 = 0.0
size_req_children : f32 = 0
for child := container.first; child != nil; child = child.next
{
using child.layout
scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags)
if .Fixed_Height in flags
{
if scaled_width_by_height {
width := size.max.x != 0 ? size.max.x : container_height
height := width * size.min.y
size_req_children += height
continue
}
size_req_children += size.min.y
continue
}
total_stretch_ratio += anchor.ratio.y
}
avail_flex_space := container_height - size_req_children
allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 )
{
using child.layout
if ! (.Fixed_Height in flags) {
size.min.y = (anchor.ratio.y * (1 / total_stretch_ratio) * avail_flex_space)
}
flags |= {.Fixed_Height}
}
space_used : f32 = 0.0
switch direction
{
case .Bottom_To_Top:
for child := container.last; child != nil; child = child.prev {
allocate_space(child, total_stretch_ratio, avail_flex_space)
using child.layout
anchor = range2({0,0}, {0, 0})
// alignment = {0, 0}
pos.y = -space_used
space_used += size.min.y
size.min.x = container.computed.content.max.x + container.computed.content.min.x
}
case .Top_To_Bottom:
for child := container.first; child != nil; child = child.next {
allocate_space(child, total_stretch_ratio, avail_flex_space)
using child.layout
anchor = range2({0, 0}, {0, 0})
// alignment = {0, 0}
pos.y = -space_used
space_used += size.min.y
size.min.x = container.computed.content.max.x - container.computed.content.min.x
}
}
}

299
code/sectr/ui/tests.odin Normal file
View File

@ -0,0 +1,299 @@
package sectr
import "core:math/linalg"
import str "core:strings"
test_hover_n_click :: proc()
{
state := get_state(); using state
first_btn := ui_button( "FIRST BOX!" )
if first_btn.left_clicked || debug.frame_2_created {
debug.frame_2_created = true
second_layout := first_btn.layout
second_layout.pos = { 250, 0 }
ui_layout( second_layout )
second_box := ui_button( "SECOND BOX!")
}
}
test_draggable :: proc()
{
state := get_state(); using state
ui := ui_context
draggable_layout := UI_Layout {
flags = {
.Fixed_Position_X, .Fixed_Position_Y,
.Fixed_Width, .Fixed_Height,
.Origin_At_Anchor_Center,
},
// alignment = { 0.0, 0.5 },
alignment = { 0.5, 0 },
text_alignment = { 0.0, 0.0 },
// alignment = { 1.0, 1.0 },
pos = { 0, 0 },
size = range2({ 200, 200 }, {}),
}
ui_layout( draggable_layout )
ui_style( UI_Style {
corner_radii = { 0.3, 0.3, 0.3, 0.3 },
})
draggable := ui_widget( "Draggable Box!", UI_BoxFlags { .Mouse_Clickable } )
if draggable.first_frame {
debug.draggable_box_pos = draggable.layout.pos + { 0, -100 }
debug.draggable_box_size = draggable.layout.size.min
}
// Dragging
if draggable.active {
debug.draggable_box_pos += mouse_world_delta()
}
if (ui.hot == draggable.key) {
draggable.style.bg_color = Color_Blue
}
draggable.layout.pos = debug.draggable_box_pos
draggable.layout.size.min = debug.draggable_box_size
draggable.text = { str_fmt_alloc("%v", debug.draggable_box_pos), {} }
draggable.text.runes = to_runes(draggable.text.str)
}
test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style )
{
state := get_state(); using state
ui := ui_context
// frame := ui_widget( "Frame", {} )
// ui_parent(frame)
parent_layout := default_layout ^
parent_layout.size = range2( { 300, 300 }, {} )
parent_layout.alignment = { 0.0, 0.0 }
// parent_layout.margins = { 100, 100, 100, 100 }
parent_layout.padding = { 5, 10, 5, 5 }
parent_layout.pos = { 0, 0 }
parent_layout.flags = {
.Fixed_Position_X, .Fixed_Position_Y,
.Fixed_Width, .Fixed_Height,
.Origin_At_Anchor_Center
}
ui_layout(parent_layout)
parent_style := frame_style_default ^
ui_style(parent_style)
parent := ui_widget( "Parent", { .Mouse_Clickable })
ui_parent_push(parent)
{
if parent.first_frame {
debug.draggable_box_pos = parent.layout.pos
debug.draggable_box_size = parent.layout.size.min
}
if parent.active {
debug.draggable_box_pos += mouse_world_delta()
}
if (ui.hot == parent.key) {
parent.style.bg_color = Color_Blue
}
parent.layout.pos = debug.draggable_box_pos
parent.layout.size.min = debug.draggable_box_size
}
ui_resizable_handles( & parent, & debug.draggable_box_pos, & debug.draggable_box_size)
child_layout := default_layout ^
child_layout.size = range2({ 100, 100 }, { 0, 0 })
child_layout.alignment = { 0.0, 0.0 }
// child_layout.margins = { 20, 20, 20, 20 }
child_layout.padding = { 5, 5, 5, 5 }
// child_layout.anchor = range2({ 0.2, 0.1 }, { 0.1, 0.15 })
child_layout.pos = { 0, 0 }
child_layout.flags = {
.Fixed_Width, .Fixed_Height,
// .Origin_At_Anchor_Center
}
child_style := frame_style_default ^
child_style.bg_color = Color_GreyRed
ui_theme(child_layout, child_style)
child := ui_widget( "Child", { .Mouse_Clickable })
ui_parent_pop()
}
test_text_box :: proc()
{
state := get_state(); using state
ui := ui_context
@static pos : Vec2
layout := ui_layout_peek().default
layout.text_alignment = { 1.0, 1.0 }
// style.flags = { .Size_To_Text }
layout.padding = { 10, 10, 10, 10 }
layout.font_size = 32
ui_layout( layout)
text := str_intern( "Lorem ipsum dolor sit amet")
text_box := ui_text("TEXT BOX!", text, flags = { .Mouse_Clickable })
if text_box.first_frame {
pos = text_box.layout.pos
}
if text_box.active {
pos += mouse_world_delta()
}
text_box.layout.pos = pos
text_box.layout.size.min = { text_box.computed.text_size.x * 1.5, text_box.computed.text_size.y * 3 }
}
test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style )
{
profile("Whitespace AST test")
state := get_state(); using state
ui := ui_context
text_layout := default_layout^
text_layout.flags = {
.Origin_At_Anchor_Center,
.Fixed_Position_X, .Fixed_Position_Y,
.Fixed_Width, .Fixed_Height,
}
text_layout.text_alignment = { 0.0, 0.5 }
text_layout.alignment = { 0.0, 1.0 }
text_layout.size.min = { 1600, 30 }
text_style := frame_style_default ^
text_style_combo := to_ui_style_combo(text_style)
text_style_combo.default.bg_color = Color_Transparent
text_style_combo.disabled.bg_color = Color_Frame_Disabled
text_style_combo.hot.bg_color = Color_Frame_Hover
text_style_combo.active.bg_color = Color_Frame_Select
ui_theme( text_layout, text_style )
alloc_error : AllocatorError; success : bool
// debug.lorem_content, success = os.read_entire_file( debug.path_lorem, frame_allocator() )
// debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, frame_slab_allocator() )
// verify( alloc_error == .None, "Faield to parse due to allocation failure" )
text_space := str_intern( " " )
text_tab := str_intern( "\t")
// index := 0
widgets : Array(UI_Widget)
// widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 8 )
widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 4 * Kilobyte )
widgets_ptr := & widgets
label_id := 0
line_id := 0
for line in array_to_slice( debug.lorem_parse.lines )
{
if line_id == 0 {
line_id += 1
continue
}
ui_layout( text_layout )
line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {.Mouse_Clickable})
if line_hbox.key == ui.hot
{
line_hbox.text = StrRunesPair {}
ui_parent(line_hbox)
chunk_layout := text_layout
chunk_layout.alignment = { 0.0, 1.0 }
chunk_layout.anchor = range2({ 0.0, 0 }, { 0.0, 0 })
chunk_layout.pos = {}
chunk_layout.flags = { .Fixed_Position_X, .Size_To_Text }
chunk_style := text_style
ui_theme( to_ui_layout_combo(chunk_layout), to_ui_style_combo(chunk_style) )
head := line.first
for ; head != nil;
{
ui_layout( chunk_layout )
widget : UI_Widget
#partial switch head.type
{
case .Visible:
label := str_intern( str_fmt_alloc( "%v %v", head.content.str, label_id ))
widget = ui_text( label.str, head.content )
label_id += 1
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
case .Spaces:
label := str_intern( str_fmt_alloc( "%v %v", "space", label_id ))
widget = ui_text_spaces( label.str )
label_id += 1
for idx in 1 ..< len( head.content.runes )
{
// TODO(Ed): VIRTUAL WHITESPACE
// widget.style.layout.size.x += range2_size( widget.computed.bounds )
}
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
case .Tabs:
label := str_intern( str_fmt_alloc( "%v %v", "tab", label_id ))
widget = ui_text_tabs( label.str )
label_id += 1
for idx in 1 ..< len( head.content.runes )
{
// widget.style.layout.size.x += range2_size( widget.computed.bounds )
}
chunk_layout.pos.x += size_range2( widget.computed.bounds ).x
}
array_append( widgets_ptr, widget )
head = head.next
}
line_hbox.layout.size.min.x = chunk_layout.pos.x
}
else
{
builder_backing : [16 * Kilobyte] byte
builder := str.builder_from_bytes( builder_backing[:] )
line_hbox.layout.flags |= { .Size_To_Text }
head := line.first.next
for ; head != nil;
{
str.write_string( & builder, head.content.str )
head = head.next
}
line_hbox.text = str_intern( to_string( builder ) )
// if len(line_hbox.text.str) == 0 {
// line_hbox.text = str_intern( " " )
// }
}
if len(line_hbox.text.str) > 0 {
array_append( widgets_ptr, line_hbox )
text_layout.pos.x = text_layout.pos.x
text_layout.pos.y += size_range2(line_hbox.computed.bounds).y
}
else {
text_layout.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y
}
line_id += 1
}
label_id += 1 // Dummy action
}

37
code/sectr/ui/theme.odin Normal file
View File

@ -0,0 +1,37 @@
package sectr
UI_ThemePtr :: struct {
layout : ^UI_LayoutCombo,
style : ^UI_StyleCombo,
}
UI_Theme :: struct {
layout : UI_LayoutCombo,
style : UI_StyleCombo,
}
ui_theme_pop :: #force_inline proc() {
ui_layout_pop()
ui_style_pop()
}
@(deferred_none = ui_theme_pop)
ui_theme_via_layout_style :: #force_inline proc( layout : UI_Layout, style : UI_Style ) {
using ui := get_state().ui_context
ui_layout_push( layout )
ui_style_push( style )
}
@(deferred_none = ui_theme_pop)
ui_theme_via_combos :: #force_inline proc( layout : UI_LayoutCombo, style : UI_StyleCombo ) {
using ui := get_state().ui_context
ui_layout_push( layout )
ui_style_push( style )
}
@(deferred_none = ui_theme_pop)
ui_theme_via_theme :: #force_inline proc( theme : UI_Theme ) {
using ui := get_state().ui_context
ui_layout_push( theme.layout )
ui_style_push( theme.style )
}

3
code/sectr/ui/util.odin Normal file
View File

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

541
code/sectr/ui/widgets.odin Normal file
View File

@ -0,0 +1,541 @@
package sectr
import "base:runtime"
import lalg "core:math/linalg"
UI_Widget :: struct {
using box : ^UI_Box,
using signal : UI_Signal,
}
ui_widget :: proc( label : string, flags : UI_BoxFlags ) -> (widget : UI_Widget)
{
widget.box = ui_box_make( flags, label )
widget.signal = ui_signal_from_box( widget.box )
return
}
ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widget)
{
btn_flags := UI_BoxFlags { .Mouse_Clickable, .Focusable, .Click_To_Focus }
btn.box = ui_box_make( btn_flags | flags, label )
btn.signal = ui_signal_from_box( btn.box )
return
}
#region("Horizontal Box")
/*
Horizontal Boxes automatically manage a collection of widgets and
attempt to slot them adjacent to each other along the x-axis.
The user must provide the direction that the hbox will append entries.
How the widgets will be scaled will be based on the individual entires style flags.
All the usual behaviors that the style and box flags do apply when managed by the box widget.
Whether or not the horizontal box will scale the widget's width is if:
fixed size or "scale by ratio" flags are not used for the width.
The hbox will use the anchor's (range2) ratio.x value to determine the "stretch ratio".
Keep in mind the stretch ratio is only respected if no size.min.x value is violated for each of the widgets.
*/
// Horizontal Widget
UI_HBox :: struct {
using widget : UI_Widget,
direction : UI_LayoutDirectionX,
}
// Boilerplate creation
ui_hbox_begin :: proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) {
// profile(#procedure)
hbox.direction = direction
hbox.box = ui_box_make( flags, label )
hbox.signal = ui_signal_from_box(hbox.box)
// ui_box_compute_layout(hbox)
return
}
// Auto-layout children
ui_hbox_end :: proc( hbox : UI_HBox, width_ref : ^f32 = nil, compute_layout := true )
{
// profile(#procedure)
if compute_layout do ui_box_compute_layout(hbox.box, dont_mark_fresh = true)
ui_layout_children_horizontally( hbox.box, hbox.direction, width_ref )
}
@(deferred_out = ui_hbox_end_auto)
ui_hbox :: #force_inline proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) {
hbox = ui_hbox_begin(direction, label, flags)
ui_parent_push(hbox.box)
return
}
// Auto-layout children and pop parent from parent stack
ui_hbox_end_auto :: proc( hbox : UI_HBox ) {
ui_hbox_end(hbox)
ui_parent_pop()
}
#endregion("Horizontal Box")
#region("Resizable")
// Parameterized widget def for ui_resizable_handles
UI_Resizable :: struct {
using widget : UI_Widget,
handle_width : f32,
theme : ^UI_Theme,
left : bool,
right : bool,
top : bool,
bottom : bool,
corner_tr : bool,
corner_tl : bool,
corner_br : bool,
corner_bl : bool,
compute_layout : bool
}
ui_resizable_begin :: proc( label : string, flags : UI_BoxFlags = {},
handle_width : f32 = 15,
theme : ^UI_Theme,
left := true,
right := true,
top := true,
bottom := true,
corner_tr := true,
corner_tl := true,
corner_br := true,
corner_bl := true,
compute_layout := true ) -> (resizable : UI_Resizable)
{
resizable.box = ui_box_make(flags, label)
resizable.signal = ui_signal_from_box(resizable.box)
resizable.handle_width = handle_width
resizable.theme = theme
resizable.left = left
resizable.right = right
resizable.top = top
resizable.bottom = bottom
resizable.corner_tr = corner_tr
resizable.corner_tl = corner_tl
resizable.corner_br = corner_br
resizable.corner_bl = corner_bl
resizable.compute_layout = compute_layout
return
}
ui_resizable_end :: proc( resizable : ^UI_Resizable, pos, size : ^Vec2 ) {
using resizable
ui_resizable_handles( & widget, pos, size,
handle_width,
theme,
left,
right,
top,
bottom,
corner_tr,
corner_tl,
corner_br,
corner_bl,
compute_layout)
}
ui_resizable_begin_auto :: proc() {
}
ui_resizable_end_auto :: proc() {
}
// Adds resizable handles to a widget
ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2,
handle_width : f32 = 15,
theme : ^UI_Theme = nil,
left := true,
right := true,
top := true,
bottom := true,
corner_tr := true,
corner_tl := true,
corner_br := true,
corner_bl := true,
compute_layout := true) -> (drag_signal : b32)
{
profile(#procedure)
handle_left : UI_Widget
handle_right : UI_Widget
handle_top : UI_Widget
handle_bottom : UI_Widget
handle_corner_tr : UI_Widget
handle_corner_tl : UI_Widget
handle_corner_br : UI_Widget
handle_corner_bl : UI_Widget
ui_parent(parent)
@(deferred_none = ui_theme_pop)
theme_handle :: proc( base : ^UI_Theme, margins, size : Vec2, flags : UI_LayoutFlags = {})
{
layout_combo : UI_LayoutCombo
style_combo : UI_StyleCombo
if base != nil
{
layout_combo = base.layout
style_combo = base.style
{
layout_combo.default.margins = {margins.x, margins.x, margins.y, margins.y}
layout_combo.default.size.min = size
}
{
layout_combo.hot.margins = {margins.x, margins.x, margins.y, margins.y}
layout_combo.hot.size.min = size
}
{
layout_combo.active.margins = {margins.x, margins.x, margins.y, margins.y}
layout_combo.active.size.min = size
}
}
else
{
layout := UI_Layout {
flags = flags,
anchor = range2({},{}),
alignment = {0, 0},
text_alignment = {0.0, 0.0},
font_size = 16,
margins = to_ui_layout_side(margins),
padding = {0, 0, 0, 0},
border_width = 0,
pos = {0, 0},
size = range2(size,{})
}
style := UI_Style {
bg_color = Color_Transparent,
border_color = Color_Transparent,
corner_radii = {5, 0, 0, 0},
blur_size = 0,
font = get_state().default_font,
text_color = Color_ThmDark_Text_Default,
cursor = {},
}
layout_combo = to_ui_layout_combo(layout)
style_combo = to_ui_style_combo(style)
{
using layout_combo.hot
using style_combo.hot
bg_color = Color_ThmDark_ResizeHandle_Hot
}
{
using layout_combo.active
using style_combo.active
bg_color = Color_ThmDark_ResizeHandle_Active
}
}
theme := UI_Theme {
layout_combo, style_combo
}
ui_layout_push(theme.layout)
ui_style_push(theme.style)
}
flags := UI_BoxFlags { .Mouse_Clickable }
name :: proc( label : string ) -> string {
parent_label := (transmute(^string) context.user_ptr) ^
return str_intern(str_fmt_alloc("%v: %v", parent_label, label )).str
}
context.user_ptr = & parent.label
#region("Handle & Corner construction")
theme_handle( theme, {handle_width, 0}, {handle_width,0})
if left {
handle_left = ui_widget(name("resize_handle_left"), flags )
handle_left.layout.anchor.left = 0
handle_left.layout.anchor.right = 1
handle_left.layout.alignment = {1, 0}
}
if right {
handle_right = ui_widget(name("resize_handle_right"), flags )
handle_right.layout.anchor.left = 1
}
theme_handle( theme, {0, handle_width}, {0, handle_width})
if top {
handle_top = ui_widget(name("resize_handle_top"), flags )
handle_top.layout.anchor.bottom = 1
handle_top.layout.alignment = {0, -1}
}
if bottom {
handle_bottom = ui_widget("resize_handle_bottom", flags)
handle_bottom.layout.anchor.top = 1
handle_bottom.layout.alignment = { 0, 0 }
}
theme_handle( theme, {0,0}, {handle_width, handle_width}, {.Fixed_Width, .Fixed_Height} )
if corner_tl {
handle_corner_tl = ui_widget(name("corner_top_left"), flags)
handle_corner_tl.layout.alignment = {1, -1}
}
if corner_tr {
handle_corner_tr = ui_widget(name("corner_top_right"), flags)
handle_corner_tr.layout.anchor = range2({1, 0}, {})
handle_corner_tr.layout.alignment = {0, -1}
}
if corner_bl {
handle_corner_bl = ui_widget("corner_bottom_left", flags)
handle_corner_bl.layout.anchor = range2({}, {0, 1})
handle_corner_bl.layout.alignment = { 1, 0 }
}
if corner_br {
handle_corner_br = ui_widget("corner_bottom_right", flags)
handle_corner_br.layout.anchor = range2({1, 0}, {0, 1})
handle_corner_br.layout.alignment = {0, 0}
}
#endregion("Handle & Corner construction")
process_handle_drag :: #force_inline proc ( handle : ^UI_Widget,
direction : Vec2,
target_alignment : Vec2,
target_center_aligned : Vec2,
pos : ^Vec2,
size : ^Vec2,
alignment : ^Vec2, ) -> b32
{
@static active_context : ^UI_State
@static was_dragging : b32 = false
@static start_size : Vec2
@static prev_left_shift_held : b8
@static prev_alignment : Vec2
ui := get_state().ui_context
using handle
if ui.last_pressed_key != key || (!active && (!released || !was_dragging)) do return false
direction := direction
align_adjsutment := left_shift_held ? target_center_aligned : target_alignment
size_delta := ui_drag_delta()
pos_adjust := size^ * (alignment^ - align_adjsutment)
pos_reverse := size^ * (alignment^ - prev_alignment)
shift_changed := (left_shift_held != prev_left_shift_held)
need_to_change_alignment_and_pos := pressed || shift_changed
if active
{
if pressed
{
active_context = ui
start_size = size^
prev_left_shift_held = left_shift_held
}
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
pos_adjust = size^ * 0.5 * direction
pos_reverse = size^ * 0.5 * direction
}
latest_size := start_size + size_delta * direction
if pressed
{
pos^ -= pos_adjust
}
else if shift_changed
{
if (.Origin_At_Anchor_Center in parent.layout.flags) {
pos^ -= pos_reverse
alignment^ = !left_shift_held ? target_center_aligned : target_alignment
}
else
{
if !left_shift_held {
pos^ -= size^ * direction * 0.5
alignment^ = target_center_aligned
}
else {
pos^ += size^ * direction * 0.5 // Right
alignment^ = target_alignment
}
}
}
else
{
size^ = latest_size
alignment^ = align_adjsutment
}
was_dragging = true
}
else if released && was_dragging
{
// This needed to be added as for some reason, this was getting called in screen_ui even when we were resizing with a handle in a worksapce
if active_context != ui do return false
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
pos_adjust = size^ * 0.5 * direction
pos_reverse = size^ * 0.5 * direction
}
pos^ += pos_adjust
alignment^ = align_adjsutment
was_dragging = false
start_size = 0
}
// text = active_context.root.label
// style.text_color = Color_White
prev_left_shift_held = handle.left_shift_held
prev_alignment = align_adjsutment
return was_dragging
}
state := get_state()
alignment := & parent.layout.alignment
if .Origin_At_Anchor_Center in parent.layout.flags
{
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0}, { 0.5, 0}, {0, 0}, pos, size, alignment )
if left do drag_signal |= process_handle_drag( & handle_left, {-1, 0}, {-0.5, 0}, {0, 0}, pos, size, alignment )
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1}, { 0, 0.5}, {0, 0}, pos, size, alignment )
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1}, { 0, -0.5}, {0, 0}, pos, size, alignment )
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1}, { 0.5, 0.5}, {0, 0}, pos, size, alignment )
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, {-1, 1}, {-0.5, 0.5}, {0, 0}, pos, size, alignment )
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1}, { 0.5, -0.5}, {0, 0}, pos, size, alignment )
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, {-1, -1}, {-0.5, -0.5}, {0, 0}, pos, size, alignment )
}
else
{
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0 }, {0, 0}, { 0.5, 0}, pos, size, alignment )
if left do drag_signal |= process_handle_drag( & handle_left, { -1, 0 }, {1, 0}, { 0.5, 0}, pos, size, alignment )
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1 }, {0, -1}, { 0.0, -0.5}, pos, size, alignment )
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1 }, {0, 0}, { 0.0, -0.5}, pos, size, alignment )
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1 }, {0, -1}, { 0.5, -0.5}, pos, size, alignment )
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, { -1, 1 }, {1, -1}, { 0.5, -0.5}, pos, size, alignment )
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1 }, {0, 0}, { 0.5, -0.5}, pos, size, alignment )
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, { -1, -1 }, {1, 0}, { 0.5, -0.5}, pos, size, alignment )
}
if drag_signal && compute_layout do ui_box_compute_layout(parent)
return
}
#endregion("Resizable")
ui_spacer :: proc( label : string ) -> (widget : UI_Widget) {
widget.box = ui_box_make( {.Mouse_Clickable}, label )
widget.signal = ui_signal_from_box( widget.box )
widget.style.bg_color = Color_Transparent
return
}
UI_ScrollBox :: struct {
using widget : UI_Widget,
scroll_bar : UI_Widget,
content : UI_Widget,
}
ui_scroll_box :: proc( label : string, flags : UI_BoxFlags ) -> (scroll_box : UI_ScrollBox) {
fatal("NOT IMPLEMENTED")
return
}
// ui_scrollable_view( )
#region("Text")
ui_text :: proc( label : string, content : StrRunesPair, flags : UI_BoxFlags = {} ) -> UI_Widget
{
// profile(#procedure)
state := get_state(); using state
box := ui_box_make( flags, label )
signal := ui_signal_from_box( box )
box.text = content
return { box, signal }
}
ui_text_spaces :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget
{
// profile(#procedure)
state := get_state(); using state
// TODO(Ed) : Move this somwhere in state.
space_str := str_intern( " " )
box := ui_box_make( flags, label )
signal := ui_signal_from_box( box )
box.text = space_str
return { box, signal }
}
ui_text_tabs :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget
{
// profile(#procedure)
state := get_state(); using state
// TODO(Ed) : Move this somwhere in state.
tab_str := str_intern( "\t" )
box := ui_box_make( flags, label )
signal := ui_signal_from_box( box )
box.text = tab_str
return { box, signal }
}
ui_text_wrap_panel :: proc( parent : ^UI_Widget )
{
fatal("NOT IMPLEMENTED")
}
#endregion("Text")
#region("Vertical Box")
/*
Vertical Boxes automatically manage a collection of widgets and
attempt to slot them adjacent to each other along the y-axis.
The user must provide the direction that the vbox will append entries.
How the widgets will be scaled will be based on the individual entires style flags.
All the usual behaviors that the style and box flags do apply when managed by the box widget.
Whether or not the horizontal box will scale the widget's width is if:
fixed size or "scale by ratio" flags are not used for the width.
The hbox will use the anchor's (range2) ratio.y value to determine the "stretch ratio".
Keep in mind the stretch ratio is only respected if no size.min.y value is violated for each of the widgets.
*/
UI_VBox :: struct {
using widget : UI_Widget,
direction : UI_LayoutDirectionY,
}
// Boilerplate creation
ui_vbox_begin :: proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {}, compute_layout := false ) -> (vbox : UI_VBox) {
// profile(#procedure)
vbox.direction = direction
vbox.box = ui_box_make( flags, label )
vbox.signal = ui_signal_from_box( vbox.box )
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
return
}
// Auto-layout children
ui_vbox_end :: proc( vbox : UI_VBox, height_ref : ^f32 = nil, compute_layout := true ) {
// profile(#procedure)
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
ui_layout_children_vertically( vbox.box, vbox.direction, height_ref )
}
// Auto-layout children and pop parent from parent stack
ui_vbox_end_pop_parent :: proc( vbox : UI_VBox ) {
ui_parent_pop()
ui_vbox_end(vbox)
}
@(deferred_out = ui_vbox_end_pop_parent)
ui_vbox :: #force_inline proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {} ) -> (vbox : UI_VBox) {
vbox = ui_vbox_begin(direction, label, flags)
ui_parent_push(vbox.widget)
return
}
#endregion("Vertical Box")