Skip to main content

mpv_engine/
render.rs

1//! Render seam — OpenGL and software backends over rsmpv's
2//! [`OwnedRenderContext`].
3//!
4//! Since rsmpv 0.2 the safe render context comes in an owned flavor that
5//! co-owns the core through `Arc<Mpv>` and is `Send` — the two upstream
6//! changes this module's previous raw-`sys` incarnation was waiting for.
7//! Free-before-terminate ordering is now structural (the context's `Arc`
8//! keeps the core alive until the context drops), and what remains here is
9//! the backend-specific param plumbing: fbo/flip wiring for GL, dimension
10//! guards and the `rgb0` alpha quirk for software.
11//!
12//! Threading contract (unchanged): the update callback fires on **mpv's
13//! render thread**, and also *synchronously during registration* —
14//! consumers must be re-entrant-safe at attach time. For the GL backend,
15//! the target GL context must be current for creation, every render, and
16//! teardown — `mpv_render_context_free` tears down GL objects, and freeing
17//! without the right context current leaks them into whatever context *is*
18//! current (in GTK that manifested as whole-window rendering artifacts
19//! after the player page was popped). rsmpv encodes that per-call rule as
20//! an `unsafe` GL constructor; this crate forwards the obligation through
21//! [`Engine::attach_gl_render`](crate::Engine::attach_gl_render) rather
22//! than hiding it behind a safe fn that could still hit undefined
23//! behavior.
24
25use std::ffi::c_void;
26use std::sync::Arc;
27
28use rsmpv::Mpv;
29use rsmpv::render::{OpenGlFbo, OwnedRenderContext, SwPixelFormat};
30
31use crate::error::Result;
32
33/// Resolves GL symbols for mpv. Called during context creation (and
34/// possibly later render calls), so it must stay alive for the context's
35/// lifetime — rsmpv keeps it boxed inside the context.
36pub type ProcAddressFn = Box<dyn FnMut(&str) -> *mut c_void + Send + 'static>;
37
38/// Attach-time knobs for the OpenGL backend
39/// ([`Engine::attach_gl_render`](crate::Engine::attach_gl_render)). The
40/// default is mpv's stock behavior — right for a toolkit paint handler
41/// (GTK GLArea); shells whose render loop must not stall override per
42/// field. Attach-time on purpose: frame pacing is a property of the
43/// shell's render loop, not of any single frame.
44///
45/// Non-exhaustive so future knobs stay additive — which also forbids
46/// struct expressions outside this crate (E0639, functional record
47/// update included), so construct through the chainable setters:
48/// `GlRenderOptions::default().block_for_target_time(false)`.
49#[derive(Debug, Clone, Copy)]
50#[non_exhaustive]
51pub struct GlRenderOptions {
52    /// Block inside [`render_gl`](crate::Engine::render_gl) until the
53    /// frame's target display time — mpv's default, and the right pacing
54    /// when the toolkit's frame clock drives drawing. Set `false` for
55    /// render loops that must not stall (e.g. preparing inside a
56    /// compositor-thread pass, where blocking would hold up the whole
57    /// scene submit): mpv then returns immediately and frame pacing is
58    /// yours — do your own timing, or set the `video-timing-offset`
59    /// property to `0` (mpv's documented alternative).
60    pub block_for_target_time: bool,
61    /// mpv's `MPV_RENDER_PARAM_ADVANCED_CONTROL`: enables direct
62    /// rendering and GPU screenshots, but obligates the shell to follow
63    /// the render API threading rules strictly and to call
64    /// [`render_update`](crate::Engine::render_update) promptly after
65    /// **every** update callback (optional when this is off).
66    pub advanced_control: bool,
67}
68
69impl Default for GlRenderOptions {
70    fn default() -> Self {
71        Self {
72            block_for_target_time: true,
73            advanced_control: false,
74        }
75    }
76}
77
78impl GlRenderOptions {
79    /// Set [`block_for_target_time`](field@Self::block_for_target_time),
80    /// chainable from [`default()`](Default::default).
81    #[must_use]
82    pub fn block_for_target_time(mut self, block: bool) -> Self {
83        self.block_for_target_time = block;
84        self
85    }
86
87    /// Set [`advanced_control`](field@Self::advanced_control), chainable
88    /// from [`default()`](Default::default).
89    #[must_use]
90    pub fn advanced_control(mut self, advanced: bool) -> Self {
91        self.advanced_control = advanced;
92        self
93    }
94}
95
96/// Which render backend is attached — what
97/// [`Engine::attached_render`](crate::Engine::attached_render) reports.
98///
99/// Non-exhaustive: a future backend (e.g. Vulkan, if mpv ever exposes it
100/// through the render API) is an additive variant, not a breaking change.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum RenderKind {
104    /// The OpenGL backend
105    /// ([`Engine::attach_gl_render`](crate::Engine::attach_gl_render)).
106    OpenGl,
107    /// The software backend
108    /// ([`Engine::attach_sw_render`](crate::Engine::attach_sw_render)).
109    Software,
110    /// The exported-frame backend (`export` feature, macOS + Linux +
111    /// Windows — `Engine::attach_exported_render`): the engine renders on
112    /// its own hidden GL context and hands the shell zero-copy exportable
113    /// frames (IOSurface-backed / DMA-BUF-backed / shared-D3D11-texture-
114    /// backed). The variant exists on every platform so cross-platform
115    /// shells can match on it unconditionally; only the feature produces
116    /// it.
117    Exported,
118}
119
120/// The one attached render backend. Backends share the engine's single
121/// slot so `AlreadyAttached` and `detach_render` behave uniformly —
122/// mpv allows one render context per handle regardless of type.
123pub(crate) enum RenderBackend {
124    Gl(GlRender),
125    Sw(SwRender),
126    #[cfg(all(
127        feature = "export",
128        any(target_os = "macos", target_os = "linux", target_os = "windows")
129    ))]
130    Exported(crate::export::ExportedRender),
131}
132
133impl RenderBackend {
134    /// Process pending render work (`mpv_render_context_update`);
135    /// `true` when a new frame should be drawn. For the exported backend
136    /// this is a no-op returning `false`: its render context lives on the
137    /// engine's own render thread, which services updates itself.
138    pub(crate) fn update(&mut self) -> bool {
139        match self {
140            RenderBackend::Gl(r) => r.ctx.update(),
141            RenderBackend::Sw(r) => r.0.update(),
142            #[cfg(all(
143                feature = "export",
144                any(target_os = "macos", target_os = "linux", target_os = "windows")
145            ))]
146            RenderBackend::Exported(_) => false,
147        }
148    }
149
150    pub(crate) fn kind(&self) -> RenderKind {
151        match self {
152            RenderBackend::Gl(_) => RenderKind::OpenGl,
153            RenderBackend::Sw(_) => RenderKind::Software,
154            #[cfg(all(
155                feature = "export",
156                any(target_os = "macos", target_os = "linux", target_os = "windows")
157            ))]
158            RenderBackend::Exported(_) => RenderKind::Exported,
159        }
160    }
161
162    /// Whether the calling thread is this backend's own render thread —
163    /// true only for the exported backend when called from inside its
164    /// `on_update`. Decides `detach_render`'s locking (see there): a
165    /// self-detach must not block on the attach lock, which a concurrent
166    /// detach may hold while joining this very thread.
167    pub(crate) fn on_own_render_thread(&self) -> bool {
168        match self {
169            #[cfg(all(
170                feature = "export",
171                any(target_os = "macos", target_os = "linux", target_os = "windows")
172            ))]
173            RenderBackend::Exported(r) => r.is_render_thread(),
174            _ => false,
175        }
176    }
177}
178
179pub(crate) struct GlRender {
180    ctx: OwnedRenderContext,
181    block_for_target_time: bool,
182}
183
184impl GlRender {
185    /// Create an OpenGL render context co-owning `core` and register
186    /// `on_update`.
187    ///
188    /// `on_update` fires once synchronously here (mpv's documented
189    /// behavior) and afterwards from the render thread.
190    ///
191    /// # Safety
192    /// Forwards rsmpv's `new_opengl` contract: the target GL context must
193    /// be current on the calling thread now, on every later
194    /// [`render`](Self::render) or update, and when this value drops.
195    pub(crate) unsafe fn create(
196        core: Arc<Mpv>,
197        get_proc_address: ProcAddressFn,
198        options: GlRenderOptions,
199        on_update: impl Fn() + Send + Sync + 'static,
200    ) -> Result<Self> {
201        // SAFETY: GL-currency contract forwarded to the caller.
202        let mut ctx = unsafe {
203            OwnedRenderContext::new_opengl(core, options.advanced_control, get_proc_address)?
204        };
205        ctx.set_update_callback(on_update);
206        Ok(Self {
207            ctx,
208            block_for_target_time: options.block_for_target_time,
209        })
210    }
211
212    /// Draw the current frame into `fbo` (`0` = default framebuffer).
213    /// `flip_y` handles targets with a flipped origin (e.g. GTK's GLArea).
214    /// Whether this blocks until the frame's target time was fixed at
215    /// attach ([`GlRenderOptions::block_for_target_time`]).
216    pub(crate) fn render(&mut self, fbo: i32, w: i32, h: i32, flip_y: bool) -> Result<()> {
217        let fbo = OpenGlFbo {
218            fbo,
219            width: w,
220            height: h,
221            internal_format: 0,
222        };
223        self.ctx
224            .render_opengl(fbo, flip_y, self.block_for_target_time)?;
225        Ok(())
226    }
227}
228
229/// Software rendering: mpv draws the frame into a caller-provided RGBA
230/// buffer. No GL anywhere — no context-current requirements for rendering
231/// *or* teardown, so unlike [`GlRender`] this backend is fully safe and
232/// drops from any thread.
233pub(crate) struct SwRender(OwnedRenderContext);
234
235impl SwRender {
236    /// Create a software render context co-owning `core` and register
237    /// `on_update` (same contract as the GL backend: fires once
238    /// synchronously here, afterwards from mpv's render thread).
239    pub(crate) fn create(
240        core: Arc<Mpv>,
241        on_update: impl Fn() + Send + Sync + 'static,
242    ) -> Result<Self> {
243        let mut ctx = OwnedRenderContext::new_software(core)?;
244        ctx.set_update_callback(on_update);
245        Ok(Self(ctx))
246    }
247
248    /// Render the current frame as RGBA8 into `buf`, resizing it to
249    /// `w * h * 4`.
250    pub(crate) fn render(&mut self, w: i32, h: i32, buf: &mut Vec<u8>) -> Result<()> {
251        // Nothing to draw for empty or negative dimensions — rsmpv would
252        // reject them as InvalidParameter, but a no-op mirrors this
253        // crate's render-before-attach philosophy. The overflow check
254        // keeps the multiply sound on 32-bit targets.
255        let (Ok(uw), Ok(uh)) = (usize::try_from(w), usize::try_from(h)) else {
256            return Ok(());
257        };
258        let Some(len) = uw.checked_mul(uh).and_then(|p| p.checked_mul(4)) else {
259            return Ok(());
260        };
261        if len == 0 {
262            return Ok(());
263        }
264        buf.resize(len, 0);
265        self.0
266            .render_software(w, h, SwPixelFormat::Rgb0, uw * 4, buf)?;
267        // "rgb0" leaves the fourth byte of each pixel undefined; a
268        // consumer treating the buffer as RGBA reads it as alpha and
269        // gets garbage transparency. Force opaque here — the quirk
270        // belongs to the seam, not to every consumer.
271        for px in buf.chunks_exact_mut(4) {
272            px[3] = 0xFF;
273        }
274        Ok(())
275    }
276}