video-timer: add a test case for display at a constant frame rate
Add a test case that simulates the timing operaton that goes on when showing a constant frame rate stream like a video - each frame is shown at the VBlank interval that is closest to when it would ideally be timed. https://bugzilla.gnome.org/show_bug.cgi?id=685460
This commit is contained in:
		
							
								
								
									
										328
									
								
								tests/video-timer.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								tests/video-timer.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,328 @@ | ||||
| #include <math.h> | ||||
| #include <gtk/gtk.h> | ||||
|  | ||||
| #include "variable.h" | ||||
|  | ||||
| typedef struct { | ||||
|   gdouble angle; | ||||
|   gint64 stream_time; | ||||
|   gint64 clock_time; | ||||
|   gint64 frame_counter; | ||||
| } FrameData; | ||||
|  | ||||
| static FrameData *displayed_frame; | ||||
| static GtkWidget *window; | ||||
| static GList *past_frames; | ||||
| static Variable latency_error = VARIABLE_INIT; | ||||
| static int dropped_frames = 0; | ||||
| static int n_frames = 0; | ||||
|  | ||||
| static int fps = 24; | ||||
|  | ||||
| /* Thread-safe frame queue */ | ||||
|  | ||||
| #define MAX_QUEUE_LENGTH 5 | ||||
|  | ||||
| static GQueue *frame_queue; | ||||
| static GMutex frame_mutex; | ||||
| static GCond frame_cond; | ||||
|  | ||||
| static void | ||||
| queue_frame (FrameData *frame_data) | ||||
| { | ||||
|   g_mutex_lock (&frame_mutex); | ||||
|  | ||||
|   while (frame_queue->length == MAX_QUEUE_LENGTH) | ||||
|     g_cond_wait (&frame_cond, &frame_mutex); | ||||
|  | ||||
|   g_queue_push_tail (frame_queue, frame_data); | ||||
|  | ||||
|   g_mutex_unlock (&frame_mutex); | ||||
| } | ||||
|  | ||||
| static FrameData * | ||||
| unqueue_frame (void) | ||||
| { | ||||
|   FrameData *frame_data; | ||||
|  | ||||
|   g_mutex_lock (&frame_mutex); | ||||
|  | ||||
|   if (frame_queue->length > 0) | ||||
|     { | ||||
|       frame_data = g_queue_pop_head (frame_queue); | ||||
|       g_cond_signal (&frame_cond); | ||||
|     } | ||||
|   else | ||||
|     { | ||||
|       frame_data = NULL; | ||||
|     } | ||||
|  | ||||
|   g_mutex_unlock (&frame_mutex); | ||||
|  | ||||
|   return frame_data; | ||||
| } | ||||
|  | ||||
| static FrameData * | ||||
| peek_pending_frame (void) | ||||
| { | ||||
|   FrameData *frame_data; | ||||
|  | ||||
|   g_mutex_lock (&frame_mutex); | ||||
|  | ||||
|   if (frame_queue->head) | ||||
|     frame_data = frame_queue->head->data; | ||||
|   else | ||||
|     frame_data = NULL; | ||||
|  | ||||
|   g_mutex_unlock (&frame_mutex); | ||||
|  | ||||
|   return frame_data; | ||||
| } | ||||
|  | ||||
| static FrameData * | ||||
| peek_next_frame (void) | ||||
| { | ||||
|   FrameData *frame_data; | ||||
|  | ||||
|   g_mutex_lock (&frame_mutex); | ||||
|  | ||||
|   if (frame_queue->head && frame_queue->head->next) | ||||
|     frame_data = frame_queue->head->next->data; | ||||
|   else | ||||
|     frame_data = NULL; | ||||
|  | ||||
|   g_mutex_unlock (&frame_mutex); | ||||
|  | ||||
|   return frame_data; | ||||
| } | ||||
|  | ||||
| /* Frame producer thread */ | ||||
|  | ||||
| static gpointer | ||||
| create_frames_thread (gpointer data) | ||||
| { | ||||
|   int frame_count = 0; | ||||
|  | ||||
|   while (TRUE) | ||||
|     { | ||||
|       FrameData *frame_data = g_slice_new0 (FrameData); | ||||
|       frame_data->angle = 2 * M_PI * (frame_count % fps) / (double)fps; | ||||
|       frame_data->stream_time = (G_GINT64_CONSTANT (1000000) * frame_count) / fps; | ||||
|  | ||||
|       queue_frame (frame_data); | ||||
|       frame_count++; | ||||
|     } | ||||
|  | ||||
|   return NULL; | ||||
| } | ||||
|  | ||||
| /* Clock management */ | ||||
|  | ||||
| #define PRE_BUFFER_TIME 500000 | ||||
|  | ||||
| static gint64 start_time; | ||||
|  | ||||
| static gint64 | ||||
| stream_time_to_clock_time (gint64 stream_time) | ||||
| { | ||||
|   return start_time + stream_time + PRE_BUFFER_TIME; | ||||
| } | ||||
|  | ||||
| /* Drawing */ | ||||
|  | ||||
| static void | ||||
| on_window_draw (GtkWidget *widget, | ||||
|                 cairo_t   *cr) | ||||
| { | ||||
|   GdkRectangle allocation; | ||||
|   double cx, cy, r; | ||||
|  | ||||
|   cairo_set_source_rgb (cr, 1., 1., 1.); | ||||
|   cairo_paint (cr); | ||||
|  | ||||
|   cairo_set_source_rgb (cr, 0., 0., 0.); | ||||
|   gtk_widget_get_allocation (widget, &allocation); | ||||
|  | ||||
|   cx = allocation.width / 2.; | ||||
|   cy = allocation.height / 2.; | ||||
|   r = MIN (allocation.width, allocation.height) / 2.; | ||||
|  | ||||
|   cairo_arc (cr, cx, cy, r, | ||||
|              0, 2 * M_PI); | ||||
|   cairo_stroke (cr); | ||||
|   if (displayed_frame) | ||||
|     { | ||||
|       cairo_move_to (cr, cx, cy); | ||||
|       cairo_line_to (cr, | ||||
|                      cx + r * cos(displayed_frame->angle - M_PI / 2), | ||||
|                      cy + r * sin(displayed_frame->angle - M_PI / 2)); | ||||
|       cairo_stroke (cr); | ||||
|  | ||||
|       if (displayed_frame->frame_counter == 0) | ||||
|         { | ||||
|           GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window); | ||||
|           GdkFrameHistory *history = gdk_frame_clock_get_history (frame_clock); | ||||
|  | ||||
|           displayed_frame->frame_counter = gdk_frame_history_get_frame_counter (history); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void | ||||
| collect_old_frames (void) | ||||
| { | ||||
|   GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window); | ||||
|   GdkFrameHistory *history = gdk_frame_clock_get_history (frame_clock); | ||||
|   GList *l, *l_next; | ||||
|  | ||||
|   for (l = past_frames; l; l = l_next) | ||||
|     { | ||||
|       FrameData *frame_data = l->data; | ||||
|       gboolean remove = FALSE; | ||||
|       l_next = l->next; | ||||
|  | ||||
|       GdkFrameTimings *timings = gdk_frame_history_get_timings (history, | ||||
|                                                                 frame_data->frame_counter); | ||||
|       if (timings == NULL) | ||||
|         { | ||||
|           remove = TRUE; | ||||
|         } | ||||
|       else if (gdk_frame_timings_get_complete (timings)) | ||||
|         { | ||||
|           gint64 presentation_time = gdk_frame_timings_get_presentation_time (timings); | ||||
|           if (presentation_time) | ||||
|             variable_add (&latency_error, | ||||
|                           presentation_time - frame_data->clock_time); | ||||
|  | ||||
|           remove = TRUE; | ||||
|         } | ||||
|  | ||||
|       if (remove) | ||||
|         { | ||||
|           past_frames = g_list_delete_link (past_frames, l); | ||||
|           g_slice_free (FrameData, frame_data); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void | ||||
| print_statistics (void) | ||||
| { | ||||
|   gint64 now = g_get_monotonic_time (); | ||||
|   static gint64 last_print_time = 0; | ||||
|  | ||||
|   if (last_print_time == 0) | ||||
|     last_print_time = now; | ||||
|   else if (now -last_print_time > 5000000) | ||||
|     { | ||||
|       g_print ("dropped_frames: %d/%d\n", | ||||
|                dropped_frames, n_frames); | ||||
|       g_print ("collected_frames: %g/%d\n", | ||||
|                latency_error.weight, n_frames); | ||||
|       g_print ("latency_error: %g +/- %g\n", | ||||
|                variable_mean (&latency_error), | ||||
|                variable_standard_deviation (&latency_error)); | ||||
|       variable_reset (&latency_error); | ||||
|       dropped_frames = 0; | ||||
|       n_frames = 0; | ||||
|       last_print_time = now; | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void | ||||
| on_update (GdkFrameClock *frame_clock, | ||||
|            gpointer       data) | ||||
| { | ||||
|   GdkFrameTimings *timings = gdk_frame_clock_get_current_frame_timings (frame_clock); | ||||
|   gint64 frame_time = gdk_frame_timings_get_frame_time (timings); | ||||
|   gint64 predicted_presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings); | ||||
|   gint64 refresh_interval; | ||||
|   FrameData *pending_frame; | ||||
|  | ||||
|   if (start_time == 0) | ||||
|     start_time = frame_time; | ||||
|  | ||||
|   gdk_frame_clock_get_refresh_info (frame_clock, frame_time, | ||||
|                                     &refresh_interval, NULL); | ||||
|  | ||||
|   pending_frame = peek_pending_frame (); | ||||
|   if (stream_time_to_clock_time (pending_frame->stream_time) | ||||
|       < predicted_presentation_time + refresh_interval / 2) | ||||
|     { | ||||
|       while (TRUE) | ||||
|         { | ||||
|           FrameData *next_frame = peek_next_frame (); | ||||
|           if (next_frame && | ||||
|               stream_time_to_clock_time (next_frame->stream_time) | ||||
|               < predicted_presentation_time + refresh_interval / 2) | ||||
|             { | ||||
|               g_slice_free (FrameData, unqueue_frame ()); | ||||
|               n_frames++; | ||||
|               dropped_frames++; | ||||
|               pending_frame = next_frame; | ||||
|             } | ||||
|           else | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|       if (displayed_frame) | ||||
|         past_frames = g_list_prepend (past_frames, displayed_frame); | ||||
|  | ||||
|       n_frames++; | ||||
|       displayed_frame = unqueue_frame (); | ||||
|       displayed_frame->clock_time = stream_time_to_clock_time (displayed_frame->stream_time); | ||||
|       displayed_frame->frame_counter = gdk_frame_timings_get_frame_counter (timings); | ||||
|  | ||||
|       collect_old_frames (); | ||||
|       print_statistics (); | ||||
|  | ||||
|       gtk_widget_queue_draw (window); | ||||
|     } | ||||
|  | ||||
|   gdk_frame_clock_request_phase (frame_clock, GDK_FRAME_CLOCK_PHASE_UPDATE); | ||||
| } | ||||
|  | ||||
| static GOptionEntry options[] = { | ||||
|   { "fps", 'f', 0, G_OPTION_ARG_INT, &fps, "Frame rate", "FPS" }, | ||||
|   { NULL } | ||||
| }; | ||||
|  | ||||
| int | ||||
| main(int argc, char **argv) | ||||
| { | ||||
|   GError *error = NULL; | ||||
|   GdkFrameClock *frame_clock; | ||||
|  | ||||
|   if (!gtk_init_with_args (&argc, &argv, "", | ||||
|                            options, NULL, &error)) | ||||
|     { | ||||
|       g_printerr ("Option parsing failed: %s\n", error->message); | ||||
|       return 1; | ||||
|     } | ||||
|  | ||||
|   window = gtk_window_new (GTK_WINDOW_TOPLEVEL); | ||||
|   gtk_widget_set_app_paintable (window, TRUE); | ||||
|   gtk_window_set_default_size (GTK_WINDOW (window), 300, 300); | ||||
|  | ||||
|   g_signal_connect (window, "draw", | ||||
|                     G_CALLBACK (on_window_draw), NULL); | ||||
|   g_signal_connect (window, "destroy", | ||||
|                     G_CALLBACK (gtk_main_quit), NULL); | ||||
|  | ||||
|   gtk_widget_show (window); | ||||
|  | ||||
|   frame_queue = g_queue_new (); | ||||
|   g_mutex_init (&frame_mutex); | ||||
|   g_cond_init (&frame_cond); | ||||
|  | ||||
|   g_thread_new ("Create Frames", create_frames_thread, NULL); | ||||
|  | ||||
|   frame_clock = gtk_widget_get_frame_clock (window); | ||||
|   g_signal_connect (frame_clock, "update", | ||||
|                     G_CALLBACK (on_update), NULL); | ||||
|   gdk_frame_clock_request_phase (frame_clock, GDK_FRAME_CLOCK_PHASE_UPDATE); | ||||
|  | ||||
|   gtk_main (); | ||||
|  | ||||
|   return 0; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Owen W. Taylor
					Owen W. Taylor