summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--DOCS/man/vo.rst9
-rw-r--r--video/out/gl_video.c139
-rw-r--r--video/out/gl_video.h5
-rw-r--r--video/out/gl_video_shaders.glsl14
-rw-r--r--video/out/vo.c52
-rw-r--r--video/out/vo.h13
-rw-r--r--video/out/vo_opengl.c25
-rw-r--r--video/out/vo_opengl_cb.c2
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);