diff options
-rw-r--r-- | DOCS/man/vo.rst | 9 | ||||
-rw-r--r-- | video/out/gl_video.c | 139 | ||||
-rw-r--r-- | video/out/gl_video.h | 5 | ||||
-rw-r--r-- | video/out/gl_video_shaders.glsl | 14 | ||||
-rw-r--r-- | video/out/vo.c | 52 | ||||
-rw-r--r-- | video/out/vo.h | 13 | ||||
-rw-r--r-- | video/out/vo_opengl.c | 25 | ||||
-rw-r--r-- | video/out/vo_opengl_cb.c | 2 |
8 files changed, 239 insertions, 20 deletions
diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst index 6190526bd7..e0845d0e54 100644 --- a/DOCS/man/vo.rst +++ b/DOCS/man/vo.rst @@ -572,6 +572,15 @@ Available video output drivers are: Color used to draw parts of the mpv window not covered by video. See ``--osd-color`` option how colors are defined. + ``smoothmotion`` + Use frame interpolation to reduce stuttering caused by mismatches in + video fps and display refresh rate (similar to MadVR's smoothmotion, + thus the naming). + + ``smoothmotion-threshold=<threshold>`` + Mix threshold at which interpolation is skipped (default: 0.0 – never + skip). + ``opengl-hq`` Same as ``opengl``, but with default settings for high quality rendering. diff --git a/video/out/gl_video.c b/video/out/gl_video.c index 7a40338ac1..2eeb86f353 100644 --- a/video/out/gl_video.c +++ b/video/out/gl_video.c @@ -122,6 +122,13 @@ struct fbotex { int vp_x, vp_y, vp_w, vp_h; // viewport of fbo / used part of the texture }; +struct fbosurface { + struct fbotex fbotex; + int64_t pts; +}; + +#define FBOSURFACES_MAX 2 + struct gl_video { GL *gl; @@ -139,7 +146,7 @@ struct gl_video { GLuint vao; GLuint osd_programs[SUBBITMAP_COUNT]; - GLuint indirect_program, scale_sep_program, final_program; + GLuint indirect_program, scale_sep_program, final_program, inter_program; struct osd_state *osd_state; struct mpgl_osd *osd; @@ -176,6 +183,9 @@ struct gl_video { struct fbotex indirect_fbo; // RGB target struct fbotex scale_sep_fbo; // first pass when doing 2 pass scaling + struct fbotex inter_fbo; // interpolation target + struct fbosurface surfaces[FBOSURFACES_MAX]; + size_t surface_num; // state for luma (0) and chroma (1) scalers struct scaler scalers[2]; @@ -402,7 +412,9 @@ const struct m_sub_options gl_video_conf = { {"blend", 2})), OPT_FLAG("rectangle-textures", use_rectangle, 0), OPT_COLOR("background", background, 0), - + OPT_FLAG("smoothmotion", smoothmotion, 0), + OPT_FLOAT("smoothmotion-threshold", smoothmotion_threshold, + CONF_RANGE, .min = 0, .max = 0.5), OPT_REMOVED("approx-gamma", "this is always enabled now"), OPT_REMOVED("cscale-down", "chroma is never downscaled"), OPT_REMOVED("scale-sep", "this is set automatically whenever sane"), @@ -649,6 +661,27 @@ static void fbotex_uninit(struct gl_video *p, struct fbotex *fbo) } } +static void fbosurfaces_uninit(struct gl_video *p, struct fbosurface *surfaces) +{ + + for (int i = 0; i < FBOSURFACES_MAX; i++) + if (surfaces[i].fbotex.fbo) + fbotex_uninit(p, &surfaces[i].fbotex); +} + +static void fbosurfaces_init(struct gl_video *p, struct fbosurface *surfaces, + int w, int h, GLenum iformat) +{ + for (int i = 0; i < FBOSURFACES_MAX; i++) + if (!surfaces[i].fbotex.fbo) + fbotex_init(p, &surfaces[i].fbotex, w, h, iformat); +} + +static size_t fbosurface_next(struct gl_video *p) +{ + return (p->surface_num + 1) % FBOSURFACES_MAX; +} + static void matrix_ortho2d(float m[3][3], float x0, float x1, float y0, float y1) { @@ -831,6 +864,7 @@ static void update_all_uniforms(struct gl_video *p) update_uniforms(p, p->indirect_program); update_uniforms(p, p->scale_sep_program); update_uniforms(p, p->final_program); + update_uniforms(p, p->inter_program); } #define SECTION_HEADER "#!section " @@ -1169,6 +1203,7 @@ static void compile_shaders(struct gl_video *p) char *header_conv = talloc_strdup(tmp, ""); char *header_final = talloc_strdup(tmp, ""); + char *header_inter = talloc_strdup(tmp, ""); char *header_sep = NULL; if (p->image_desc.id == IMGFMT_NV12 || p->image_desc.id == IMGFMT_NV21) { @@ -1211,12 +1246,20 @@ static void compile_shaders(struct gl_video *p) shader_setup_scaler(&header_final, &p->scalers[0], -1); } + bool use_interpolation = p->opts.smoothmotion; + + if (use_interpolation) { + shader_def_opt(&header_inter, "FIXED_SCALE", true); + shader_def_opt(&header_inter, "USE_LINEAR_INTERPOLATION", 1); + } + // The indirect pass is used to preprocess the image before scaling. bool use_indirect = false; // Don't sample from input video textures before converting the input to // its proper gamma. - if (use_input_gamma || use_conv_gamma || use_linear_light || use_const_luma) + if (use_input_gamma || use_conv_gamma || use_linear_light || + use_const_luma || use_interpolation) use_indirect = true; // Trivial scalers are implemented directly and efficiently by the GPU. @@ -1261,6 +1304,12 @@ static void compile_shaders(struct gl_video *p) create_program(p, "scale_sep", header_sep, vertex_shader, s_video); } + if (use_interpolation) { + header_inter = t_concat(tmp, header, header_inter); + p->inter_program = + create_program(p, "inter", header_inter, vertex_shader, s_video); + } + header_final = t_concat(tmp, header, header_final); p->final_program = create_program(p, "final", header_final, vertex_shader, s_video); @@ -1285,6 +1334,7 @@ static void delete_shaders(struct gl_video *p) delete_program(gl, &p->indirect_program); delete_program(gl, &p->scale_sep_program); delete_program(gl, &p->final_program); + delete_program(gl, &p->inter_program); } static void get_scale_factors(struct gl_video *p, double xy[2]) @@ -1528,6 +1578,14 @@ static void reinit_rendering(struct gl_video *p) if (p->indirect_program && !p->indirect_fbo.fbo) fbotex_init(p, &p->indirect_fbo, w, h, p->opts.fbo_format); + if (p->inter_program && !p->inter_fbo.fbo) { + fbotex_init(p, &p->inter_fbo, w, h, p->opts.fbo_format); + } + + if (p->inter_program) { + fbosurfaces_init(p, p->surfaces, w, h, p->opts.fbo_format); + } + recreate_osd(p); } @@ -1733,8 +1791,10 @@ static void uninit_video(struct gl_video *p) } mp_image_unrefp(&vimg->mpi); + fbotex_uninit(p, &p->inter_fbo); fbotex_uninit(p, &p->indirect_fbo); fbotex_uninit(p, &p->scale_sep_fbo); + fbosurfaces_uninit(p, p->surfaces); // Invalidate image_params to ensure that gl_video_config() will call // init_video() on uninitialized gl_video. @@ -1819,8 +1879,54 @@ static void handle_pass(struct gl_video *p, struct pass *chain, }; } +static void gl_video_interpolate_frame(struct gl_video *p, + struct pass *chain, + struct frame_timing *t) +{ + GL *gl = p->gl; + double inter_coeff = 0.0; + int64_t prev_pts = p->surfaces[fbosurface_next(p)].pts; + + if (prev_pts < t->pts) { + MP_STATS(p, "new-pts"); + // fbosurface 0 is already bound from the caller + p->surfaces[p->surface_num].pts = t->pts; + p->surface_num = fbosurface_next(p); + gl->ActiveTexture(GL_TEXTURE0 + 1); + gl->BindTexture(p->gl_target, p->surfaces[p->surface_num].fbotex.texture); + gl->ActiveTexture(GL_TEXTURE0); + MP_DBG(p, "frame ppts: %lld, pts: %lld, vsync: %lld, DIFF: %lld\n", + (long long)prev_pts, (long long)t->pts, + (long long)t->next_vsync, (long long)t->next_vsync - t->pts); + if (prev_pts < t->next_vsync && t->pts > t->next_vsync) { + double N = t->next_vsync - prev_pts; + double P = t->pts - prev_pts; + double prev_pts_component = N / P; + float ts = p->opts.smoothmotion_threshold; + inter_coeff = 1 - prev_pts_component; + inter_coeff = inter_coeff < 0.0 + ts ? 0.0 : inter_coeff; + inter_coeff = inter_coeff > 1.0 - ts ? 1.0 : inter_coeff; + MP_DBG(p, "inter frame ppts: %lld, pts: %lld, " + "vsync: %lld, mix: %f\n", + (long long)prev_pts, (long long)t->pts, + (long long)t->next_vsync, inter_coeff); + MP_STATS(p, "frame-mix"); + + // the value is scaled to fit in the graph with the completely + // unrelated "phase" value (which is stupid) + MP_STATS(p, "value-timed %lld %f mix-value", + (long long)t->pts, inter_coeff * 10000); + } + } + + gl->UseProgram(p->inter_program); + GLint loc = gl->GetUniformLocation(p->inter_program, "inter_coeff"); + gl->Uniform1f(loc, inter_coeff); + handle_pass(p, chain, &p->inter_fbo, p->inter_program); +} + // (fbo==0 makes BindFramebuffer select the screen backbuffer) -void gl_video_render_frame(struct gl_video *p, int fbo) +void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t) { GL *gl = p->gl; struct video_image *vimg = &p->image; @@ -1844,7 +1950,7 @@ void gl_video_render_frame(struct gl_video *p, int fbo) } // Order of processing: - // [indirect -> [scale_sep ->]] final + // [indirect -> [interpolate -> [scale_sep ->]]] final GLuint imgtex[4] = {0}; set_image_textures(p, vimg, imgtex); @@ -1859,7 +1965,18 @@ void gl_video_render_frame(struct gl_video *p, int fbo) }, }; - handle_pass(p, &chain, &p->indirect_fbo, p->indirect_program); + int64_t prev_pts = p->surfaces[fbosurface_next(p)].pts; + struct fbotex *indirect_target; + if (p->inter_program && t && prev_pts < t->pts) { + indirect_target = &p->surfaces[p->surface_num].fbotex; + } else { + indirect_target = &p->indirect_fbo; + } + + handle_pass(p, &chain, indirect_target, p->indirect_program); + + if (t && p->inter_program) + gl_video_interpolate_frame(p, &chain, t); // Clip to visible height so that separate scaling scales the visible part // only (and the target FBO texture can have a bounded size). @@ -2403,6 +2520,14 @@ void gl_video_unset_gl_state(struct gl_video *p) } } +void gl_video_reset(struct gl_video *p) +{ + for (int i = 0; i < FBOSURFACES_MAX; i++) { + p->surfaces[i].pts = 0; + } + p->surface_num = 0; +} + // dest = src.<w> (always using 4 components) static void packed_fmt_swizzle(char w[5], const struct fmt_entry *texfmt, const struct packed_fmt_entry *fmt) @@ -2697,7 +2822,7 @@ void gl_video_resize_redraw(struct gl_video *p, int w, int h) { p->vp_w = w; p->vp_h = h; - gl_video_render_frame(p, 0); + gl_video_render_frame(p, 0, NULL); } void gl_video_set_hwdec(struct gl_video *p, struct gl_hwdec *hwdec) diff --git a/video/out/gl_video.h b/video/out/gl_video.h index b287658bef..f62a292785 100644 --- a/video/out/gl_video.h +++ b/video/out/gl_video.h @@ -53,6 +53,8 @@ struct gl_video_opts { int chroma_location; int use_rectangle; struct m_color background; + int smoothmotion; + float smoothmotion_threshold; }; extern const struct m_sub_options gl_video_conf; @@ -69,7 +71,7 @@ void gl_video_config(struct gl_video *p, struct mp_image_params *params); void gl_video_set_output_depth(struct gl_video *p, int r, int g, int b); void gl_video_set_lut3d(struct gl_video *p, struct lut3d *lut3d); void gl_video_upload_image(struct gl_video *p, struct mp_image *img); -void gl_video_render_frame(struct gl_video *p, int fbo); +void gl_video_render_frame(struct gl_video *p, int fbo, struct frame_timing *t); struct mp_image *gl_video_download_image(struct gl_video *p); void gl_video_resize(struct gl_video *p, struct mp_rect *window, struct mp_rect *src, struct mp_rect *dst, @@ -84,6 +86,7 @@ void gl_video_resize_redraw(struct gl_video *p, int w, int h); void gl_video_set_gl_state(struct gl_video *p); void gl_video_unset_gl_state(struct gl_video *p); +void gl_video_reset(struct gl_video *p); struct gl_hwdec; void gl_video_set_hwdec(struct gl_video *p, struct gl_hwdec *hwdec); diff --git a/video/out/gl_video_shaders.glsl b/video/out/gl_video_shaders.glsl index e40a94e185..c9398c8cd5 100644 --- a/video/out/gl_video_shaders.glsl +++ b/video/out/gl_video_shaders.glsl @@ -190,6 +190,7 @@ uniform float filter_param1_l; uniform float filter_param1_c; uniform float antiring_factor; uniform vec2 dither_size; +uniform float inter_coeff; in vec2 texcoord; DECLARE_FRAGPARMS @@ -374,7 +375,18 @@ void main() { #ifndef USE_CONV #define USE_CONV 0 #endif -#if USE_CONV == CONV_PLANAR +#ifndef USE_LINEAR_INTERPOLATION +#define USE_LINEAR_INTERPOLATION 0 +#endif +#if USE_LINEAR_INTERPOLATION == 1 + vec4 acolor = mix( + texture(texture0, texcoord), + texture(texture1, texcoord), + inter_coeff); + // debug code to visually check the interpolation amount + // vec4 acolor = texture(texture0, texcoord) - + // inter_coeff * texture(texture1, texcoord); +#elif USE_CONV == CONV_PLANAR vec4 acolor = vec4(SAMPLE(texture0, textures_size[0], texcoord).r, SAMPLE_C(texture1, textures_size[1], chr_texcoord).r, SAMPLE_C(texture2, textures_size[2], chr_texcoord).r, diff --git a/video/out/vo.c b/video/out/vo.c index 0d135c2842..943e99d246 100644 --- a/video/out/vo.c +++ b/video/out/vo.c @@ -127,6 +127,8 @@ struct vo_internal { bool want_redraw; // redraw request from VO to player bool send_reset; // send VOCTRL_RESET bool paused; + bool vsync_timed; // the VO redraws itself as fast as possible + // at every vsync int queued_events; int64_t flip_queue_offset; // queue flip events at most this much in advance @@ -144,6 +146,7 @@ struct vo_internal { // --- The following fields can be accessed from the VO thread only int64_t vsync_interval; + int64_t vsync_interval_approx; int64_t last_flip; char *window_title; }; @@ -486,7 +489,7 @@ bool vo_is_ready_for_frame(struct vo *vo, int64_t next_pts) if (next_pts > now) r = false; if (!in->wakeup_pts || next_pts < in->wakeup_pts) { - in->wakeup_pts = next_pts; + in->wakeup_pts = in->vsync_timed ? 0 : next_pts; wakeup_locked(vo); } } @@ -507,7 +510,7 @@ void vo_queue_frame(struct vo *vo, struct mp_image *image, in->frame_queued = image; in->frame_pts = pts_us; in->frame_duration = duration; - in->wakeup_pts = in->frame_pts + MPMAX(duration, 0); + in->wakeup_pts = in->vsync_timed ? 0 : in->frame_pts + MPMAX(duration, 0); wakeup_locked(vo); pthread_mutex_unlock(&in->lock); } @@ -545,7 +548,13 @@ static bool render_frame(struct vo *vo) int64_t pts = in->frame_pts; int64_t duration = in->frame_duration; struct mp_image *img = in->frame_queued; - if (!img) { + + if (!img && (!in->vsync_timed || in->paused || pts <= 0)) { + pthread_mutex_unlock(&in->lock); + return false; + } + + if (in->vsync_timed && !in->hasframe) { pthread_mutex_unlock(&in->lock); return false; } @@ -556,7 +565,8 @@ static bool render_frame(struct vo *vo) in->frame_queued = NULL; // The next time a flip (probably) happens. - int64_t next_vsync = prev_sync(vo, mp_time_us()) + in->vsync_interval; + int64_t prev_vsync = prev_sync(vo, mp_time_us()); + int64_t next_vsync = prev_vsync + in->vsync_interval; int64_t end_time = pts + duration; if (!in->hasframe_rendered) @@ -568,6 +578,18 @@ static bool render_frame(struct vo *vo) // Even if we're hopelessly behind, rather degrade to 10 FPS playback, // instead of just freezing the display forever. in->dropped_frame &= mp_time_us() - in->last_flip < 100 * 1000; + in->dropped_frame &= in->vsync_timed && !!img; + + if (in->vsync_timed && !img && in->hasframe_rendered && + prev_vsync > pts + in->vsync_interval_approx) { + // we are very late with the frame and using vsync timing: probably + // no new frames are coming in. This must be done whether or not + // framedrop is enabled. + in->dropped_frame = false; + in->rendering = false; + pthread_mutex_unlock(&in->lock); + return false; + } if (in->dropped_frame) { in->dropped_image = img; @@ -578,9 +600,21 @@ static bool render_frame(struct vo *vo) MP_STATS(vo, "start video"); - vo->driver->draw_image(vo, img); + if (in->vsync_timed) { + struct frame_timing t = (struct frame_timing) { + .pts = pts, + .next_vsync = next_vsync, + }; + vo->driver->draw_image_timed(vo, img, &t); + } else { + vo->driver->draw_image(vo, img); + } - int64_t target = pts - in->flip_queue_offset; + int64_t target = !in->vsync_timed ? + pts - in->flip_queue_offset : + // this is a heuristic that wakes the thread up some + // time before the next vsync + next_vsync - MPMIN(in->vsync_interval / 3, 4e3); while (1) { int64_t now = mp_time_us(); if (target <= now) @@ -594,6 +628,8 @@ static bool render_frame(struct vo *vo) else vo->driver->flip_page(vo); + int64_t prev_flip = in->last_flip; + in->last_flip = -1; vo->driver->control(vo, VOCTRL_GET_RECENT_FLIP_TIME, &in->last_flip); @@ -601,10 +637,13 @@ static bool render_frame(struct vo *vo) if (in->last_flip < 0) in->last_flip = mp_time_us(); + in->vsync_interval_approx = in->last_flip - prev_flip; + long phase = in->last_flip % in->vsync_interval; MP_DBG(vo, "phase: %ld\n", phase); MP_STATS(vo, "value %ld phase", phase); + MP_STATS(vo, "display"); MP_STATS(vo, "end video"); pthread_mutex_lock(&in->lock); @@ -669,6 +708,7 @@ static void *vo_thread(void *ptr) mpthread_set_name("vo"); int r = vo->driver->preinit(vo) ? -1 : 0; + vo->driver->control(vo, VOCTRL_GET_VSYNC_TIMED, &in->vsync_timed); mp_rendezvous(vo, r); // init barrier if (r < 0) return NULL; diff --git a/video/out/vo.h b/video/out/vo.h index d943d4ad78..33a2513c1f 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -106,6 +106,8 @@ enum mp_voctrl { VOCTRL_GET_RECENT_FLIP_TIME, // int64_t* (using mp_time_us()) VOCTRL_GET_PREF_DEINT, // int* + + VOCTRL_GET_VSYNC_TIMED, // bool* }; // VOCTRL_SET_EQUALIZER @@ -171,6 +173,11 @@ struct vo_extra { struct mpv_opengl_cb_context *opengl_cb_context; }; +struct frame_timing { + int64_t pts; + int64_t next_vsync; +}; + struct vo_driver { // Encoding functionality, which can be invoked via --o only. bool encode; @@ -218,6 +225,12 @@ struct vo_driver { */ void (*draw_image)(struct vo *vo, struct mp_image *mpi); + /* Like draw image, but is called before every vsync with timing + * information + */ + void (*draw_image_timed)(struct vo *vo, struct mp_image *mpi, + struct frame_timing *t); + /* * Blit/Flip buffer to the screen. Must be called after each frame! */ diff --git a/video/out/vo_opengl.c b/video/out/vo_opengl.c index 058b305c2f..d3f1e7da7d 100644 --- a/video/out/vo_opengl.c +++ b/video/out/vo_opengl.c @@ -154,7 +154,8 @@ static void flip_page(struct vo *vo) mpgl_unlock(p->glctx); } -static void draw_image(struct vo *vo, mp_image_t *mpi) +static void draw_image_timed(struct vo *vo, mp_image_t *mpi, + struct frame_timing *t) { struct gl_priv *p = vo->priv; GL *gl = p->gl; @@ -164,8 +165,9 @@ static void draw_image(struct vo *vo, mp_image_t *mpi) mpgl_lock(p->glctx); - gl_video_upload_image(p->renderer, mpi); - gl_video_render_frame(p->renderer, 0); + if (mpi) + gl_video_upload_image(p->renderer, mpi); + gl_video_render_frame(p->renderer, 0, t); // The playloop calls this last before waiting some time until it decides // to call flip_page(). Tell OpenGL to start execution of the GPU commands @@ -178,6 +180,11 @@ static void draw_image(struct vo *vo, mp_image_t *mpi) mpgl_unlock(p->glctx); } +static void draw_image(struct vo *vo, mp_image_t *mpi) +{ + draw_image_timed(vo, mpi, NULL); +} + static int query_format(struct vo *vo, int format) { struct gl_priv *p = vo->priv; @@ -361,13 +368,21 @@ static int control(struct vo *vo, uint32_t request, void *data) return true; case VOCTRL_REDRAW_FRAME: mpgl_lock(p->glctx); - gl_video_render_frame(p->renderer, 0); + gl_video_render_frame(p->renderer, 0, NULL); mpgl_unlock(p->glctx); return true; case VOCTRL_SET_COMMAND_LINE: { char *arg = data; return reparse_cmdline(p, arg); } + case VOCTRL_GET_VSYNC_TIMED: + *(bool *)data = p->renderer_opts->smoothmotion; + return VO_TRUE; + case VOCTRL_RESET: + mpgl_lock(p->glctx); + gl_video_reset(p->renderer); + mpgl_unlock(p->glctx); + return true; } mpgl_lock(p->glctx); @@ -474,6 +489,7 @@ const struct vo_driver video_out_opengl = { .reconfig = reconfig, .control = control, .draw_image = draw_image, + .draw_image_timed = draw_image_timed, .flip_page = flip_page, .uninit = uninit, .priv_size = sizeof(struct gl_priv), @@ -489,6 +505,7 @@ const struct vo_driver video_out_opengl_hq = { .reconfig = reconfig, .control = control, .draw_image = draw_image, + .draw_image_timed = draw_image_timed, .flip_page = flip_page, .uninit = uninit, .priv_size = sizeof(struct gl_priv), diff --git a/video/out/vo_opengl_cb.c b/video/out/vo_opengl_cb.c index 1a2e44f718..091a376dfa 100644 --- a/video/out/vo_opengl_cb.c +++ b/video/out/vo_opengl_cb.c @@ -339,7 +339,7 @@ int mpv_opengl_cb_render(struct mpv_opengl_cb_context *ctx, int fbo, int vp[4]) if (mpi) gl_video_upload_image(ctx->renderer, mpi); - gl_video_render_frame(ctx->renderer, fbo); + gl_video_render_frame(ctx->renderer, fbo, NULL); gl_video_unset_gl_state(ctx->renderer); |