/* GStreamer DVD Sub-Picture Unit * Copyright (C) 2007 Fluendo S.A. * Copyright (C) 2009 Jan Schmidt * * 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 #endif #include #include #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 initially */ 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); }