Started to do manual control of the frame timing (no longer using raylib)

This commit is contained in:
Edward R. Gonzalez 2024-03-08 18:45:08 -05:00
parent 191d5076ea
commit 4b026c379a
10 changed files with 286 additions and 82 deletions

View File

@ -2,11 +2,41 @@
This prototype aims to flesh out ideas I've wanted to explore futher when it came to code editing and tools for code in general.
The things to explore:
* 2D canvas for laying out code visualized in various types of ASTs
* WYSIWYG frontend ASTs
* Making AST editing as versatile as text editing.
* High-performance generating a large amount of UI widget boxes with proper auto-layout & no perceptible rendering-lag or input lag for interactions (framtimes stable).
The project is so far in a "codebase boostrapping" phase.
The code is organized into 2 modules sectr_host & sectr.
The project's is organized into 2 modules sectr_host & sectr.
The host module loads the main module & its memory. Hot-reloading it's dll when it detects a change.
The main module only depends on libraries provided by odin repo's base, core, or vendor related packages, and a ini-parsing library.
The dependencies are:
* Odin Compiler
* Odin repo's base, core, and vendor(raylib) libaries
* An ini parser
The client(sectr) module's organization is relatively flat due to the nature of odin's package management not allowing for cyclic dependencies across modules, and modules can only be in one directory.
Even so the notatble groups are:
* API : Provides the overarching interface of the app's general behavior. Host uses this to provide the client its necessary data and exection env.
* Has the following definitions: startup, shutdown, reload, tick, clean_frame
* Grime : Name speaks for itself, stuff not directly related to the target features to iterate upon for the prototype.
* Defining dependency aliases or procedure overload tables, rolling own allocator, data structures, etc.
* Font Provider : Manages fonts.
* When loading fonts, the provider currently uses raylib to generate bitmap glyth sheets for a range of font sizes at once.
* Goal is to eventually render using SDF shaders.
* Input : Standard input pooling and related features. Platform abstracted via raylib for now.
* Parser : AST generation, editing, and serialization. A 1/3 of this prototype will most likely be this alone.
* UI : AST visualzation & editing, backend visualization, project organizationa via workspaces (2d cavnases)
* Will most likely be the bulk of this prototype.
* PIMGUI (Persistent Immediate Mode User Interface);
* Auto-layout with heavy procedural generation of box widgets

View File

@ -8,6 +8,7 @@ import "core:mem/virtual"
import "core:os"
import "core:slice"
import "core:strings"
import "core:time"
import rl "vendor:raylib"
Path_Assets :: "../assets/"
@ -28,6 +29,8 @@ ModuleAPI :: struct {
@export
startup :: proc( persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^VArena, host_logger : ^ Logger )
{
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 )
@ -41,6 +44,7 @@ startup :: proc( persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^V
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() )
@ -94,9 +98,14 @@ startup :: proc( persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^V
cam_zoom_sensitivity_digital = 0.2
cam_zoom_sensitivity_smooth = 4.0
engine_refresh_hz = 30
ui_resize_border_width = 20
}
Desired_OS_Scheduler_MS :: 1
sleep_is_granular = set__scheduler_granularity( Desired_OS_Scheduler_MS )
// rl.Odin_SetMalloc( RL_MALLOC )
rl.SetConfigFlags( {
@ -122,7 +131,7 @@ startup :: proc( persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^V
// Determining current monitor and setting the target frametime based on it..
monitor_id = rl.GetCurrentMonitor()
monitor_refresh_hz = rl.GetMonitorRefreshRate( monitor_id )
rl.SetTargetFPS( monitor_refresh_hz )
rl.SetTargetFPS( 60 * 24 )
log( str_fmt_tmp( "Set target FPS to: %v", monitor_refresh_hz ) )
// Basic Font Setup
@ -166,6 +175,14 @@ startup :: proc( persistent_mem, frame_mem, transient_mem, files_buffer_mem : ^V
ui_startup( & workspace.ui, cache_allocator = general_slab_allocator() )
}
}
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.
free_all( transient_allocator() )
}
// For some reason odin's symbols conflict with native foreign symbols...
@ -220,17 +237,60 @@ swap :: proc( a, b : ^ $Type ) -> ( ^ Type, ^ Type ) {
}
@export
tick :: proc( delta_time : f64, delta_ns : Duration ) -> b32
tick :: proc( host_delta_time : f64, host_delta_ns : Duration ) -> b32
{
client_tick := time.tick_now()
context.allocator = frame_allocator()
context.temp_allocator = transient_allocator()
state := get_state(); using state
state := get_state()
state.frametime_delta_ns = delta_ns
state.frametime_delta_seconds = delta_time
rl.PollInputEvents()
result := update( delta_time )
result := update( host_delta_time )
render()
rl.SwapScreenBuffer()
config.engine_refresh_hz = uint(monitor_refresh_hz)
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
}
}
if frametime_elapsed_ms > 60.0 {
log( str_fmt_tmp("Big tick! %v ms", frametime_elapsed_ms), LogLevel.Warning )
}
return result
}

35
code/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

View File

@ -126,6 +126,8 @@ AppConfig :: struct {
cam_zoom_sensitivity_smooth : f32,
cam_zoom_sensitivity_digital : f32,
engine_refresh_hz : uint,
ui_resize_border_width : uint,
}
@ -148,11 +150,14 @@ State :: struct {
monitor_id : i32,
monitor_refresh_hz : i32,
engine_refresh_hz : i32,
engine_refresh_target : i32,
sleep_is_granular : b32,
frametime_delta_seconds : f64,
frametime_delta_ns : Duration,
frametime_delta_seconds : f64,
frametime_delta_ms : f64,
frametime_delta_ns : Duration,
frametime_target_ms : f64,
frametime_elapsed_ms : f64,
font_firacode : FontID,
font_squidgy_slimes : FontID,
@ -195,12 +200,24 @@ Project :: struct {
workspace : Workspace,
}
Frame :: struct
{
pos : Vec2,
size : Vec2,
ui : ^UI_Box,
}
Workspace :: struct {
name : string,
cam : Camera,
zoom_target : f32,
frames : Array(Frame),
test_frame : Frame,
// TODO(Ed) : The workspace is mainly a 'UI' conceptually...
ui : UI_State,
}
@ -223,8 +240,4 @@ DebugData :: struct {
draggable_box_pos : Vec2,
draggable_box_size : Vec2,
box_original_size : Vec2,
box_resize_started : b32,
ui_drag_delta : Vec2,
ui_drag_start : Vec2,
}

View File

@ -22,6 +22,7 @@ import "core:hash"
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
@ -67,7 +68,10 @@ import "core:path/filepath"
import str "core:strings"
str_builder_to_string :: str.to_string
import "core:time"
Duration :: time.Duration
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"

View File

@ -219,7 +219,7 @@ varena_allocator_proc :: proc(
old_memory_offset := uintptr(old_memory) + uintptr(old_size)
current_offset := uintptr(arena.reserve_start) + uintptr(arena.commit_used)
verify( old_memory_offset != current_offset, "Cannot resize existing allocation in vitual arena to a larger size unless it was the last allocated" )
verify( old_memory_offset == current_offset, "Cannot resize existing allocation in vitual arena to a larger size unless it was the last allocated" )
new_region : []byte
new_region, alloc_error = varena_alloc( arena, size - old_size, alignment, (mode != .Resize_Non_Zeroed), location )
@ -228,7 +228,7 @@ varena_allocator_proc :: proc(
return
}
data := byte_slice( old_memory, size )
data = byte_slice( old_memory, size )
return
case .Query_Features:

View File

@ -1,52 +0,0 @@
/* Windows Virtual Memory
Windows is the only os getting special vmem definitions
since I want full control of it for debug purposes.
*/
package sectr
import core_virtual "core:mem/virtual"
import win32 "core:sys/windows"
when ODIN_OS == OS_Type.Windows {
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) size_of(VirtualMemoryRegion)
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 = memory_after_header(vmem.base_address)
vmem.reserved = size
vmem.committed = header_size
alloc_error = .None
return
}
} // END: ODIN_OS == runtime.Odin_OS_Type.Windows

112
code/grime_windows.odin Normal file
View File

@ -0,0 +1,112 @@
package sectr
import "core:c"
import "core:c/libc"
import "core:fmt"
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) size_of(VirtualMemoryRegion)
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 = memory_after_header(vmem.base_address)
vmem.reserved = size
vmem.committed = header_size
alloc_error = .None
return
}
} // END: ODIN_OS == runtime.Odin_OS_Type.Windows

View File

@ -293,18 +293,19 @@ main :: proc()
delta_ns : Duration
host_tick := time.tick_now()
// TODO(Ed) : This should have an end status so that we know the reason the engine stopped.
for ; running ;
{
start_tick := time.tick_now()
// Hot-Reload
sync_sectr_api( & sectr_api, & memory, & logger )
running = sectr_api.tick( duration_seconds( delta_ns ), delta_ns )
sectr_api.clean_frame()
delta_ns = time.tick_lap_time( & start_tick )
delta_ns = time.tick_lap_time( & host_tick )
host_tick = time.tick_now()
}
// Determine how the run_cyle completed, if it failed due to an error,
@ -318,5 +319,7 @@ main :: proc()
log("Succesfuly closed")
file_close( logger.file )
file_rename( logger.file_path, path_logger_finalized )
// TODO(Ed) : Add string interning!!!!!!!!!
// file_rename( logger.file_path, path_logger_finalized )
file_rename( str_fmt_tmp( "%s/sectr.log", Path_Logs), path_logger_finalized )
}

View File

@ -21,7 +21,7 @@ render :: proc()
render_mode_2d()
//region Render Screenspace
{
fps_msg := str_fmt_tmp( "FPS: %v", rl.GetFPS() )
fps_msg := str_fmt_tmp( "FPS: %f", 1 / (frametime_elapsed_ms * MS_To_S) )
fps_msg_width := measure_text_size( fps_msg, default_font, 16.0, 0.0 ).x
fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 }
debug_draw_text( fps_msg, fps_msg_pos, 16.0, color = rl.GREEN )
@ -43,15 +43,18 @@ render :: proc()
position.y += debug.draw_debug_text_y
content := str_fmt_buffer( draw_text_scratch[:], format, ..args )
debug_draw_text( content, position, 16.0 )
debug_draw_text( content, position, 14.0 )
debug.draw_debug_text_y += 16
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")
}
@ -70,10 +73,6 @@ render :: proc()
rl.DrawCircleV( cursor_pos, 10, Color_White_A125 )
}
debug_text( "ui_drag_start : %v", debug.ui_drag_start )
debug_text( "ui_drag_delta : %v", debug.ui_drag_delta )
debug_text( "Draggable Box Pos: %v", debug.draggable_box_pos )
debug.draw_debug_text_y = 50
}
//endregion Render Screenspace