From 4b026c379ae2f99f99c1dffa4f8addd5da52d58e Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 8 Mar 2024 18:45:08 -0500 Subject: [PATCH] Started to do manual control of the frame timing (no longer using raylib) --- Readme.md | 34 ++++++++++- code/api.odin | 72 ++++++++++++++++++++-- code/chrono.odin | 35 +++++++++++ code/env.odin | 29 ++++++--- code/grime.odin | 6 +- code/grime_virtual_arena.odin | 4 +- code/grime_vmem_windows.odin | 52 ---------------- code/grime_windows.odin | 112 ++++++++++++++++++++++++++++++++++ code/host/host.odin | 11 ++-- code/tick_render.odin | 13 ++-- 10 files changed, 286 insertions(+), 82 deletions(-) create mode 100644 code/chrono.odin delete mode 100644 code/grime_vmem_windows.odin create mode 100644 code/grime_windows.odin diff --git a/Readme.md b/Readme.md index d913c39..c13e8b6 100644 --- a/Readme.md +++ b/Readme.md @@ -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 diff --git a/code/api.odin b/code/api.odin index e760a75..851c3fc 100644 --- a/code/api.odin +++ b/code/api.odin @@ -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 } diff --git a/code/chrono.odin b/code/chrono.odin new file mode 100644 index 0000000..ef6dafc --- /dev/null +++ b/code/chrono.odin @@ -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 diff --git a/code/env.odin b/code/env.odin index 35277bf..4f16c9f 100644 --- a/code/env.odin +++ b/code/env.odin @@ -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, } diff --git a/code/grime.odin b/code/grime.odin index 12f7fe8..5af86c5 100644 --- a/code/grime.odin +++ b/code/grime.odin @@ -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" diff --git a/code/grime_virtual_arena.odin b/code/grime_virtual_arena.odin index 9e2bf82..c0162ec 100644 --- a/code/grime_virtual_arena.odin +++ b/code/grime_virtual_arena.odin @@ -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: diff --git a/code/grime_vmem_windows.odin b/code/grime_vmem_windows.odin deleted file mode 100644 index 82bd5c4..0000000 --- a/code/grime_vmem_windows.odin +++ /dev/null @@ -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 diff --git a/code/grime_windows.odin b/code/grime_windows.odin new file mode 100644 index 0000000..e27235f --- /dev/null +++ b/code/grime_windows.odin @@ -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 diff --git a/code/host/host.odin b/code/host/host.odin index d79c240..fd7b756 100644 --- a/code/host/host.odin +++ b/code/host/host.odin @@ -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 ) } diff --git a/code/tick_render.odin b/code/tick_render.odin index ae289b4..f0b20e0 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -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