From fa1091b4774f51e71dbe7fcbdcbaf414cd95dd5e Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 8 Jun 2026 22:29:49 +0530 Subject: [PATCH 1/5] f_auto_filters: destroy deint filter immediately on format change When the image format changes (e.g. switching hwdec), immediately destroy the existing deinterlace filter instead of draining it. Draining feeds new frames through the old filter, which can cause issues. (e.g. bwdif_vulkan receiving vaapi frames). This fixes userdeint filter failure when switching from hwdec=vulkan to hwdec=vaapi while the filter is applied via --deinterlace=auto/yes --- filters/f_auto_filters.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/filters/f_auto_filters.c b/filters/f_auto_filters.c index 6040d6be3add1..c247728e5acad 100644 --- a/filters/f_auto_filters.c +++ b/filters/f_auto_filters.c @@ -63,12 +63,15 @@ static void deint_process(struct mp_filter *f) bool filter_needed = opts->deinterlace == 1 || (opts->deinterlace == -1 && (p->interlaced_frame || p->sub.filter)); - // If the image format changed, or if we no longer need a filter, - // destroy any existing filter. - if (img->imgfmt != p->prev_imgfmt || (p->sub.filter && !filter_needed)) { + // If the image format changed, destroy any existing filter immediately since + // it may not support the new format. If we no longer need a filter, drain + // and destroy it gracefully. + if (img->imgfmt != p->prev_imgfmt) { + mp_subfilter_destroy(&p->sub); + p->prev_imgfmt = img->imgfmt; + } else if (p->sub.filter && !filter_needed) { if (!mp_subfilter_drain_destroy(&p->sub)) return; - p->prev_imgfmt = img->imgfmt; } // If no filter is needed or if the filter is already inserted and we reach From 9afe5543b13cf0e4c2500a7c189ad31587a6acdb Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:40:16 +0530 Subject: [PATCH 2/5] mp_image: add field_tick field to mp_image Used in next commits --- video/mp_image.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/video/mp_image.h b/video/mp_image.h index 5fe523dd64e96..6ad78091e39a5 100644 --- a/video/mp_image.h +++ b/video/mp_image.h @@ -38,6 +38,12 @@ #define MP_IMGFIELD_REPEAT_FIRST 0x04 #define MP_IMGFIELD_INTERLACED 0x20 +enum mp_image_field_tick { + MP_FIELD_TICK_NONE = 0, + MP_FIELD_TICK_FIRST, + MP_FIELD_TICK_SECOND, +}; + // Describes image parameters that usually stay constant. // New fields can be added in the future. Code changing the parameters should // usually copy the whole struct, so that fields added later will be preserved. @@ -97,6 +103,8 @@ typedef struct mp_image { int pict_type; // 0->unknown, 1->I, 2->P, 3->B int fields; + enum mp_image_field_tick field_tick; + /* only inside filter chain */ double pts; /* only after decoder */ From 2d4eac6f1c2cdd26777911c91fc89547d8998074 Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:41:28 +0530 Subject: [PATCH 3/5] vf_fieldrate: add this filter This is used to emit frame per field, so the VO is asked to draw both fields. --- filters/user_filters.c | 1 + filters/user_filters.h | 1 + meson.build | 1 + video/filter/vf_fieldrate.c | 107 ++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 video/filter/vf_fieldrate.c diff --git a/filters/user_filters.c b/filters/user_filters.c index 7bdda6965369f..49b674111f6ff 100644 --- a/filters/user_filters.c +++ b/filters/user_filters.c @@ -106,6 +106,7 @@ const struct mp_user_filter_entry *vf_list[] = { #if (HAVE_GL && HAVE_EGL) || HAVE_VULKAN &vf_gpu, #endif + &vf_fieldrate, }; static bool get_vf_desc(struct m_obj_desc *dst, int index) diff --git a/filters/user_filters.h b/filters/user_filters.h index aa88649b33dec..8698ba2c1a099 100644 --- a/filters/user_filters.h +++ b/filters/user_filters.h @@ -38,3 +38,4 @@ extern const struct mp_user_filter_entry vf_d3d11vpp; extern const struct mp_user_filter_entry vf_amf_frc; extern const struct mp_user_filter_entry vf_fingerprint; extern const struct mp_user_filter_entry vf_gpu; +extern const struct mp_user_filter_entry vf_fieldrate; diff --git a/meson.build b/meson.build index 1cd2570f0d7ef..1cd90648e64a7 100644 --- a/meson.build +++ b/meson.build @@ -217,6 +217,7 @@ sources = files( 'video/filter/refqueue.c', 'video/filter/vf_format.c', 'video/filter/vf_sub.c', + 'video/filter/vf_fieldrate.c', 'video/fmt-conversion.c', 'video/hwdec.c', 'video/image_loader.c', diff --git a/video/filter/vf_fieldrate.c b/video/filter/vf_fieldrate.c new file mode 100644 index 0000000000000..d2d66b30d5c54 --- /dev/null +++ b/video/filter/vf_fieldrate.c @@ -0,0 +1,107 @@ +#include "filters/filter_internal.h" +#include "filters/user_filters.h" +#include "refqueue.h" + +struct opts { + int field_parity; + bool interlaced_only; +}; + +struct priv { + struct opts *opts; + struct mp_refqueue *queue; +}; + +static void vf_field_process(struct mp_filter *f) +{ + struct priv *p = f->priv; + mp_refqueue_execute_reinit(p->queue); + + if (!mp_refqueue_can_output(p->queue)) + return; + + struct mp_image *in = mp_refqueue_get(p->queue, 0); + struct mp_image *out = mp_image_new_ref(in); + if (!out) { + mp_refqueue_write_out_pin(p->queue, NULL); + return; + } + // This filter does not deinterlace. It only emits one output per field so + // the VO gets called at fieldrate cadence. + if (mp_refqueue_should_deint(p->queue)) { + out->field_tick = mp_refqueue_is_second_field(p->queue) ? + MP_FIELD_TICK_SECOND : MP_FIELD_TICK_FIRST; + out->fields |= MP_IMGFIELD_INTERLACED; + if (mp_refqueue_top_field_first(p->queue)) { + out->fields |= MP_IMGFIELD_TOP_FIRST; + } else { + out->fields &= ~MP_IMGFIELD_TOP_FIRST; + } + } else { + out->field_tick = MP_FIELD_TICK_NONE; + } + mp_refqueue_write_out_pin(p->queue, out); +} + +static void vf_field_reset(struct mp_filter *f) +{ + struct priv *p = f->priv; + mp_refqueue_flush(p->queue); +} + +static void vf_field_destroy(struct mp_filter *f) +{ + struct priv *p = f->priv; + mp_refqueue_flush(p->queue); + talloc_free(p->queue); +} + +static const struct mp_filter_info vf_field_filter = { + .name = "fieldrate", + .process = vf_field_process, + .reset = vf_field_reset, + .destroy = vf_field_destroy, + .priv_size = sizeof(struct priv), +}; + +static struct mp_filter *vf_field_create(struct mp_filter *parent, void *options) +{ + struct mp_filter *f = mp_filter_create(parent, &vf_field_filter); + if (!f) + return NULL; + struct priv *p = f->priv; + p->opts = talloc_steal(p, options); + + mp_filter_add_pin(f, MP_PIN_IN, "in"); + mp_filter_add_pin(f, MP_PIN_OUT, "out"); + + p->queue = mp_refqueue_alloc(f); + + mp_refqueue_set_refs(p->queue, 0, 0); + mp_refqueue_set_mode(p->queue, + MP_MODE_DEINT | + MP_MODE_OUTPUT_FIELDS | + (p->opts->interlaced_only ? MP_MODE_INTERLACED_ONLY : 0)); + mp_refqueue_set_parity(p->queue, p->opts->field_parity); + + return f; +} +#define OPT_BASE_STRUCT struct opts +static const m_option_t vf_opts_fields[] = { + {"interlaced-only", OPT_BOOL(interlaced_only)}, + {"parity", OPT_CHOICE(field_parity, + {"tff", MP_FIELD_PARITY_TFF}, + {"bff", MP_FIELD_PARITY_BFF}, + {"auto", MP_FIELD_PARITY_AUTO})}, + {0} +}; + +const struct mp_user_filter_entry vf_fieldrate = { + .desc = { + .description = "Emit one frame per field", + .name = "fieldrate", + .priv_size = sizeof(OPT_BASE_STRUCT), + .options = vf_opts_fields, + }, + .create = vf_field_create, +}; From eb14dcd272e6dd1cce7f5166b3b3e09b75e4355c Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:43:05 +0530 Subject: [PATCH 4/5] vo: add VO_CAP_DEINTERLACE This is used to signal when the VO can do deinterlacing itself, instead of using a filter --- filters/f_output_chain.c | 1 + filters/filter.h | 1 + video/out/vo.h | 2 ++ 3 files changed, 4 insertions(+) diff --git a/filters/f_output_chain.c b/filters/f_output_chain.c index 8ded23177da84..7c9ab99acf55c 100644 --- a/filters/f_output_chain.c +++ b/filters/f_output_chain.c @@ -387,6 +387,7 @@ void mp_output_chain_set_vo(struct mp_output_chain *c, struct vo *vo) p->stream_info.osd = vo ? vo->osd : NULL; p->stream_info.vflip = vo ? vo->driver->caps & VO_CAP_VFLIP : false; p->stream_info.rotate90 = vo ? vo->driver->caps & VO_CAP_ROTATE90 : false; + p->stream_info.deinterlace = vo ? vo->driver->caps & VO_CAP_DEINTERLACE : false; p->stream_info.dr_vo = vo; p->vo = vo; update_output_caps(p); diff --git a/filters/filter.h b/filters/filter.h index 5bf1e48a84958..334c46e64a024 100644 --- a/filters/filter.h +++ b/filters/filter.h @@ -408,6 +408,7 @@ struct mp_stream_info { bool vflip; bool rotate90; bool force_swdec; + bool deinterlace; struct vo *dr_vo; // for calling vo_get_image() }; diff --git a/video/out/vo.h b/video/out/vo.h index a98b0f9496853..89afa9e6731b9 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -205,6 +205,8 @@ enum { VO_CAP_FRAMEOWNER = 1 << 5, // VO does handle mp_image_params.vflip VO_CAP_VFLIP = 1 << 6, + // VO supports deinterlacing + VO_CAP_DEINTERLACE = 1 << 7, }; enum { From 09c39d9f2ee511318fd98829394bce0ae2cd910b Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 18:02:54 +0530 Subject: [PATCH 5/5] vo_gpu_next: use pl bwdif shader instead of software bwdif --- filters/f_auto_filters.c | 6 ++++++ video/out/vo_gpu_next.c | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/filters/f_auto_filters.c b/filters/f_auto_filters.c index c247728e5acad..11303e385d63c 100644 --- a/filters/f_auto_filters.c +++ b/filters/f_auto_filters.c @@ -95,6 +95,7 @@ static void deint_process(struct mp_filter *f) field_parity = "auto"; } + struct mp_stream_info *info = mp_filter_find_stream_info(f); bool has_filter = true; if (img->imgfmt == IMGFMT_VDPAU) { char *args[] = {"deint", "yes", @@ -126,6 +127,11 @@ static void deint_process(struct mp_filter *f) "parity", field_parity, NULL}; p->sub.filter = mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, "vavpp", args); + } else if (info && info->deinterlace && !IMGFMT_IS_HWACCEL(img->imgfmt)) { + char *args[] = {"interlaced-only", opts->deinterlace == 1 ? "no" : "yes", + "parity", field_parity, NULL}; + p->sub.filter = mp_create_user_filter(f, MP_OUTPUT_CHAIN_VIDEO, + "fieldrate", args); } else { has_filter = false; } diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index 308a9fb02f5b6..3731663c34adc 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -147,6 +147,7 @@ struct priv { pl_options pars; struct m_config_cache *opts_cache; struct m_config_cache *next_opts_cache; + struct m_config_cache *filter_opts_cache; struct gl_next_opts *next_opts; struct cache shader_cache, icc_cache; struct mp_csp_equalizer_state *video_eq; @@ -890,6 +891,7 @@ static void update_options(struct vo *vo) pl_options pars = p->pars; bool changed = m_config_cache_update(p->opts_cache); changed = m_config_cache_update(p->next_opts_cache) || changed; + changed = m_config_cache_update(p->filter_opts_cache) || changed; if (changed) update_render_options(vo); @@ -1143,13 +1145,29 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame) mpi->priv = fp; fp->vo = vo; + bool use_fields = params.deinterlace_params && + mpi->fields & MP_IMGFIELD_INTERLACED && + !IMGFMT_IS_HWACCEL(mpi->imgfmt); + // vf_fieldrate emits a frame for each field only to make mpv render + // at the second field PTS. But don't actually push it to pl_queue, + // because it already derives it internally from first_field. + if (use_fields && mpi->field_tick == MP_FIELD_TICK_SECOND) { + talloc_free(mpi); + p->last_id = id; + continue; + } + int first_field = !use_fields ? PL_FIELD_NONE : + (mpi->fields & MP_IMGFIELD_TOP_FIRST) ? PL_FIELD_TOP : PL_FIELD_BOTTOM; + pl_queue_push(p->queue, &(struct pl_source_frame) { .pts = mpi->pts, - .duration = can_interpolate ? frame->approx_duration : 0, + .duration = can_interpolate ? frame->approx_duration : + use_fields ? mpi->pkt_duration : 0, .frame_data = mpi, .map = map_frame, .unmap = unmap_frame, .discard = discard_frame, + .first_field = first_field, }); p->last_id = id; @@ -2229,6 +2247,7 @@ static int preinit(struct vo *vo) { struct priv *p = vo->priv; p->opts_cache = m_config_cache_alloc(p, vo->global, &gl_video_conf); + p->filter_opts_cache = m_config_cache_alloc(p, vo->global, &filter_conf); p->next_opts_cache = m_config_cache_alloc(p, vo->global, &gl_next_conf); p->next_opts = p->next_opts_cache->opts; p->video_eq = mp_csp_equalizer_create(p, vo->global); @@ -2559,6 +2578,7 @@ static void update_render_options(struct vo *vo) struct priv *p = vo->priv; pl_options pars = p->pars; const struct gl_video_opts *opts = p->opts_cache->opts; + const struct filter_opts *fopts = p->filter_opts_cache->opts; pars->params.background_color[0] = opts->background_color.r / 255.0; pars->params.background_color[1] = opts->background_color.g / 255.0; pars->params.background_color[2] = opts->background_color.b / 255.0; @@ -2592,6 +2612,9 @@ static void update_render_options(struct vo *vo) pars->params.plane_upscaler = map_scaler(p, SCALER_CSCALE); pars->params.frame_mixer = opts->interpolation ? map_scaler(p, SCALER_TSCALE) : NULL; + pars->params.deinterlace_params = fopts->deinterlace != 0 ? &pars->deinterlace_params : NULL; + pars->deinterlace_params.algo = PL_DEINTERLACE_BWDIF; + // Request as many frames as required from the decoder, depending on the // speed VPS/FPS ratio libplacebo may need more frames. Request frames up to // ratio of 1/2, but only if anti aliasing is enabled. @@ -2709,6 +2732,7 @@ const struct vo_driver video_out_gpu_next = { .caps = VO_CAP_ROTATE90 | VO_CAP_FILM_GRAIN | VO_CAP_VFLIP | + VO_CAP_DEINTERLACE | 0x0, .preinit = preinit, .query_format = query_format,