revision 2
[carveJwlIkooP6JGAAIwe30JlM.git] / player_replay.c
1 #include "skaterift.h"
2 #include "player_replay.h"
3 #include "input.h"
4 #include "gui.h"
5 #include "freecam.h"
6
7 #include "player_walk.h"
8 #include "player_skate.h"
9 #include "player_dead.h"
10 #include "player_glide.h"
11
12 struct replay_globals player_replay;
13
14 void replay_clear( replay_buffer *replay )
15 {
16 replay->head = NULL;
17 replay->tail = NULL;
18 replay->cursor_frame = NULL;
19 replay->statehead = NULL;
20 replay->cursor = -99999.9;
21 }
22
23 void *replay_frame_data( replay_frame *frame, enum replay_framedata type )
24 {
25 if( frame->data_table[type][1] == 0 )
26 return NULL;
27
28 void *baseptr = frame;
29 return baseptr + frame->data_table[type][0];
30 }
31
32 static u16 replay_frame_calculate_data_offsets(
33 u16 data_table[k_replay_framedata_rows][2] ){
34
35 u32 total = vg_align8( sizeof(replay_frame) );
36 for( u32 i=0; i<k_replay_framedata_rows; i++ ){
37 data_table[i][0] = total;
38 total += vg_align8(data_table[i][1]);
39
40 if( total > 0xffff )
41 vg_fatal_error( "Exceeded frame storage capacity\n" );
42 }
43 return total;
44 }
45
46 static void replay_tailpop( replay_buffer *replay ){
47 if( replay->cursor_frame == replay->tail )
48 replay->cursor_frame = NULL;
49 if( replay->statehead == replay->tail )
50 replay->statehead = NULL;
51
52 replay->tail = replay->tail->r;
53
54 if( replay->tail )
55 replay->tail->l = NULL;
56 else
57 replay->head = NULL;
58 }
59
60 static replay_frame *replay_newframe( replay_buffer *replay,
61 u16 animator_size,
62 u16 gamestate_size,
63 u16 sfx_count,
64 bool save_glider ){
65 u16 data_table[ k_replay_framedata_rows ][2];
66 data_table[ k_replay_framedata_animator ][1] = animator_size;
67 data_table[ k_replay_framedata_gamestate ][1] = gamestate_size;
68 data_table[ k_replay_framedata_sfx ][1] = sfx_count*sizeof(struct net_sfx);
69 data_table[ k_replay_framedata_internal_gamestate ][1] = 0;
70 if( gamestate_size ){
71 data_table[ k_replay_framedata_internal_gamestate ][1] =
72 sizeof( replay_gamestate );
73 }
74
75 data_table[ k_replay_framedata_glider ][1] = 0;
76 if( save_glider ){
77 data_table[ k_replay_framedata_glider ][1] =
78 sizeof(struct replay_glider_data);
79 }
80
81 u32 nextsize = replay_frame_calculate_data_offsets( data_table );
82
83 replay_frame *frame = NULL;
84 if( replay->head ){
85 u32 headsize = replay->head->total_size,
86 nextpos = ((void *)replay->head - replay->data) + headsize;
87
88 if( nextpos + nextsize > replay->size ){
89 nextpos = 0;
90
91 /* maintain contiguity */
92 while( replay->tail ){
93 if( (void *)replay->tail - replay->data )
94 replay_tailpop( replay );
95 else break;
96 }
97 }
98
99 check_again:;
100 u32 tailpos = (void *)replay->tail - replay->data;
101
102 if( tailpos >= nextpos ){
103 if( nextpos + nextsize > tailpos ){
104 replay_tailpop( replay );
105
106 if( replay->tail )
107 goto check_again;
108 }
109 }
110
111 frame = replay->data + nextpos;
112
113 if( replay->head )
114 replay->head->r = frame;
115 }
116 else
117 frame = replay->data;
118
119 for( u32 i=0; i<k_replay_framedata_rows; i++ ){
120 frame->data_table[i][0] = data_table[i][0];
121 frame->data_table[i][1] = data_table[i][1];
122 }
123
124 frame->total_size = nextsize;
125 frame->l = replay->head;
126 frame->r = NULL;
127 replay->head = frame;
128 if( !replay->tail ) replay->tail = frame;
129 if( gamestate_size ) replay->statehead = frame;
130
131 return frame;
132 }
133
134 static void replay_emit_frame_sounds( replay_frame *frame ){
135 void *baseptr = frame;
136 u16 *inf = frame->data_table[k_replay_framedata_sfx];
137 struct net_sfx *buffer = baseptr + inf[0];
138 u32 count = inf[1] / sizeof(struct net_sfx);
139
140 for( u32 i=0; i<count; i ++ ){
141 net_sfx_play( buffer + i );
142 }
143 }
144
145 int replay_seek( replay_buffer *replay, f64 t )
146 {
147 if( !replay->head ) return 0;
148
149 if( t < replay->tail->time ) t = replay->tail->time;
150 if( t > replay->head->time ) t = replay->head->time;
151
152 if( !replay->cursor_frame ) {
153 replay->cursor = replay->head->time;
154 replay->cursor_frame = replay->head;
155
156 if( fabs(replay->head->time-t) > fabs(replay->tail->time-t) ){
157 replay->cursor = replay->tail->time;
158 replay->cursor_frame = replay->tail;
159 }
160 }
161
162 f64 dir = t - replay->cursor;
163 if( dir == 0.0 ) return 0;
164 dir = vg_signf( dir );
165
166
167 u32 i=4096;
168 while( i --> 0 ){
169 if( dir < 0.0 ){
170 if( t > replay->cursor_frame->time ) {
171 replay->cursor = t;
172 return 1;
173 }
174 }
175
176 replay_frame *next;
177 if( dir > 0.0 ) next = replay->cursor_frame->r;
178 else next = replay->cursor_frame->l;
179
180 if( !next ) break;
181
182 if( dir > 0.0 ){
183 if( t < next->time ){
184 replay->cursor = t;
185 return 1;
186 }
187 }
188
189 replay_emit_frame_sounds( next );
190
191 replay->cursor_frame = next;
192 replay->cursor = next->time;
193
194 if( !i ) return 1;
195 }
196
197 replay->cursor = t;
198 return 0;
199 }
200
201 replay_frame *replay_find_recent_stateframe( replay_buffer *replay )
202 {
203 replay_frame *frame = replay->cursor_frame;
204 u32 i=4096;
205 while( i --> 0 ){
206 if( !frame ) return frame;
207 if( frame->data_table[ k_replay_framedata_gamestate ][1] ) return frame;
208 frame = frame->l;
209 }
210
211 return NULL;
212 }
213
214 f32 replay_subframe_time( replay_buffer *replay )
215 {
216 replay_frame *frame = replay->cursor_frame;
217 if( !frame ) return 0.0f;
218 replay_frame *next = frame->r;
219 if( next ){
220 f64 l = next->time - frame->time,
221 t = (l <= (1.0/128.0))? 0.0: (replay->cursor - frame->time) / l;
222 return vg_clampf( t, 0.0f, 1.0f );
223 }
224 else
225 return 0.0f;
226 }
227
228 void replay_get_frame_camera( replay_frame *frame, vg_camera *cam )
229 {
230 cam->fov = frame->cam_fov;
231 v3_copy( frame->cam_pos, cam->pos );
232 v3_copy( frame->cam_angles, cam->angles );
233 }
234
235 void replay_get_camera( replay_buffer *replay, vg_camera *cam )
236 {
237 cam->nearz = 0.1f;
238 cam->farz = 100.0f;
239 if( replay->cursor_frame ){
240 replay_frame *next = replay->cursor_frame->r;
241
242 if( next ){
243 vg_camera temp;
244
245 replay_get_frame_camera( replay->cursor_frame, cam );
246 replay_get_frame_camera( next, &temp );
247 vg_camera_lerp( cam, &temp, replay_subframe_time( replay ), cam );
248 }
249 else {
250 replay_get_frame_camera( replay->cursor_frame, cam );
251 }
252 }
253 else {
254 v3_zero( cam->pos );
255 v3_zero( cam->angles );
256 cam->fov = 90.0f;
257 }
258 }
259
260 struct replay_rb{
261 v3f co, v, w;
262 v4f q;
263 };
264
265 void skaterift_record_frame( replay_buffer *replay, int force_gamestate )
266 {
267 f64 delta = 9999999.9,
268 statedelta = 9999999.9;
269
270 if( replay->head )
271 delta = vg.time - replay->head->time;
272
273 if( replay->statehead )
274 statedelta = vg.time - replay->statehead->time;
275
276 const f64 k_replay_rate = 1.0/30.0,
277 k_gamestate_rate = 0.5;
278
279 int save_frame = 0,
280 save_state = 0,
281 save_glider = 0;
282
283 if( force_gamestate ) save_state = 1;
284 if( statedelta > k_gamestate_rate ) save_state = 1;
285 if( delta > k_replay_rate ) save_frame = 1;
286 if( save_state ) save_frame = 1;
287
288 if( localplayer.have_glider || localplayer.glider_orphan ||
289 localplayer.subsystem == k_player_subsystem_glide ){
290 save_glider = 1;
291 }
292
293 if( !save_frame ) return;
294
295 u16 gamestate_size = 0;
296 if( save_state ){
297 /* TODO: have as part of system struct */
298 gamestate_size = (u32 []){
299 [k_player_subsystem_walk ] = sizeof(struct player_walk_state),
300 [k_player_subsystem_drive] = 0,
301 [k_player_subsystem_skate] = sizeof(struct player_skate_state),
302 [k_player_subsystem_dead ] = localplayer.ragdoll.part_count *
303 sizeof(struct replay_rb),
304 [k_player_subsystem_glide] = sizeof(struct replay_rb),
305 }[ localplayer.subsystem ];
306 }
307
308 u16 animator_size = player_subsystems[localplayer.subsystem]->animator_size;
309
310 replay_frame *frame = replay_newframe( replay,
311 animator_size, gamestate_size,
312 localplayer.local_sfx_buffer_count,
313 save_glider );
314 frame->system = localplayer.subsystem;
315
316 if( save_state ){
317 replay_gamestate *gs =
318 replay_frame_data( frame, k_replay_framedata_internal_gamestate );
319
320 gs->current_run_version = world_static.current_run_version;
321
322 /* permanent block */
323 memcpy( &gs->rb, &localplayer.rb, sizeof(rigidbody) );
324 memcpy( &gs->glider_rb, &player_glide.rb, sizeof(rigidbody) );
325 memcpy( &gs->cam_control, &localplayer.cam_control,
326 sizeof(struct player_cam_controller) );
327 v3_copy( localplayer.angles, gs->angles );
328
329 void *dst = replay_frame_data( frame, k_replay_framedata_gamestate );
330
331 /* subsytem/dynamic block */
332 if( localplayer.subsystem == k_player_subsystem_walk )
333 memcpy( dst, &player_walk.state, gamestate_size );
334 else if( localplayer.subsystem == k_player_subsystem_skate )
335 memcpy( dst, &player_skate.state, gamestate_size );
336 else if( localplayer.subsystem == k_player_subsystem_dead ){
337 struct replay_rb *arr = dst;
338 for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
339 rigidbody *rb = &localplayer.ragdoll.parts[i].rb;
340 v3_copy( rb->co, arr[i].co );
341 v3_copy( rb->w, arr[i].w );
342 v3_copy( rb->v, arr[i].v );
343 v4_copy( rb->q, arr[i].q );
344 }
345 }
346 else if( localplayer.subsystem == k_player_subsystem_glide ){
347 struct replay_rb *arr = dst;
348 rigidbody *rb = &player_glide.rb;
349 v3_copy( rb->co, arr[0].co );
350 v3_copy( rb->w, arr[0].w );
351 v3_copy( rb->v, arr[0].v );
352 v4_copy( rb->q, arr[0].q );
353 }
354 }
355
356 if( save_glider ){
357 struct replay_glider_data *inf =
358 replay_frame_data( frame, k_replay_framedata_glider );
359
360 inf->have_glider = localplayer.have_glider;
361 inf->glider_orphan = localplayer.glider_orphan;
362 inf->t = player_glide.t;
363 v3_copy( player_glide.rb.co, inf->co );
364 v4_copy( player_glide.rb.q, inf->q );
365 }
366
367 replay->cursor = vg.time;
368 replay->cursor_frame = frame;
369 frame->time = vg.time;
370
371 /* camera */
372 v3_copy( localplayer.cam.pos, frame->cam_pos );
373 if( localplayer.gate_waiting ){
374 m4x3_mulv( localplayer.gate_waiting->transport,
375 frame->cam_pos, frame->cam_pos );
376
377 v3f v0;
378 v3_angles_vector( localplayer.cam.angles, v0 );
379 m3x3_mulv( localplayer.gate_waiting->transport, v0, v0 );
380 v3_angles( v0, frame->cam_angles );
381 }
382 else
383 v3_copy( localplayer.cam.angles, frame->cam_angles );
384
385 frame->cam_fov = localplayer.cam.fov;
386
387 /* animator */
388 void *dst = replay_frame_data( frame, k_replay_framedata_animator ),
389 *src = player_subsystems[localplayer.subsystem]->animator_data;
390 memcpy( dst, src, animator_size );
391
392 /* sound effects */
393 memcpy( replay_frame_data( frame, k_replay_framedata_sfx ),
394 localplayer.local_sfx_buffer,
395 sizeof(struct net_sfx)*localplayer.local_sfx_buffer_count );
396
397 localplayer.local_sfx_buffer_count = 0;
398 }
399
400 static
401 void skaterift_restore_frame( replay_frame *frame ){
402 replay_gamestate *gs =
403 replay_frame_data( frame, k_replay_framedata_internal_gamestate );
404 void *src = replay_frame_data( frame, k_replay_framedata_gamestate );
405 u16 src_size = frame->data_table[ k_replay_framedata_gamestate ][1];
406 world_static.current_run_version = gs->current_run_version;
407
408 if(frame->system == k_player_subsystem_walk ){
409 memcpy( &player_walk.state, src, src_size );
410 }
411 else if( frame->system == k_player_subsystem_skate ){
412 memcpy( &player_skate.state, src, src_size );
413 }
414 else if( frame->system == k_player_subsystem_dead ){
415 player__dead_transition(0);
416 struct replay_rb *arr = src;
417
418 for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
419 struct ragdoll_part *part = &localplayer.ragdoll.parts[i];
420 rigidbody *rb = &part->rb;
421
422 v3_copy( arr[i].co, rb->co );
423 v3_copy( arr[i].w, rb->w );
424 v3_copy( arr[i].v, rb->v );
425 v4_copy( arr[i].q, rb->q );
426
427 v3_copy( arr[i].co, part->prev_co );
428 v4_copy( arr[i].q, part->prev_q );
429 rb_update_matrices( rb );
430 }
431 }
432 else if( frame->system == k_player_subsystem_glide ){
433 struct replay_rb *arr = src;
434 rigidbody *rb = &player_glide.rb;
435 v3_copy( arr[0].co, rb->co );
436 v3_copy( arr[0].w, rb->w );
437 v3_copy( arr[0].v, rb->v );
438 v4_copy( arr[0].q, rb->q );
439 rb_update_matrices( rb );
440 }
441
442 localplayer.subsystem = frame->system;
443
444 /* restore the seperated glider data if we have it */
445 if( frame->data_table[ k_replay_framedata_glider ][1] ){
446 struct replay_glider_data *inf =
447 replay_frame_data( frame, k_replay_framedata_glider );
448
449 localplayer.have_glider = inf->have_glider;
450 localplayer.glider_orphan = inf->glider_orphan;
451 player_glide.t = inf->t;
452 }
453 else {
454 localplayer.have_glider = 0;
455 localplayer.glider_orphan = 0;
456 player_glide.t = 0.0f;
457 }
458
459 memcpy( &localplayer.rb, &gs->rb, sizeof(rigidbody) );
460 memcpy( &player_glide.rb, &gs->glider_rb, sizeof(rigidbody) );
461 v3_copy( gs->angles, localplayer.angles );
462
463 v3_copy( frame->cam_pos, localplayer.cam.pos );
464 v3_copy( frame->cam_angles, localplayer.cam.angles );
465 localplayer.cam.fov = frame->cam_fov;
466
467 memcpy( &localplayer.cam_control, &gs->cam_control,
468 sizeof(struct player_cam_controller) );
469
470 /* chop end off replay */
471 frame->r = NULL;
472 player_replay.local.statehead = frame;
473 player_replay.local.head = frame;
474 player_replay.local.cursor_frame = frame;
475 player_replay.local.cursor = frame->time;
476 player_replay.replay_control = k_replay_control_scrub;
477 skaterift.activity = k_skaterift_default;
478 vg.time = frame->time;
479 }
480
481 static void skaterift_replay_resume(void){
482 replay_frame *prev = replay_find_recent_stateframe(&player_replay.local);
483
484 if( prev ){
485 player_replay.replay_control = k_replay_control_resume;
486 player_replay.resume_target = prev;
487 player_replay.resume_begin = player_replay.local.cursor;
488 player_replay.resume_transition = 0.0f;
489 }
490
491 gui_helper_clear();
492 }
493
494 static void skaterift_replay_update_helpers(void);
495
496 void skaterift_replay_pre_update(void)
497 {
498 if( skaterift.activity != k_skaterift_replay ) return;
499
500 if( player_replay.replay_control == k_replay_control_resume )
501 {
502 if( player_replay.local.cursor_frame == player_replay.resume_target ||
503 player_replay.local.cursor_frame == NULL )
504 {
505 skaterift_restore_frame( player_replay.resume_target );
506 }
507 else
508 {
509 vg_slewf( &player_replay.resume_transition, 1.0f,
510 vg.time_frame_delta * (1.0f/1.0f) );
511
512 if( player_replay.resume_transition >= 1.0f )
513 skaterift_restore_frame( player_replay.resume_target );
514 else {
515 f64 target = vg_lerp( player_replay.resume_begin,
516 player_replay.resume_target->time,
517 vg_smoothstepf( player_replay.resume_transition ) );
518 if( replay_seek( &player_replay.local, target ) )
519 player_replay.track_velocity = 1.0f;
520 else
521 player_replay.track_velocity = 0.0f;
522 }
523 }
524 }
525 else {
526 if( button_down( k_srbind_replay_play ) )
527 player_replay.replay_control = k_replay_control_play;
528 if( button_down( k_srbind_replay_freecam ) ){
529 player_replay.freecam = player_replay.freecam ^ 0x1;
530
531 if( player_replay.freecam )
532 replay_get_camera( &player_replay.local,
533 &player_replay.replay_freecam );
534 skaterift_replay_update_helpers();
535 }
536
537 f32 target_speed = axis_state( k_sraxis_replay_h ) * 5.0;
538 if( button_press( k_srbind_reset ) )
539 target_speed += -2.0;
540
541 if( fabsf(target_speed) > 0.01f )
542 player_replay.replay_control = k_replay_control_scrub;
543
544 if( player_replay.replay_control == k_replay_control_play )
545 target_speed = 1.0;
546
547 vg_slewf( &player_replay.track_velocity, target_speed,
548 18.0f*vg.time_frame_delta );
549
550 if( fabsf( player_replay.track_velocity ) > 0.0001f )
551 {
552 f64 target = player_replay.local.cursor;
553 target += player_replay.track_velocity * vg.time_frame_delta;
554
555 if( !replay_seek( &player_replay.local, target ) )
556 player_replay.track_velocity = 0.0f;
557 }
558
559 if( button_down( k_srbind_mback ) )
560 {
561 if( player_replay.local.statehead )
562 skaterift_restore_frame( player_replay.local.statehead );
563 else
564 skaterift.activity = k_skaterift_default;
565 srinput.state = k_input_state_resume;
566 gui_helper_clear();
567 }
568
569 if( player_replay.freecam ){
570 //freecam_preupdate();
571 }
572 else {
573 if( button_down( k_srbind_replay_resume ) ){
574 skaterift_replay_resume();
575 }
576 }
577 }
578 }
579
580 static void skaterift_replay_update_helpers(void){
581 player_replay.helper_resume->greyed = player_replay.freecam;
582
583 vg_str freecam_text;
584 vg_strnull( &freecam_text, player_replay.helper_freecam->text,
585 GUI_HELPER_TEXT_LENGTH );
586 vg_strcat( &freecam_text, player_replay.freecam? "exit freecam": "freecam" );
587 }
588
589 void skaterift_replay_post_render(void)
590 {
591 #ifndef SR_ALLOW_REWIND_HUB
592 if( world_static.active_instance != k_world_purpose_client )
593 return;
594 #endif
595
596 /* capture the current resume frame at the very last point */
597 if( button_down( k_srbind_reset ) )
598 {
599 if( skaterift.activity == k_skaterift_default )
600 {
601 localplayer.rewinded_since_last_gate = 1;
602 skaterift.activity = k_skaterift_replay;
603 skaterift_record_frame( &player_replay.local, 1 );
604 if( player_replay.local.head )
605 {
606 player_replay.local.cursor = player_replay.local.head->time;
607 player_replay.local.cursor_frame = player_replay.local.head;
608 }
609 player_replay.replay_control = k_replay_control_scrub;
610
611 gui_helper_clear();
612 vg_str text;
613
614 if( gui_new_helper( input_axis_list[k_sraxis_replay_h], &text ) )
615 vg_strcat( &text, "scrub" );
616
617 if( (player_replay.helper_resume = gui_new_helper(
618 input_button_list[k_srbind_replay_resume], &text )) )
619 vg_strcat( &text, "resume" );
620
621 if( gui_new_helper( input_button_list[k_srbind_replay_play], &text ))
622 vg_strcat( &text, "playback" );
623
624 player_replay.helper_freecam = gui_new_helper(
625 input_button_list[k_srbind_replay_freecam], &text );
626
627 skaterift_replay_update_helpers();
628 }
629 }
630 }
631
632 void skaterift_replay_debug_info(void)
633 {
634 player__debugtext( 2, "replay info" );
635 replay_buffer *replay = &player_replay.local;
636
637 u32 head = 0,
638 tail = 0;
639 if( replay->tail ) tail = (void *)replay->tail - replay->data;
640 if( replay->head ) head = (void *)replay->head - replay->data;
641
642 player__debugtext( 1, "head @%u | tail @%u\n", head, tail );
643
644 if( replay->statehead ){
645 for( u32 i=0; i<k_replay_framedata_rows; i++ ){
646 player__debugtext( 1, "[%u]: [%hu, %hu]\n", i,
647 replay->statehead->data_table[i][0],
648 replay->statehead->data_table[i][1] );
649 }
650 u32 state = (void *)replay->statehead - replay->data;
651 player__debugtext( 1, "gs @%u\n", state );
652 player__debugtext( 1, "gamestate_size: %hu\n",
653 replay->statehead->data_table[k_replay_framedata_gamestate][1] );
654 }
655 else
656 player__debugtext( 1, "gs @NULL\n" );
657
658 f64 start = replay->cursor,
659 end = replay->cursor;
660 if( replay->tail ) start = replay->tail->time;
661 if( replay->head ) end = replay->head->time;
662
663 f64 cur = replay->cursor - start,
664 len = end - start;
665
666 player__debugtext( 1, "cursor: %.2fs / %.2fs\n", cur, len );
667 }
668
669 void skaterift_replay_imgui(void)
670 {
671 if( skaterift.activity != k_skaterift_replay ) return;
672
673 replay_buffer *replay = &player_replay.local;
674 f64 start = replay->cursor,
675 end = replay->cursor;
676 if( replay->tail ) start = replay->tail->time;
677 if( replay->head ) end = replay->head->time;
678 f64 len = end - start,
679 cur = (replay->cursor - start) / len;
680
681 char buffer[ 128 ];
682
683 /* mainbar */
684 ui_px height = 32,
685 cwidth = 2;
686 ui_rect bar = { 0, 0, vg.window_x, height };
687 ui_fill( bar, ui_colour( k_ui_bg ) );
688
689 /* cursor frame block */
690 if( replay->cursor_frame ){
691 if( replay->cursor_frame->r ){
692 f64 l = (replay->cursor_frame->r->time-replay->cursor_frame->time)/len,
693 s = (replay->cursor_frame->time - start) / len;
694 ui_rect box = { s*(f64)vg.window_x, 0,
695 VG_MAX(4,(ui_px)(l*vg.window_x)), bar[3]+2 };
696 ui_fill( box, ui_colour( k_ui_bg+4 ) );
697 }
698 }
699
700 /* cursor */
701 ui_rect cusor = { cur * (f64)vg.window_x - (cwidth/2), 0,
702 cwidth, bar[3] };
703 ui_fill( cusor, ui_colour( k_ui_bg+7 ) );
704
705 /* latest state marker */
706 if( replay->statehead ){
707 f64 t = (replay->statehead->time - start) / len;
708 ui_rect tag = { t*(f64)vg.window_x, 0, 2, bar[3]+8 };
709 ui_fill( tag, ui_colour( k_ui_green+k_ui_brighter ) );
710 }
711
712 /* previous state marker */
713 replay_frame *prev = replay_find_recent_stateframe( replay );
714 if( prev ){
715 f64 t = (prev->time - start) / len;
716 ui_rect tag = { t*(f64)vg.window_x, 0, 2, bar[3]+8 };
717 ui_fill( tag, ui_colour( k_ui_yellow+k_ui_brighter ) );
718 }
719
720 cusor[1] -= height;
721 cusor[2] = 200;
722 snprintf( buffer, 128, "-%.2fs\n", (end-replay->cursor) );
723 ui_text( cusor, buffer, 1, k_ui_align_middle_left, 0 );
724
725 snprintf( buffer, 128, "-%.2fs\n", len );
726 ui_text( bar, buffer, 1, k_ui_align_middle_left, 0 );
727 ui_text( bar, "0s", 1, k_ui_align_middle_right, 0 );
728 }