Got replay working

Doesn't work across app runs, unlike handmade the crux here is raylib. Even if we did raylibs features ourselves there would still be an issue of restoring the gpu memory state. So in order to have replays work across app runs is proper state serialization.

I'll leave that for later and focus on the next core features.
This commit is contained in:
Edward R. Gonzalez 2024-02-08 16:05:15 -05:00
parent 9b4ceeffda
commit 84d9675a27
7 changed files with 231 additions and 44 deletions

View File

@ -37,6 +37,9 @@ startup :: proc( live_mem : virtual.Arena, snapshot_mem : []u8 )
using memory;
block := live_mem.curr_block
live = live_mem
snapshot = snapshot_mem
persistent_slice := slice_ptr( block.base, memory_persistent_size )
transient_slice := slice_ptr( memory_after( persistent_slice), memory_trans_temp_size )
temp_slice := slice_ptr( memory_after( transient_slice), memory_trans_temp_size )
@ -91,7 +94,7 @@ sectr_shutdown :: proc()
// Replay
{
os.close( state.replay.active_file )
os.close( memory.replay.active_file )
}
// Raylib
@ -107,6 +110,9 @@ reload :: proc( live_mem : virtual.Arena, snapshot_mem : []u8 )
using memory;
block := live_mem.curr_block
live = live_mem
snapshot = snapshot_mem
persistent_slice := slice_ptr( block.base, memory_persistent_size )
transient_slice := slice_ptr( memory_after( persistent_slice), memory_trans_temp_size )
temp_slice := slice_ptr( memory_after( transient_slice), memory_trans_temp_size )
@ -114,8 +120,6 @@ reload :: proc( live_mem : virtual.Arena, snapshot_mem : []u8 )
persistent = cast( ^TrackedAllocator ) & persistent_slice[0]
transient = cast( ^TrackedAllocator ) & transient_slice[0]
temp = cast( ^TrackedAllocator ) & temp_slice[0]
snapshot = snapshot_mem
}
// TODO(Ed) : This lang really not have a fucking swap?
@ -126,12 +130,13 @@ swap :: proc( a, b : ^ $Type ) -> ( ^ Type, ^ Type ) {
@export
update :: proc() -> b32
{
state := get_state(); using state
state := get_state(); using state
replay := & memory.replay
state.input, state.input_prev = swap( state.input, state.input_prev )
poll_input( state.input_prev, state.input )
debug_actions : DebugActions
debug_actions : DebugActions = {}
poll_debug_actions( & debug_actions, state.input )
// Input Replay
@ -147,30 +152,40 @@ update :: proc() -> b32
}
}}
DO_NOT_CONTINUE : b32 = false
if debug_actions.play_replay { switch replay.mode
{
case ReplayMode.Off : {
replay_playback_begin( Path_Input_Replay )
if ! file_exists( Path_Input_Replay ) {
save_snapshot( & memory.snapshot[0] )
replay_recording_begin( Path_Input_Replay )
break
}
else {
load_snapshot( & memory.snapshot[0] )
replay_playback_begin( Path_Input_Replay )
break
}
}
case ReplayMode.Playback : {
replay_playback_end()
load_snapshot( & memory.snapshot[0] )
break
}
case ReplayMode.Record : {
replay_recording_end( )
replay_recording_end()
load_snapshot( & memory.snapshot[0] )
replay_playback_begin( Path_Input_Replay )
break
}
}}
if replay.loop_active
{
if replay.mode == ReplayMode.Record {
record_input( replay.active_file, input )
}
else if replay.mode == ReplayMode.Playback {
play_input( replay.active_file, input )
}
if replay.mode == ReplayMode.Record {
record_input( replay.active_file, input )
}
else if replay.mode == ReplayMode.Playback {
play_input( replay.active_file, input )
}
}
@ -178,6 +193,8 @@ update :: proc() -> b32
debug.mouse_vis = !debug.mouse_vis
}
debug.mouse_pos.basis = { input.mouse.X, input.mouse.Y }
should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose()
return should_shutdown
}
@ -185,7 +202,8 @@ update :: proc() -> b32
@export
render :: proc()
{
state := get_state(); using state
state := get_state(); using state
replay := & memory.replay
rl.BeginDrawing()
rl.ClearBackground( Color_BG )
@ -213,17 +231,19 @@ render :: proc()
draw_text( "Screen Width : %v", rl.GetScreenWidth () )
draw_text( "Screen Height: %v", rl.GetScreenHeight() )
if pressed( input.keyboard.M ) {
draw_text( "M Prssed" )
if replay.mode == ReplayMode.Record {
draw_text( "Recording Input")
}
if pressed( input.keyboard.right_alt ) {
draw_text( "Alt Pressed")
if replay.mode == ReplayMode.Playback {
draw_text( "Replaying Input")
}
if debug.mouse_vis {
width : f32 = 32
pos := debug.mouse_pos
draw_text( "Position: %v", rl.GetMousePosition() )
mouse_rect : rl.Rectangle
mouse_rect.x = pos.x - width/2
mouse_rect.y = pos.y - width/2

View File

@ -19,7 +19,9 @@ Memory :: struct {
snapshot : []u8,
persistent : ^ TrackedAllocator,
transient : ^ TrackedAllocator,
temp : ^ TrackedAllocator
temp : ^ TrackedAllocator,
replay : ReplayState
}
State :: struct {
@ -28,7 +30,6 @@ State :: struct {
input_prev : ^ InputState,
input : ^ InputState,
replay : ReplayState,
debug : DebugData,
project : Project,
@ -66,7 +67,7 @@ DebugData :: struct {
draw_debug_text_y : f32,
mouse_vis : b32,
mouse_pos : vec3,
mouse_pos : vec2,
}
DebugActions :: struct {
@ -84,11 +85,11 @@ poll_debug_actions :: proc( actions : ^ DebugActions, input : ^ InputState )
using actions
using input
base_replay_bind := pressed(keyboard.right_alt) && pressed( keyboard.L)
record_replay = base_replay_bind && pressed(keyboard.right_shift)
play_replay = base_replay_bind
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 = pressed(keyboard.right_alt) && pressed(keyboard.M)
show_mouse_pos = keyboard.right_alt.ended_down && pressed(keyboard.M)
}
save_snapshot :: proc( snapshot : [^]u8 ) {
@ -115,14 +116,16 @@ ReplayState :: struct {
replay_recording_begin :: proc( path : string )
{
result := os.remove( path )
if ( result != os.ERROR_NONE )
{
// TODO(Ed) : Setup a proper logging interface
fmt. printf( "Failed to delete replay file before beginning a new one" )
runtime.debug_trap()
os. exit( -1 )
// TODO(Ed) : Figure out the error code enums..
if file_exists( path ) {
result := os.remove( path )
if ( result != os.ERROR_NONE )
{
// TODO(Ed) : Setup a proper logging interface
fmt. printf( "Failed to delete replay file before beginning a new one" )
runtime.debug_trap()
os. exit( -1 )
// TODO(Ed) : Figure out the error code enums..
}
}
replay_file, open_error := os.open( path, os.O_RDWR | os.O_CREATE )
@ -134,15 +137,18 @@ replay_recording_begin :: proc( path : string )
os. exit( -1 )
// TODO(Ed) : Figure out the error code enums..
}
os.seek( replay_file, 0, 0 )
state := get_state(); using state
replay := & memory.replay
replay.active_file = replay_file
replay.mode = ReplayMode.Record
}
replay_recording_end :: proc() {
state := get_state(); using state
replay := & memory.replay
replay.mode = ReplayMode.Off
os.seek( replay.active_file, 0, 0 )
os.close( replay.active_file )
}
@ -167,14 +173,17 @@ replay_playback_begin :: proc( path : string )
// TODO(Ed) : Figure out the error code enums..
}
// TODO(Ed): WE need to wrap any actions that can throw a fatal like this. Files need a grime wrap.
os.seek( replay_file, 0, 0 )
state := get_state(); using state
replay := & memory.replay
replay.active_file = replay_file
replay.mode = ReplayMode.Playback
}
replay_playback_end :: proc() {
state := get_state(); using state
input := get_state().input
replay := & memory.replay
replay.mode = ReplayMode.Off
os.seek( replay.active_file, 0, 0 )
os.close( replay.active_file )
}

View File

@ -52,3 +52,14 @@ is_file_locked :: proc( file_path : string ) -> b32 {
os.close(handle)
return false
}
rewind :: proc ( file : os.Handle ) {
os.seek( file, 0, 0 )
}
read_looped :: proc ( file : os.Handle, data : []byte ) {
total_read, result_code := os.read( file, data )
if result_code == os.ERROR_HANDLE_EOF {
rewind( file )
}
}

View File

@ -62,7 +62,9 @@ setup_memory :: proc () -> VMemChunk
// Setup the static arena for the entire application
{
result := virtual.arena_init_static( & sectr_live, sectr.memory_chunk_size, sectr.memory_chunk_size )
base_address : rawptr = transmute( rawptr) u64(Terabyte * 2)
result := arena_init_static( & sectr_live, base_address, sectr.memory_chunk_size, sectr.memory_chunk_size )
if result != runtime.Allocator_Error.None
{
// TODO(Ed) : Setup a proper logging interface
@ -86,7 +88,12 @@ setup_memory :: proc () -> VMemChunk
os. exit( -1 )
// TODO(Ed) : Figure out the error code enums..
}
file_resize( snapshot_file, sectr.memory_chunk_size )
file_info, stat_code := os.stat( path_snapshot )
{
if file_info.size != sectr.memory_chunk_size {
file_resize( snapshot_file, sectr.memory_chunk_size )
}
}
map_error : virtual.Map_File_Error
sectr_snapshot, map_error = virtual.map_file_from_file_descriptor( uintptr(snapshot_file), { virtual.Map_File_Flag.Read, virtual.Map_File_Flag.Write } )

View File

@ -0,0 +1,126 @@
// TODO(Ed): Move this to the grime module when its made
// This was made becaause odin didn't expose the base_address param that virtual alloc allows.
package host
import "core:mem"
import "core:mem/virtual"
import win32 "core:sys/windows"
@(private="file")
virtual_Platform_Memory_Block :: struct {
block: virtual.Memory_Block,
committed: uint,
reserved: uint,
}
@(private="file", require_results)
align_formula :: #force_inline proc "contextless" (size, align: uint) -> uint {
result := size + align-1
return result - result%align
}
@(private="file")
win32_reserve :: proc "contextless" (base_address : rawptr, size: uint) -> (data: []byte, err: virtual.Allocator_Error) {
result := win32.VirtualAlloc(base_address, size, win32.MEM_RESERVE, win32.PAGE_READWRITE)
if result == nil {
err = .Out_Of_Memory
return
}
data = ([^]byte)(result)[:size]
return
}
@(private="file")
platform_memory_alloc :: proc "contextless" (to_commit, to_reserve: uint, base_address : rawptr) ->
(block: ^virtual_Platform_Memory_Block, err: virtual.Allocator_Error)
{
to_commit, to_reserve := to_commit, to_reserve
to_reserve = max(to_commit, to_reserve)
total_to_reserved := max(to_reserve, size_of( virtual_Platform_Memory_Block))
to_commit = clamp(to_commit, size_of( virtual_Platform_Memory_Block), total_to_reserved)
data := win32_reserve(base_address, total_to_reserved) or_return
virtual.commit(raw_data(data), to_commit)
block = (^virtual_Platform_Memory_Block)(raw_data(data))
block.committed = to_commit
block.reserved = to_reserve
return
}
@(private="file")
platform_memory_commit :: proc "contextless" (block: ^virtual_Platform_Memory_Block, to_commit: uint) -> (err: virtual.Allocator_Error) {
if to_commit < block.committed {
return nil
}
if to_commit > block.reserved {
return .Out_Of_Memory
}
virtual.commit(block, to_commit) or_return
block.committed = to_commit
return nil
}
@(private="file", require_results)
memory_block_alloc :: proc(committed, reserved: uint, base_address : rawptr,
alignment : uint = 0,
flags : virtual.Memory_Block_Flags = {}
) -> (block: ^virtual.Memory_Block, err: virtual.Allocator_Error)
{
page_size := virtual.DEFAULT_PAGE_SIZE
assert(mem.is_power_of_two(uintptr(page_size)))
committed := committed
reserved := reserved
committed = align_formula(committed, page_size)
reserved = align_formula(reserved, page_size)
committed = clamp(committed, 0, reserved)
total_size := uint(reserved + max(alignment, size_of( virtual_Platform_Memory_Block)))
base_offset := uintptr(max(alignment, size_of( virtual_Platform_Memory_Block)))
protect_offset := uintptr(0)
do_protection := false
if .Overflow_Protection in flags { // overflow protection
rounded_size := reserved
total_size = uint(rounded_size + 2*page_size)
base_offset = uintptr(page_size + rounded_size - uint(reserved))
protect_offset = uintptr(page_size + rounded_size)
do_protection = true
}
pmblock := platform_memory_alloc(0, total_size, base_address) or_return
pmblock.block.base = ([^]byte)(pmblock)[base_offset:]
platform_memory_commit(pmblock, uint(base_offset) + committed) or_return
// Should be zeroed
assert(pmblock.block.used == 0)
assert(pmblock.block.prev == nil)
if do_protection {
virtual.protect(([^]byte)(pmblock)[protect_offset:], page_size, virtual.Protect_No_Access)
}
pmblock.block.committed = committed
pmblock.block.reserved = reserved
return &pmblock.block, nil
}
// This is the same as odin's virtual library, except I use my own allocation implementation to set the address space base.
@(require_results)
arena_init_static :: proc(arena: ^virtual.Arena, base_address : rawptr,
reserved : uint = virtual.DEFAULT_ARENA_STATIC_RESERVE_SIZE,
commit_size : uint = virtual.DEFAULT_ARENA_STATIC_COMMIT_SIZE
) -> (err: virtual.Allocator_Error)
{
arena.kind = .Static
arena.curr_block = memory_block_alloc(commit_size, reserved, base_address, {}) or_return
arena.total_used = 0
arena.total_reserved = arena.curr_block.reserved
return
}

View File

@ -299,7 +299,8 @@ poll_input :: proc( old, new : ^ InputState )
{
check_range :: proc( old, new : ^ InputState, start, end : i32 )
{
for id := start; id < end; id += 1 {
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]
@ -329,7 +330,8 @@ poll_input :: proc( old, new : ^ InputState )
// Mouse
{
// Process Buttons
for id : i32 = 0; id < i32(MouseBtn.count); id += 1 {
for id : i32 = 0; id < i32(MouseBtn.count); id += 1
{
old_btn := & old.mouse.btns[id]
new_btn := & new.mouse.btns[id]
@ -353,7 +355,11 @@ record_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
play_input :: proc( replay_file : os.Handle, input : ^ InputState ) {
raw_data := slice_ptr( transmute(^ byte) input, size_of(InputState) )
os.read( replay_file, raw_data )
total_read, result_code := os.read( replay_file, raw_data )
if result_code == os.ERROR_HANDLE_EOF {
rewind( replay_file )
load_snapshot( & memory.snapshot[0] )
}
}
to_raylib_key :: proc ( key : i32 ) -> rl.KeyboardKey {

View File

@ -2,6 +2,14 @@ package sectr
// TODO(Ed) : Evaluate if this is needed
vec2 :: vec2_f32
vec2_f32 :: struct #raw_union {
basis : [2] f32,
using components : struct {
x, y : f32
}
}
vec3 :: vec3_f32
vec3_f32 :: struct #raw_union {
basis : [3] f32,