#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux as platform;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos as platform;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
use self::windows as platform;
#[cfg(all(feature = "wgpu", target_os = "linux"))]
mod wgpu_linux;
#[cfg(all(feature = "wgpu", target_os = "macos"))]
mod wgpu_macos;
#[cfg(all(feature = "wgpu", target_os = "windows"))]
mod wgpu_windows;
use std::ffi::c_void;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread::JoinHandle;
use parking_lot::{Condvar, Mutex};
use rsmpv::Mpv;
use rsmpv::render::{OpenGlFbo, OwnedRenderContext};
use crate::error::{Error, Result};
use platform::SurfaceBuffer;
#[allow(dead_code)]
mod gl_consts {
pub(crate) const GL_TEXTURE_2D: u32 = 0x0DE1;
pub(crate) const GL_TEXTURE_MIN_FILTER: u32 = 0x2801;
pub(crate) const GL_TEXTURE_MAG_FILTER: u32 = 0x2800;
pub(crate) const GL_NEAREST: i32 = 0x2600;
pub(crate) const GL_RGBA8: u32 = 0x8058;
pub(crate) const GL_FRAMEBUFFER: u32 = 0x8D40;
pub(crate) const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
pub(crate) const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
}
pub(crate) trait ExportBackend: Sized {
type Buffer: ExportBuffer<Backend = Self>;
const FBO_INTERNAL_FORMAT: i32;
fn new() -> Result<Self>;
fn make_current(&self) -> Result<()>;
fn begin_render(&self, buffer: &Self::Buffer);
fn publish_barrier(&self, buffer: &Self::Buffer);
fn proc_address(name: &str) -> *mut c_void;
}
pub(crate) trait ExportBuffer: Sized {
type Backend: ExportBackend<Buffer = Self>;
fn new(gl: &Self::Backend, width: u32, height: u32) -> Result<Self>;
fn fbo(&self) -> u32;
fn size(&self) -> (u32, u32);
fn delete_gl(&mut self, gl: &Self::Backend);
fn copy_pixels(&self) -> Vec<u8>;
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ExportOptions {
pub width: u32,
pub height: u32,
pub pool_size: usize,
}
impl ExportOptions {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pool_size: 3,
}
}
#[must_use]
pub fn pool_size(mut self, pool_size: usize) -> Self {
self.pool_size = pool_size.max(2);
self
}
}
fn pack_size(width: u32, height: u32) -> u64 {
(u64::from(width) << 32) | u64::from(height)
}
fn unpack_size(packed: u64) -> (u32, u32) {
((packed >> 32) as u32, packed as u32)
}
pub(crate) struct ExportShared {
state: Mutex<PoolState>,
cond: Condvar,
shutdown: AtomicBool,
update_pending: AtomicBool,
force_render: AtomicBool,
swap_pending: AtomicBool,
render_dropped: AtomicBool,
target_size: AtomicU64,
pool_size: usize,
}
#[derive(Default)]
struct PoolState {
free: Vec<SurfaceBuffer>,
published: Option<SurfaceBuffer>,
in_use: usize,
live: usize,
retired: Vec<SurfaceBuffer>,
}
impl ExportShared {
fn notify(&self) {
let _state = self.state.lock();
self.cond.notify_all();
}
pub(crate) fn set_target_size(&self, width: u32, height: u32) {
self.target_size
.store(pack_size(width, height), Ordering::SeqCst);
self.force_render.store(true, Ordering::SeqCst);
self.notify();
}
pub(crate) fn has_published(&self) -> bool {
self.state.lock().published.is_some()
}
fn rearm_dropped_render(&self) {
if self.render_dropped.swap(false, Ordering::SeqCst) {
self.force_render.store(true, Ordering::SeqCst);
self.notify();
}
}
}
pub(crate) struct ExportedRender {
shared: Arc<ExportShared>,
thread: Option<JoinHandle<()>>,
}
impl ExportedRender {
pub(crate) fn create(
core: Arc<Mpv>,
options: ExportOptions,
relay: impl Fn() + Send + Sync + 'static,
) -> Result<Self> {
let shared = Arc::new(ExportShared {
state: Mutex::new(PoolState::default()),
cond: Condvar::new(),
shutdown: AtomicBool::new(false),
update_pending: AtomicBool::new(false),
force_render: AtomicBool::new(false),
swap_pending: AtomicBool::new(false),
render_dropped: AtomicBool::new(false),
target_size: AtomicU64::new(pack_size(options.width, options.height)),
pool_size: options.pool_size.max(2),
});
let (init_tx, init_rx) = std::sync::mpsc::channel();
let thread_shared = Arc::clone(&shared);
let thread = std::thread::Builder::new()
.name("mpv-export-render".into())
.spawn(move || render_thread(core, thread_shared, Box::new(relay), init_tx))
.map_err(|e| Error::ExportSetup(format!("spawning render thread: {e}")))?;
match init_rx.recv() {
Ok(Ok(())) => Ok(Self {
shared,
thread: Some(thread),
}),
Ok(Err(e)) => {
let _ = thread.join();
Err(e)
}
Err(_) => {
let _ = thread.join();
Err(Error::ExportSetup("render thread died during setup".into()))
}
}
}
pub(crate) fn shared(&self) -> &Arc<ExportShared> {
&self.shared
}
pub(crate) fn is_render_thread(&self) -> bool {
self.thread
.as_ref()
.is_some_and(|t| t.thread().id() == std::thread::current().id())
}
pub(crate) fn take_thread(&mut self) -> Option<JoinHandle<()>> {
self.thread.take()
}
}
impl Drop for ExportedRender {
fn drop(&mut self) {
self.shared.shutdown.store(true, Ordering::SeqCst);
self.shared.notify();
if let Some(thread) = self.thread.take() {
if thread.thread().id() == std::thread::current().id() {
return;
}
let _ = thread.join();
}
}
}
fn render_thread(
core: Arc<Mpv>,
shared: Arc<ExportShared>,
relay: Box<dyn Fn() + Send + Sync>,
init_tx: std::sync::mpsc::Sender<Result<()>>,
) {
let gl = match platform::GlContext::new() {
Ok(gl) => gl,
Err(e) => {
let _ = init_tx.send(Err(e));
return;
}
};
if let Err(e) = gl.make_current() {
let _ = init_tx.send(Err(e));
return;
}
let proc_address: Box<dyn FnMut(&str) -> *mut c_void + Send + 'static> =
Box::new(platform::GlContext::proc_address);
let mut ctx = match unsafe { OwnedRenderContext::new_opengl(core, true, proc_address) } {
Ok(ctx) => ctx,
Err(e) => {
let _ = init_tx.send(Err(e.into()));
return;
}
};
{
let signal = Arc::clone(&shared);
ctx.set_update_callback(move || {
signal.update_pending.store(true, Ordering::SeqCst);
signal.notify();
});
}
let _ = init_tx.send(Ok(()));
loop {
{
let mut state = shared.state.lock();
while !shared.shutdown.load(Ordering::SeqCst)
&& !shared.update_pending.load(Ordering::SeqCst)
&& !shared.force_render.load(Ordering::SeqCst)
&& !shared.swap_pending.load(Ordering::SeqCst)
&& state.retired.is_empty()
{
shared.cond.wait(&mut state);
}
}
drain_retired(&shared, &gl);
if shared.shutdown.load(Ordering::SeqCst) {
break;
}
if shared.swap_pending.swap(false, Ordering::SeqCst) {
ctx.report_swap();
}
let force = shared.force_render.swap(false, Ordering::SeqCst);
let updated = shared.update_pending.swap(false, Ordering::SeqCst);
let wants_frame = updated && ctx.update();
if wants_frame || force {
let retry_alloc = render_one(&mut ctx, &gl, &shared, &relay);
if retry_alloc {
shared.force_render.store(true, Ordering::SeqCst);
let mut state = shared.state.lock();
let _ = shared
.cond
.wait_for(&mut state, std::time::Duration::from_millis(100));
}
}
}
{
let mut state = shared.state.lock();
for mut buffer in state.free.drain(..) {
buffer.delete_gl(&gl);
}
for mut buffer in state.retired.drain(..) {
buffer.delete_gl(&gl);
}
if let Some(mut buffer) = state.published.take() {
buffer.delete_gl(&gl);
}
}
drop(ctx);
drop(gl);
}
fn drain_retired(shared: &ExportShared, gl: &platform::GlContext) {
let retired = std::mem::take(&mut shared.state.lock().retired);
for mut buffer in retired {
buffer.delete_gl(gl);
}
}
fn render_one(
ctx: &mut OwnedRenderContext,
gl: &platform::GlContext,
shared: &Arc<ExportShared>,
relay: &dyn Fn(),
) -> bool {
let (width, height) = unpack_size(shared.target_size.load(Ordering::SeqCst));
if width == 0 || height == 0 {
return false;
}
let mut retry_alloc = false;
let buffer = {
let mut state = shared.state.lock();
let mut kept = Vec::with_capacity(state.free.len());
for mut buffer in std::mem::take(&mut state.free) {
if buffer.size() == (width, height) {
kept.push(buffer);
} else {
buffer.delete_gl(gl);
state.live -= 1;
}
}
state.free = kept;
if let Some(buffer) = state.free.pop() {
Some(buffer)
} else {
if let Some(mut stale) = state.published.take_if(|b| b.size() != (width, height)) {
stale.delete_gl(gl);
state.live -= 1;
}
if state.live < shared.pool_size {
match SurfaceBuffer::new(gl, width, height) {
Ok(buffer) => {
state.live += 1;
Some(buffer)
}
Err(e) => {
tracing::warn!("export: buffer allocation failed: {e}");
if let Some(buffer) = state.published.take() {
Some(buffer)
} else if state.in_use == 0 {
retry_alloc = true;
None
} else {
shared.render_dropped.store(true, Ordering::SeqCst);
None
}
}
}
} else if let Some(buffer) = state.published.take() {
Some(buffer)
} else {
tracing::debug!(
"export: shell holds all {} buffers; dropping frame",
shared.pool_size
);
shared.render_dropped.store(true, Ordering::SeqCst);
None
}
}
};
let Some(buffer) = buffer else {
return retry_alloc;
};
let fbo = OpenGlFbo {
fbo: buffer.fbo() as i32,
width: width as i32,
height: height as i32,
internal_format: platform::GlContext::FBO_INTERNAL_FORMAT,
};
gl.begin_render(&buffer);
let rendered = ctx.render_opengl(fbo, false, true);
gl.publish_barrier(&buffer);
if let Err(e) = rendered {
tracing::warn!("export render failed: {e}");
shared.state.lock().free.push(buffer);
return false;
}
{
let mut state = shared.state.lock();
if let Some(previous) = state.published.replace(buffer) {
state.free.push(previous);
}
}
relay();
false
}
pub struct ExportedFrame {
buffer: Option<SurfaceBuffer>,
shared: Arc<ExportShared>,
}
impl ExportedFrame {
pub(crate) fn take_published(shared: Arc<ExportShared>) -> Option<Self> {
let mut state = shared.state.lock();
let buffer = state.published.take()?;
state.in_use += 1;
drop(state);
Some(Self {
buffer: Some(buffer),
shared,
})
}
fn buffer(&self) -> &SurfaceBuffer {
self.buffer
.as_ref()
.expect("buffer present until drop/presented")
}
#[cfg(target_os = "macos")]
pub fn io_surface(&self) -> *mut c_void {
self.buffer().io_surface()
}
#[cfg(target_os = "linux")]
pub fn dma_buf_fd(&self) -> std::os::fd::BorrowedFd<'_> {
self.buffer().dma_buf_fd()
}
#[cfg(target_os = "linux")]
pub fn stride(&self) -> u32 {
self.buffer().stride()
}
#[cfg(target_os = "linux")]
pub fn fourcc(&self) -> u32 {
self.buffer().fourcc()
}
#[cfg(target_os = "linux")]
pub fn modifier(&self) -> u64 {
platform::MODIFIER_LINEAR
}
#[cfg(target_os = "windows")]
pub fn shared_handle(&self) -> *mut c_void {
self.buffer().shared_handle()
}
#[cfg(target_os = "windows")]
pub fn dxgi_format(&self) -> u32 {
platform::DXGI_FORMAT_BGRA8_UNORM
}
pub fn width(&self) -> u32 {
self.buffer().size().0
}
pub fn height(&self) -> u32 {
self.buffer().size().1
}
pub fn copy_pixels(&self) -> Vec<u8> {
self.buffer().copy_pixels()
}
pub fn presented(self) {
self.shared.swap_pending.store(true, Ordering::SeqCst);
self.shared.notify();
}
}
impl Drop for ExportedFrame {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
return_buffer(&self.shared, buffer);
}
}
}
fn return_buffer(shared: &ExportShared, buffer: SurfaceBuffer) {
{
let mut state = shared.state.lock();
state.in_use -= 1;
state.free.push(buffer);
}
shared.rearm_dropped_render();
}
#[cfg(all(feature = "wgpu", target_os = "windows"))]
fn retire_buffer(shared: &ExportShared, buffer: SurfaceBuffer) {
{
let mut state = shared.state.lock();
state.in_use -= 1;
state.live -= 1;
state.retired.push(buffer);
}
shared.rearm_dropped_render();
shared.notify();
}
unsafe fn pack_bgra_rows(base: *const u8, src_stride: usize, w: usize, h: usize) -> Vec<u8> {
let mut out = vec![0u8; w * h * 4];
for row in 0..h {
unsafe {
std::ptr::copy_nonoverlapping(
base.add(row * src_stride),
out.as_mut_ptr().add(row * w * 4),
w * 4,
);
}
}
out
}
#[cfg(all(feature = "wgpu", target_os = "macos"))]
pub const WGPU_BACKEND: wgpu::Backends = wgpu::Backends::METAL;
#[cfg(all(feature = "wgpu", target_os = "linux"))]
pub const WGPU_BACKEND: wgpu::Backends = wgpu::Backends::VULKAN;
#[cfg(all(feature = "wgpu", target_os = "windows"))]
pub const WGPU_BACKEND: wgpu::Backends = wgpu::Backends::DX12;
#[cfg(all(feature = "wgpu", target_os = "linux"))]
pub const REQUIRED_WGPU_FEATURES: wgpu::Features = wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
#[cfg(all(feature = "wgpu", any(target_os = "macos", target_os = "windows")))]
pub const REQUIRED_WGPU_FEATURES: wgpu::Features = wgpu::Features::empty();
#[cfg(feature = "wgpu")]
pub(super) fn exported_texture_desc(width: u32, height: u32) -> wgpu::TextureDescriptor<'static> {
wgpu::TextureDescriptor {
label: Some("mpv-exported-frame"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
}
}
impl std::fmt::Debug for ExportedFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExportedFrame")
.field("width", &self.width())
.field("height", &self.height())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_packing_round_trips() {
assert_eq!(unpack_size(pack_size(3840, 2160)), (3840, 2160));
assert_eq!(unpack_size(pack_size(0, 0)), (0, 0));
assert_eq!(unpack_size(pack_size(u32::MAX, 1)), (u32::MAX, 1));
}
#[test]
fn pool_size_clamps_to_two() {
assert_eq!(ExportOptions::new(64, 64).pool_size(0).pool_size, 2);
assert_eq!(ExportOptions::new(64, 64).pool_size(5).pool_size, 5);
}
}