1 | #include <math.h> |
2 | #include <gtk/gtk.h> |
3 | |
4 | #include "variable.h" |
5 | |
6 | typedef struct { |
7 | double angle; |
8 | gint64 stream_time; |
9 | gint64 clock_time; |
10 | gint64 frame_counter; |
11 | } FrameData; |
12 | |
13 | static FrameData *displayed_frame; |
14 | static GtkWidget *window; |
15 | static GList *past_frames; |
16 | static Variable latency_error = VARIABLE_INIT; |
17 | static Variable time_factor_stats = VARIABLE_INIT; |
18 | static int dropped_frames = 0; |
19 | static int n_frames = 0; |
20 | |
21 | static gboolean pll; |
22 | static int fps = 24; |
23 | |
24 | /* Thread-safe frame queue */ |
25 | |
26 | #define MAX_QUEUE_LENGTH 5 |
27 | |
28 | static GQueue *frame_queue; |
29 | static GMutex frame_mutex; |
30 | static GCond frame_cond; |
31 | |
32 | static void |
33 | queue_frame (FrameData *frame_data) |
34 | { |
35 | g_mutex_lock (mutex: &frame_mutex); |
36 | |
37 | while (frame_queue->length == MAX_QUEUE_LENGTH) |
38 | g_cond_wait (cond: &frame_cond, mutex: &frame_mutex); |
39 | |
40 | g_queue_push_tail (queue: frame_queue, data: frame_data); |
41 | |
42 | g_mutex_unlock (mutex: &frame_mutex); |
43 | } |
44 | |
45 | static FrameData * |
46 | unqueue_frame (void) |
47 | { |
48 | FrameData *frame_data; |
49 | |
50 | g_mutex_lock (mutex: &frame_mutex); |
51 | |
52 | if (frame_queue->length > 0) |
53 | { |
54 | frame_data = g_queue_pop_head (queue: frame_queue); |
55 | g_cond_signal (cond: &frame_cond); |
56 | } |
57 | else |
58 | { |
59 | frame_data = NULL; |
60 | } |
61 | |
62 | g_mutex_unlock (mutex: &frame_mutex); |
63 | |
64 | return frame_data; |
65 | } |
66 | |
67 | static FrameData * |
68 | peek_pending_frame (void) |
69 | { |
70 | FrameData *frame_data; |
71 | |
72 | g_mutex_lock (mutex: &frame_mutex); |
73 | |
74 | if (frame_queue->head) |
75 | frame_data = frame_queue->head->data; |
76 | else |
77 | frame_data = NULL; |
78 | |
79 | g_mutex_unlock (mutex: &frame_mutex); |
80 | |
81 | return frame_data; |
82 | } |
83 | |
84 | static FrameData * |
85 | peek_next_frame (void) |
86 | { |
87 | FrameData *frame_data; |
88 | |
89 | g_mutex_lock (mutex: &frame_mutex); |
90 | |
91 | if (frame_queue->head && frame_queue->head->next) |
92 | frame_data = frame_queue->head->next->data; |
93 | else |
94 | frame_data = NULL; |
95 | |
96 | g_mutex_unlock (mutex: &frame_mutex); |
97 | |
98 | return frame_data; |
99 | } |
100 | |
101 | /* Frame producer thread */ |
102 | |
103 | static gpointer |
104 | create_frames_thread (gpointer data) |
105 | { |
106 | int frame_count = 0; |
107 | |
108 | while (TRUE) |
109 | { |
110 | FrameData *frame_data = g_slice_new0 (FrameData); |
111 | frame_data->angle = 2 * M_PI * (frame_count % fps) / (double)fps; |
112 | frame_data->stream_time = (G_GINT64_CONSTANT (1000000) * frame_count) / fps; |
113 | |
114 | queue_frame (frame_data); |
115 | frame_count++; |
116 | } |
117 | |
118 | return NULL; |
119 | } |
120 | |
121 | /* Clock management: |
122 | * |
123 | * The logic here, which is activated by the --pll argument |
124 | * demonstrates adjusting the playback rate so that the frames exactly match |
125 | * when they are displayed both frequency and phase. If there was an |
126 | * accompanying audio track, you would need to resample the audio to match |
127 | * the clock. |
128 | * |
129 | * The algorithm isn't exactly a PLL - I wrote it first that way, but |
130 | * it oscillicated before coming into sync and this approach was easier than |
131 | * fine-tuning the PLL filter. |
132 | * |
133 | * A more complicated algorithm could also establish sync when the playback |
134 | * rate isn't exactly an integral divisor of the VBlank rate, such as 24fps |
135 | * video on a 60fps display. |
136 | */ |
137 | #define PRE_BUFFER_TIME 500000 |
138 | |
139 | static gint64 stream_time_base; |
140 | static gint64 clock_time_base; |
141 | static double time_factor = 1.0; |
142 | static double frequency_time_factor = 1.0; |
143 | static double phase_time_factor = 1.0; |
144 | |
145 | static gint64 |
146 | stream_time_to_clock_time (gint64 stream_time) |
147 | { |
148 | return clock_time_base + (stream_time - stream_time_base) * time_factor; |
149 | } |
150 | |
151 | static void |
152 | adjust_clock_for_phase (gint64 frame_clock_time, |
153 | gint64 presentation_time) |
154 | { |
155 | static int count = 0; |
156 | static gint64 previous_frame_clock_time; |
157 | static gint64 previous_presentation_time; |
158 | gint64 phase = presentation_time - frame_clock_time; |
159 | |
160 | count++; |
161 | if (count >= fps) /* Give a second of warmup */ |
162 | { |
163 | gint64 time_delta = frame_clock_time - previous_frame_clock_time; |
164 | gint64 previous_phase = previous_presentation_time - previous_frame_clock_time; |
165 | |
166 | double expected_phase_delta; |
167 | |
168 | stream_time_base += (frame_clock_time - clock_time_base) / time_factor; |
169 | clock_time_base = frame_clock_time; |
170 | |
171 | expected_phase_delta = time_delta * (1 - phase_time_factor); |
172 | |
173 | /* If the phase is increasing that means the computed clock times are |
174 | * increasing too slowly. We increase the frequency time factor to compensate, |
175 | * but decrease the compensation so that it takes effect over 1 second to |
176 | * avoid jitter */ |
177 | frequency_time_factor += (phase - previous_phase - expected_phase_delta) / (double)time_delta / fps; |
178 | |
179 | /* We also want to increase or decrease the frequency to bring the phase |
180 | * into sync. We do that again so that the phase should sync up over 1 seconds |
181 | */ |
182 | phase_time_factor = 1 + phase / 2000000.; |
183 | |
184 | time_factor = frequency_time_factor * phase_time_factor; |
185 | } |
186 | |
187 | previous_frame_clock_time = frame_clock_time; |
188 | previous_presentation_time = presentation_time; |
189 | } |
190 | |
191 | /* Drawing */ |
192 | |
193 | static void |
194 | on_draw (GtkDrawingArea *da, |
195 | cairo_t *cr, |
196 | int width, |
197 | int height, |
198 | gpointer data) |
199 | { |
200 | double cx, cy, r; |
201 | |
202 | cairo_set_source_rgb (cr, red: 1., green: 1., blue: 1.); |
203 | cairo_paint (cr); |
204 | |
205 | cairo_set_source_rgb (cr, red: 0., green: 0., blue: 0.); |
206 | |
207 | cx = width / 2.; |
208 | cy = height / 2.; |
209 | r = MIN (width, height) / 2.; |
210 | |
211 | cairo_arc (cr, xc: cx, yc: cy, radius: r, |
212 | angle1: 0, angle2: 2 * M_PI); |
213 | cairo_stroke (cr); |
214 | if (displayed_frame) |
215 | { |
216 | cairo_move_to (cr, x: cx, y: cy); |
217 | cairo_line_to (cr, |
218 | x: cx + r * cos(x: displayed_frame->angle - M_PI / 2), |
219 | y: cy + r * sin(x: displayed_frame->angle - M_PI / 2)); |
220 | cairo_stroke (cr); |
221 | |
222 | if (displayed_frame->frame_counter == 0) |
223 | { |
224 | GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (widget: window); |
225 | displayed_frame->frame_counter = gdk_frame_clock_get_frame_counter (frame_clock); |
226 | } |
227 | } |
228 | } |
229 | |
230 | static void |
231 | collect_old_frames (void) |
232 | { |
233 | GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (widget: window); |
234 | GList *l, *l_next; |
235 | |
236 | for (l = past_frames; l; l = l_next) |
237 | { |
238 | FrameData *frame_data = l->data; |
239 | gboolean remove = FALSE; |
240 | l_next = l->next; |
241 | |
242 | GdkFrameTimings *timings = gdk_frame_clock_get_timings (frame_clock, |
243 | frame_counter: frame_data->frame_counter); |
244 | if (timings == NULL) |
245 | { |
246 | remove = TRUE; |
247 | } |
248 | else if (gdk_frame_timings_get_complete (timings)) |
249 | { |
250 | gint64 presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings); |
251 | gint64 refresh_interval = gdk_frame_timings_get_refresh_interval (timings); |
252 | |
253 | if (pll && |
254 | presentation_time && refresh_interval && |
255 | presentation_time > frame_data->clock_time - refresh_interval / 2 && |
256 | presentation_time < frame_data->clock_time + refresh_interval / 2) |
257 | adjust_clock_for_phase (frame_clock_time: frame_data->clock_time, presentation_time); |
258 | |
259 | if (presentation_time) |
260 | variable_add (variable: &latency_error, |
261 | value: presentation_time - frame_data->clock_time); |
262 | |
263 | remove = TRUE; |
264 | } |
265 | |
266 | if (remove) |
267 | { |
268 | past_frames = g_list_delete_link (list: past_frames, link_: l); |
269 | g_slice_free (FrameData, frame_data); |
270 | } |
271 | } |
272 | } |
273 | |
274 | static void |
275 | print_statistics (void) |
276 | { |
277 | gint64 now = g_get_monotonic_time (); |
278 | static gint64 last_print_time = 0; |
279 | |
280 | if (last_print_time == 0) |
281 | last_print_time = now; |
282 | else if (now -last_print_time > 5000000) |
283 | { |
284 | g_print (format: "dropped_frames: %d/%d\n" , |
285 | dropped_frames, n_frames); |
286 | g_print (format: "collected_frames: %g/%d\n" , |
287 | latency_error.weight, n_frames); |
288 | g_print (format: "latency_error: %g +/- %g\n" , |
289 | variable_mean (variable: &latency_error), |
290 | variable_standard_deviation (variable: &latency_error)); |
291 | if (pll) |
292 | g_print (format: "playback rate adjustment: %g +/- %g %%\n" , |
293 | (variable_mean (variable: &time_factor_stats) - 1) * 100, |
294 | variable_standard_deviation (variable: &time_factor_stats) * 100); |
295 | variable_init (variable: &latency_error); |
296 | variable_init (variable: &time_factor_stats); |
297 | dropped_frames = 0; |
298 | n_frames = 0; |
299 | last_print_time = now; |
300 | } |
301 | } |
302 | |
303 | static void |
304 | on_update (GdkFrameClock *frame_clock, |
305 | gpointer data) |
306 | { |
307 | GdkFrameTimings *timings = gdk_frame_clock_get_current_timings (frame_clock); |
308 | gint64 frame_time = gdk_frame_timings_get_frame_time (timings); |
309 | gint64 predicted_presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings); |
310 | gint64 refresh_interval; |
311 | FrameData *pending_frame; |
312 | |
313 | if (clock_time_base == 0) |
314 | clock_time_base = frame_time + PRE_BUFFER_TIME; |
315 | |
316 | gdk_frame_clock_get_refresh_info (frame_clock, base_time: frame_time, |
317 | refresh_interval_return: &refresh_interval, NULL); |
318 | |
319 | pending_frame = peek_pending_frame (); |
320 | g_assert (pending_frame); |
321 | |
322 | if (stream_time_to_clock_time (stream_time: pending_frame->stream_time) |
323 | < predicted_presentation_time + refresh_interval / 2) |
324 | { |
325 | while (TRUE) |
326 | { |
327 | FrameData *next_frame = peek_next_frame (); |
328 | if (next_frame && |
329 | stream_time_to_clock_time (stream_time: next_frame->stream_time) |
330 | < predicted_presentation_time + refresh_interval / 2) |
331 | { |
332 | g_slice_free (FrameData, unqueue_frame ()); |
333 | n_frames++; |
334 | dropped_frames++; |
335 | pending_frame = next_frame; |
336 | } |
337 | else |
338 | break; |
339 | } |
340 | |
341 | if (displayed_frame) |
342 | past_frames = g_list_prepend (list: past_frames, data: displayed_frame); |
343 | |
344 | n_frames++; |
345 | displayed_frame = unqueue_frame (); |
346 | g_assert (displayed_frame); |
347 | displayed_frame->clock_time = stream_time_to_clock_time (stream_time: displayed_frame->stream_time); |
348 | |
349 | displayed_frame->frame_counter = gdk_frame_timings_get_frame_counter (timings); |
350 | variable_add (variable: &time_factor_stats, value: time_factor); |
351 | |
352 | collect_old_frames (); |
353 | print_statistics (); |
354 | |
355 | gtk_widget_queue_draw (widget: window); |
356 | } |
357 | } |
358 | |
359 | static GOptionEntry options[] = { |
360 | { "pll" , 'p', 0, G_OPTION_ARG_NONE, &pll, "Sync frame rate to refresh" , NULL }, |
361 | { "fps" , 'f', 0, G_OPTION_ARG_INT, &fps, "Frame rate" , "FPS" }, |
362 | { NULL } |
363 | }; |
364 | |
365 | static void |
366 | quit_cb (GtkWidget *widget, |
367 | gpointer data) |
368 | { |
369 | gboolean *done = data; |
370 | |
371 | *done = TRUE; |
372 | |
373 | g_main_context_wakeup (NULL); |
374 | } |
375 | |
376 | int |
377 | main(int argc, char **argv) |
378 | { |
379 | GtkWidget *da; |
380 | GError *error = NULL; |
381 | GdkFrameClock *frame_clock; |
382 | GOptionContext *context; |
383 | gboolean done = FALSE; |
384 | |
385 | context = g_option_context_new (parameter_string: "" ); |
386 | g_option_context_add_main_entries (context, entries: options, NULL); |
387 | |
388 | if (!g_option_context_parse (context, argc: &argc, argv: &argv, error: &error)) |
389 | { |
390 | g_printerr (format: "Option parsing failed: %s\n" , error->message); |
391 | return 1; |
392 | } |
393 | |
394 | g_option_context_free (context); |
395 | |
396 | gtk_init (); |
397 | |
398 | window = gtk_window_new (); |
399 | gtk_window_set_default_size (GTK_WINDOW (window), width: 300, height: 300); |
400 | g_signal_connect (window, "destroy" , |
401 | G_CALLBACK (quit_cb), &done); |
402 | |
403 | da = gtk_drawing_area_new (); |
404 | gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), draw_func: on_draw, NULL, NULL); |
405 | gtk_window_set_child (GTK_WINDOW (window), child: da); |
406 | |
407 | gtk_widget_show (widget: window); |
408 | |
409 | frame_queue = g_queue_new (); |
410 | g_mutex_init (mutex: &frame_mutex); |
411 | g_cond_init (cond: &frame_cond); |
412 | |
413 | g_thread_new (name: "Create Frames" , func: create_frames_thread, NULL); |
414 | |
415 | frame_clock = gtk_widget_get_frame_clock (widget: window); |
416 | g_signal_connect (frame_clock, "update" , |
417 | G_CALLBACK (on_update), NULL); |
418 | gdk_frame_clock_begin_updating (frame_clock); |
419 | |
420 | while (!done) |
421 | g_main_context_iteration (NULL, TRUE); |
422 | |
423 | return 0; |
424 | } |
425 | |