Skip to main content

jellytau_lib/player/
mpv_render.rs

1//! mpv's render API, driven into an OpenGL framebuffer we own.
2//!
3//! This is the half of native video that is not GTK: create a render context
4//! over the mpv handle the audio backend already drives, render a frame into a
5//! texture, and hand that texture id back for the toolkit to composite.
6//!
7//! Kept apart from `video_surface` deliberately — everything here is portable
8//! across the platforms this app targets, while the surface that consumes it is
9//! not. Windows reuses this file unchanged (DR-237).
10//!
11//! TRACES: UR-080 | DR-231, DR-232, IR-033
12
13use std::ffi::{c_void, CStr, CString};
14use std::os::raw::{c_char, c_int};
15use std::ptr;
16
17use log::{error, info, warn};
18
19/// GL entry points, resolved once.
20///
21/// Only the handful needed to own a framebuffer; mpv resolves everything else
22/// it needs through [`get_proc_address`].
23struct Gl {
24    gen_framebuffers: unsafe extern "C" fn(c_int, *mut u32),
25    delete_framebuffers: unsafe extern "C" fn(c_int, *const u32),
26    bind_framebuffer: unsafe extern "C" fn(u32, u32),
27    framebuffer_texture_2d: unsafe extern "C" fn(u32, u32, u32, u32, c_int),
28    gen_textures: unsafe extern "C" fn(c_int, *mut u32),
29    delete_textures: unsafe extern "C" fn(c_int, *const u32),
30    bind_texture: unsafe extern "C" fn(u32, u32),
31    tex_image_2d:
32        unsafe extern "C" fn(u32, c_int, c_int, c_int, c_int, c_int, u32, u32, *const c_void),
33    tex_parameteri: unsafe extern "C" fn(u32, u32, c_int),
34    check_framebuffer_status: unsafe extern "C" fn(u32) -> u32,
35}
36
37const GL_TEXTURE_2D: u32 = 0x0DE1;
38const GL_FRAMEBUFFER: u32 = 0x8D40;
39const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
40const GL_RGBA: u32 = 0x1908;
41const GL_RGBA8: c_int = 0x8058;
42const GL_UNSIGNED_BYTE: u32 = 0x1401;
43const GL_LINEAR: c_int = 0x2601;
44const GL_TEXTURE_MIN_FILTER: u32 = 0x2801;
45const GL_TEXTURE_MAG_FILTER: u32 = 0x2800;
46const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
47
48/// Resolve a GL symbol the way libepoxy actually exports it.
49///
50/// **This is the trap that cost the spike a debugging cycle.** libepoxy does not
51/// export `glFoo` as a function. It exports `epoxy_glFoo` as a *data* symbol
52/// holding a lazily-resolving function pointer. So the address `dlsym` returns
53/// is the address *of the pointer*, not of any code: returning it makes mpv jump
54/// into non-executable data and take SIGSEGV/SEGV_ACCERR on the very first GL
55/// call. The value must be read *out of* that location.
56///
57/// The `epoxy` crate does this correctly and is unusable here — its
58/// `gl_generator` dependency pulls a yanked `xml-rs`.
59///
60/// TRACES: UR-080 | IR-033
61unsafe fn resolve(name: &str) -> *mut c_void {
62    let epoxy_name = match CString::new(format!("epoxy_{name}")) {
63        Ok(n) => n,
64        Err(_) => return ptr::null_mut(),
65    };
66    let slot = libc::dlsym(libc::RTLD_DEFAULT, epoxy_name.as_ptr());
67    if !slot.is_null() {
68        // The symbol holds the function pointer; return what is stored there.
69        return *(slot as *mut *mut c_void);
70    }
71
72    // Fall back to a plain symbol, for a GL stack that is not behind epoxy.
73    match CString::new(name) {
74        Ok(n) => libc::dlsym(libc::RTLD_DEFAULT, n.as_ptr()),
75        Err(_) => ptr::null_mut(),
76    }
77}
78
79/// What mpv calls to find GL entry points. Same rule as [`resolve`].
80unsafe extern "C" fn get_proc_address(_ctx: *mut c_void, name: *const c_char) -> *mut c_void {
81    if name.is_null() {
82        return ptr::null_mut();
83    }
84    match CStr::from_ptr(name).to_str() {
85        Ok(n) => resolve(n),
86        Err(_) => ptr::null_mut(),
87    }
88}
89
90macro_rules! load {
91    ($name:literal) => {{
92        let p = resolve($name);
93        if p.is_null() {
94            error!("[MpvRender] GL symbol not found: {}", $name);
95            return None;
96        }
97        std::mem::transmute(p)
98    }};
99}
100
101impl Gl {
102    /// Resolve every entry point, or none — a partially-loaded table would fail
103    /// later at a call site with no context.
104    ///
105    /// The transmutes are unannotated on purpose: each target type is declared
106    /// once on the struct field above, and repeating it at the call site would
107    /// be two places to get the same signature wrong.
108    #[allow(clippy::missing_transmute_annotations)]
109    unsafe fn load() -> Option<Self> {
110        Some(Gl {
111            gen_framebuffers: load!("glGenFramebuffers"),
112            delete_framebuffers: load!("glDeleteFramebuffers"),
113            bind_framebuffer: load!("glBindFramebuffer"),
114            framebuffer_texture_2d: load!("glFramebufferTexture2D"),
115            gen_textures: load!("glGenTextures"),
116            delete_textures: load!("glDeleteTextures"),
117            bind_texture: load!("glBindTexture"),
118            tex_image_2d: load!("glTexImage2D"),
119            tex_parameteri: load!("glTexParameteri"),
120            check_framebuffer_status: load!("glCheckFramebufferStatus"),
121        })
122    }
123}
124
125/// A colour-renderable framebuffer mpv draws into, sized to the widget.
126struct Target {
127    fbo: u32,
128    texture: u32,
129    width: i32,
130    height: i32,
131}
132
133/// mpv's render context plus the framebuffer it draws into.
134///
135/// # Lifetime (DR-232)
136///
137/// The render context must not outlive the GL context it was created against.
138/// `Drop` unregisters mpv's update callback *before* freeing the context, so a
139/// callback cannot land on a freed pointer, and frees the GL objects while the
140/// caller still has the context current. The caller is responsible for making
141/// the GL context current around both creation and drop — see `video_surface`.
142///
143/// This is DR-184 on Android restated: a surface outliving its player. The spike
144/// had no defence at all and saw one unexplained SIGSEGV in a decoder thread.
145pub struct MpvRenderContext {
146    ctx: *mut libmpv_sys::mpv_render_context,
147    gl: Gl,
148    target: Option<Target>,
149}
150
151// The render context is driven only from the GTK main thread; the update
152// callback merely schedules a redraw and touches nothing here.
153unsafe impl Send for MpvRenderContext {}
154
155impl MpvRenderContext {
156    /// Create a render context over an existing mpv handle.
157    ///
158    /// The GL context must already be current on this thread.
159    ///
160    /// TRACES: UR-080 | DR-231, IR-033
161    pub unsafe fn new(mpv: *mut libmpv_sys::mpv_handle) -> Option<Self> {
162        let gl = Gl::load()?;
163
164        let mut init = libmpv_sys::mpv_opengl_init_params {
165            get_proc_address: Some(get_proc_address),
166            get_proc_address_ctx: ptr::null_mut(),
167        };
168        let mut api_type = CString::new("opengl").ok()?;
169        // Advanced control is deliberately OFF.
170        //
171        // With it on, mpv expects the client to drive rendering to a stricter
172        // contract than a GTK draw handler can promise — it will wait on us, and
173        // if we in turn wait on its update callback, neither side proceeds. That
174        // deadlock presents as a file that loads, renders one frame, and then
175        // sits there with no audio and a spinner.
176        //
177        // Off, mpv is tolerant of being rendered on the toolkit's schedule,
178        // which is what the frame clock gives us.
179        let mut advanced: c_int = 0;
180
181        let mut params = [
182            libmpv_sys::mpv_render_param {
183                type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_API_TYPE,
184                data: api_type.as_ptr() as *mut c_void,
185            },
186            libmpv_sys::mpv_render_param {
187                type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
188                data: &mut init as *mut _ as *mut c_void,
189            },
190            libmpv_sys::mpv_render_param {
191                type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_ADVANCED_CONTROL,
192                data: &mut advanced as *mut _ as *mut c_void,
193            },
194            libmpv_sys::mpv_render_param {
195                type_: 0,
196                data: ptr::null_mut(),
197            },
198        ];
199
200        let mut ctx: *mut libmpv_sys::mpv_render_context = ptr::null_mut();
201        let rc = libmpv_sys::mpv_render_context_create(&mut ctx, mpv, params.as_mut_ptr());
202        // Keep the CString alive until after the call.
203        let _ = &mut api_type;
204
205        if rc < 0 || ctx.is_null() {
206            error!("[MpvRender] mpv_render_context_create failed: {rc}");
207            return None;
208        }
209
210        info!("[MpvRender] render context created");
211        Some(MpvRenderContext {
212            ctx,
213            gl,
214            target: None,
215        })
216    }
217
218    /// Ask to be told when a new frame is ready.
219    ///
220    /// Paired with [`report_swap`](Self::report_swap): without both, mpv has
221    /// nothing to time against. The symptom is misleading — playback looks fine
222    /// in a window and judders at fullscreen, which reads as a compositing or
223    /// GPU limit and is neither (DR-233).
224    ///
225    /// TRACES: UR-080 | DR-233
226    pub unsafe fn set_update_callback(
227        &mut self,
228        callback: libmpv_sys::mpv_render_update_fn,
229        ctx: *mut c_void,
230    ) {
231        libmpv_sys::mpv_render_context_set_update_callback(self.ctx, callback, ctx);
232    }
233
234    /// Whether mpv has a new frame waiting.
235    ///
236    /// Asked of mpv directly rather than inferred from its update callback, and
237    /// that distinction is the whole of frame pacing here:
238    ///
239    ///   - Waiting only on the callback deadlocks — mpv will not progress until
240    ///     the client renders, so if the client will not render until mpv says
241    ///     so, neither moves. That presents as a file that loads, shows one
242    ///     frame, and then sits silent.
243    ///   - Rendering on *every* frame-clock tick regardless is the opposite
244    ///     error: `report_swap` then claims a presentation far more often than
245    ///     real frames exist, mpv has nothing coherent to time against, and
246    ///     playback judders badly.
247    ///
248    /// Polling is neither. It runs on the main thread, costs a single atomic
249    /// read inside mpv, and answers the only question that matters.
250    ///
251    /// TRACES: UR-080 | DR-233
252    pub unsafe fn has_frame(&self) -> bool {
253        let flags = libmpv_sys::mpv_render_context_update(self.ctx);
254        (flags & libmpv_sys::mpv_render_update_flag_MPV_RENDER_UPDATE_FRAME as u64) != 0
255    }
256
257    /// Render the current frame at `width` x `height`, returning the texture id
258    /// holding it. The GL context must be current.
259    ///
260    /// TRACES: UR-080 | DR-231
261    pub unsafe fn render(&mut self, width: i32, height: i32) -> Option<u32> {
262        if width <= 0 || height <= 0 {
263            return None;
264        }
265        self.ensure_target(width, height)?;
266        let target = self.target.as_ref()?;
267
268        let mut fbo = libmpv_sys::mpv_opengl_fbo {
269            fbo: target.fbo as c_int,
270            w: width as c_int,
271            h: height as c_int,
272            internal_format: 0,
273        };
274        // GTK's cairo surface has its origin at the top left; mpv defaults to
275        // OpenGL's bottom-left. Without this the picture is drawn upside down —
276        // which looks like a broken decode rather than a coordinate convention.
277        let mut flip: c_int = 1;
278
279        let mut params = [
280            libmpv_sys::mpv_render_param {
281                type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_OPENGL_FBO,
282                data: &mut fbo as *mut _ as *mut c_void,
283            },
284            libmpv_sys::mpv_render_param {
285                type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_FLIP_Y,
286                data: &mut flip as *mut _ as *mut c_void,
287            },
288            libmpv_sys::mpv_render_param {
289                type_: 0,
290                data: ptr::null_mut(),
291            },
292        ];
293
294        let rc = libmpv_sys::mpv_render_context_render(self.ctx, params.as_mut_ptr());
295        if rc < 0 {
296            warn!("[MpvRender] render failed: {rc}");
297            return None;
298        }
299        Some(target.texture)
300    }
301
302    /// Tell mpv the frame reached the screen. See [`set_update_callback`].
303    ///
304    /// TRACES: UR-080 | DR-233
305    pub unsafe fn report_swap(&self) {
306        libmpv_sys::mpv_render_context_report_swap(self.ctx);
307    }
308
309    /// Create or resize the framebuffer. Reused across frames — reallocating per
310    /// frame would churn GPU memory at the display rate.
311    unsafe fn ensure_target(&mut self, width: i32, height: i32) -> Option<()> {
312        if let Some(t) = &self.target {
313            if t.width == width && t.height == height {
314                return Some(());
315            }
316        }
317        self.drop_target();
318
319        let gl = &self.gl;
320        let mut texture: u32 = 0;
321        (gl.gen_textures)(1, &mut texture);
322        (gl.bind_texture)(GL_TEXTURE_2D, texture);
323        (gl.tex_image_2d)(
324            GL_TEXTURE_2D,
325            0,
326            GL_RGBA8,
327            width,
328            height,
329            0,
330            GL_RGBA,
331            GL_UNSIGNED_BYTE,
332            ptr::null(),
333        );
334        (gl.tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
335        (gl.tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
336        (gl.bind_texture)(GL_TEXTURE_2D, 0);
337
338        let mut fbo: u32 = 0;
339        (gl.gen_framebuffers)(1, &mut fbo);
340        (gl.bind_framebuffer)(GL_FRAMEBUFFER, fbo);
341        (gl.framebuffer_texture_2d)(
342            GL_FRAMEBUFFER,
343            GL_COLOR_ATTACHMENT0,
344            GL_TEXTURE_2D,
345            texture,
346            0,
347        );
348        let status = (gl.check_framebuffer_status)(GL_FRAMEBUFFER);
349        (gl.bind_framebuffer)(GL_FRAMEBUFFER, 0);
350
351        if status != GL_FRAMEBUFFER_COMPLETE {
352            error!("[MpvRender] framebuffer incomplete: 0x{status:x}");
353            (gl.delete_framebuffers)(1, &fbo);
354            (gl.delete_textures)(1, &texture);
355            return None;
356        }
357
358        self.target = Some(Target {
359            fbo,
360            texture,
361            width,
362            height,
363        });
364        Some(())
365    }
366
367    unsafe fn drop_target(&mut self) {
368        if let Some(t) = self.target.take() {
369            (self.gl.delete_framebuffers)(1, &t.fbo);
370            (self.gl.delete_textures)(1, &t.texture);
371        }
372    }
373
374    /// Free everything, with the GL context current.
375    ///
376    /// Explicit rather than left to `Drop` because the ordering matters and the
377    /// caller is the only one that can guarantee the GL context is current. See
378    /// DR-232.
379    pub unsafe fn destroy(mut self) {
380        // Unregister first: a callback arriving after the free would be a use
381        // after free, and it is scheduled from mpv's own threads.
382        libmpv_sys::mpv_render_context_set_update_callback(self.ctx, None, ptr::null_mut());
383        self.drop_target();
384        libmpv_sys::mpv_render_context_free(self.ctx);
385        self.ctx = ptr::null_mut();
386        info!("[MpvRender] render context freed");
387        std::mem::forget(self);
388    }
389}
390
391impl Drop for MpvRenderContext {
392    fn drop(&mut self) {
393        if !self.ctx.is_null() {
394            // Reached only if `destroy` was not called — the GL context may not
395            // be current, so the GL objects are deliberately leaked rather than
396            // deleted against whatever context happens to be bound. Freeing the
397            // render context is still safe and is the part that matters.
398            warn!("[MpvRender] dropped without destroy(); GL objects leaked deliberately");
399            unsafe {
400                libmpv_sys::mpv_render_context_set_update_callback(self.ctx, None, ptr::null_mut());
401                libmpv_sys::mpv_render_context_free(self.ctx);
402            }
403        }
404    }
405}