From 822c6a24e0ccf00d9806434a2a9fb79547764978 Mon Sep 17 00:00:00 2001 From: llyyr Date: Sun, 28 Jun 2026 03:31:52 +0530 Subject: [PATCH 1/7] vo_gpu_next: fix stray comma --- video/out/vo_gpu_next.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index bf4f80b7ee3f2..a9089b5ffbdbe 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -210,8 +210,8 @@ const struct m_sub_options gl_next_conf = { {"border-background", OPT_CHOICE(border_background, {"none", BACKGROUND_NONE}, {"color", BACKGROUND_COLOR}, - {"tiles", BACKGROUND_TILES} - ,{"blur", BACKGROUND_BLUR})}, + {"tiles", BACKGROUND_TILES}, + {"blur", BACKGROUND_BLUR})}, {"background-blur-radius", OPT_FLOAT(background_blur_radius)}, {"corner-rounding", OPT_FLOAT(corner_rounding), M_RANGE(0, 1)}, {"interpolation-preserve", OPT_BOOL(inter_preserve)}, From 2d8a9b5a3d64dea819170b3e7614b05c5decefc1 Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:40:16 +0530 Subject: [PATCH 2/7] mp_image: add MP_IMGFIELD_TICK_{FIRST,SECOND} defines Used in next commits --- video/mp_image.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/video/mp_image.h b/video/mp_image.h index f5c1562815a21..6bb3b1fc3b82d 100644 --- a/video/mp_image.h +++ b/video/mp_image.h @@ -36,6 +36,8 @@ #define MP_IMGFIELD_TOP_FIRST 0x02 #define MP_IMGFIELD_REPEAT_FIRST 0x04 +#define MP_IMGFIELD_TICK_FIRST 0x08 +#define MP_IMGFIELD_TICK_SECOND 0x10 #define MP_IMGFIELD_INTERLACED 0x20 // Describes image parameters that usually stay constant. From 569d27fd464a18b80f5b57d39f1edaf3104a65d3 Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:41:28 +0530 Subject: [PATCH 3/7] 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 | 124 ++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 video/filter/vf_fieldrate.c diff --git a/filters/user_filters.c b/filters/user_filters.c index ea7b69c990e76..fbd523b3d2bb6 100644 --- a/filters/user_filters.c +++ b/filters/user_filters.c @@ -107,6 +107,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 f2c31ef66b094..6760758e8825e 100644 --- a/filters/user_filters.h +++ b/filters/user_filters.h @@ -39,3 +39,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 80bd39d7ec3bf..afd4e04f03818 100644 --- a/meson.build +++ b/meson.build @@ -222,6 +222,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..7c93e41acc7cd --- /dev/null +++ b/video/filter/vf_fieldrate.c @@ -0,0 +1,124 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#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. + out->fields &= ~(MP_IMGFIELD_TICK_FIRST | MP_IMGFIELD_TICK_SECOND); + if (mp_refqueue_should_deint(p->queue)) { + out->fields |= mp_refqueue_is_second_field(p->queue) ? + MP_IMGFIELD_TICK_SECOND : MP_IMGFIELD_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; + } + } + 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 e4783478053ba5f20fc27c7d1261ceecd098aeda Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 17:43:05 +0530 Subject: [PATCH 4/7] 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 e512f613c804d..96a35eedce8dd 100644 --- a/filters/f_output_chain.c +++ b/filters/f_output_chain.c @@ -394,6 +394,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 f24918d4607e0..3f37cd898e2e7 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 97d4e503c40dc57f439adb2bb07f7145d1c03c4c Mon Sep 17 00:00:00 2001 From: llyyr Date: Thu, 30 Jul 2026 00:18:34 +0530 Subject: [PATCH 5/7] video: allow VOs to request past frames Allow setting the requested number of past frames via vo_set_queue_params, and make them available in vo_frame. --- player/core.h | 5 ++- player/video.c | 87 +++++++++++++++++++++++++++++++++++------ video/out/gpu/video.c | 2 +- video/out/vo.c | 22 ++++++++++- video/out/vo.h | 9 ++++- video/out/vo_gpu_next.c | 2 +- video/out/vo_vdpau.c | 2 +- 7 files changed, 109 insertions(+), 20 deletions(-) diff --git a/player/core.h b/player/core.h index ec6784411c969..c4e15500f4921 100644 --- a/player/core.h +++ b/player/core.h @@ -339,7 +339,10 @@ typedef struct MPContext { // The +1 is for adding 1 additional frame in backstep mode. struct mp_image *next_frames[VO_MAX_REQ_FRAMES + 1]; int num_next_frames; - struct mp_image *saved_frame; // for hrseek_lastframe and hrseek_backstep + + // saved_frames[0] is the last frame, saved_frames[1] the one before that. + struct mp_image *saved_frames[VO_MAX_REQ_FRAMES + 1]; + int num_saved_frames; enum playback_status video_status, audio_status; bool restart_complete; diff --git a/player/video.c b/player/video.c index e2cc4801f28a0..1fe499251ec7f 100644 --- a/player/video.c +++ b/player/video.c @@ -95,6 +95,17 @@ static void vo_chain_reset_state(struct vo_chain *vo_c) vo_c->underrun_signaled = false; } +static void trim_saved_frames(struct MPContext *mpctx, int max_frames) +{ + mp_assert(max_frames >= 0); + mp_assert(max_frames <= MP_ARRAY_SIZE(mpctx->saved_frames)); + + while (mpctx->num_saved_frames > max_frames) { + mpctx->num_saved_frames--; + mp_image_unrefp(&mpctx->saved_frames[mpctx->num_saved_frames]); + } +} + void reset_video_state(struct MPContext *mpctx) { if (mpctx->vo_chain) { @@ -107,7 +118,7 @@ void reset_video_state(struct MPContext *mpctx) for (int n = 0; n < mpctx->num_next_frames; n++) mp_image_unrefp(&mpctx->next_frames[n]); mpctx->num_next_frames = 0; - mp_image_unrefp(&mpctx->saved_frame); + trim_saved_frames(mpctx, 0); mpctx->delay = 0; mpctx->time_frame = 0; @@ -401,15 +412,59 @@ static void handle_new_frame(struct MPContext *mpctx) MP_TRACE(mpctx, "frametime=%5.3f\n", frame_time); } +static void add_saved_frame(struct MPContext *mpctx, struct mp_image *frame, + int max_frames) +{ + mp_assert(frame); + mp_assert(max_frames >= 0); + mp_assert(max_frames <= MP_ARRAY_SIZE(mpctx->saved_frames)); + + if (!max_frames) { + trim_saved_frames(mpctx, 0); + talloc_free(frame); + return; + } + + // Make room for the new newest frame. + trim_saved_frames(mpctx, max_frames - 1); + + for (int n = mpctx->num_saved_frames; n > 0; n--) + mpctx->saved_frames[n] = mpctx->saved_frames[n - 1]; + + mpctx->saved_frames[0] = frame; + mpctx->num_saved_frames++; +} + +static struct mp_image *take_saved_frame(struct MPContext *mpctx) +{ + if (!mpctx->num_saved_frames) + return NULL; + + struct mp_image *frame = mpctx->saved_frames[0]; + + for (int n = 1; n < mpctx->num_saved_frames; n++) + mpctx->saved_frames[n - 1] = mpctx->saved_frames[n]; + + mpctx->num_saved_frames--; + mpctx->saved_frames[mpctx->num_saved_frames] = NULL; + + return frame; +} + // Remove the first frame in mpctx->next_frames static void shift_frames(struct MPContext *mpctx) { if (mpctx->num_next_frames < 1) return; - talloc_free(mpctx->next_frames[0]); + + struct mp_image *frame = mpctx->next_frames[0]; + for (int n = 0; n < mpctx->num_next_frames - 1; n++) mpctx->next_frames[n] = mpctx->next_frames[n + 1]; mpctx->num_next_frames -= 1; + mpctx->next_frames[mpctx->num_next_frames] = NULL; + + add_saved_frame(mpctx, frame, vo_get_num_req_past_frames(mpctx->video_out)); } static bool use_video_lookahead(struct MPContext *mpctx) @@ -528,18 +583,19 @@ static int video_output_image(struct MPContext *mpctx, bool *logical_eof) mpctx->hrseek_lastframe)) { /* just skip - but save in case it was the last frame */ - mp_image_setrefp(&mpctx->saved_frame, img); + add_saved_frame(mpctx, img, + vo_get_num_req_past_frames(mpctx->video_out) + 1); + img = NULL; } else { if (hrseek && mpctx->hrseek_backstep) { - if (mpctx->saved_frame) { - add_new_frame(mpctx, mpctx->saved_frame); - mpctx->saved_frame = NULL; + struct mp_image *saved = take_saved_frame(mpctx); + if (saved) { + add_new_frame(mpctx, saved); } else { MP_WARN(mpctx, "Backstep failed.\n"); } mpctx->hrseek_backstep = false; } - mp_image_unrefp(&mpctx->saved_frame); add_new_frame(mpctx, img); img = NULL; } @@ -547,14 +603,13 @@ static int video_output_image(struct MPContext *mpctx, bool *logical_eof) } } - if (!hrseek) - mp_image_unrefp(&mpctx->saved_frame); - if (r == VD_EOF) { // If hr-seek went past EOF, use the last frame. - if (mpctx->saved_frame) - add_new_frame(mpctx, mpctx->saved_frame); - mpctx->saved_frame = NULL; + if (hrseek) { + struct mp_image *saved = take_saved_frame(mpctx); + if (saved) + add_new_frame(mpctx, saved); + } *logical_eof = true; } @@ -1225,16 +1280,22 @@ void write_video(struct MPContext *mpctx) }; calculate_frame_duration(mpctx); + int req_past = vo_get_num_req_past_frames(mpctx->video_out); int req = vo_get_num_req_frames(mpctx->video_out); + mp_assert(req_past >= 0 && req_past <= VO_MAX_REQ_FRAMES); mp_assert(req >= 1 && req <= VO_MAX_REQ_FRAMES); + trim_saved_frames(mpctx, req_past); struct vo_frame dummy = { .pts = pts, .duration = -1, .still = mpctx->step_frames > 0, .can_drop = opts->frame_dropping & 1, + .num_past_frames = mpctx->num_saved_frames, .num_frames = MPMIN(mpctx->num_next_frames, req), .num_vsyncs = 1, }; + for (int n = 0; n < dummy.num_past_frames; n++) + dummy.past_frames[n] = mpctx->saved_frames[n]; for (int n = 0; n < dummy.num_frames; n++) dummy.frames[n] = mpctx->next_frames[n]; struct vo_frame *frame = vo_frame_ref(&dummy); diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c index 656c11c29f275..16e63e7d78873 100644 --- a/video/out/gpu/video.c +++ b/video/out/gpu/video.c @@ -4396,7 +4396,7 @@ void gl_video_configure_queue(struct gl_video *p, struct vo *vo) } queue_size = MPMIN(queue_size, VO_MAX_REQ_FRAMES); - vo_set_queue_params(vo, 0, queue_size, queue_size + 1); + vo_set_queue_params(vo, 0, 0, queue_size, queue_size + 1); } static int validate_error_diffusion_opt(struct mp_log *log, const m_option_t *opt, diff --git a/video/out/vo.c b/video/out/vo.c index f93f6cf979c82..6f76b2a17a8d1 100644 --- a/video/out/vo.c +++ b/video/out/vo.c @@ -169,6 +169,7 @@ struct vo_internal { bool rendering; // true if an image is being rendered struct vo_frame *frame_queued; // should be drawn next + int req_past_frames; // VO's requested value of num_past_frames int req_frames; // VO's requested value of num_frames int frame_refs; // max frames the VO may reference at once uint64_t current_frame_id; @@ -1332,22 +1333,35 @@ void vo_get_src_dst_rects(struct vo *vo, struct mp_rect *out_src, // flip_page[_timed] will be called offset_us nanoseconds too early. // (For vo_vdpau, which does its own timing.) +// num_req_past_frames sets the requested number of past vo_frame.past_frames // num_req_frames set the requested number of requested vo_frame.frames. // (For vo_gpu interpolation.) // num_frame_refs sets the total number of frames the VO may reference at the // same time, including retained past frames. (For sizing fixed hardware // decoder surface pools.) -void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames, +void vo_set_queue_params(struct vo *vo, int64_t offset_ns, + int num_req_past_frames, int num_req_frames, int num_frame_refs) { struct vo_internal *in = vo->in; mp_mutex_lock(&in->lock); in->flip_queue_offset = offset_ns; + in->req_past_frames = MPCLAMP(num_req_past_frames, 0, VO_MAX_REQ_FRAMES); in->req_frames = MPCLAMP(num_req_frames, 1, VO_MAX_REQ_FRAMES); - in->frame_refs = MPCLAMP(num_frame_refs, in->req_frames, 2 * VO_MAX_REQ_FRAMES); + in->frame_refs = MPCLAMP(num_frame_refs, in->req_frames + in->req_past_frames, + 2 * VO_MAX_REQ_FRAMES); mp_mutex_unlock(&in->lock); } +int vo_get_num_req_past_frames(struct vo *vo) +{ + struct vo_internal *in = vo->in; + mp_mutex_lock(&in->lock); + int res = in->req_past_frames; + mp_mutex_unlock(&in->lock); + return res; +} + int vo_get_num_req_frames(struct vo *vo) { struct vo_internal *in = vo->in; @@ -1499,6 +1513,8 @@ struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h, static void destroy_frame(void *p) { struct vo_frame *frame = p; + for (int n = 0; n < frame->num_past_frames; n++) + talloc_free(frame->past_frames[n]); for (int n = 0; n < frame->num_frames; n++) talloc_free(frame->frames[n]); } @@ -1514,6 +1530,8 @@ struct vo_frame *vo_frame_ref(struct vo_frame *frame) struct vo_frame *new = talloc_ptrtype(NULL, new); talloc_set_destructor(new, destroy_frame); *new = *frame; + for (int n = 0; n < frame->num_past_frames; n++) + new->past_frames[n] = mp_image_new_ref(frame->past_frames[n]); for (int n = 0; n < frame->num_frames; n++) new->frames[n] = mp_image_new_ref(frame->frames[n]); new->current = new->num_frames ? new->frames[0] : NULL; diff --git a/video/out/vo.h b/video/out/vo.h index 3f37cd898e2e7..a060127969aff 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -267,6 +267,11 @@ struct vo_frame { // Warning: When OSD should be redrawn in --force-window --idle mode, this // can be NULL. The VO should draw a black background, OSD on top. struct mp_image *current; + // List of images immediately preceding the current one. + // past_frames[0] is the newest, immediately preceding frame. + // The actual number delivered can be lower than requested. + int num_past_frames; + struct mp_image *past_frames[VO_MAX_REQ_FRAMES]; // List of future images, starting with the current one. This does not // care about repeated frames - it simply contains the next real frames. // vo_set_queue_params() sets how many future frames this should include. @@ -551,8 +556,10 @@ void vo_query_formats(struct vo *vo, uint8_t *list); void vo_event(struct vo *vo, int event); int vo_query_and_reset_events(struct vo *vo, int events); struct mp_image *vo_get_current_frame(struct vo *vo); -void vo_set_queue_params(struct vo *vo, int64_t offset_ns, int num_req_frames, +void vo_set_queue_params(struct vo *vo, int64_t offset_ns, + int num_req_past_frames, int num_req_frames, int num_frame_refs); +int vo_get_num_req_past_frames(struct vo *vo); int vo_get_num_req_frames(struct vo *vo); int vo_get_num_frame_refs(struct vo *vo); double vo_get_vsync_interval(struct vo *vo); diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index a9089b5ffbdbe..2c02625c4eb9a 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -2758,7 +2758,7 @@ static void update_render_options(struct vo *vo) } req_frames = MPMIN(VO_MAX_REQ_FRAMES, req_frames); // pl_queue also retains past frames for the symmetric mixing window, - vo_set_queue_params(vo, 0, req_frames, 2 * req_frames - 1); + vo_set_queue_params(vo, 0, 0, req_frames, 2 * req_frames - 1); pars->params.deband_params = opts->deband ? &pars->deband_params : NULL; pars->deband_params.iterations = opts->deband_opts->iterations; diff --git a/video/out/vo_vdpau.c b/video/out/vo_vdpau.c index 2ba0be0dc8940..7a0a13bce6c87 100644 --- a/video/out/vo_vdpau.c +++ b/video/out/vo_vdpau.c @@ -279,7 +279,7 @@ static void resize(struct vo *vo) vc->flip_offset_us = vo->opts->fullscreen ? 1000LL * vc->flip_offset_fs : 1000LL * vc->flip_offset_window; - vo_set_queue_params(vo, vc->flip_offset_us * 1000, 1, 2); + vo_set_queue_params(vo, vc->flip_offset_us * 1000, 0, 1, 2); if (vc->output_surface_w < vo->dwidth || vc->output_surface_h < vo->dheight || vc->rotation != vo->params->rotate) From 0da5fc6e1a7d859b740fbaa284117ce6e2b552d9 Mon Sep 17 00:00:00 2001 From: llyyr Date: Mon, 15 Jun 2026 18:02:54 +0530 Subject: [PATCH 6/7] vo_gpu_next: use shader deinterlacer instead of software bwdif --- .../add-deinterlace-algorithm.txt | 1 + DOCS/man/options.rst | 14 +- filters/f_auto_filters.c | 6 + video/out/vo_gpu_next.c | 125 ++++++++++++++++-- 4 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 DOCS/interface-changes/add-deinterlace-algorithm.txt diff --git a/DOCS/interface-changes/add-deinterlace-algorithm.txt b/DOCS/interface-changes/add-deinterlace-algorithm.txt new file mode 100644 index 0000000000000..c0a9fab6fa38e --- /dev/null +++ b/DOCS/interface-changes/add-deinterlace-algorithm.txt @@ -0,0 +1 @@ +add `--deinterlace-algorithm` option diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 09b9075e8f98f..e6f32f0d226f6 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1815,9 +1815,11 @@ Video ``--deinterlace=`` Enable or disable deinterlacing (default: no). Interlaced video shows ugly comb-like artifacts, which are visible on - fast movement. Enabling this typically inserts the bwdif video filter in - order to deinterlace the video, or lets the video output apply deinterlacing - if supported. + fast movement. Enabling this inserts the hardware deinterlacing filter if + ``--hwdec`` is used, otherwise the video output applies deinterlacing. If + neither is possible, a software bwdif filter is used instead. The + ``--deinterlace-algorithm`` option can be used to select the deinterlacing + algorithm used by the video output (``gpu-next`` only). When using ``auto``, mpv will insert a deinterlacing filter if ffmpeg detects that the video frame is interlaced. Be aware that there can be false @@ -6266,6 +6268,12 @@ them. Among these kernels, ``burkes`` achieves a good balance between performance and quality, and probably is the one you want to try first. +``--deinterlace-algorithm=`` + Sets the deinterlacing algorithm to use when the VO is doing deinterlacing. + Currently only ``gpu-next`` supports this. + + Default: ``bwdif``. + ``--gpu-debug`` Enables GPU debugging. What this means depends on the API type. For OpenGL, it calls ``glGetError()``, and requests a debug context. For Vulkan, it 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 2c02625c4eb9a..29faa4e55797e 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -149,6 +149,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; @@ -182,6 +183,7 @@ struct gl_next_opts { float background_blur_radius; float corner_rounding; bool inter_preserve; + int deint_algo; struct user_lut lut; struct user_lut image_lut; struct user_lut target_lut; @@ -215,6 +217,11 @@ const struct m_sub_options gl_next_conf = { {"background-blur-radius", OPT_FLOAT(background_blur_radius)}, {"corner-rounding", OPT_FLOAT(corner_rounding), M_RANGE(0, 1)}, {"interpolation-preserve", OPT_BOOL(inter_preserve)}, + {"deinterlace-algorithm", OPT_CHOICE(deint_algo, + {"weave", PL_DEINTERLACE_WEAVE}, + {"bob", PL_DEINTERLACE_BOB}, + {"yadif", PL_DEINTERLACE_YADIF}, + {"bwdif", PL_DEINTERLACE_BWDIF})}, {"lut", OPT_STRING(lut.opt), .flags = M_OPT_FILE}, {"lut-type", OPT_CHOICE_C(lut.type, lut_types)}, {"image-lut", OPT_STRING(image_lut.opt), .flags = M_OPT_FILE}, @@ -230,6 +237,7 @@ const struct m_sub_options gl_next_conf = { .defaults = &(struct gl_next_opts) { .border_background = BACKGROUND_COLOR, .background_blur_radius = 16.0f, + .deint_algo = PL_DEINTERLACE_BWDIF, .inter_preserve = true, .image_subs_hdr_peak = 1000, .target_hint = -1, @@ -1036,6 +1044,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); @@ -1215,6 +1224,88 @@ static void update_tm_viz(struct pl_color_map_params *params, static void update_hook_opts_dynamic(struct priv *p, const struct pl_hook *hook, const struct mp_image *mpi); +static bool push_queue_frame(struct vo *vo, struct vo_frame *frame, int index, + bool context_frame, bool deinterlace, + bool can_interpolate) +{ + struct priv *p = vo->priv; + struct mp_image *src = NULL, *prev = NULL, *next = NULL; + + if (context_frame) { + // past_frames is reverse chronological + src = frame->past_frames[index]; + + if (index + 1 < frame->num_past_frames) + prev = frame->past_frames[index + 1]; + + if (index > 0) { + next = frame->past_frames[index - 1]; + } else if (frame->num_frames) { + next = frame->frames[0]; + } + } else { + // frames is chronological + src = frame->frames[index]; + + if (index > 0) { + prev = frame->frames[index - 1]; + } else if (frame->num_past_frames) { + prev = frame->past_frames[0]; + } + + if (index + 1 < frame->num_frames) + next = frame->frames[index + 1]; + } + + bool fieldrate = src->fields & (MP_IMGFIELD_TICK_FIRST | MP_IMGFIELD_TICK_SECOND); + bool can_deint = deinterlace && fieldrate && (src->fields & MP_IMGFIELD_INTERLACED); + bool second_tick = can_deint && (src->fields & MP_IMGFIELD_TICK_SECOND); + + // Both fieldrate ticks refer to the same source image. + if (second_tick && prev && (prev->fields & MP_IMGFIELD_TICK_FIRST)) + return false; + + double pts = src->pts; + int first_field = PL_FIELD_NONE; + double duration = 0; + + if (second_tick) { + // refqueue uses the midpoint of both frames as the PTS for the second + // tick, so we need to undo it + if (next && pts != MP_NOPTS_VALUE && next->pts != MP_NOPTS_VALUE) { + pts = 2 * pts - next->pts; + } else if (src->pkt_duration > 0) { + pts -= src->pkt_duration / 2; + } + } + + if (can_deint) + first_field = src->fields & MP_IMGFIELD_TOP_FIRST ? PL_FIELD_TOP : PL_FIELD_BOTTOM; + + if (can_interpolate) { + duration = frame->approx_duration; + } else if (can_deint && src->pkt_duration > 0) { + duration = src->pkt_duration; + } + + struct mp_image *mpi = mp_image_new_ref(src); + struct frame_priv *fp = talloc_zero(mpi, struct frame_priv); + mpi->priv = fp; + fp->vo = vo; + + pl_queue_push(p->queue, &(struct pl_source_frame) { + .pts = pts, + .duration = duration, + .frame_data = mpi, + .map = map_frame, + .unmap = unmap_frame, + .discard = discard_frame, + .first_field = first_field, + }); + + return true; +} + static bool draw_frame(struct vo *vo, struct vo_frame *frame) { struct priv *p = vo->priv; @@ -1285,19 +1376,11 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame) if (id <= p->last_id) continue; // ignore already seen frames - struct mp_image *mpi = mp_image_new_ref(frame->frames[n]); - struct frame_priv *fp = talloc_zero(mpi, struct frame_priv); - mpi->priv = fp; - fp->vo = vo; - - pl_queue_push(p->queue, &(struct pl_source_frame) { - .pts = mpi->pts, - .duration = can_interpolate ? frame->approx_duration : 0, - .frame_data = mpi, - .map = map_frame, - .unmap = unmap_frame, - .discard = discard_frame, - }); + if (!p->last_id && frame->num_past_frames) + for (int i = frame->num_past_frames - 1; i >= 0; i--) + push_queue_frame(vo, frame, i, true, params.deinterlace_params, can_interpolate); + + push_queue_frame(vo, frame, n, false, params.deinterlace_params, can_interpolate); p->last_id = id; } @@ -2388,6 +2471,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); @@ -2715,6 +2799,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; @@ -2748,17 +2833,28 @@ 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 = p->next_opts->deint_algo; + // 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. int req_frames = 2; + int req_past_frames = 0; if (pars->params.frame_mixer) { req_frames += ceilf(pars->params.frame_mixer->kernel->radius) * (pars->params.skip_anti_aliasing ? 1 : 2); } + + if (pars->params.deinterlace_params) { + req_past_frames += 1; + req_frames += 1; + } + req_frames = MPMIN(VO_MAX_REQ_FRAMES, req_frames); + // pl_queue also retains past frames for the symmetric mixing window, - vo_set_queue_params(vo, 0, 0, req_frames, 2 * req_frames - 1); + vo_set_queue_params(vo, 0, req_past_frames, req_frames, 2 * req_frames - 1); pars->params.deband_params = opts->deband ? &pars->deband_params : NULL; pars->deband_params.iterations = opts->deband_opts->iterations; @@ -2867,6 +2963,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, From d09129c5f5e9c16b7312c867c830a9a0b0d8d79f Mon Sep 17 00:00:00 2001 From: llyyr Date: Thu, 30 Jul 2026 01:54:09 +0530 Subject: [PATCH 7/7] filters/f_auto_filters: recreate deint filter for auto/yes cycle --- filters/f_auto_filters.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/filters/f_auto_filters.c b/filters/f_auto_filters.c index 11303e385d63c..1f15e73ea6290 100644 --- a/filters/f_auto_filters.c +++ b/filters/f_auto_filters.c @@ -22,6 +22,7 @@ struct deint_priv { struct mp_subfilter sub; int prev_imgfmt; + int prev_deint_opt; bool interlaced_frame; struct m_config_cache *opts; }; @@ -52,10 +53,6 @@ static void deint_process(struct mp_filter *f) m_config_cache_update(p->opts); struct filter_opts *opts = p->opts->opts; - // Handle deinterlace=0 case by destroying any existing filter immediately. - if (!opts->deinterlace) - mp_subfilter_destroy(&p->sub); - // Insert a deinterlace filter in the chain if: // 1) deinterlace=1 (always), or // 2) deinterlace=-1 (auto) and the frame is interlaced. @@ -66,9 +63,17 @@ static void deint_process(struct mp_filter *f) // 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) { + bool imgfmt_changed = img->imgfmt != p->prev_imgfmt; + // If we already have a deinterlace filter, and the option value changed + // (i.e. we switched from auto to yes or vice versa), we need to recreate it + // immediately to reflect the new value. + bool deint_opt_changed = p->sub.filter && opts->deinterlace != p->prev_deint_opt; + // Handle deinterlace=0 case by destroying any existing filter immediately. + bool deint_off = opts->deinterlace == 0; + if (imgfmt_changed || deint_opt_changed || deint_off) { mp_subfilter_destroy(&p->sub); - p->prev_imgfmt = img->imgfmt; + if (imgfmt_changed) + p->prev_imgfmt = img->imgfmt; } else if (p->sub.filter && !filter_needed) { if (!mp_subfilter_drain_destroy(&p->sub)) return; @@ -167,6 +172,7 @@ static void deint_process(struct mp_filter *f) mp_chain_filters(subf->ppins[0], subf->ppins[1], filters, 2); p->sub.filter = subf; } + p->prev_deint_opt = opts->deinterlace; mp_subfilter_continue(&p->sub); }