// SPDX-License-Identifier: LGPL-2.1-or-later // vpx_alpha.c : pseudo-decoder for VP8/VP9 streams with alpha side-channel // Copyright © 2023 VideoLabs, VLC authors and VideoLAN // Authors: Steve Lhomme // Alexandre Janniaux #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #include "alpha_combine.h" static int OpenDecoder(vlc_object_t *); static void CloseDecoder(vlc_object_t *); vlc_module_begin () set_description(N_("VPx+alpha video decoder")) set_capability("video decoder", 150) set_callbacks(OpenDecoder, CloseDecoder) set_subcategory(SUBCAT_INPUT_VCODEC) vlc_module_end () struct vp_decoder { decoder_t dec; es_format_t fmt_in; es_format_t fmt_out; vlc_picture_chain_t decoded; }; typedef struct { struct vp_decoder *opaque; struct vp_decoder *alpha; vlc_mutex_t lock; vlc_video_context *vctx; picture_t *(*pf_combine)(decoder_t *, picture_t *opaque, picture_t *alpha, vlc_video_context *); struct VLC_VECTOR(vlc_tick_t) missing_alpha; picture_pool_t *pool; } vpx_alpha; static vlc_decoder_device *GetDevice( decoder_t *dec ) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); vpx_alpha *p_sys = bdec->p_sys; vlc_mutex_lock(&p_sys->lock); vlc_decoder_device *res = decoder_GetDecoderDevice(bdec); vlc_mutex_unlock(&p_sys->lock); return res; } struct cpu_alpha_context { vlc_atomic_rc_t rc; picture_context_t ctx; picture_t *opaque; picture_t *alpha; // may be NULL if the alpha layer was missing struct pic_alpha_plane *plane; }; static void cpu_alpha_destroy(picture_context_t *ctx) { struct cpu_alpha_context *pctx = container_of(ctx, struct cpu_alpha_context, ctx); if (!vlc_atomic_rc_dec(&pctx->rc)) return; free(pctx->plane); picture_Release(pctx->opaque); if (pctx->alpha) picture_Release(pctx->alpha); free(pctx); } static picture_context_t *cpu_alpha_copy(picture_context_t *src) { struct cpu_alpha_context *pctx = container_of(src, struct cpu_alpha_context, ctx); vlc_atomic_rc_inc(&pctx->rc); return &pctx->ctx; } struct pic_alpha_plane { plane_t p; uint8_t buffer[]; }; static void DestroyPoolPic(picture_t *pic) { free(pic->p_sys); } static picture_t *CombinePicturesCPU(decoder_t *bdec, picture_t *opaque, picture_t *alpha, vlc_video_context *vctx) { assert(vctx == NULL); VLC_UNUSED(vctx); vpx_alpha *p_sys = bdec->p_sys; picture_t *out = picture_pool_Wait(p_sys->pool); if (out == NULL) return NULL; struct cpu_alpha_context *alpha_ctx = calloc(1, sizeof(*alpha_ctx)); if (unlikely(alpha_ctx == NULL)) { picture_Release(out); return NULL; } vlc_atomic_rc_init(&alpha_ctx->rc); alpha_ctx->ctx = (picture_context_t) { cpu_alpha_destroy, cpu_alpha_copy, NULL }; alpha_ctx->opaque = picture_Hold(opaque); alpha_ctx->alpha = alpha ? picture_Hold(alpha) : NULL; out->context = &alpha_ctx->ctx; for (int i=0; ii_planes; i++) out->p[i] = opaque->p[i]; if (alpha) { out->p[opaque->i_planes] = alpha->p[0]; return out; } // use the dummy opaque plane attached in the picture context struct pic_alpha_plane *p = alpha_ctx->plane; if (p == NULL) { int plane_size = bdec->fmt_out.video.i_width * bdec->fmt_out.video.i_height; p = malloc(sizeof(*p) + plane_size); if (unlikely(p == NULL)) { picture_Release(out); return NULL; } p->p.i_lines = bdec->fmt_out.video.i_height; p->p.i_visible_lines = bdec->fmt_out.video.i_y_offset + bdec->fmt_out.video.i_visible_height; p->p.i_pitch = bdec->fmt_out.video.i_width; p->p.i_visible_pitch = bdec->fmt_out.video.i_x_offset + bdec->fmt_out.video.i_visible_width; p->p.i_pixel_pitch = 1; p->p.p_pixels = p->buffer; memset(p->p.p_pixels, 0xFF, plane_size); alpha_ctx->plane = p; } out->p[opaque->i_planes] = p->p; return out; } static int SetupCPU(decoder_t *bdec) { vpx_alpha *p_sys = bdec->p_sys; picture_t *pics[4]; size_t i=0; if (p_sys->pool) { picture_pool_Release(p_sys->pool); p_sys->pool = NULL; } for (; ifmt_out.video, &res); if (pics[i] == NULL) goto error; } p_sys->pool = picture_pool_New(ARRAY_SIZE(pics), pics); if (p_sys->pool) return VLC_SUCCESS; error: while (i-- > 0) picture_Release(pics[i]); return VLC_EGENERIC; } static picture_t *CombineKeepAlpha(decoder_t *bdec, picture_t *opaque, picture_t *alpha, vlc_video_context *vctx) { VLC_UNUSED(bdec); VLC_UNUSED(opaque); VLC_UNUSED(vctx); return picture_Hold(alpha); } static picture_t *CombineKeepOpaque(decoder_t *bdec, picture_t *opaque, picture_t *alpha, vlc_video_context *vctx) { VLC_UNUSED(bdec); VLC_UNUSED(alpha); VLC_UNUSED(vctx); return picture_Hold(opaque); } static int FormatUpdate( decoder_t *dec, vlc_video_context *vctx ) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); vpx_alpha *p_sys = bdec->p_sys; vlc_mutex_lock(&p_sys->lock); int res = VLC_SUCCESS; if (dec == &p_sys->alpha->dec) { if (es_format_IsSimilar(&p_sys->alpha->fmt_out, &dec->fmt_out)) // nothing changed goto done; es_format_Clean(&p_sys->alpha->fmt_out); es_format_Copy(&p_sys->alpha->fmt_out, &dec->fmt_out); if (p_sys->opaque->dec.fmt_out.video.i_chroma == 0) { // not ready bdec->fmt_out.video.i_chroma = bdec->fmt_out.i_codec = dec->fmt_out.video.i_chroma; p_sys->pf_combine = CombineKeepAlpha; goto done; } } else { if (es_format_IsSimilar(&p_sys->opaque->fmt_out, &dec->fmt_out)) // nothing changed goto done; es_format_Clean(&p_sys->opaque->fmt_out); es_format_Copy(&p_sys->opaque->fmt_out, &dec->fmt_out); if (p_sys->alpha->dec.fmt_out.video.i_chroma == 0) { // not ready bdec->fmt_out.video.i_chroma = bdec->fmt_out.i_codec = dec->fmt_out.video.i_chroma; p_sys->pf_combine = CombineKeepOpaque; if (p_sys->missing_alpha.size == 0) goto done; // we need to send pictures without waiting for the alpha } } es_format_Clean(&bdec->fmt_out); es_format_Copy(&bdec->fmt_out, &dec->fmt_out); switch (dec->fmt_out.video.i_chroma) { case VLC_CODEC_I420: // TODO support other formats bdec->fmt_out.video.i_chroma = bdec->fmt_out.i_codec = VLC_CODEC_YUV420A; res = SetupCPU(bdec); if (res == VLC_SUCCESS) { p_sys->pf_combine = CombinePicturesCPU; } break; #ifdef _WIN32 case VLC_CODEC_D3D11_OPAQUE: if (dec == &p_sys->alpha->dec) { switch (p_sys->opaque->dec.fmt_out.video.i_chroma) { case VLC_CODEC_D3D11_OPAQUE: res = SetupD3D11(bdec, vctx, &p_sys->vctx); if (res == VLC_SUCCESS) { p_sys->pf_combine = CombineD3D11; vctx = p_sys->vctx; } break; default: msg_Err(dec, "unsupported opaque D3D11 combination %4.4s", (char*)&p_sys->opaque->dec.fmt_out.video.i_chroma); res = VLC_EGENERIC; } } else { switch (p_sys->alpha->dec.fmt_out.video.i_chroma) { case VLC_CODEC_D3D11_OPAQUE: res = SetupD3D11(bdec, vctx, &p_sys->vctx); if (res == VLC_SUCCESS) { p_sys->pf_combine = CombineD3D11; vctx = p_sys->vctx; } break; default: msg_Err(dec, "unsupported opaque D3D11 combination %4.4s", (char*)&p_sys->alpha->dec.fmt_out.video.i_chroma); res = VLC_EGENERIC; } } break; #endif // _WIN32 default: msg_Warn(dec, "unsupported decoder output %4.4s, not handling alpha", (char*)&dec->fmt_out.video.i_chroma); break; } if (res == VLC_SUCCESS) res = decoder_UpdateVideoOutput(bdec, vctx); done: vlc_mutex_unlock(&p_sys->lock); return res; } static bool CheckMissingAlpha(decoder_t *bdec, vlc_tick_t pts) { vpx_alpha *p_sys = bdec->p_sys; vlc_tick_t missing_pts; vlc_vector_foreach(missing_pts, &p_sys->missing_alpha) { if (missing_pts == pts) return true; if (missing_pts < pts) break; } return false; } static void PurgeMissingAlpha(decoder_t *bdec, vlc_tick_t pts) { vpx_alpha *p_sys = bdec->p_sys; size_t count = 0; vlc_tick_t missing_pts; vlc_vector_foreach(missing_pts, &p_sys->missing_alpha) { if (missing_pts > pts) break; // in VPx there are not frames out of order count++; } if (count > 0) vlc_vector_remove_slice(&p_sys->missing_alpha, 0, count); } static bool SendMergedLocked(decoder_t *bdec) { vpx_alpha *p_sys = bdec->p_sys; picture_t *opaque = vlc_picture_chain_PeekFront(&p_sys->opaque->decoded); picture_t *alpha = vlc_picture_chain_PeekFront(&p_sys->alpha->decoded); while (opaque != NULL && (alpha != NULL || CheckMissingAlpha(bdec, opaque->date))) { if (alpha == NULL || opaque->date == alpha->date) { // dequeue if both first of the queue match DTS/PTS // merge alpha and opaque pictures with same DTS/PTS and send them picture_t *out = p_sys->pf_combine(bdec, opaque, alpha, p_sys->vctx); if (out != NULL) { video_format_CopyCropAr(&out->format, &opaque->format); picture_CopyProperties(out, opaque); } vlc_picture_chain_PopFront(&p_sys->opaque->decoded); picture_Release(opaque); if (alpha != NULL) { vlc_picture_chain_PopFront(&p_sys->alpha->decoded); picture_Release(alpha); } PurgeMissingAlpha(bdec, opaque->date); if (out == NULL) break; decoder_QueueVideo(bdec, out); return true; } // in case decoders drop some frames if (opaque->date > alpha->date) { msg_Dbg(bdec, "missing decoded opaque at %" PRId64 " dropping alpha", alpha->date); vlc_picture_chain_PopFront(&p_sys->alpha->decoded); picture_Release(alpha); } else { msg_Dbg(bdec, "missing decoded alpha at %" PRId64 " dropping opaque", opaque->date); vlc_picture_chain_PopFront(&p_sys->opaque->decoded); picture_Release(opaque); } opaque = vlc_picture_chain_PeekFront(&p_sys->opaque->decoded); alpha = vlc_picture_chain_PeekFront(&p_sys->alpha->decoded); } return false; } static void QueuePic( decoder_t *dec, picture_t *pic ) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); vpx_alpha *p_sys = bdec->p_sys; vlc_mutex_lock(&p_sys->lock); if (dec == &p_sys->alpha->dec) { vlc_picture_chain_Append(&p_sys->alpha->decoded, pic); } else { vlc_picture_chain_Append(&p_sys->opaque->decoded, pic); } SendMergedLocked(bdec); vlc_mutex_unlock(&p_sys->lock); } static vlc_tick_t GetDisplayDate( decoder_t *dec, vlc_tick_t sys_now, vlc_tick_t ts) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); return decoder_GetDisplayDate(bdec, sys_now, ts); } static float GetDisplayRate( decoder_t *dec ) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); return decoder_GetDisplayRate(bdec); } static int GetAttachments( decoder_t *dec, input_attachment_t ***ppp_attachment, int *pi_attachment ) { decoder_t *bdec = container_of(vlc_object_parent(dec), decoder_t, obj); return decoder_GetInputAttachments(bdec, ppp_attachment, pi_attachment); } struct alpha_frame { vlc_atomic_rc_t rc; vlc_frame_t *frame; // source frame vlc_frame_t opaque; // opaque bitstream vlc_frame_t alpha; // alpha bitstream }; static void ReleaseAlphaFrame(vlc_frame_t *frame) { struct alpha_frame *alpha_frame = container_of(frame, struct alpha_frame, alpha); if (vlc_atomic_rc_dec(&alpha_frame->rc)) { vlc_frame_Release(alpha_frame->frame); free(alpha_frame); } } static void ReleaseOpaqueFrame(vlc_frame_t *frame) { struct alpha_frame *alpha_frame = container_of(frame, struct alpha_frame, opaque); if (vlc_atomic_rc_dec(&alpha_frame->rc)) { vlc_frame_Release(alpha_frame->frame); free(alpha_frame); } } static int Decode( decoder_t *dec, vlc_frame_t *frame ) { vpx_alpha *p_sys = dec->p_sys; int res; if (frame != NULL) { struct vlc_ancillary *p_alpha; p_alpha = vlc_frame_GetAncillary(frame, VLC_ANCILLARY_ID_VPX_ALPHA); struct alpha_frame *alpha_frame = malloc(sizeof(*alpha_frame)); if (unlikely(alpha_frame == NULL)) return VLCDEC_ECRITICAL; vlc_vpx_alpha_t *alpha = p_alpha ? vlc_ancillary_GetData(p_alpha) : NULL; static const struct vlc_frame_callbacks cbs_alpha = { ReleaseAlphaFrame, }; static const struct vlc_frame_callbacks cbs_opaque = { ReleaseOpaqueFrame, }; alpha_frame->frame = frame; vlc_atomic_rc_init(&alpha_frame->rc); bool b_has_alpha = alpha != NULL; if (b_has_alpha) { vlc_frame_Init(&alpha_frame->alpha, &cbs_alpha, alpha->data, alpha->size); vlc_atomic_rc_inc(&alpha_frame->rc); alpha_frame->alpha.i_dts = frame->i_dts; alpha_frame->alpha.i_pts = frame->i_pts; alpha_frame->alpha.i_length = frame->i_length; alpha_frame->alpha.i_flags = frame->i_flags; } vlc_frame_Init(&alpha_frame->opaque, &cbs_opaque, frame->p_buffer, frame->i_buffer); alpha_frame->opaque.i_dts = frame->i_dts; alpha_frame->opaque.i_pts = frame->i_pts; alpha_frame->opaque.i_length = frame->i_length; alpha_frame->opaque.i_flags = frame->i_flags; if (b_has_alpha) { res = p_sys->alpha->dec.pf_decode(&p_sys->alpha->dec, &alpha_frame->alpha); if (res != VLCDEC_SUCCESS) { ReleaseAlphaFrame(&alpha_frame->alpha); return VLCDEC_ECRITICAL; } } else { assert(frame->i_pts != VLC_TICK_INVALID); vlc_vector_push(&p_sys->missing_alpha, frame->i_pts); } res = p_sys->opaque->dec.pf_decode(&p_sys->opaque->dec, &alpha_frame->opaque); if (res != VLCDEC_SUCCESS) { ReleaseOpaqueFrame(&alpha_frame->opaque); return VLCDEC_ECRITICAL; } } else { // drain vlc_mutex_lock(&p_sys->lock); while ( !vlc_picture_chain_IsEmpty(&p_sys->opaque->decoded) && !vlc_picture_chain_IsEmpty(&p_sys->alpha->decoded) ) SendMergedLocked(dec); // drain remaining pushed pictures from one decoder picture_t *picture; while ((picture = vlc_picture_chain_PopFront(&p_sys->alpha->decoded)) != NULL) picture_Release(picture); while ((picture = vlc_picture_chain_PopFront(&p_sys->opaque->decoded)) != NULL) picture_Release(picture); vlc_mutex_unlock(&p_sys->lock); } return VLCDEC_SUCCESS; } static void Flush( decoder_t *dec ) { vpx_alpha *p_sys = dec->p_sys; if ( p_sys->opaque->dec.pf_flush != NULL ) p_sys->opaque->dec.pf_flush( &p_sys->opaque->dec ); if ( p_sys->alpha->dec.pf_flush != NULL ) p_sys->alpha->dec.pf_flush( &p_sys->alpha->dec ); vlc_mutex_lock(&p_sys->lock); picture_t *picture; while ((picture = vlc_picture_chain_PopFront(&p_sys->opaque->decoded)) != NULL) picture_Release(picture); while ((picture = vlc_picture_chain_PopFront(&p_sys->alpha->decoded)) != NULL) picture_Release(picture); vlc_mutex_unlock(&p_sys->lock); } int OpenDecoder(vlc_object_t *o) { decoder_t *dec = container_of(o, decoder_t, obj); if (dec->fmt_in->i_codec != VLC_CODEC_VP8 && dec->fmt_in->i_codec != VLC_CODEC_VP9) return VLC_ENOTSUP; if (!es_format_HasVpxAlpha(dec->fmt_in)) return VLC_ENOTSUP; vpx_alpha *p_sys = vlc_obj_calloc(o, 1, sizeof(*p_sys)); if (unlikely(p_sys == NULL)) return VLC_ENOMEM; p_sys->opaque = vlc_object_create( o, sizeof( *p_sys->opaque ) ); if (unlikely(p_sys->opaque == NULL)) return VLC_EGENERIC; p_sys->alpha = vlc_object_create( o, sizeof( *p_sys->alpha ) ); if (unlikely(p_sys->alpha == NULL)) { vlc_object_delete(&p_sys->opaque->dec); return VLC_EGENERIC; } es_format_t fmt; es_format_Copy(&fmt, dec->fmt_in); if (dec->fmt_in->i_codec == VLC_CODEC_VP8) fmt.i_codec = VLC_CODEC_VP8; else fmt.i_codec = VLC_CODEC_VP9; fmt.i_level &= ~0x1000; decoder_Init( &p_sys->opaque->dec, &p_sys->opaque->fmt_in, &fmt ); vlc_picture_chain_Init(&p_sys->opaque->decoded); es_format_Init(&p_sys->opaque->fmt_out, VIDEO_ES, 0); if (dec->fmt_in->i_codec == VLC_CODEC_VP8) fmt.i_codec = VLC_CODEC_VP8ALPHA_ES; else fmt.i_codec = VLC_CODEC_VP9ALPHA_ES; decoder_Init( &p_sys->alpha->dec, &p_sys->alpha->fmt_in, &fmt ); vlc_picture_chain_Init(&p_sys->alpha->decoded); es_format_Init(&p_sys->alpha->fmt_out, VIDEO_ES, 0); es_format_Clean(&fmt); vlc_mutex_init(&p_sys->lock); vlc_vector_init(&p_sys->missing_alpha); dec->p_sys = p_sys; static const struct decoder_owner_callbacks dec_cbs = { .video = { .get_device = GetDevice, .format_update = FormatUpdate, .queue = QueuePic, .get_display_date = GetDisplayDate, .get_display_rate = GetDisplayRate, }, .get_attachments = GetAttachments, }; p_sys->opaque->dec.cbs = &dec_cbs; decoder_LoadModule(&p_sys->opaque->dec, false, true); if (p_sys->opaque->dec.p_module == NULL) { decoder_Destroy(&p_sys->alpha->dec); decoder_Destroy(&p_sys->opaque->dec); return VLC_EGENERIC; } p_sys->alpha->dec.cbs = &dec_cbs; decoder_LoadModule(&p_sys->alpha->dec, false, true); if (p_sys->alpha->dec.p_module == NULL) { decoder_Destroy(&p_sys->alpha->dec); decoder_Destroy(&p_sys->opaque->dec); return VLC_EGENERIC; } dec->pf_decode = Decode; dec->pf_flush = Flush; return VLC_SUCCESS; } void CloseDecoder(vlc_object_t *o) { decoder_t *dec = container_of(o, decoder_t, obj); vpx_alpha *p_sys = dec->p_sys; es_format_Clean(&p_sys->opaque->fmt_out); es_format_Clean(&p_sys->opaque->fmt_in); decoder_Destroy(&p_sys->opaque->dec); es_format_Clean(&p_sys->alpha->fmt_out); es_format_Clean(&p_sys->alpha->fmt_in); decoder_Destroy(&p_sys->alpha->dec); if (p_sys->pool) picture_pool_Release(p_sys->pool); if (p_sys->vctx) vlc_video_context_Release(p_sys->vctx); vlc_vector_destroy(&p_sys->missing_alpha); }