/* GStreamer DVD Sub-Picture Unit
 * Copyright (C) 2007 Fluendo S.A. <info@fluendo.com>
 * Copyright (C) 2009 Jan Schmidt <thaytan@noraisin.net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <string.h>

#include <gst/gst.h>

#include "gstdvdspu.h"

GST_DEBUG_CATEGORY_EXTERN (dvdspu_debug);
#define GST_CAT_DEFAULT dvdspu_debug

static void
gstspu_vobsub_recalc_palette (GstDVDSpu * dvdspu,
    SpuColour * dest, guint8 * idx, guint8 * alpha)
{
  SpuState *state = &dvdspu->spu_state;
  gint i;

  if (state->vobsub.current_clut[idx[0]] != 0) {
    for (i = 0; i < 4; i++, dest++) {
      guint32 col = state->vobsub.current_clut[idx[i]];

      /* Convert incoming 4-bit alpha to 8 bit for blending */
      dest->A = (alpha[i] << 4) | alpha[i];
      dest->Y = ((guint16) ((col >> 16) & 0xff)) * dest->A;
      /* U/V are stored as V/U in the clut words, so switch them */
      dest->V = ((guint16) ((col >> 8) & 0xff)) * dest->A;
      dest->U = ((guint16) (col & 0xff)) * dest->A;
    }
  } else {
    int y = 240;

    /* The CLUT presumably hasn't been set, so we'll just guess some
     * values for the non-transparent colors (white, grey, black) */
    for (i = 0; i < 4; i++, dest++) {
      dest->A = (alpha[i] << 4) | alpha[i];
      if (alpha[i] != 0) {
        dest[0].Y = y * dest[0].A;
        y -= 112;
        if (y < 0)
          y = 0;
      }
      dest[0].U = 128 * dest[0].A;
      dest[0].V = 128 * dest[0].A;
    }
  }
}

/* Recalculate the main, HL & ChgCol palettes */
static void
gstspu_vobsub_update_palettes (GstDVDSpu * dvdspu, SpuState * state)
{
  guint8 index[4];              /* Indices for the palette */
  guint8 alpha[4];              /* Alpha values the palette */

  if (state->vobsub.main_pal_dirty) {
    gstspu_vobsub_recalc_palette (dvdspu, state->vobsub.main_pal,
        state->vobsub.main_idx, state->vobsub.main_alpha);

    /* Need to refresh the hl_ctrl info copies of the main palette too */
    memcpy (state->vobsub.hl_ctrl_i.pix_ctrl_i[0].pal_cache,
        state->vobsub.main_pal, 4 * sizeof (SpuColour));
    memcpy (state->vobsub.hl_ctrl_i.pix_ctrl_i[2].pal_cache,
        state->vobsub.main_pal, 4 * sizeof (SpuColour));

    state->vobsub.main_pal_dirty = FALSE;
  }

  if (state->vobsub.hl_pal_dirty) {
    gstspu_vobsub_recalc_palette (dvdspu,
        state->vobsub.hl_ctrl_i.pix_ctrl_i[1].pal_cache, state->vobsub.hl_idx,
        state->vobsub.hl_alpha);
    state->vobsub.hl_pal_dirty = FALSE;
  }

  /* Update the offset positions for the highlight region */
  if (state->vobsub.hl_rect.top != -1) {
    state->vobsub.hl_ctrl_i.top = state->vobsub.hl_rect.top;
    state->vobsub.hl_ctrl_i.bottom = state->vobsub.hl_rect.bottom;
    state->vobsub.hl_ctrl_i.n_changes = 3;
    state->vobsub.hl_ctrl_i.pix_ctrl_i[0].left = 0;
    state->vobsub.hl_ctrl_i.pix_ctrl_i[1].left = state->vobsub.hl_rect.left;
    state->vobsub.hl_ctrl_i.pix_ctrl_i[2].left =
        state->vobsub.hl_rect.right + 1;
  }

  if (state->vobsub.line_ctrl_i_pal_dirty) {
    gint16 l, c;
    GST_LOG_OBJECT (dvdspu, "Updating chg-col-con palettes");
    for (l = 0; l < state->vobsub.n_line_ctrl_i; l++) {
      SpuVobsubLineCtrlI *cur_line_ctrl = state->vobsub.line_ctrl_i + l;

      for (c = 0; c < cur_line_ctrl->n_changes; c++) {
        SpuVobsubPixCtrlI *cur = cur_line_ctrl->pix_ctrl_i + c;

        index[3] = (cur->palette >> 28) & 0x0f;
        index[2] = (cur->palette >> 24) & 0x0f;
        index[1] = (cur->palette >> 20) & 0x0f;
        index[0] = (cur->palette >> 16) & 0x0f;

        alpha[3] = (cur->palette >> 12) & 0x0f;
        alpha[2] = (cur->palette >> 8) & 0x0f;
        alpha[1] = (cur->palette >> 4) & 0x0f;
        alpha[0] = (cur->palette) & 0x0f;
        gstspu_vobsub_recalc_palette (dvdspu, cur->pal_cache, index, alpha);
      }
    }
    state->vobsub.line_ctrl_i_pal_dirty = FALSE;
  }
}

static inline guint8
gstspu_vobsub_get_nibble (SpuState * state, guint16 * rle_offset)
{
  guint8 ret;

  if (G_UNLIKELY (*rle_offset >= state->vobsub.max_offset))
    return 0;                   /* Overran the buffer */

  ret = state->vobsub.pix_buf_map.data[(*rle_offset) / 2];

  /* If the offset is even, we shift the answer down 4 bits, otherwise not */
  if (*rle_offset & 0x01)
    ret &= 0x0f;
  else
    ret = ret >> 4;

  (*rle_offset)++;
  return ret;
}

static guint16
gstspu_vobsub_get_rle_code (SpuState * state, guint16 * rle_offset)
{
  guint16 code;

  code = gstspu_vobsub_get_nibble (state, rle_offset);
  if (code < 0x4) {             /* 4 .. f */
    code = (code << 4) | gstspu_vobsub_get_nibble (state, rle_offset);
    if (code < 0x10) {          /* 1x .. 3x */
      code = (code << 4) | gstspu_vobsub_get_nibble (state, rle_offset);
      if (code < 0x40) {        /* 04x .. 0fx */
        code = (code << 4) | gstspu_vobsub_get_nibble (state, rle_offset);
      }
    }
  }
  return code;
}

static inline gboolean
gstspu_vobsub_draw_rle_run (SpuState * state, gint16 x, gint16 end,
    SpuColour * colour)
{
#if 0
  GST_LOG ("Y: %d x: %d end %d col %d %d %d %d",
      state->vobsub.cur_Y, x, end, colour->Y, colour->U, colour->V, colour->A);
#endif

  if (colour->A != 0) {
    guint32 inv_A = 0xff - colour->A;

    /* FIXME: This could be more efficient */
    while (x < end) {
      state->vobsub.out_Y[x] =
          (inv_A * state->vobsub.out_Y[x] + colour->Y) / 0xff;
      state->vobsub.out_U[x / 2] += colour->U;
      state->vobsub.out_V[x / 2] += colour->V;
      state->vobsub.out_A[x / 2] += colour->A;
      x++;
    }
    /* Update the compositing buffer so we know how much to blend later */
    *(state->vobsub.comp_last_x_ptr) = end - 1; /* end is the start of the *next* run */

    return TRUE;
  }
  return FALSE;
}

static inline gint16
rle_end_x (guint16 rle_code, gint16 x, gint16 end)
{
  /* run length = rle_code >> 2 */
  if (G_UNLIKELY (((rle_code >> 2) == 0)))
    return end;
  else
    return MIN (end, x + (rle_code >> 2));
}

static gboolean gstspu_vobsub_render_line_with_chgcol (SpuState * state,
    guint8 * planes[3], guint16 * rle_offset);
static gboolean gstspu_vobsub_update_chgcol (SpuState * state);

static gboolean
gstspu_vobsub_render_line (SpuState * state, guint8 * planes[3],
    guint16 * rle_offset)
{
  gint16 x, next_x, end, rle_code, next_draw_x;
  SpuColour *colour;
  gboolean visible = FALSE;

  /* Check for special case of chg_col info to use (either highlight or
   * ChgCol command */
  if (state->vobsub.cur_chg_col != NULL) {
    if (gstspu_vobsub_update_chgcol (state)) {
      /* Check the top & bottom, because we might not be within the region yet */
      if (state->vobsub.cur_Y >= state->vobsub.cur_chg_col->top &&
          state->vobsub.cur_Y <= state->vobsub.cur_chg_col->bottom) {
        return gstspu_vobsub_render_line_with_chgcol (state, planes,
            rle_offset);
      }
    }
  }

  /* No special case. Render as normal */

  /* Set up our output pointers */
  state->vobsub.out_Y = planes[0];
  state->vobsub.out_U = state->comp_bufs[0];
  state->vobsub.out_V = state->comp_bufs[1];
  state->vobsub.out_A = state->comp_bufs[2];
  /* We always need to start our RLE decoding byte_aligned */
  *rle_offset = GST_ROUND_UP_2 (*rle_offset);

  x = state->vobsub.disp_rect.left;
  end = state->vobsub.disp_rect.right + 1;
  while (x < end) {
    rle_code = gstspu_vobsub_get_rle_code (state, rle_offset);
    colour = &state->vobsub.main_pal[rle_code & 3];
    next_x = rle_end_x (rle_code, x, end);
    next_draw_x = next_x;
    if (next_draw_x > state->vobsub.clip_rect.right)
      next_draw_x = state->vobsub.clip_rect.right;      /* ensure no overflow */
    /* Now draw the run between [x,next_x) */
    if (state->vobsub.cur_Y >= state->vobsub.clip_rect.top &&
        state->vobsub.cur_Y <= state->vobsub.clip_rect.bottom)
      visible |= gstspu_vobsub_draw_rle_run (state, x, next_draw_x, colour);
    x = next_x;
  }

  return visible;
}

static gboolean
gstspu_vobsub_update_chgcol (SpuState * state)
{
  if (state->vobsub.cur_chg_col == NULL)
    return FALSE;

  if (state->vobsub.cur_Y <= state->vobsub.cur_chg_col->bottom)
    return TRUE;

  while (state->vobsub.cur_chg_col < state->vobsub.cur_chg_col_end) {
    if (state->vobsub.cur_Y >= state->vobsub.cur_chg_col->top &&
        state->vobsub.cur_Y <= state->vobsub.cur_chg_col->bottom) {
#if 0
      g_print ("Stopped @ entry %d with top %d bottom %d, cur_y %d",
          (gint16) (state->vobsub.cur_chg_col - state->vobsub.line_ctrl_i),
          state->vobsub.cur_chg_col->top, state->vobsub.cur_chg_col->bottom, y);
#endif
      return TRUE;
    }
    state->vobsub.cur_chg_col++;
  }

  /* Finished all our cur_chg_col entries. Use the main palette from here on */
  state->vobsub.cur_chg_col = NULL;
  return FALSE;
}

static gboolean
gstspu_vobsub_render_line_with_chgcol (SpuState * state, guint8 * planes[3],
    guint16 * rle_offset)
{
  SpuVobsubLineCtrlI *chg_col = state->vobsub.cur_chg_col;

  gint16 x, next_x, disp_end, rle_code, run_end, run_draw_end;
  SpuColour *colour;
  SpuVobsubPixCtrlI *cur_pix_ctrl;
  SpuVobsubPixCtrlI *next_pix_ctrl;
  SpuVobsubPixCtrlI *end_pix_ctrl;
  SpuVobsubPixCtrlI dummy_pix_ctrl;
  gboolean visible = FALSE;
  gint16 cur_reg_end;
  gint i;

  state->vobsub.out_Y = planes[0];
  state->vobsub.out_U = state->comp_bufs[0];
  state->vobsub.out_V = state->comp_bufs[1];
  state->vobsub.out_A = state->comp_bufs[2];

  /* We always need to start our RLE decoding byte_aligned */
  *rle_offset = GST_ROUND_UP_2 (*rle_offset);

  /* Our run will cover the display rect */
  x = state->vobsub.disp_rect.left;
  disp_end = state->vobsub.disp_rect.right + 1;

  /* Work out the first pixel control info, which may point to the dummy entry if
   * the global palette/alpha need using initally */
  cur_pix_ctrl = chg_col->pix_ctrl_i;
  end_pix_ctrl = chg_col->pix_ctrl_i + chg_col->n_changes;

  if (cur_pix_ctrl->left != 0) {
    next_pix_ctrl = cur_pix_ctrl;
    cur_pix_ctrl = &dummy_pix_ctrl;
    for (i = 0; i < 4; i++)     /* Copy the main palette to our dummy entry */
      dummy_pix_ctrl.pal_cache[i] = state->vobsub.main_pal[i];
  } else {
    next_pix_ctrl = cur_pix_ctrl + 1;
  }
  if (next_pix_ctrl < end_pix_ctrl)
    cur_reg_end = next_pix_ctrl->left;
  else
    cur_reg_end = disp_end;

  /* Render stuff */
  while (x < disp_end) {
    rle_code = gstspu_vobsub_get_rle_code (state, rle_offset);
    next_x = rle_end_x (rle_code, x, disp_end);

    /* Now draw the run between [x,next_x), crossing palette regions as needed */
    while (x < next_x) {
      run_end = MIN (next_x, cur_reg_end);

      run_draw_end = run_end;
      if (run_draw_end > state->vobsub.clip_rect.right)
        run_draw_end = state->vobsub.clip_rect.right;   /* ensure no overflow */

      if (G_LIKELY (x < run_end)) {
        colour = &cur_pix_ctrl->pal_cache[rle_code & 3];
        visible |= gstspu_vobsub_draw_rle_run (state, x, run_draw_end, colour);
        x = run_end;
      }

      if (x >= cur_reg_end) {
        /* Advance to next region */
        cur_pix_ctrl = next_pix_ctrl;
        next_pix_ctrl++;

        if (next_pix_ctrl < end_pix_ctrl)
          cur_reg_end = next_pix_ctrl->left;
        else
          cur_reg_end = disp_end;
      }
    }
  }

  return visible;
}

static void
gstspu_vobsub_blend_comp_buffers (SpuState * state, guint8 * planes[3])
{
  state->comp_left = state->vobsub.disp_rect.left;
  state->comp_right =
      MAX (state->vobsub.comp_last_x[0], state->vobsub.comp_last_x[1]);

  state->comp_left = MAX (state->comp_left, state->vobsub.clip_rect.left);
  state->comp_right = MIN (state->comp_right, state->vobsub.clip_rect.right);

  gstspu_blend_comp_buffers (state, planes);
}

static void
gstspu_vobsub_clear_comp_buffers (SpuState * state)
{
  state->comp_left = state->vobsub.clip_rect.left;
  state->comp_right = state->vobsub.clip_rect.right;

  gstspu_clear_comp_buffers (state);

  state->vobsub.comp_last_x[0] = -1;
  state->vobsub.comp_last_x[1] = -1;
}

static void
gstspu_vobsub_draw_highlight (SpuState * state,
    GstVideoFrame * frame, SpuRect * rect)
{
  guint8 *cur;
  gint16 pos;
  gint ystride;

  ystride = GST_VIDEO_FRAME_COMP_STRIDE (frame, 0);

  cur = GST_VIDEO_FRAME_COMP_DATA (frame, 0) + ystride * rect->top;
  for (pos = rect->left + 1; pos < rect->right; pos++)
    cur[pos] = (cur[pos] / 2) + 0x8;
  cur = GST_VIDEO_FRAME_COMP_DATA (frame, 0) + ystride * rect->bottom;
  for (pos = rect->left + 1; pos < rect->right; pos++)
    cur[pos] = (cur[pos] / 2) + 0x8;
  cur = GST_VIDEO_FRAME_COMP_DATA (frame, 0) + ystride * rect->top;
  for (pos = rect->top; pos <= rect->bottom; pos++) {
    cur[rect->left] = (cur[rect->left] / 2) + 0x8;
    cur[rect->right] = (cur[rect->right] / 2) + 0x8;
    cur += ystride;
  }
}

void
gstspu_vobsub_render (GstDVDSpu * dvdspu, GstVideoFrame * frame)
{
  SpuState *state = &dvdspu->spu_state;
  guint8 *planes[3];            /* YUV frame pointers */
  gint y, last_y;
  gint width, height;
  gint strides[3];
  gint offset_index = 0;

  /* Set up our initial state */
  if (G_UNLIKELY (state->vobsub.pix_buf == NULL))
    return;

  if (!gst_buffer_map (state->vobsub.pix_buf, &state->vobsub.pix_buf_map,
          GST_MAP_READ))
    return;

  /* Store the start of each plane */
  planes[0] = GST_VIDEO_FRAME_COMP_DATA (frame, 0);
  planes[1] = GST_VIDEO_FRAME_COMP_DATA (frame, 1);
  planes[2] = GST_VIDEO_FRAME_COMP_DATA (frame, 2);

  strides[0] = GST_VIDEO_FRAME_COMP_STRIDE (frame, 0);
  strides[1] = GST_VIDEO_FRAME_COMP_STRIDE (frame, 1);
  strides[2] = GST_VIDEO_FRAME_COMP_STRIDE (frame, 2);

  width = GST_VIDEO_FRAME_WIDTH (frame);
  height = GST_VIDEO_FRAME_HEIGHT (frame);

  GST_DEBUG_OBJECT (dvdspu,
      "Rendering SPU. disp_rect %d,%d to %d,%d. hl_rect %d,%d to %d,%d",
      state->vobsub.disp_rect.left, state->vobsub.disp_rect.top,
      state->vobsub.disp_rect.right, state->vobsub.disp_rect.bottom,
      state->vobsub.hl_rect.left, state->vobsub.hl_rect.top,
      state->vobsub.hl_rect.right, state->vobsub.hl_rect.bottom);

  GST_DEBUG_OBJECT (dvdspu, "video size %d,%d", width, height);

  /* When reading RLE data, we track the offset in nibbles... */
  state->vobsub.cur_offsets[0] = state->vobsub.pix_data[0] * 2;
  state->vobsub.cur_offsets[1] = state->vobsub.pix_data[1] * 2;
  state->vobsub.max_offset = state->vobsub.pix_buf_map.size * 2;

  /* Update all the palette caches */
  gstspu_vobsub_update_palettes (dvdspu, state);

  /* Set up HL or Change Color & Contrast rect tracking */
  if (state->vobsub.hl_rect.top != -1) {
    state->vobsub.cur_chg_col = &state->vobsub.hl_ctrl_i;
    state->vobsub.cur_chg_col_end = state->vobsub.cur_chg_col + 1;
  } else if (state->vobsub.n_line_ctrl_i > 0) {
    state->vobsub.cur_chg_col = state->vobsub.line_ctrl_i;
    state->vobsub.cur_chg_col_end =
        state->vobsub.cur_chg_col + state->vobsub.n_line_ctrl_i;
  } else
    state->vobsub.cur_chg_col = NULL;

  state->vobsub.clip_rect.left = state->vobsub.disp_rect.left;
  state->vobsub.clip_rect.right = state->vobsub.disp_rect.right;

  /* center the image when display rectangle exceeds the video width */
  if (width <= state->vobsub.disp_rect.right) {
    gint left, disp_width;

    disp_width = state->vobsub.disp_rect.right - state->vobsub.disp_rect.left
        + 1;
    left = (width - disp_width) / 2;
    state->vobsub.disp_rect.left = left;
    state->vobsub.disp_rect.right = left + disp_width - 1;

    /* if it clips to the right, shift it left, but only till zero */
    if (state->vobsub.disp_rect.right >= width) {
      gint shift = state->vobsub.disp_rect.right - width - 1;
      if (shift > state->vobsub.disp_rect.left)
        shift = state->vobsub.disp_rect.left;
      state->vobsub.disp_rect.left -= shift;
      state->vobsub.disp_rect.right -= shift;
    }

    /* init clip to disp */
    state->vobsub.clip_rect.left = state->vobsub.disp_rect.left;
    state->vobsub.clip_rect.right = state->vobsub.disp_rect.right;

    /* clip right after the shift */
    if (state->vobsub.clip_rect.right >= width)
      state->vobsub.clip_rect.right = width - 1;

    GST_DEBUG_OBJECT (dvdspu,
        "clipping width to %d,%d", state->vobsub.clip_rect.left,
        state->vobsub.clip_rect.right);
  }

  /* for the height, bring it up till it fits as well as it can. We
   * assume the picture is in the lower part. We should better check where it
   * is and do something more clever. */
  state->vobsub.clip_rect.top = state->vobsub.disp_rect.top;
  state->vobsub.clip_rect.bottom = state->vobsub.disp_rect.bottom;
  if (height <= state->vobsub.disp_rect.bottom) {

    /* shift it up, but only till zero */
    gint shift = state->vobsub.disp_rect.bottom - height - 1;
    if (shift > state->vobsub.disp_rect.top)
      shift = state->vobsub.disp_rect.top;
    state->vobsub.disp_rect.top -= shift;
    state->vobsub.disp_rect.bottom -= shift;

    /* start on even line */
    if (state->vobsub.disp_rect.top & 1) {
      state->vobsub.disp_rect.top--;
      state->vobsub.disp_rect.bottom--;
    }

    /* init clip to disp */
    state->vobsub.clip_rect.top = state->vobsub.disp_rect.top;
    state->vobsub.clip_rect.bottom = state->vobsub.disp_rect.bottom;

    /* clip bottom after the shift */
    if (state->vobsub.clip_rect.bottom >= height)
      state->vobsub.clip_rect.bottom = height - 1;

    GST_DEBUG_OBJECT (dvdspu,
        "clipping height to %d,%d", state->vobsub.clip_rect.top,
        state->vobsub.clip_rect.bottom);
  }

  /* We start rendering from the first line of the display rect */
  y = state->vobsub.disp_rect.top;
  /* We render most lines in pairs starting from an even y,
   * accumulating 2 lines of chroma then blending it. We might need to render a
   * single line at the start and end if the display rect starts on an odd line
   * or ends on an even one */
  if (y > state->vobsub.disp_rect.bottom)
    return;                     /* Empty clip rect, nothing to do */

  /* Update our plane references to the first line of the disp_rect */
  planes[0] += strides[0] * y;
  planes[1] += strides[1] * (y / 2);
  planes[2] += strides[2] * (y / 2);

  /* If the render rect starts on an odd line, render that only to start */
  state->vobsub.cur_Y = y;
  if (state->vobsub.cur_Y & 0x1) {
    gboolean clip, visible = FALSE;

    clip = (state->vobsub.cur_Y < state->vobsub.clip_rect.top
        || state->vobsub.cur_Y > state->vobsub.clip_rect.bottom);

    if (!clip) {
      /* Render a first odd line. */
      gstspu_vobsub_clear_comp_buffers (state);
      state->vobsub.comp_last_x_ptr = state->vobsub.comp_last_x + 1;
      visible |=
          gstspu_vobsub_render_line (state, planes,
          &state->vobsub.cur_offsets[offset_index]);
      if (visible)
        gstspu_vobsub_blend_comp_buffers (state, planes);
    }

    /* Update all the output pointers */
    state->vobsub.cur_Y++;
    planes[0] += strides[0];
    planes[1] += strides[1];
    planes[2] += strides[2];
    /* Switch the offset index 0 <=> 1 */
    offset_index ^= 0x1;
  }

  last_y = (state->vobsub.disp_rect.bottom - 1) & ~(0x01);
  for (; state->vobsub.cur_Y <= last_y; state->vobsub.cur_Y++) {
    gboolean clip, visible = FALSE;

    clip = (state->vobsub.cur_Y < state->vobsub.clip_rect.top
        || state->vobsub.cur_Y > state->vobsub.clip_rect.bottom);

    /* Reset the compositing buffer */
    gstspu_vobsub_clear_comp_buffers (state);
    /* Render even line */
    state->vobsub.comp_last_x_ptr = state->vobsub.comp_last_x;
    gstspu_vobsub_render_line (state, planes,
        &state->vobsub.cur_offsets[offset_index]);

    /* Advance the luminance output pointer */
    planes[0] += strides[0];
    /* Switch the offset index 0 <=> 1 */
    offset_index ^= 0x1;

    state->vobsub.cur_Y++;

    /* Render odd line */
    state->vobsub.comp_last_x_ptr = state->vobsub.comp_last_x + 1;
    visible |=
        gstspu_vobsub_render_line (state, planes,
        &state->vobsub.cur_offsets[offset_index]);

    if (visible && !clip) {
      /* Blend the accumulated UV compositing buffers onto the output */
      gstspu_vobsub_blend_comp_buffers (state, planes);
    }

    /* Update all the output pointers */
    planes[0] += strides[0];
    planes[1] += strides[1];
    planes[2] += strides[2];
    /* Switch the offset index 0 <=> 1 */
    offset_index ^= 0x1;
  }

  if (state->vobsub.cur_Y == state->vobsub.disp_rect.bottom) {
    gboolean clip, visible = FALSE;

    clip = (state->vobsub.cur_Y < state->vobsub.clip_rect.top
        || state->vobsub.cur_Y > state->vobsub.clip_rect.bottom);

    g_return_if_fail ((state->vobsub.disp_rect.bottom & 0x01) == 0);

    if (!clip) {
      /* Render a remaining lone last even line. y already has the correct value
       * after the above loop exited. */
      gstspu_vobsub_clear_comp_buffers (state);
      state->vobsub.comp_last_x_ptr = state->vobsub.comp_last_x;
      visible |=
          gstspu_vobsub_render_line (state, planes,
          &state->vobsub.cur_offsets[offset_index]);
      if (visible)
        gstspu_vobsub_blend_comp_buffers (state, planes);
    }
  }

  /* for debugging purposes, draw a faint rectangle at the edges of the disp_rect */
  if ((dvdspu_debug_flags & GST_DVD_SPU_DEBUG_RENDER_RECTANGLE) != 0) {
    gstspu_vobsub_draw_highlight (state, frame, &state->vobsub.disp_rect);
  }
  /* For debugging purposes, draw a faint rectangle around the highlight rect */
  if ((dvdspu_debug_flags & GST_DVD_SPU_DEBUG_HIGHLIGHT_RECTANGLE) != 0
      && state->vobsub.hl_rect.top != -1) {
    gstspu_vobsub_draw_highlight (state, frame, &state->vobsub.hl_rect);
  }

  gst_buffer_unmap (state->vobsub.pix_buf, &state->vobsub.pix_buf_map);
}