From c92962ddfdbc43c162d32e5b259bb1a644373375 Mon Sep 17 00:00:00 2001 From: AnataBakka <78822218+AnataBakka@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:56:18 +0300 Subject: [PATCH] vo: add vrr functionality for non-display sync modes The intention is for vrr mode to be a change to base behaviour, and thus be compatible with every selection of video-sync modes. However, currently it is only compatible for non-display sync ones. For display sync modes in the future, the expectation is to continue using this vrr structure, but just have vo immediatelly output frames one after the other like display sync does, and then use api functions like presentAt to present the frame based on the data from voframe. The expectation would be for display-sync vrr to exclude some of the estimations that display-sync uses, since they are not required and for cleaner design. One other expectation is for the driver to immediatelly drop frames when the swapchain is fully filled out, so that vo can then decide whether to move to a newer frame or recalculate the dropped frame position before trying to send it again, instead of the driver dropping older valid frames, or delaying the newer frame to later. Additional changes: #. merged the repeat functionality from display-sync modes into a new variable that can be used by vrr. it is not expected but it is possible for display-sync behaviour to have changed because of that. #. moved in->wakeup_pts set up inside render_frame instead of vo_queue_frame. This is because it doesn't appear to have a pre-render_frame purpose inside vo_queue_frame, and to allow decision based set up in render_frame. #. we now directly update frame-> information when pts and duration changes (i.e like when "now" has moved past our pts), to give the driver accurate data. #. after doing the initial duration < 0 check, we then clamp duration to 0, as non-valid negative duration input messes up the logic. #. display-sync now also uses a simple pts + duration for its in->wakeup_pts instead of an "= 0" special case. Since it already modifies pts to 0 and duration to -1 -> going to 0, end time is expected to also result in 0. Trying to re-use existing logic than making a lot of special cases. #. The "strict drop threshold" has been changed to include the case where end_time == now (as per duration <= 0), instead of just end_time < now. The vrr logic is cleaner when we drop frames at duration = 0, and i believe makes sense. #. Added a in->current_frame check for VO_CAP_NORETAIN, since stuff may randomly set current_frame to null (like vo_reconfig?), and just generally safer. #. Clarified the VO_CAP_FRAMEOWNER condition to be compatible with vrr, and also put up that the addition of the driver_dropped_frame condition (the logic of which is exactly the same as before this change) doesn't make much sense and should be removed, but i'll leave it to somebody else. --- DOCS/man/input.rst | 7 +- DOCS/man/options.rst | 54 +++++++ options/options.c | 8 ++ options/options.h | 4 + player/command.c | 2 +- player/video.c | 5 + video/out/gpu/video.c | 2 +- video/out/vo.c | 309 +++++++++++++++++++++++++++++++++++----- video/out/vo.h | 3 + video/out/vo_gpu_next.c | 2 +- 10 files changed, 358 insertions(+), 38 deletions(-) diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 4741e5570cfc4..8a2d776942fff 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -2311,11 +2311,12 @@ Property list ``mistimed-frame-count`` Number of video frames that were not timed correctly in display-sync mode - for the sake of keeping A/V sync. This does not include external + for the sake of keeping A/V sync (i.e using the ``display-desync`` mode should + not change this value from 0), or in ``vrr-adjust`` mode for the sake of keeping + within the defined refresh rate range. This does not include external circumstances, such as video rendering being too slow or the graphics driver somehow skipping a vsync. It does not include rounding errors either - (which can happen especially with bad source timestamps). For example, - using the ``display-desync`` mode should never change this value from 0. + (which can happen especially with bad source timestamps). ``vsync-ratio`` For how many vsyncs a frame is displayed on average. This is available if diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 2a359d4692d5b..a24f747da6f3e 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1320,6 +1320,10 @@ Video Set this option only if you have reason to believe the automatically determined value is wrong. +``--minimum-display-fps=`` + Set the minimum display FPS used with the ``--vrr-adjust=true`` mode. By + default, it uses the maximum display fps. + ``--hwdec=`` Specify the hardware video decoding API that should be used if possible. Whether hardware decoding is actually done depends on the video codec. If @@ -8224,6 +8228,56 @@ Video Sync frame dropping due to the audio "overshooting" and skipping multiple video frames before the sync logic can react. +``--vrr-adjust=`` + This option tries to increase the chance of the display being ready for + frames the moment they are sent (default: no), for cases where the + display is unreliable, primarily useful for VRR displays. This uses + ``--minimum-display-fps`` and display-fps (maximum fps). + + This is done by observing each individual frame and dynamically: + + 1. Not doing anything if they are perfectly between the minimum refresh time + and the middle refresh time. + 2. Repeating frames at a middle refresh time if possible, else repeating + tending towards the middle refresh time (the middle refresh time gives + us the best error leeway in case there are random application or OS delays + that would put us in a position that exceeds either the minimum or + the maximum refresh time). + 3. If we can't reliably repeat it, then we send it even if it's between the + middle refresh time and the maximum refresh time. Note that because of the + above mentioned potential delays, this may cause us to exceed our target + even if we initially appear in valid range. + 4. If the next frame exceeds our minimum refresh time, whether because our + repetition failed or that's how the frames exist, we will move it to the + closest valid position that's within the refresh range. If two frames are + put on the same position, we send the newest one only. + + Note: This is not compatible with ``--video-sync=display-...`` modes, yet, + which would be the only ones capable of getting rid of the last displaying + weirdnesses. + +``--vrr-adjust-max-refresh-variance=<0.0-1.0>`` + How much percentage wise the refresh time can vary from one frame to the next, + where 1.0 means the refresh time can immediatelly move between the minimum and + maximum refresh time, while 0 means the refresh time is static. (default: 1.0) + + The default does not take into account refresh rate flicker (which may happen + in specific cases, eg. when time between frames is constantly higher than + minimum refresh time + middle refresh time), and optimizes just for reaching + target position. If that's a problem, then this option can limit sudden changes in refresh + time. The lower the value set, the more refresh time stability, the higher chance + of missing our target. + +``--vrr-adjust-target-refresh-rate=`` + Set the refresh rate we should tend towards, instead of the default "middle refresh + time". Setting it at a lower refresh rate will reduce the amount of repetitions, + which can help if computational burden is a problem. However, increasing it or + reducing it will come at a cost of potential chance increase of missing our target. + + When needed, we will still use the entire range of ``--minimum-display-fps`` and + ``--display-fps-override``, so this option only increases the chance towards + the refresh rate we tend towards. + Miscellaneous ------------- diff --git a/options/options.c b/options/options.c index 2a5373d44d7b5..55e683f71d045 100644 --- a/options/options.c +++ b/options/options.c @@ -189,6 +189,8 @@ static const m_option_t mp_vo_opt_list[] = { {"show-in-taskbar", OPT_BOOL(show_in_taskbar)}, {"display-fps-override", OPT_DOUBLE(display_fps_override), M_RANGE(0, DBL_MAX)}, + {"minimum-display-fps", OPT_DOUBLE(minimum_display_fps), + M_RANGE(0, DBL_MAX)}, {"video-timing-offset", OPT_DOUBLE(timing_offset), M_RANGE(0.0, 1.0)}, {"video-sync", OPT_CHOICE(video_sync, {"audio", VS_DEFAULT}, @@ -200,6 +202,9 @@ static const m_option_t mp_vo_opt_list[] = { {"display-vdrop", VS_DISP_VDROP}, {"display-desync", VS_DISP_NONE}, {"desync", VS_NONE})}, + {"vrr-adjust", OPT_BOOL(vrr_adjust)}, + {"vrr-adjust-max-refresh-variance", OPT_DOUBLE(vrr_max_refresh_variance), M_RANGE(0.0, 1.0)}, + {"vrr-adjust-target-refresh-rate", OPT_DOUBLE(vrr_target_refresh_rate)}, #if HAVE_X11 {"x11-netwm", OPT_CHOICE(x11_netwm, {"auto", 0}, {"no", -1}, {"yes", 1})}, {"x11-bypass-compositor", OPT_CHOICE(x11_bypass_compositor, @@ -292,7 +297,10 @@ const struct m_sub_options vo_sub_opts = { .wl_present = true, .mmcss_profile = "Playback", .ontop_level = -1, + .minimum_display_fps = -1, .timing_offset = 0.050, + .vrr_max_refresh_variance = 1, + .vrr_target_refresh_rate = -1, .swapchain_depth = 2, .focus_on = 1, }, diff --git a/options/options.h b/options/options.h index 1524262379a66..a18d80b91dd6b 100644 --- a/options/options.h +++ b/options/options.h @@ -82,8 +82,12 @@ typedef struct mp_vo_opts { int window_corners; double display_fps_override; + double minimum_display_fps; double timing_offset; int video_sync; + bool vrr_adjust; + double vrr_max_refresh_variance; + double vrr_target_refresh_rate; struct m_geometry android_surface_size; diff --git a/player/command.c b/player/command.c index a0dccf4a37452..f50b93f99bbfb 100644 --- a/player/command.c +++ b/player/command.c @@ -765,7 +765,7 @@ static int mp_property_mistimed_frame_count(void *ctx, struct m_property *prop, int action, void *arg) { MPContext *mpctx = ctx; - if (!mpctx->vo_chain || !mpctx->display_sync_active) + if (!mpctx->vo_chain || !(mpctx->display_sync_active || mpctx->video_out->opts->vrr_adjust)) return M_PROPERTY_UNAVAILABLE; return m_property_int_ro(action, arg, mpctx->mistimed_frames_total); diff --git a/player/video.c b/player/video.c index e2cc4801f28a0..5fac138508dff 100644 --- a/player/video.c +++ b/player/video.c @@ -808,6 +808,11 @@ static void handle_display_sync_frame(struct MPContext *mpctx, mpctx->display_sync_active = false; + if (vo->opts->vrr_adjust && vo_get_pts_offset(vo) != 0) { + mpctx->mistimed_frames_total += 1; + MP_STATS(mpctx, "mistimed"); + } + if (!VS_IS_DISP(mode) || !vo_is_visible(vo)) return; diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c index 5150c900a782a..3602255995bb5 100644 --- a/video/out/gpu/video.c +++ b/video/out/gpu/video.c @@ -3596,7 +3596,7 @@ void gl_video_render_frame(struct gl_video *p, struct vo_frame *frame, // For the non-interpolation case, we draw to a single "cache" // texture to speed up subsequent re-draws (if any exist) - bool repeats = frame->num_vsyncs > 1 && frame->display_synced; + bool repeats = frame->request_repeat; bool r = false; if ((repeats || frame->still) && !p->dumb_mode && (p->ra->caps & RA_CAP_BLIT) && fbo->tex->params.blit_dst) diff --git a/video/out/vo.c b/video/out/vo.c index 8318a2ec6709d..8004a2e393737 100644 --- a/video/out/vo.c +++ b/video/out/vo.c @@ -156,8 +156,13 @@ struct vo_internal { bool expecting_vsync; int64_t num_successive_vsyncs; - int64_t flip_queue_offset; // queue flip events at most this much in advance - int64_t timing_offset; // same (but from options; not VO configured) + // queue flip events at most this much in advance, without changing the + // presentation time + int64_t flip_queue_offset; + // render this much in advance, without changing the flip event time + //(from options; not VO configured) + int64_t timing_offset; + double pts_offset; // for vrr int64_t delayed_count; int64_t drop_count; @@ -174,6 +179,12 @@ struct vo_internal { double display_fps; double reported_display_fps; + double minimum_display_time; // for vrr + double minimum_display_fps; //from options + double maximum_display_time; // for vrr + double vrr_target_refresh_rate; //from options + double vrr_target_refresh_time; + double prev_valid_duration; // for vrr struct stats_ctx *stats; }; @@ -391,6 +402,15 @@ static void reset_vsync_timings(struct vo *vo) in->num_successive_vsyncs = 0; } +//always called locked +static void discard_timing_info(struct vo *vo) +{ + struct vo_internal *in = vo->in; + in->pts_offset = 0; + in->prev_valid_duration = 0; + reset_vsync_timings(vo); +} + static double vsync_stddef(struct vo *vo, double ref_vsync) { struct vo_internal *in = vo->in; @@ -548,6 +568,7 @@ static void update_display_fps(struct vo *vo) in->reported_display_fps = fps; } + bool vrr_update = false; double display_fps = vo->opts->display_fps_override; if (display_fps <= 0) display_fps = in->reported_display_fps; @@ -557,6 +578,10 @@ static void update_display_fps(struct vo *vo) in->vsync_interval = MPMAX(in->nominal_vsync_interval, 1); in->display_fps = display_fps; + in->minimum_display_time = display_fps > 0 ? + MPMAX(MP_TIME_S_TO_NS(1) / display_fps, 1) : DBL_MAX; + vrr_update = true; + MP_VERBOSE(vo, "Assuming %f FPS for display sync.\n", display_fps); // make sure to update the player @@ -564,6 +589,44 @@ static void update_display_fps(struct vo *vo) wakeup_core(vo); } + double minimum_display_fps = vo->opts->minimum_display_fps; + + if (minimum_display_fps < 0) { + minimum_display_fps = display_fps; + } + + if (in->minimum_display_fps != minimum_display_fps || vrr_update) { + in->minimum_display_fps = minimum_display_fps; + + //even when minimum_display_fps does not change, we still recalculate for + //the minimum_display_time's MPMAX + in->maximum_display_time = minimum_display_fps > 0 ? + MPMAX(MP_TIME_S_TO_NS(1) / minimum_display_fps, in->minimum_display_time) : DBL_MAX; + + vrr_update = true; + } + + double vrr_target_refresh_rate = vo->opts->vrr_target_refresh_rate; + + if (vrr_target_refresh_rate != in->vrr_target_refresh_rate) { + in->vrr_target_refresh_rate = vrr_target_refresh_rate; + + if (vrr_target_refresh_rate < 0) { + vrr_update = true; + } + else { + in->vrr_target_refresh_time = vrr_target_refresh_rate > 0 ? + MPCLAMP(MP_TIME_S_TO_NS(1) / vrr_target_refresh_rate, in->minimum_display_time, in->maximum_display_time) + : in->maximum_display_time; + vrr_update = false; + } + } + + if (vrr_update) { + in->vrr_target_refresh_time = in->maximum_display_time < DBL_MAX ? + (in->minimum_display_time + in->maximum_display_time) / 2 : in->maximum_display_time; + } + mp_mutex_unlock(&in->lock); } @@ -625,7 +688,7 @@ static void run_reconfig(void *p) talloc_free(in->current_frame); in->current_frame = NULL; forget_frames(vo); - reset_vsync_timings(vo); + discard_timing_info(vo); mp_mutex_unlock(&in->lock); update_display_fps(vo); @@ -705,6 +768,7 @@ static void forget_frames(struct vo *vo) if (in->current_frame) { in->current_frame->num_vsyncs = 0; // but reset future repeats in->current_frame->display_synced = false; // mark discontinuity + in->current_frame->request_repeat = false; } } @@ -837,7 +901,7 @@ bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts) struct vo_internal *in = vo->in; mp_mutex_lock(&in->lock); bool r = vo->config_ok && !in->frame_queued && - (!in->current_frame || in->current_frame->num_vsyncs < 1); + (!in->current_frame || !in->current_frame->request_repeat); if (r && next_pts >= 0) { // Don't show the frame too early - it would basically freeze the // display by disallowing OSD redrawing or VO interaction. @@ -845,6 +909,7 @@ bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts) // time. next_pts -= in->timing_offset; next_pts -= in->flip_queue_offset; + next_pts += in->pts_offset; int64_t now = mp_time_ns(); if (next_pts > now) r = false; @@ -877,12 +942,10 @@ void vo_queue_frame(struct vo *vo, struct vo_frame *frame) struct vo_internal *in = vo->in; mp_mutex_lock(&in->lock); mp_assert(vo->config_ok && !in->frame_queued && - (!in->current_frame || in->current_frame->num_vsyncs < 1)); + (!in->current_frame || !in->current_frame->request_repeat)); in->hasframe = true; frame->frame_id = ++(in->current_frame_id); in->frame_queued = frame; - in->wakeup_pts = frame->display_synced - ? 0 : frame->pts + MPMAX(frame->duration, 0); wakeup_locked(vo); mp_mutex_unlock(&in->lock); } @@ -917,41 +980,160 @@ static bool render_frame(struct vo *vo) { struct vo_internal *in = vo->in; struct vo_frame *frame = NULL; + struct vo_frame *unmodified_frame = NULL; bool more_frames = false; update_display_fps(vo); mp_mutex_lock(&in->lock); - if (in->frame_queued) { + //if by odd chance we have a frame queued while request_repeat is true, + //prioritize ending request_repeat first, which may still have time to be validly + //displayed before frame_queued. + if (in->frame_queued && + !(vo->opts->vrr_adjust && in->current_frame && in->current_frame->request_repeat)) { talloc_free(in->current_frame); in->current_frame = in->frame_queued; in->frame_queued = NULL; - } else if (in->paused || !in->current_frame || !in->hasframe || - (in->current_frame->display_synced && in->current_frame->num_vsyncs < 1) || - !in->current_frame->display_synced) + } else if (in->paused || !in->current_frame || !in->hasframe || !in->current_frame->request_repeat) { goto done; } + //in->current_frame may be modified by vo. + //this below frame may be modified by vo and driver. + //unmodified_frame may not be modified. frame = vo_frame_ref(in->current_frame); + unmodified_frame = vo_frame_ref(in->current_frame); mp_assert(frame); - - if (frame->display_synced) { + mp_assert(unmodified_frame); + double unmodified_pts_offset = in->pts_offset; //storing for later + + if (frame->display_synced && !vo->opts->vrr_adjust) { frame->pts = 0; frame->duration = -1; } + //we special case negative frame->duration inputs to not drop frames. + //if this becomes true, we may later switch to false if needed. + in->dropped_frame = frame->duration >= 0; + + //we are assuming valid frame inputs should always have >= 0 frame->duration, + //except the above special case. so now make sure it's non-negative so that + //it doesn't mess up future calculations. + frame->duration = MPMAX(frame->duration, 0); + + //we adjust the pts, while maintaining the end time the same. + frame->pts += in->pts_offset; + frame->duration -= in->pts_offset; + + in->current_frame->request_repeat = false; int64_t now = mp_time_ns(); - int64_t pts = frame->pts; - int64_t duration = frame->duration; - int64_t end_time = pts + duration; + if (now > frame->pts) { + //if time has moved past our starting position, then this reduces our + //expected duration. even if frame->duration becomes negative now, we allow frame drops. + //for purposes of vrr_max_refresh_variance, we do not update prev_valid_duration, + //as we only consider valid duration the moment it was given to the driver. + double offset = now-frame->pts; + frame->duration -= offset; + frame->pts = now; + } + + if (frame->duration <= 0) { + //move next frame to current position. this helps maintain the previously + //defined valid pts_offset. + in->pts_offset = -frame->duration; + } + else if (vo->opts->vrr_adjust){ + double minimum_display_time = in->minimum_display_time; + double maximum_display_time = in->maximum_display_time; + double target_refresh_time = in->vrr_target_refresh_time; + + if (vo->opts->vrr_max_refresh_variance < 1 && in->prev_valid_duration > 0) { + minimum_display_time = MPCLAMP(in->prev_valid_duration * (1 - vo->opts->vrr_max_refresh_variance), minimum_display_time, maximum_display_time); + maximum_display_time = MPCLAMP(in->prev_valid_duration / (1 - vo->opts->vrr_max_refresh_variance), minimum_display_time, maximum_display_time); + //we also meed to clamp target_refresh_time else we end up forcefully + //pushing towards it even when the clamped thresholds would cause us + //to miss our target. + //we will still slowly push towards it each time we do this clamp. + target_refresh_time = MPCLAMP(target_refresh_time, minimum_display_time, maximum_display_time); + } + + for (int i = 0; i <= 1; i++) { + //we put the effort into always reaching target_refresh_time positions, + //which gives us some error leeway in case there are random application + //or os delays that would cause us to go beyond our pts. + double targetDuration = frame->duration; + + //get the first target_refresh_time at i == 0, or the next at i == 1. + //we could perform this just once at i == 0 and then do a + target_refresh_time + //at i == 1, however that may cause loss of precision. + //since target_refresh_time is higher or equal to minimum_display_time, this + //guarantees i == 1 is always higher than minimum_display_time, which is + //our only significant threshold. + targetDuration -= (floor(frame->duration / target_refresh_time) - i) * target_refresh_time; + + bool newFrameRepeat = targetDuration != frame->duration; + + if (targetDuration < minimum_display_time) { + //move the next frame towards the closest valid position, to reduce any stuttering effects. + //this also benefits when the next frame is repeating, as it puts us closer to + //our target_refresh_time. + //we have to avoid reaching this point as much as reasonable. + //the lrint should return either 0 or 1, so the result is either 0 or in->minimum_display_time + targetDuration = lrint(targetDuration / minimum_display_time) * minimum_display_time; + } + else if (targetDuration > maximum_display_time) { + //reaching target_refresh_time positions should avoid us getting in this position, + //unless the frame is repeating. + //using lrint to get the closest full divisible of duration by target_refresh_time + //and capping it appropriatelly. + + //divisions should never be smaller than 1 + int64_t divisions = lrint(targetDuration / target_refresh_time); + targetDuration = MPCLAMP(targetDuration / divisions, minimum_display_time, maximum_display_time); + newFrameRepeat = true; + } + else { + //targetDuration should be perfectly fine in range + } + + if (targetDuration <= 0) { + if (!newFrameRepeat) { + //drop this frame and get the next frameQueued. + goto endLoop; + } + else { + //we only get here when the duration is lower than the minimum_display_time. + //so we loop back around to increase the targetDuration range that we consider, + //which should add an additional target_refresh_time, which should guarantee + //it is no longer lower than the minimum_display_time, so we only get here + //when i == 0 + mp_assert(i == 0); + } + } + else { + //we good + in->current_frame->request_repeat |= newFrameRepeat; + goto endLoop; + } + + continue; + endLoop: + in->pts_offset = targetDuration - frame->duration; + frame->duration = targetDuration; + break; + } + } + + //don't think there is a need to worry about any issues with pts or duration being negative + int64_t end_time = frame->pts + frame->duration; // Time at which we should flip_page on the VO. - int64_t target = frame->display_synced ? 0 : pts - in->flip_queue_offset; + int64_t target = frame->display_synced ? 0 : frame->pts - in->flip_queue_offset; // "normal" strict drop threshold. - in->dropped_frame = duration >= 0 && end_time < now; + in->dropped_frame &= frame->duration <= 0; in->dropped_frame &= !frame->display_synced; in->dropped_frame &= !(vo->driver->caps & VO_CAP_FRAMEDROP); @@ -974,8 +1156,14 @@ static bool render_frame(struct vo *vo) } in->dropped_frame |= in->current_frame->num_vsyncs < 1; } - if (in->current_frame->num_vsyncs > 0) + + if (in->current_frame->num_vsyncs > 0) { in->current_frame->num_vsyncs -= 1; + in->current_frame->request_repeat |= !!in->current_frame->num_vsyncs; + } + + //also update on this frame to let the driver know we will repeat + frame->request_repeat = in->current_frame->request_repeat; // Always render when paused (it's typically the last frame for a while). in->dropped_frame &= !in->paused; @@ -987,18 +1175,32 @@ static bool render_frame(struct vo *vo) // Store the initial value before we unlock. bool request_redraw = in->request_redraw; + bool current_controlled_drop = false; + + if (vo->opts->vrr_adjust && in->dropped_frame && frame->repeat) { + in->dropped_frame = false; + current_controlled_drop = true; + } + + bool driver_has_received_frame = false; + bool driver_dropped_frame = false; + + if (in->dropped_frame || current_controlled_drop) { + //do not log vrr repeat frame drops + if (in->dropped_frame) + in->drop_count += 1; - if (in->dropped_frame) { - in->drop_count += 1; wakeup_core(vo); } else { + double unmodified_prev_valid_duration = in->prev_valid_duration; + in->prev_valid_duration = frame->duration; in->rendering = true; in->hasframe_rendered = true; int64_t prev_drop_count = vo->in->drop_count; // Can the core queue new video now? Non-display-sync uses a separate // timer instead, but possibly benefits from preparing a frame early. bool can_queue = !in->frame_queued && - (in->current_frame->num_vsyncs < 1 || !use_vsync); + (!in->current_frame->request_repeat || in->paused); mp_mutex_unlock(&in->lock); if (can_queue) @@ -1007,6 +1209,7 @@ static bool render_frame(struct vo *vo) stats_time_start(in->stats, "video-draw"); in->visible = vo->driver->draw_frame(vo, frame); + driver_has_received_frame = true; stats_time_end(in->stats, "video-draw"); @@ -1030,19 +1233,42 @@ static bool render_frame(struct vo *vo) stats_time_end(in->stats, "video-flip"); mp_mutex_lock(&in->lock); - in->dropped_frame = prev_drop_count < vo->in->drop_count; + //if in->drop_count increases, assumption is that the current frame may have been dropped + driver_dropped_frame = prev_drop_count < vo->in->drop_count; + //if multiple frames have been dropped, always set to true. + in->dropped_frame = in->drop_count - prev_drop_count > 1; + + //we might still have valid time to output the current frame even after, for whatever + //reason, the driver has dropped it, so retry. we won't be retrying forever + //since it will become old and vo will properly drop it to go next. + if (vo->opts->vrr_adjust && in->current_frame && driver_dropped_frame) { + talloc_free(in->current_frame); + //reverting timing info + in->current_frame = unmodified_frame; + unmodified_frame = NULL; + in->current_frame->request_repeat = true; + in->pts_offset = unmodified_pts_offset; + in->prev_valid_duration = unmodified_prev_valid_duration; + in->drop_count -= 1; + current_controlled_drop = true; + } + else { + in->dropped_frame = driver_dropped_frame; + } + in->rendering = false; update_vsync_timing_after_swap(vo, &vsync); } - if (vo->driver->caps & VO_CAP_NORETAIN) { + if (vo->driver->caps & VO_CAP_NORETAIN && in->current_frame) { talloc_free(in->current_frame); in->current_frame = NULL; } - if (in->dropped_frame) { - MP_STATS(vo, "drop-vo"); + if (in->dropped_frame || current_controlled_drop) { + if (in->dropped_frame) + MP_STATS(vo, "drop-vo"); } else { // If the initial redraw request was true and mpv is still playing, // then we can clear it here since the next loop will guarantee that @@ -1054,9 +1280,14 @@ static bool render_frame(struct vo *vo) in->request_redraw = false; } - if (in->current_frame && in->current_frame->num_vsyncs && - in->current_frame->display_synced) + if (in->current_frame && in->current_frame->request_repeat) { more_frames = true; + //set it to 0 while repeating + in->wakeup_pts = 0; + } + else { + in->wakeup_pts = end_time; + } if (in->frame_queued && in->frame_queued->display_synced) more_frames = true; @@ -1064,8 +1295,13 @@ static bool render_frame(struct vo *vo) mp_cond_broadcast(&in->wakeup); // for vo_wait_frame() done: - if (!(vo->driver->caps & VO_CAP_FRAMEOWNER) || in->dropped_frame) + //why do we need to check for when the driver has dropped frame if we are not the frameowner? + if (!(vo->driver->caps & VO_CAP_FRAMEOWNER) || !driver_has_received_frame || driver_dropped_frame) talloc_free(frame); + + if (unmodified_frame) + talloc_free(unmodified_frame); + mp_mutex_unlock(&in->lock); return more_frames; @@ -1232,7 +1468,7 @@ void vo_set_paused(struct vo *vo, bool paused) in->request_redraw = true; wakeup_core(vo); } - reset_vsync_timings(vo); + discard_timing_info(vo); wakeup_locked(vo); } mp_mutex_unlock(&in->lock); @@ -1280,7 +1516,7 @@ void vo_seek_reset(struct vo *vo) struct vo_internal *in = vo->in; mp_mutex_lock(&in->lock); forget_frames(vo); - reset_vsync_timings(vo); + discard_timing_info(vo); in->send_reset = true; wakeup_locked(vo); mp_mutex_unlock(&in->lock); @@ -1392,11 +1628,20 @@ double vo_get_delay(struct vo *vo) return res ? MP_TIME_NS_TO_S(res - mp_time_ns()) : 0; } +double vo_get_pts_offset(struct vo *vo) +{ + struct vo_internal *in = vo->in; + mp_mutex_lock(&in->lock); + double res = in->pts_offset; + mp_mutex_unlock(&in->lock); + return res; +} + void vo_discard_timing_info(struct vo *vo) { struct vo_internal *in = vo->in; mp_mutex_lock(&in->lock); - reset_vsync_timings(vo); + discard_timing_info(vo); mp_mutex_unlock(&in->lock); } diff --git a/video/out/vo.h b/video/out/vo.h index 8e397618eb911..09ccfbd79e11b 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -255,6 +255,8 @@ struct vo_frame { // A repeat frame can be redrawn, in which case repeat==redraw==true, and // OSD should be updated. bool redraw, repeat; + // Set to request the vo to continue repeating the frame. + bool request_repeat; // The frame is not in movement - e.g. redrawing while paused. bool still; // Frames are output as fast as possible, with implied vsync blocking. @@ -557,6 +559,7 @@ double vo_get_estimated_vsync_jitter(struct vo *vo); double vo_get_display_fps(struct vo *vo); void * vo_get_display_swapchain(struct vo *vo); double vo_get_delay(struct vo *vo); +double vo_get_pts_offset(struct vo *vo); void vo_discard_timing_info(struct vo *vo); struct vo_frame *vo_get_current_vo_frame(struct vo *vo); struct mp_image *vo_get_image(struct vo *vo, int imgfmt, int w, int h, diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index 87d6610600263..f49652fb1039d 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -1224,7 +1224,7 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame) struct pl_render_params params = pars->params; const struct gl_video_opts *opts = p->opts_cache->opts; - bool will_redraw = frame->display_synced && frame->num_vsyncs > 1; + bool will_redraw = frame->request_repeat; bool cache_frame = will_redraw || frame->still || p->paused; bool can_interpolate = opts->interpolation && frame->display_synced && !frame->still && frame->num_frames > 1 && !p->paused;