use eframe::glow::{self, HasContext as _};
use std::ffi::c_void;
use std::sync::{Arc, OnceLock};
use tracing::debug;
use super::{BackendError, FramebufferSize, GpaFn, RenderedFrame};
pub(super) static OFFSCREEN_EGL: OnceLock<Arc<OffscreenEglState>> = OnceLock::new();
pub(super) struct OffscreenEglState {
egl: khronos_egl::DynamicInstance<khronos_egl::EGL1_5>,
display: khronos_egl::Display,
surface: khronos_egl::Surface,
context: khronos_egl::Context,
gl: Arc<glow::Context>,
}
unsafe impl Send for OffscreenEglState {}
unsafe impl Sync for OffscreenEglState {}
impl OffscreenEglState {
pub fn make_current(&self) -> Result<(), BackendError> {
self.egl.make_current(
self.display,
Some(self.surface),
Some(self.surface),
Some(self.context),
)?;
Ok(())
}
pub fn unbind(&self) -> Result<(), BackendError> {
self.egl.make_current(self.display, None, None, None)?;
Ok(())
}
}
pub(super) struct WgpuState {
pub is_offscreen: bool,
pub frame_texture: Option<eframe::egui::TextureHandle>,
pub pixel_buffer: Vec<u8>,
}
impl super::PlayerState {
pub(super) fn init_offscreen_egl() -> Result<(Arc<glow::Context>, Arc<GpaFn>), BackendError> {
use khronos_egl as egl;
const EGL_PLATFORM_SURFACELESS_MESA: u32 = 0x31DD;
const EGL_PLATFORM_DEVICE_EXT: u32 = 0x313F;
if let Some(state) = OFFSCREEN_EGL.get() {
debug!("Reusing shared offscreen EGL context");
let gpa: Arc<GpaFn> = Arc::new(|name: &str| -> *mut c_void {
OFFSCREEN_EGL
.get()
.and_then(|s| s.egl.get_proc_address(name))
.map_or(std::ptr::null(), |f| f as usize as *const c_void)
.cast_mut()
});
return Ok((Arc::clone(&state.gl), gpa));
}
debug!("Initialising shared offscreen EGL context for libmpv (wgpu path)");
let lib = unsafe { libloading::Library::new("libEGL.so.1") }.map_err(egl::LoadError::Library)?;
let egl_inst = unsafe { egl::DynamicInstance::<egl::EGL1_5>::load_required_from(lib) }?;
let display = unsafe {
egl_inst.get_platform_display(EGL_PLATFORM_SURFACELESS_MESA, egl::DEFAULT_DISPLAY, &[])
}
.or_else(|_| {
tracing::debug!("Surfaceless failed, trying EGL_EXT_platform_device (NVIDIA)");
let query_devices_ptr = egl_inst.get_proc_address("eglQueryDevicesEXT");
if let Some(query_devices_ptr) = query_devices_ptr {
let query_devices: unsafe extern "system" fn(
i32,
*mut *mut std::ffi::c_void,
*mut i32,
) -> u32 = unsafe { std::mem::transmute(query_devices_ptr) };
let mut num_devices = 0;
let mut device = std::ptr::null_mut();
unsafe {
if query_devices(1, &raw mut device, &raw mut num_devices) != 0 && num_devices > 0 {
return egl_inst.get_platform_display(
EGL_PLATFORM_DEVICE_EXT,
device,
&[egl::NONE as usize],
);
}
}
}
tracing::warn!("Device platform failed, falling back to DEFAULT_DISPLAY.");
Err(khronos_egl::Error::BadParameter)
})
.unwrap_or_else(|e| {
tracing::warn!("Surfaceless EGL platform unavailable: {e}; falling back to default display.");
unsafe { egl_inst.get_display(egl::DEFAULT_DISPLAY).unwrap() }
});
egl_inst.initialize(display)?;
egl_inst.bind_api(egl::OPENGL_API)?;
#[rustfmt::skip]
let config_attribs = [
egl::RENDERABLE_TYPE, egl::OPENGL_BIT,
egl::SURFACE_TYPE, egl::PBUFFER_BIT,
egl::RED_SIZE, 8,
egl::GREEN_SIZE, 8,
egl::BLUE_SIZE, 8,
egl::ALPHA_SIZE, 8,
egl::NONE,
];
let config = egl_inst
.choose_first_config(display, &config_attribs)?
.ok_or(BackendError::NoEglConfig)?;
#[rustfmt::skip]
let ctx_attribs = [
egl::CONTEXT_MAJOR_VERSION, 3,
egl::CONTEXT_MINOR_VERSION, 3,
egl::CONTEXT_OPENGL_PROFILE_MASK, egl::CONTEXT_OPENGL_CORE_PROFILE_BIT,
egl::NONE,
];
let context = egl_inst.create_context(display, config, None, &ctx_attribs)?;
let surf_attribs = [egl::WIDTH, 1, egl::HEIGHT, 1, egl::NONE];
let surface = egl_inst.create_pbuffer_surface(display, config, &surf_attribs)?;
egl_inst.make_current(display, Some(surface), Some(surface), Some(context))?;
let gl = Arc::new(unsafe {
glow::Context::from_loader_function(|name| {
egl_inst
.get_proc_address(name)
.map_or(std::ptr::null(), |f| f as usize as *const _)
})
});
let state = Arc::new(OffscreenEglState {
egl: egl_inst,
display,
surface,
context,
gl: Arc::clone(&gl),
});
let _ = OFFSCREEN_EGL.set(Arc::clone(&state));
let gpa: Arc<GpaFn> = Arc::new(|name: &str| -> *mut c_void {
OFFSCREEN_EGL
.get()
.and_then(|s| s.egl.get_proc_address(name))
.map_or(std::ptr::null(), |f| f as usize as *const c_void)
.cast_mut()
});
state.unbind()?;
debug!("Offscreen EGL/OpenGL 3.3 context ready");
Ok((gl, gpa))
}
#[cfg(feature = "wgpu")]
pub(super) fn render_frame_offscreen(
&mut self,
ctx: &eframe::egui::Context,
size: FramebufferSize,
) -> Result<RenderedFrame, BackendError> {
let egl_state = OFFSCREEN_EGL.get().expect("Offscreen EGL uninitialized");
egl_state.make_current()?;
let res = {
#[expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
if self.wgpu.is_offscreen {
let size = if let Ok(Some((vid_w, vid_h))) = self.dimensions() {
let req_w = f64::from(size.width);
let req_h = f64::from(size.height);
if req_w > 0.0 && req_h > 0.0 {
let scale = 1.0_f64.min((vid_w as f64 / req_w).min(vid_h as f64 / req_h));
FramebufferSize {
width: (req_w * scale) as i32,
height: (req_h * scale) as i32,
}
} else {
size
}
} else {
size
};
self.render_glow(size)
} else {
self.render_glow(size)
}
};
let (res, frame_updated) = match res {
Ok(res) => res,
Err(e) => {
egl_state.unbind()?;
return Err(e);
}
};
if !frame_updated && let Some(ref tex) = self.wgpu.frame_texture {
egl_state.unbind()?;
return Ok(RenderedFrame::EguiTexture(tex.id()));
}
let FramebufferSize { width, height } = res.framebuffer_size();
let expected_len = (width * height * 4).cast_unsigned() as usize;
if self.wgpu.pixel_buffer.len() != expected_len {
self.wgpu.pixel_buffer.resize(expected_len, 0);
}
unsafe {
self.gl
.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(res.framebuffer()));
self.gl.read_pixels(
0,
0,
width,
height,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(&mut self.wgpu.pixel_buffer)),
);
self.gl.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
}
egl_state.unbind()?;
let color_image = eframe::egui::ColorImage::from_rgba_unmultiplied(
[width.cast_unsigned() as usize, height.cast_unsigned() as usize],
&self.wgpu.pixel_buffer,
);
let tex_id = if let Some(ref mut tex) = self.wgpu.frame_texture {
tex.set(color_image, eframe::egui::TextureOptions::LINEAR);
tex.id()
} else {
let tex = ctx.load_texture(
"sharkplayer_frame",
color_image,
eframe::egui::TextureOptions::LINEAR,
);
let id = tex.id();
self.wgpu.frame_texture = Some(tex);
id
};
Ok(RenderedFrame::EguiTexture(tex_id))
}
}