When we connect to GtkWidget::draw, the signal handler should have a return value. This fixes overdrawing client-side decorations.
		
			
				
	
	
		
			402 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
#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 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 gboolean
 | 
						|
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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
  return FALSE;
 | 
						|
}
 | 
						|
 | 
						|
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_init (&latency_error);
 | 
						|
      variable_init (&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;
 | 
						|
}
 |