#include #include #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 Variable time_factor_stats = VARIABLE_INIT; static int dropped_frames = 0; static int n_frames = 0; static gboolean pll; 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: * * The logic here, which is activated by the --pll argument * demonstrates adjusting the playback rate so that the frames exactly match * when they are displayed both frequency and phase. If there was an * accompanying audio track, you would need to resample the audio to match * the clock. * * The algorithm isn't exactly a PLL - I wrote it first that way, but * it oscillicated before coming into sync and this approach was easier than * fine-tuning the PLL filter. * * A more complicated algorithm could also establish sync when the playback * rate isn't exactly an integral divisor of the VBlank rate, such as 24fps * video on a 60fps display. */ #define PRE_BUFFER_TIME 500000 static gint64 stream_time_base; static gint64 clock_time_base; static double time_factor = 1.0; static double frequency_time_factor = 1.0; static double phase_time_factor = 1.0; static gint64 stream_time_to_clock_time (gint64 stream_time) { return clock_time_base + (stream_time - stream_time_base) * time_factor; } static void adjust_clock_for_phase (gint64 frame_clock_time, gint64 presentation_time) { static gint count = 0; static gint64 previous_frame_clock_time; static gint64 previous_presentation_time; gint64 phase = presentation_time - frame_clock_time; count++; if (count >= fps) /* Give a second of warmup */ { gint64 time_delta = frame_clock_time - previous_frame_clock_time; gint64 previous_phase = previous_presentation_time - previous_frame_clock_time; double expected_phase_delta; stream_time_base += (frame_clock_time - clock_time_base) / time_factor; clock_time_base = frame_clock_time; expected_phase_delta = time_delta * (1 - phase_time_factor); /* If the phase is increasing that means the computed clock times are * increasing too slowly. We increase the frequency time factor to compensate, * but decrease the compensation so that it takes effect over 1 second to * avoid jitter */ frequency_time_factor += (phase - previous_phase - expected_phase_delta) / (double)time_delta / fps; /* We also want to increase or decrease the frequency to bring the phase * into sync. We do that again so that the phase should sync up over 1 seconds */ phase_time_factor = 1 + phase / 2000000.; time_factor = frequency_time_factor * phase_time_factor; } previous_frame_clock_time = frame_clock_time; previous_presentation_time = presentation_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); displayed_frame->frame_counter = gdk_frame_clock_get_frame_counter (frame_clock); } } } static void collect_old_frames (void) { GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window); 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_clock_get_timings (frame_clock, frame_data->frame_counter); if (timings == NULL) { remove = TRUE; } else if (gdk_frame_timings_get_complete (timings)) { gint64 presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings); gint64 refresh_interval = gdk_frame_timings_get_refresh_interval (timings); if (pll && presentation_time && refresh_interval && presentation_time > frame_data->clock_time - refresh_interval / 2 && presentation_time < frame_data->clock_time + refresh_interval / 2) adjust_clock_for_phase (frame_data->clock_time, presentation_time); 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)); if (pll) g_print ("playback rate adjustment: %g +/- %g %%\n", (variable_mean (&time_factor_stats) - 1) * 100, variable_standard_deviation (&time_factor_stats) * 100); variable_reset (&latency_error); variable_reset (&time_factor_stats); 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_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 (clock_time_base == 0) clock_time_base = frame_time + PRE_BUFFER_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); variable_add (&time_factor_stats, time_factor); collect_old_frames (); print_statistics (); gtk_widget_queue_draw (window); } } static GOptionEntry options[] = { { "pll", 'p', 0, G_OPTION_ARG_NONE, &pll, "Sync frame rate to refresh", NULL }, { "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_begin_updating (frame_clock); gtk_main (); return 0; }