/*
 * Copyright (C) 2023 Pascal Nowack
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#include "config.h"

#include "grd-rdp-renderer.h"

#include "grd-context.h"
#include "grd-encode-session.h"
#include "grd-hwaccel-nvidia.h"
#include "grd-hwaccel-vaapi.h"
#include "grd-hwaccel-vulkan.h"
#include "grd-rdp-dvc-graphics-pipeline.h"
#include "grd-rdp-frame.h"
#include "grd-rdp-private.h"
#include "grd-rdp-render-context.h"
#include "grd-rdp-server.h"
#include "grd-rdp-surface.h"
#include "grd-rdp-surface-renderer.h"
#include "grd-rdp-sw-encoder-ca.h"
#include "grd-rdp-view-creator.h"
#include "grd-session-rdp.h"

enum
{
  INHIBITION_DONE,
  GFX_INITABLE,

  N_SIGNALS
};

static guint signals[N_SIGNALS];

struct _GrdRdpRenderer
{
  GObject parent;

  gboolean in_shutdown;

  GrdSessionRdp *session_rdp;
  GrdVkPhysicalDevice *vk_physical_device;
  GrdVkDevice *vk_device;
  GrdHwAccelVaapi *hwaccel_vaapi;
  GrdRdpSwEncoderCa *encoder_ca;

  GThread *graphics_thread;
  GMainContext *graphics_context;

  gboolean graphics_subsystem_failed;

  gboolean stop_rendering;
  GCond stop_rendering_cond;

  gboolean rendering_inhibited;
  gboolean output_suppressed;

  gboolean pending_gfx_init;
  gboolean pending_gfx_graphics_reset;

  GMutex surface_renderers_mutex;
  GHashTable *surface_renderer_table;

  GMutex inhibition_mutex;
  GHashTable *render_context_table;
  GHashTable *acquired_render_contexts;
  GHashTable *render_resource_mappings;

  GSource *surface_disposal_source;
  GAsyncQueue *disposal_queue;

  GSource *surface_render_source;
  GHashTable *queued_frames;

  GMutex view_creations_mutex;
  GHashTable *finished_view_creations;

  GMutex frame_encodings_mutex;
  GHashTable *finished_frame_encodings;
};

G_DEFINE_TYPE (GrdRdpRenderer, grd_rdp_renderer, G_TYPE_OBJECT)

GMainContext *
grd_rdp_renderer_get_graphics_context (GrdRdpRenderer *renderer)
{
  return renderer->graphics_context;
}

GrdSessionRdp *
grd_rdp_renderer_get_session (GrdRdpRenderer *renderer)
{
  return renderer->session_rdp;
}

GrdVkDevice *
grd_rdp_renderer_get_vk_device (GrdRdpRenderer *renderer)
{
  return renderer->vk_device;
}

GrdHwAccelVaapi *
grd_rdp_renderer_get_hwaccel_vaapi (GrdRdpRenderer *renderer)
{
  return renderer->hwaccel_vaapi;
}

GrdRdpSwEncoderCa *
grd_rdp_renderer_get_encoder_ca (GrdRdpRenderer *renderer)
{
  return renderer->encoder_ca;
}

static void
trigger_render_sources (GrdRdpRenderer *renderer)
{
  GrdRdpSurfaceRenderer *surface_renderer = NULL;
  g_autoptr (GMutexLocker) locker = NULL;
  GHashTableIter iter;

  locker = g_mutex_locker_new (&renderer->surface_renderers_mutex);
  g_hash_table_iter_init (&iter, renderer->surface_renderer_table);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_renderer))
    grd_rdp_surface_renderer_trigger_render_source (surface_renderer);
}

void
grd_rdp_renderer_update_output_suppression_state (GrdRdpRenderer *renderer,
                                                  gboolean        suppress_output)
{
  renderer->output_suppressed = suppress_output;

  if (!renderer->output_suppressed)
    trigger_render_sources (renderer);
}

static void
stop_rendering (GrdRdpRenderer *renderer)
{
  g_mutex_lock (&renderer->inhibition_mutex);
  renderer->stop_rendering = TRUE;

  while (g_hash_table_size (renderer->acquired_render_contexts) > 0)
    g_cond_wait (&renderer->stop_rendering_cond, &renderer->inhibition_mutex);
  g_mutex_unlock (&renderer->inhibition_mutex);
}

void
grd_rdp_renderer_invoke_shutdown (GrdRdpRenderer *renderer)
{
  g_assert (renderer->graphics_context);

  stop_rendering (renderer);

  renderer->in_shutdown = TRUE;

  g_main_context_wakeup (renderer->graphics_context);
  g_clear_pointer (&renderer->graphics_thread, g_thread_join);
}

static gpointer
graphics_thread_func (gpointer data)
{
  GrdRdpRenderer *renderer = data;
  GrdRdpServer *rdp_server = grd_session_rdp_get_server (renderer->session_rdp);
  GrdHwAccelNvidia *hwaccel_nvidia =
    grd_rdp_server_get_hwaccel_nvidia (rdp_server);

  if (hwaccel_nvidia)
    grd_hwaccel_nvidia_push_cuda_context (hwaccel_nvidia);

  while (!renderer->in_shutdown)
    g_main_context_iteration (renderer->graphics_context, TRUE);

  if (hwaccel_nvidia)
    grd_hwaccel_nvidia_pop_cuda_context (hwaccel_nvidia);

  return NULL;
}

static gboolean
maybe_initialize_hardware_acceleration (GrdRdpRenderer   *renderer,
                                        GrdHwAccelVulkan *hwaccel_vulkan)
{
  GrdVkPhysicalDevice *vk_physical_device;
  g_autoptr (GError) error = NULL;

  vk_physical_device =
    grd_hwaccel_vulkan_acquire_physical_device (hwaccel_vulkan, &error);
  if (!vk_physical_device)
    {
      g_message ("[HWAccel.Vulkan] Could not acquire Vulkan physical "
                 "device: %s", error->message);
      return TRUE;
    }
  renderer->vk_physical_device = vk_physical_device;

  renderer->vk_device = grd_hwaccel_vulkan_acquire_device (hwaccel_vulkan,
                                                           vk_physical_device,
                                                           &error);
  if (!renderer->vk_device)
    {
      g_warning ("[RDP] Failed to acquire Vulkan device: %s",
                 error->message);
      return FALSE;
    }

  renderer->hwaccel_vaapi = grd_hwaccel_vaapi_new (renderer->vk_device,
                                                   &error);
  if (!renderer->hwaccel_vaapi)
    {
      g_message ("[RDP] Did not initialize VAAPI: %s", error->message);
      g_clear_object (&renderer->vk_device);
    }

  return TRUE;
}

gboolean
grd_rdp_renderer_start (GrdRdpRenderer *renderer)
{
  GrdRdpServer *rdp_server = grd_session_rdp_get_server (renderer->session_rdp);
  GrdHwAccelVulkan *hwaccel_vulkan =
    grd_rdp_server_get_hwaccel_vulkan (rdp_server);
  g_autoptr (GError) error = NULL;

  if (hwaccel_vulkan &&
      !maybe_initialize_hardware_acceleration (renderer, hwaccel_vulkan))
    return FALSE;

  renderer->encoder_ca = grd_rdp_sw_encoder_ca_new (&error);
  if (!renderer->encoder_ca)
    {
      g_warning ("[RDP] Failed to create fallback software renderer: %s",
                 error->message);
      return FALSE;
    }

  renderer->graphics_thread = g_thread_new ("RDP graphics thread",
                                            graphics_thread_func,
                                            renderer);

  return TRUE;
}

static GrdRdpDvcGraphicsPipeline *
graphics_pipeline_from_renderer (GrdRdpRenderer *renderer)
{
  return grd_session_rdp_get_graphics_pipeline (renderer->session_rdp);
}

void
grd_rdp_renderer_notify_new_desktop_layout (GrdRdpRenderer *renderer,
                                            uint32_t        desktop_width,
                                            uint32_t        desktop_height)
{
  rdpContext *rdp_context =
    grd_session_rdp_get_rdp_context (renderer->session_rdp);
  rdpSettings *rdp_settings = rdp_context->settings;

  g_assert (graphics_pipeline_from_renderer (renderer));
  renderer->pending_gfx_graphics_reset = TRUE;

  freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopWidth,
                               desktop_width);
  freerdp_settings_set_uint32 (rdp_settings, FreeRDP_DesktopHeight,
                               desktop_height);
}

void
grd_rdp_renderer_notify_graphics_pipeline_ready (GrdRdpRenderer *renderer)
{
  g_debug ("[RDP] Renderer: Received Graphics Pipeline ready notification");

  renderer->pending_gfx_graphics_reset = TRUE;
  renderer->pending_gfx_init = FALSE;

  trigger_render_sources (renderer);
}

void
grd_rdp_renderer_notify_graphics_pipeline_reset (GrdRdpRenderer *renderer)
{
  gboolean gfx_initable = FALSE;

  g_debug ("[RDP] Renderer: Received Graphics Pipeline reset notification");

  g_mutex_lock (&renderer->inhibition_mutex);
  renderer->pending_gfx_init = TRUE;

  if (g_hash_table_size (renderer->acquired_render_contexts) == 0)
    gfx_initable = TRUE;
  g_mutex_unlock (&renderer->inhibition_mutex);

  if (gfx_initable)
    g_signal_emit (renderer, signals[GFX_INITABLE], 0);
}

void
grd_rdp_renderer_inhibit_rendering (GrdRdpRenderer *renderer)
{
  gboolean inhibition_done = FALSE;

  g_mutex_lock (&renderer->inhibition_mutex);
  renderer->rendering_inhibited = TRUE;

  if (g_hash_table_size (renderer->acquired_render_contexts) == 0)
    inhibition_done = TRUE;
  g_mutex_unlock (&renderer->inhibition_mutex);

  if (inhibition_done)
    g_signal_emit (renderer, signals[INHIBITION_DONE], 0);
}

void
grd_rdp_renderer_uninhibit_rendering (GrdRdpRenderer *renderer)
{
  renderer->rendering_inhibited = FALSE;

  trigger_render_sources (renderer);
}

GrdRdpSurface *
grd_rdp_renderer_try_acquire_surface (GrdRdpRenderer *renderer,
                                      uint32_t        refresh_rate)
{
  GrdRdpSurface *rdp_surface;
  GrdRdpSurfaceRenderer *surface_renderer;

  rdp_surface = grd_rdp_surface_new (renderer);
  if (!rdp_surface)
    return NULL;

  surface_renderer = grd_rdp_surface_renderer_new (rdp_surface, renderer,
                                                   refresh_rate);
  grd_rdp_surface_attach_surface_renderer (rdp_surface, surface_renderer);

  g_mutex_lock (&renderer->surface_renderers_mutex);
  g_hash_table_insert (renderer->surface_renderer_table,
                       rdp_surface, surface_renderer);
  g_mutex_unlock (&renderer->surface_renderers_mutex);

  return rdp_surface;
}

void
grd_rdp_renderer_release_surface (GrdRdpRenderer *renderer,
                                  GrdRdpSurface  *rdp_surface)
{
  g_assert (rdp_surface);

  g_async_queue_push (renderer->disposal_queue, rdp_surface);
  g_source_set_ready_time (renderer->surface_disposal_source, 0);
}

static void
invalidate_surfaces (GrdRdpRenderer *renderer,
                     gboolean        locked)
{
  GrdRdpSurfaceRenderer *surface_renderer = NULL;
  g_autoptr (GMutexLocker) locker = NULL;
  GHashTableIter iter;

  locker = g_mutex_locker_new (&renderer->surface_renderers_mutex);
  g_hash_table_iter_init (&iter, renderer->surface_renderer_table);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &surface_renderer))
    {
      if (!locked)
        grd_rdp_surface_renderer_invalidate_surface_unlocked (surface_renderer);
      else
        grd_rdp_surface_renderer_invalidate_surface (surface_renderer);
    }
}

static void
clear_render_contexts (GrdRdpRenderer *renderer,
                       gboolean        locked)
{
  g_assert (g_hash_table_size (renderer->acquired_render_contexts) == 0);
  g_assert (g_hash_table_size (renderer->queued_frames) == 0);

  invalidate_surfaces (renderer, locked);

  g_hash_table_remove_all (renderer->render_resource_mappings);
  g_hash_table_remove_all (renderer->render_context_table);
}

static void
maybe_reset_graphics (GrdRdpRenderer *renderer)
{
  GrdRdpDvcGraphicsPipeline *graphics_pipeline =
    graphics_pipeline_from_renderer (renderer);
  rdpContext *rdp_context =
    grd_session_rdp_get_rdp_context (renderer->session_rdp);
  rdpSettings *rdp_settings = rdp_context->settings;
  uint32_t desktop_width =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopWidth);
  uint32_t desktop_height =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopHeight);
  g_autofree MONITOR_DEF *monitor_defs = NULL;
  uint32_t n_monitors;
  uint32_t i;

  if (!renderer->pending_gfx_graphics_reset)
    return;

  clear_render_contexts (renderer, FALSE);

  n_monitors = freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount);
  g_assert (n_monitors > 0);

  monitor_defs = g_new0 (MONITOR_DEF, n_monitors);

  for (i = 0; i < n_monitors; ++i)
    {
      const rdpMonitor *monitor =
        freerdp_settings_get_pointer_array (rdp_settings,
                                            FreeRDP_MonitorDefArray, i);
      MONITOR_DEF *monitor_def = &monitor_defs[i];

      monitor_def->left = monitor->x;
      monitor_def->top = monitor->y;
      monitor_def->right = monitor_def->left + monitor->width - 1;
      monitor_def->bottom = monitor_def->top + monitor->height - 1;

      if (monitor->is_primary)
        monitor_def->flags = MONITOR_PRIMARY;
    }

  grd_rdp_dvc_graphics_pipeline_reset_graphics (graphics_pipeline,
                                                desktop_width, desktop_height,
                                                monitor_defs, n_monitors);
  renderer->pending_gfx_graphics_reset = FALSE;
}

static void
destroy_render_context_locked (GrdRdpRenderer *renderer,
                               GrdRdpSurface  *rdp_surface)
{
  GrdRdpRenderContext *render_context = NULL;

  if (!g_hash_table_lookup_extended (renderer->render_context_table,
                                     rdp_surface,
                                     NULL, (gpointer *) &render_context))
    return;

  g_assert (render_context);
  g_assert (!g_hash_table_contains (renderer->acquired_render_contexts,
                                    render_context));
  g_assert (!g_hash_table_contains (renderer->queued_frames,
                                    render_context));

  g_hash_table_remove (renderer->render_resource_mappings, render_context);
  g_hash_table_remove (renderer->render_context_table, rdp_surface);
}

static void
destroy_render_context (GrdRdpRenderer *renderer,
                        GrdRdpSurface  *rdp_surface)
{
  g_autoptr (GMutexLocker) locker = NULL;

  locker = g_mutex_locker_new (&renderer->inhibition_mutex);
  destroy_render_context_locked (renderer, rdp_surface);
}

static GrdRdpRenderContext *
render_context_ref (GrdRdpRenderer      *renderer,
                    GrdRdpRenderContext *render_context)
{
  uint32_t *ref_count = NULL;

  if (g_hash_table_lookup_extended (renderer->acquired_render_contexts,
                                    render_context,
                                    NULL, (gpointer *) &ref_count))
    {
      g_assert (*ref_count > 0);
      ++(*ref_count);

      return render_context;
    }

  ref_count = g_new0 (uint32_t, 1);
  *ref_count = 1;

  g_hash_table_insert (renderer->acquired_render_contexts,
                       render_context, ref_count);

  return render_context;
}

static void
render_context_unref (GrdRdpRenderer      *renderer,
                      GrdRdpRenderContext *render_context)
{
  uint32_t *ref_count = NULL;

  if (!g_hash_table_lookup_extended (renderer->acquired_render_contexts,
                                     render_context,
                                     NULL, (gpointer *) &ref_count))
    g_assert_not_reached ();

  g_assert (*ref_count > 0);
  --(*ref_count);

  if (*ref_count == 0)
    g_hash_table_remove (renderer->acquired_render_contexts, render_context);
}

static void
handle_graphics_subsystem_failure (GrdRdpRenderer *renderer)
{
  renderer->graphics_subsystem_failed = TRUE;
  renderer->stop_rendering = TRUE;

  grd_session_rdp_notify_error (renderer->session_rdp,
                                GRD_SESSION_RDP_ERROR_GRAPHICS_SUBSYSTEM_FAILED);
}

GrdRdpRenderContext *
grd_rdp_renderer_try_acquire_render_context (GrdRdpRenderer            *renderer,
                                             GrdRdpSurface             *rdp_surface,
                                             GrdRdpAcquireContextFlags  flags)
{
  GrdRdpRenderContext *render_context = NULL;
  g_autoptr (GMutexLocker) locker = NULL;

  g_assert (!((flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET) &&
              (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL)));

  locker = g_mutex_locker_new (&renderer->inhibition_mutex);
  if (renderer->stop_rendering ||
      renderer->rendering_inhibited ||
      renderer->pending_gfx_init ||
      renderer->output_suppressed)
    return NULL;

  maybe_reset_graphics (renderer);

  if (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_FORCE_RESET)
    destroy_render_context_locked (renderer, rdp_surface);

  if (g_hash_table_lookup_extended (renderer->render_context_table, rdp_surface,
                                    NULL, (gpointer *) &render_context))
    return render_context_ref (renderer, render_context);

  if (flags & GRD_RDP_ACQUIRE_CONTEXT_FLAG_RETAIN_OR_NULL)
    return NULL;

  render_context = grd_rdp_render_context_new (renderer, rdp_surface);
  if (!render_context)
    {
      handle_graphics_subsystem_failure (renderer);
      return NULL;
    }

  g_hash_table_insert (renderer->render_context_table,
                       rdp_surface, render_context);
  g_hash_table_insert (renderer->render_resource_mappings,
                       render_context, g_hash_table_new (NULL, NULL));

  return render_context_ref (renderer, render_context);
}

void
grd_rdp_renderer_release_render_context (GrdRdpRenderer      *renderer,
                                         GrdRdpRenderContext *render_context)
{
  gboolean inhibition_done = FALSE;
  gboolean gfx_initable = FALSE;

  g_mutex_lock (&renderer->inhibition_mutex);
  render_context_unref (renderer, render_context);

  if (renderer->stop_rendering &&
      g_hash_table_size (renderer->acquired_render_contexts) == 0)
    g_cond_signal (&renderer->stop_rendering_cond);

  if (renderer->rendering_inhibited &&
      g_hash_table_size (renderer->acquired_render_contexts) == 0)
    inhibition_done = TRUE;

  if (renderer->pending_gfx_init &&
      g_hash_table_size (renderer->acquired_render_contexts) == 0)
    gfx_initable = TRUE;
  g_mutex_unlock (&renderer->inhibition_mutex);

  if (inhibition_done)
    g_signal_emit (renderer, signals[INHIBITION_DONE], 0);
  if (gfx_initable)
    g_signal_emit (renderer, signals[GFX_INITABLE], 0);
}

void
grd_rdp_renderer_clear_render_contexts (GrdRdpRenderer *renderer)
{
  clear_render_contexts (renderer, TRUE);
}

static void
queue_prepared_frame (GrdRdpRenderer      *renderer,
                      GrdRdpRenderContext *render_context,
                      GrdRdpFrame         *rdp_frame)
{
  GHashTable *acquired_resources = NULL;
  GrdRdpViewCreator *view_creator;

  if (!g_hash_table_lookup_extended (renderer->render_resource_mappings,
                                     render_context,
                                     NULL, (gpointer *) &acquired_resources))
    g_assert_not_reached ();

  view_creator = grd_rdp_render_context_get_view_creator (render_context);
  g_assert (!g_hash_table_contains (acquired_resources, view_creator));

  grd_rdp_frame_notify_picked_up (rdp_frame);
  g_hash_table_insert (acquired_resources, view_creator, rdp_frame);

  g_mutex_lock (&renderer->view_creations_mutex);
  g_hash_table_add (renderer->finished_view_creations, rdp_frame);
  g_mutex_unlock (&renderer->view_creations_mutex);
}

void
grd_rdp_renderer_submit_frame (GrdRdpRenderer      *renderer,
                               GrdRdpRenderContext *render_context,
                               GrdRdpFrame         *rdp_frame)
{
  grd_rdp_frame_set_renderer (rdp_frame, renderer);

  if (grd_rdp_frame_has_valid_view (rdp_frame))
    queue_prepared_frame (renderer, render_context, rdp_frame);
  else
    g_hash_table_insert (renderer->queued_frames, render_context, rdp_frame);

  g_source_set_ready_time (renderer->surface_render_source, 0);
}

gboolean
grd_rdp_renderer_render_frame (GrdRdpRenderer      *renderer,
                               GrdRdpSurface       *rdp_surface,
                               GrdRdpRenderContext *render_context,
                               GrdRdpLegacyBuffer  *buffer)
{
  GrdRdpDvcGraphicsPipeline *graphics_pipeline =
    graphics_pipeline_from_renderer (renderer);

  return grd_rdp_dvc_graphics_pipeline_refresh_gfx (graphics_pipeline,
                                                    rdp_surface, render_context,
                                                    buffer);
}

GrdRdpRenderer *
grd_rdp_renderer_new (GrdSessionRdp *session_rdp)
{
  GrdRdpRenderer *renderer;

  renderer = g_object_new (GRD_TYPE_RDP_RENDERER, NULL);
  renderer->session_rdp = session_rdp;

  return renderer;
}

static gboolean
dispose_surfaces (gpointer user_data)
{
  GrdRdpRenderer *renderer = user_data;
  GrdRdpSurface *rdp_surface;

  while ((rdp_surface = g_async_queue_try_pop (renderer->disposal_queue)))
    {
      destroy_render_context (renderer, rdp_surface);

      g_mutex_lock (&renderer->surface_renderers_mutex);
      g_hash_table_remove (renderer->surface_renderer_table, rdp_surface);
      g_mutex_unlock (&renderer->surface_renderers_mutex);

      grd_rdp_surface_free (rdp_surface);
    }

  return G_SOURCE_CONTINUE;
}

static void
grd_rdp_renderer_dispose (GObject *object)
{
  GrdRdpRenderer *renderer = GRD_RDP_RENDERER (object);

  grd_rdp_renderer_invoke_shutdown (renderer);

  if (renderer->acquired_render_contexts)
    g_assert (g_hash_table_size (renderer->acquired_render_contexts) == 0);
  if (renderer->render_resource_mappings)
    g_assert (g_hash_table_size (renderer->render_resource_mappings) == 0);
  if (renderer->queued_frames)
    g_assert (g_hash_table_size (renderer->queued_frames) == 0);
  if (renderer->finished_view_creations)
    g_assert (g_hash_table_size (renderer->finished_view_creations) == 0);
  if (renderer->finished_frame_encodings)
    g_assert (g_hash_table_size (renderer->finished_frame_encodings) == 0);

  if (renderer->surface_render_source)
    {
      g_source_destroy (renderer->surface_render_source);
      g_clear_pointer (&renderer->surface_render_source, g_source_unref);
    }
  if (renderer->surface_disposal_source)
    {
      g_source_destroy (renderer->surface_disposal_source);
      g_clear_pointer (&renderer->surface_disposal_source, g_source_unref);
    }

  g_clear_pointer (&renderer->graphics_context, g_main_context_unref);
  dispose_surfaces (renderer);

  g_clear_pointer (&renderer->finished_frame_encodings, g_hash_table_unref);
  g_clear_pointer (&renderer->finished_view_creations, g_hash_table_unref);
  g_clear_pointer (&renderer->queued_frames, g_hash_table_unref);
  g_clear_pointer (&renderer->render_resource_mappings, g_hash_table_unref);
  g_clear_pointer (&renderer->acquired_render_contexts, g_hash_table_unref);
  g_clear_pointer (&renderer->render_context_table, g_hash_table_unref);

  g_clear_pointer (&renderer->disposal_queue, g_async_queue_unref);

  g_assert (g_hash_table_size (renderer->surface_renderer_table) == 0);

  g_clear_object (&renderer->encoder_ca);
  g_clear_object (&renderer->hwaccel_vaapi);
  g_clear_object (&renderer->vk_device);
  g_clear_object (&renderer->vk_physical_device);

  G_OBJECT_CLASS (grd_rdp_renderer_parent_class)->dispose (object);
}

static void
grd_rdp_renderer_finalize (GObject *object)
{
  GrdRdpRenderer *renderer = GRD_RDP_RENDERER (object);

  g_mutex_clear (&renderer->frame_encodings_mutex);
  g_mutex_clear (&renderer->view_creations_mutex);

  g_cond_clear (&renderer->stop_rendering_cond);
  g_mutex_clear (&renderer->inhibition_mutex);
  g_mutex_clear (&renderer->surface_renderers_mutex);

  g_clear_pointer (&renderer->surface_renderer_table, g_hash_table_unref);

  G_OBJECT_CLASS (grd_rdp_renderer_parent_class)->finalize (object);
}

static void
release_acquired_resource (GrdRdpRenderer      *renderer,
                           GrdRdpRenderContext *render_context,
                           gpointer             resource)
{
  GHashTable *acquired_resources = NULL;

  if (!g_hash_table_lookup_extended (renderer->render_resource_mappings,
                                     render_context,
                                     NULL, (gpointer *) &acquired_resources))
    g_assert_not_reached ();

  if (!g_hash_table_remove (acquired_resources, resource))
    g_assert_not_reached ();
}

static GList *
fetch_rendered_frames (GrdRdpRenderer *renderer)
{
  g_autoptr (GMutexLocker) locker = NULL;
  GrdRdpFrame *rdp_frame = NULL;
  GHashTableIter iter;
  GList *rendered_frames;

  locker = g_mutex_locker_new (&renderer->frame_encodings_mutex);
  g_hash_table_iter_init (&iter, renderer->finished_frame_encodings);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame))
    {
      GrdRdpRenderContext *render_context =
        grd_rdp_frame_get_render_context (rdp_frame);
      GrdEncodeSession *encode_session =
        grd_rdp_render_context_get_encode_session (render_context);

      release_acquired_resource (renderer, render_context, encode_session);

      if (!grd_rdp_frame_get_bitstreams (rdp_frame))
        g_hash_table_iter_remove (&iter);
    }

  rendered_frames =
    g_hash_table_get_values (renderer->finished_frame_encodings);
  g_hash_table_steal_all (renderer->finished_frame_encodings);

  return rendered_frames;
}

static void
on_bitstream_locked (GrdEncodeSession *encode_session,
                     GrdBitstream     *bitstream,
                     gpointer          user_data,
                     GError           *error)
{
  GrdRdpFrame *rdp_frame = user_data;
  GrdRdpRenderer *renderer = grd_rdp_frame_get_renderer (rdp_frame);

  if (bitstream)
    {
      GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame);

      bitstreams = g_list_append (bitstreams, bitstream);
      grd_rdp_frame_set_bitstreams (rdp_frame, bitstreams);
    }
  else
    {
      g_warning ("[RDP] Failed to lock bitstream: %s", error->message);
    }

  g_mutex_lock (&renderer->frame_encodings_mutex);
  if (!grd_encode_session_has_pending_frames (encode_session))
    g_hash_table_add (renderer->finished_frame_encodings, rdp_frame);

  if (!bitstream)
    renderer->graphics_subsystem_failed = TRUE;
  g_mutex_unlock (&renderer->frame_encodings_mutex);

  g_source_set_ready_time (renderer->surface_render_source, 0);
}

static gboolean
encode_image_views (GrdRdpRenderer *renderer,
                    GrdRdpFrame    *rdp_frame)
{
  g_autoptr (GMutexLocker) locker = NULL;
  gboolean submitted_frame = FALSE;
  g_autoptr (GError) error = NULL;
  GrdImageView *image_view;

  locker = g_mutex_locker_new (&renderer->frame_encodings_mutex);
  while ((image_view = grd_rdp_frame_pop_image_view (rdp_frame)))
    {
      GrdRdpRenderContext *render_context =
        grd_rdp_frame_get_render_context (rdp_frame);
      GrdEncodeSession *encode_session =
        grd_rdp_render_context_get_encode_session (render_context);
      GrdEncodeContext *encode_context =
        grd_rdp_frame_get_encode_context (rdp_frame);

      if (!grd_encode_session_encode_frame (encode_session, encode_context,
                                            image_view, &error))
        {
          g_warning ("[RDP] Failed to encode frame: %s", error->message);

          if (!submitted_frame)
            {
              release_acquired_resource (renderer, render_context,
                                         encode_session);
              grd_rdp_frame_free (rdp_frame);
            }

          return FALSE;
        }
      submitted_frame = TRUE;

      grd_encode_session_lock_bitstream (encode_session, image_view,
                                         on_bitstream_locked, rdp_frame);
    }

  return TRUE;
}

static gboolean
maybe_start_encodings (GrdRdpRenderer *renderer)
{
  g_autoptr (GMutexLocker) locker = NULL;
  GrdRdpFrame *rdp_frame = NULL;
  GHashTableIter iter;

  if (renderer->stop_rendering)
    return TRUE;

  locker = g_mutex_locker_new (&renderer->view_creations_mutex);
  if (renderer->graphics_subsystem_failed)
    return FALSE;

  g_hash_table_iter_init (&iter, renderer->finished_view_creations);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame))
    {
      GHashTable *acquired_resources = NULL;
      GrdRdpRenderContext *render_context;
      GrdRdpViewCreator *view_creator;
      GrdEncodeSession *encode_session;

      g_assert (grd_rdp_frame_has_valid_view (rdp_frame));

      render_context = grd_rdp_frame_get_render_context (rdp_frame);
      view_creator = grd_rdp_render_context_get_view_creator (render_context);

      if (!grd_rdp_frame_is_surface_damaged (rdp_frame))
        {
          release_acquired_resource (renderer, render_context, view_creator);
          g_hash_table_iter_remove (&iter);
          continue;
        }

      if (!g_hash_table_lookup_extended (renderer->render_resource_mappings,
                                         render_context,
                                         NULL, (gpointer *) &acquired_resources))
        g_assert_not_reached ();

      encode_session =
        grd_rdp_render_context_get_encode_session (render_context);
      if (g_hash_table_contains (acquired_resources, encode_session))
        continue;

      release_acquired_resource (renderer, render_context, view_creator);
      g_hash_table_insert (acquired_resources, encode_session, rdp_frame);
      g_hash_table_iter_steal (&iter);

      if (!encode_image_views (renderer, rdp_frame))
        return FALSE;
    }

  return TRUE;
}

static void
on_view_created (GrdRdpFrame *rdp_frame,
                 GError      *error)
{
  GrdRdpRenderer *renderer = grd_rdp_frame_get_renderer (rdp_frame);

  if (!grd_rdp_frame_has_valid_view (rdp_frame))
    g_warning ("[RDP] Failed to create image view: %s", error->message);

  g_mutex_lock (&renderer->view_creations_mutex);
  g_hash_table_add (renderer->finished_view_creations, rdp_frame);

  if (!grd_rdp_frame_has_valid_view (rdp_frame))
    renderer->graphics_subsystem_failed = TRUE;
  g_mutex_unlock (&renderer->view_creations_mutex);

  g_source_set_ready_time (renderer->surface_render_source, 0);
}

static gboolean
maybe_start_view_creations (GrdRdpRenderer *renderer)
{
  GrdRdpFrame *rdp_frame = NULL;
  GHashTableIter iter;

  if (renderer->stop_rendering)
    return TRUE;

  g_hash_table_iter_init (&iter, renderer->queued_frames);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame))
    {
      GHashTable *acquired_resources = NULL;
      GrdRdpRenderContext *render_context;
      GrdRdpViewCreator *view_creator;
      g_autoptr (GError) error = NULL;

      render_context = grd_rdp_frame_get_render_context (rdp_frame);
      if (!g_hash_table_lookup_extended (renderer->render_resource_mappings,
                                         render_context,
                                         NULL, (gpointer *) &acquired_resources))
        g_assert_not_reached ();

      view_creator = grd_rdp_render_context_get_view_creator (render_context);
      if (g_hash_table_contains (acquired_resources, view_creator))
        continue;

      grd_rdp_frame_notify_picked_up (rdp_frame);

      /* There is no resource to release here (no predecessor) */
      g_hash_table_insert (acquired_resources, view_creator, rdp_frame);
      g_hash_table_iter_steal (&iter);

      if (!grd_rdp_view_creator_create_view (view_creator, rdp_frame,
                                             on_view_created, &error))
        {
          g_warning ("[RDP] Failed to create view: %s", error->message);
          grd_rdp_frame_free (rdp_frame);
          return FALSE;
        }
    }

  return TRUE;
}

static void
submit_rendered_frames (GrdRdpRenderer *renderer,
                        GList          *frames)
{
  GrdRdpDvcGraphicsPipeline *graphics_pipeline =
    graphics_pipeline_from_renderer (renderer);
  GList *l;

  for (l = frames; l; l = l->next)
    {
      GrdRdpFrame *rdp_frame = l->data;

      grd_rdp_dvc_graphics_pipeline_submit_frame (graphics_pipeline,
                                                  rdp_frame);
      grd_rdp_frame_notify_frame_submission (rdp_frame);
    }
}

static void
release_bitstreams (gpointer data,
                    gpointer user_data)
{
  GrdRdpRenderer *renderer = user_data;
  GrdRdpFrame *rdp_frame = data;
  GList *bitstreams = grd_rdp_frame_get_bitstreams (rdp_frame);
  GList *l;

  for (l = bitstreams; l; l = l->next)
    {
      GrdRdpRenderContext *render_context =
        grd_rdp_frame_get_render_context (rdp_frame);
      GrdEncodeSession *encode_session =
        grd_rdp_render_context_get_encode_session (render_context);
      GrdBitstream *bitstream = l->data;
      g_autoptr (GError) error = NULL;

      if (!grd_encode_session_unlock_bitstream (encode_session, bitstream,
                                                &error))
        {
          g_warning ("[RDP] Renderer: Failed to unlock bitstream %s",
                     error->message);
          handle_graphics_subsystem_failure (renderer);
        }
    }
  g_list_free (bitstreams);

  grd_rdp_frame_set_bitstreams (rdp_frame, NULL);
}

static void
clear_pending_frames (GrdRdpRenderer *renderer)
{
  GrdRdpFrame *rdp_frame = NULL;
  GHashTableIter iter;

  g_hash_table_remove_all (renderer->queued_frames);

  g_mutex_lock (&renderer->view_creations_mutex);
  g_hash_table_iter_init (&iter, renderer->finished_view_creations);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame))
    {
      GrdRdpRenderContext *render_context =
        grd_rdp_frame_get_render_context (rdp_frame);
      GrdRdpViewCreator *view_creator =
        grd_rdp_render_context_get_view_creator (render_context);

      release_acquired_resource (renderer, render_context, view_creator);
      g_hash_table_iter_remove (&iter);
    }
  g_mutex_unlock (&renderer->view_creations_mutex);

  g_mutex_lock (&renderer->frame_encodings_mutex);
  g_hash_table_iter_init (&iter, renderer->finished_frame_encodings);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &rdp_frame))
    {
      GrdRdpRenderContext *render_context =
        grd_rdp_frame_get_render_context (rdp_frame);
      GrdEncodeSession *encode_session =
        grd_rdp_render_context_get_encode_session (render_context);

      release_acquired_resource (renderer, render_context, encode_session);
      release_bitstreams (rdp_frame, renderer);
      g_hash_table_iter_remove (&iter);
    }
  g_mutex_unlock (&renderer->frame_encodings_mutex);
}

static gboolean
render_surfaces (gpointer user_data)
{
  GrdRdpRenderer *renderer = user_data;
  GList *finished_frames;

  finished_frames = fetch_rendered_frames (renderer);

  if (!maybe_start_encodings (renderer) ||
      !maybe_start_view_creations (renderer))
    handle_graphics_subsystem_failure (renderer);

  if (!renderer->stop_rendering)
    submit_rendered_frames (renderer, finished_frames);
  else
    clear_pending_frames (renderer);

  g_list_foreach (finished_frames, release_bitstreams, renderer);
  g_clear_list (&finished_frames, (GDestroyNotify) grd_rdp_frame_free);

  return G_SOURCE_CONTINUE;
}

static gboolean
source_dispatch (GSource     *source,
                 GSourceFunc  callback,
                 gpointer     user_data)
{
  g_source_set_ready_time (source, -1);

  return callback (user_data);
}

static GSourceFuncs source_funcs =
{
  .dispatch = source_dispatch,
};

static void
grd_rdp_renderer_init (GrdRdpRenderer *renderer)
{
  GSource *surface_disposal_source;
  GSource *surface_render_source;

  renderer->surface_renderer_table = g_hash_table_new (NULL, NULL);
  renderer->render_context_table = g_hash_table_new_full (NULL, NULL,
                                                          NULL, g_object_unref);
  renderer->acquired_render_contexts = g_hash_table_new_full (NULL, NULL,
                                                              NULL, g_free);
  renderer->render_resource_mappings =
    g_hash_table_new_full (NULL, NULL,
                           NULL, (GDestroyNotify) g_hash_table_unref);
  renderer->disposal_queue = g_async_queue_new ();

  renderer->queued_frames =
    g_hash_table_new_full (NULL, NULL,
                           NULL, (GDestroyNotify) grd_rdp_frame_free);
  renderer->finished_view_creations =
    g_hash_table_new_full (NULL, NULL,
                           NULL, (GDestroyNotify) grd_rdp_frame_free);
  renderer->finished_frame_encodings =
    g_hash_table_new_full (NULL, NULL,
                           NULL, (GDestroyNotify) grd_rdp_frame_free);

  g_mutex_init (&renderer->surface_renderers_mutex);
  g_mutex_init (&renderer->inhibition_mutex);
  g_cond_init (&renderer->stop_rendering_cond);

  g_mutex_init (&renderer->view_creations_mutex);
  g_mutex_init (&renderer->frame_encodings_mutex);

  renderer->graphics_context = g_main_context_new ();

  surface_disposal_source = g_source_new (&source_funcs, sizeof (GSource));
  g_source_set_callback (surface_disposal_source, dispose_surfaces,
                         renderer, NULL);
  g_source_set_ready_time (surface_disposal_source, -1);
  g_source_attach (surface_disposal_source, renderer->graphics_context);
  renderer->surface_disposal_source = surface_disposal_source;

  surface_render_source = g_source_new (&source_funcs, sizeof (GSource));
  g_source_set_callback (surface_render_source, render_surfaces,
                         renderer, NULL);
  g_source_set_ready_time (surface_render_source, -1);
  g_source_attach (surface_render_source, renderer->graphics_context);
  renderer->surface_render_source = surface_render_source;
}

static void
grd_rdp_renderer_class_init (GrdRdpRendererClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = grd_rdp_renderer_dispose;
  object_class->finalize = grd_rdp_renderer_finalize;

  signals[INHIBITION_DONE] = g_signal_new ("inhibition-done",
                                           G_TYPE_FROM_CLASS (klass),
                                           G_SIGNAL_RUN_LAST,
                                           0,
                                           NULL, NULL, NULL,
                                           G_TYPE_NONE, 0);
  signals[GFX_INITABLE] = g_signal_new ("gfx-initable",
                                        G_TYPE_FROM_CLASS (klass),
                                        G_SIGNAL_RUN_LAST,
                                        0,
                                        NULL, NULL, NULL,
                                        G_TYPE_NONE, 0);
}
