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}