diff --git a/server/Cargo.lock b/server/Cargo.lock index f1d0d35..26bcf06 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -714,6 +714,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -739,6 +749,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1026,6 +1042,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1109,6 +1134,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1257,6 +1292,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1322,6 +1383,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1344,12 +1411,16 @@ dependencies = [ "ffmpeg-next", "futures", "futures-util", + "gl", "letsplay_gpu", "rand", "retro_frontend", "serde", "serde_json", "tokio", + "tracing", + "tracing-subscriber", + "xkeysym", ] [[package]] @@ -1358,6 +1429,28 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" @@ -1440,6 +1533,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xml-rs" version = "0.8.22" diff --git a/server/Cargo.toml b/server/Cargo.toml index bb93ee5..739aced 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" anyhow = "1.0.86" +# Libretro Sex letsplay_gpu.path = "/home/lily/source/lets-play/crates/letsplay_gpu" retro_frontend.path = "/home/lily/source/lets-play/crates/retro_frontend" +gl = "0.14.0" # async tokio = { version = "1.39.3", features = ["full"] } @@ -26,6 +28,9 @@ rand = "0.8.5" serde = "1.0.209" serde_json = "1.0.128" cudarc = { version = "0.12.1", features = [ "cuda-11050" ] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +xkeysym = "0.2.1" [patch.crates-io] diff --git a/server/src/main.rs b/server/src/main.rs index 7432bff..a4a321f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,21 +1,25 @@ +mod retro_thread; mod surface; mod types; mod video; + +use retro_thread::{spawn_retro_thread, App, RetroEvent}; use video::ffmpeg; use video::h264_encoder::H264Encoder; use std::{ sync::{Arc, Mutex}, + thread, time::Duration, }; use rand::distributions::DistString; +use std::net::SocketAddr; use tokio::sync::{ broadcast, mpsc::{self, error::TryRecvError}, Mutex as TokioMutex, }; -use std::net::SocketAddr; use axum::{ extract::{ @@ -32,18 +36,18 @@ use futures::{sink::SinkExt, stream::StreamExt}; struct AppState { encoder_tx: Arc>>, + inputs: Arc>>, websocket_broadcast_tx: broadcast::Sender, websocket_count: TokioMutex, } impl AppState { - fn new( - encoder_tx: mpsc::Sender<()>, - ) -> Self { + fn new(encoder_tx: mpsc::Sender<()>) -> Self { let (chat_tx, _chat_rx) = broadcast::channel(10); Self { encoder_tx: Arc::new(TokioMutex::new(encoder_tx)), + inputs: Arc::new(TokioMutex::new(Vec::new())), websocket_broadcast_tx: chat_tx, websocket_count: TokioMutex::const_new(0usize), } @@ -74,7 +78,7 @@ impl EncoderState { // FIXME: use create_frame() on H264Encoder self.frame = Some(ffmpeg::frame::Video::new( - ffmpeg::format::Pixel::BGRA, + ffmpeg::format::Pixel::RGBA, size.width, size.height, )); @@ -109,7 +113,9 @@ impl EncoderState { } encoder.send_frame(&*frame); - encoder.receive_packet(&mut self.packet).expect("Failed to recieve packet"); + encoder + .receive_packet(&mut self.packet) + .expect("Failed to recieve packet"); unsafe { if !self.packet.is_empty() { @@ -123,6 +129,13 @@ impl EncoderState { #[tokio::main(flavor = "multi_thread", worker_threads = 8)] async fn main() -> anyhow::Result<()> { + // Setup a tracing subscriber + let subscriber = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::INFO) + .finish(); + + tracing::subscriber::set_global_default(subscriber).unwrap(); + let surface = Arc::new(Mutex::new(surface::Surface::new())); // H.264 encoder related @@ -131,6 +144,126 @@ async fn main() -> anyhow::Result<()> { let state = Arc::new(AppState::new(encoder_tx)); + let (mut event_rx, event_in_tx) = spawn_retro_thread(surface.clone()); + + let state_clone = state.clone(); + let encoder_state_clone = encoder_state.clone(); + + let vnc_recv_handle = tokio::spawn(async move { + let surface_clone = surface.clone(); + + // first frame is always a key frame + let mut pts = 0u64; + let mut force_keyframe = true; + let mut frame_update = false; + + // start the thread now that we're alive + let _ = event_in_tx.send(retro_thread::RetroInEvent::Start).await; + + loop { + match encoder_rx.try_recv() { + Ok(()) => { + // force keyframe + force_keyframe = true; + frame_update = true; + } + + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => {} + } + + match event_rx.try_recv() { + Ok(msg) => match msg { + RetroEvent::Frame => { + { + let mut state_locked = encoder_state_clone.lock().await; + + let mut_frame = state_locked.frame(); + + let width = mut_frame.width(); + let height = mut_frame.height(); + + let mut surf = surface_clone.lock().expect( + "locking the VNC surface to paint it to the ffmpeg frame failed", + ); + let surf_buf = surf.get_buffer(); + + let buf_ptr = + unsafe { (*(*mut_frame.as_mut_ptr()).buf[0]).data as *mut u32 }; + + for y in 0..height { + let line_stride = (y * width) as usize; + // Make a slice for the line + // SAFETY: The allocation is guaranteed to be large enough + // for this to work from y = 0..height + let dest_line_slice = unsafe { + let dest_line_ptr = buf_ptr.add(line_stride); + std::slice::from_raw_parts_mut(dest_line_ptr, width as usize) + }; + + dest_line_slice.copy_from_slice( + &surf_buf[line_stride..line_stride + width as usize], + ); + } + } + + frame_update = true; + } + + RetroEvent::Resize { size } => { + { + let mut state_locked = encoder_state_clone.lock().await; + state_locked.init(size).expect("fuck you"); + + // reset our internal state + pts = 0; + force_keyframe = true; + frame_update = false; + } + } + + RetroEvent::WantInputs { tx } => { + let inputs = state_clone.inputs.lock().await; + //tracing::info!("giving inputs {:?}", inputs); + tx.send(inputs.clone()).expect("FUCK"); + } + }, + + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => {} + } + + // send frame if we should. + if frame_update { + let mut state_locked = encoder_state_clone.lock().await; + + match state_locked.send_frame(pts, force_keyframe) { + Some(mut packet) => { + let vec = { + let data = packet.data_mut().expect("packet is empty somehow"); + data.to_vec() + }; + + let _ = state_clone + .websocket_broadcast_tx + .send(ws::Message::Binary(vec)); + + pts += 1; + + if force_keyframe { + force_keyframe = false; + } + } + + None => {} + } + + frame_update = false; + } + + tokio::time::sleep(Duration::from_millis(1)).await; + } + }); // Axum websocket server let app: Router<()> = Router::new() @@ -260,13 +393,28 @@ async fn handle_socket(socket: WebSocket, who: SocketAddr, state: Arc) let keysym = json["keysym"].as_u64().unwrap() as u32; let pressed = json["pressed"].as_u64().unwrap() == 1; + // FIXME: This would be MUCH better off being a set, so we don't + // hack-code set semantics here. Oh well. + { + let mut lock = recv_clone.inputs.lock().await; + if pressed { + if let None = lock.iter().position(|e| *e == keysym) { + lock.push(keysym); + } + } else { + if let Some(at) = lock.iter().position(|e| *e == keysym) { + lock.remove(at); + } + } + } + /*let _ = recv_clone - .engine_tx - .send(vnc_engine::VncMessageInput::KeyEvent { - keysym: keysym, - pressed: pressed, - }) - .await;*/ + .engine_tx + .send(vnc_engine::VncMessageInput::KeyEvent { + keysym: keysym, + pressed: pressed, + }) + .await;*/ } "mouse" => { @@ -287,12 +435,12 @@ async fn handle_socket(socket: WebSocket, who: SocketAddr, state: Arc) let mask = json["mask"].as_u64().unwrap() as u8; /*let _ = recv_clone - .engine_tx - .send(vnc_engine::VncMessageInput::MouseEvent { - pt: types::Point { x: x, y: y }, - buttons: mask, - }) - .await;*/ + .engine_tx + .send(vnc_engine::VncMessageInput::MouseEvent { + pt: types::Point { x: x, y: y }, + buttons: mask, + }) + .await;*/ } _ => {} } diff --git a/server/src/retro_thread.rs b/server/src/retro_thread.rs new file mode 100644 index 0000000..3a56e5a --- /dev/null +++ b/server/src/retro_thread.rs @@ -0,0 +1,465 @@ +use std::{ + path::Path, + sync::{Arc, Mutex}, + time::Duration, +}; + +use tokio::sync::{mpsc, oneshot}; + +use anyhow::Result; + +use retro_frontend::{ + frontend::{Frontend, FrontendInterface, HwGlInitData}, + input_devices::{InputDevice, RetroPad}, + libretro_sys_new, +}; + +use gpu::egl_helpers::DeviceContext; +use letsplay_gpu as gpu; + +use crate::{ + surface::Surface, + types::{Rect, Size}, +}; + +/// Called by OpenGL. We use this to dump errors. +extern "system" fn opengl_message_callback( + source: gl::types::GLenum, + _type: gl::types::GLenum, + id: gl::types::GLuint, + _severity: gl::types::GLenum, + _length: gl::types::GLsizei, + message: *const gl::types::GLchar, + _user: *mut std::ffi::c_void, +) { + unsafe { + let message = std::ffi::CStr::from_ptr(message); + if _type == gl::DEBUG_TYPE_ERROR { + tracing::error!( + "OpenGL error: {:?} (res {:08x}, id = {:08x}, source = {:08x})", + message, + _type, + id, + source + ); + } + } +} + +pub struct App { + frontend: Option>, + + pad: RetroPad, + + // EGL state + egl_context: Option, + + framebuffer: Arc>, + + // OpenGL object IDs + gl_framebuffer: gpu::GlFramebuffer, + + /// Cached readback buffer. + readback_buffer: Surface, + + event_tx: mpsc::Sender, +} + +impl App { + pub fn new(framebuffer: Arc>, event_tx: mpsc::Sender) -> Box { + let mut boxed = Box::new(Self { + frontend: None, + pad: RetroPad::new(), + + egl_context: None, + framebuffer, + gl_framebuffer: gpu::GlFramebuffer::new(), + readback_buffer: Surface::new(), + event_tx, + }); + + // SAFETY: The only way to touch the pointer involves the frontend library calling retro_run, + // and the core calling one of the given callbacks. Therefore this is gnarly, but "fine", + // since once the main loop ends, there won't be an opporturnity for said callbacks to be called again. + // + // I'm still not really sure how to tell the borrow checker that this is alright, + // short of Box::leak() (which I don't want to do, since ideally I'd like actual cleanup to occur). + let obj = &mut *boxed as *mut dyn FrontendInterface; + boxed.frontend = Some(Frontend::new(obj)); + + boxed + } + + fn get_frontend(&mut self) -> &mut Frontend { + self.frontend.as_mut().unwrap() + } + + /// Inserts our RetroPad and initalizes the display. + pub fn init(&mut self) { + // SAFETY: This too won't ever be Use-After-Free'd because the only chance to + // goes away on drop as well. That's a bit flaky reasoning wise, but is true. + // + // In all honesty, I'm not sure this even needs to be a *mut so I could see if + // making it a immutable reference works. + let pad = &mut self.pad as *mut dyn InputDevice; + self.get_frontend().plug_input_device(0, pad); + + self.init_display(); + } + + fn init_display(&mut self) { + let av_info = self.get_frontend().get_av_info().expect("No AV info"); + + //self.window.resize( + // av_info.geometry.base_width as u16, + // av_info.geometry.base_height as u16, + //); + } + + pub fn load_core>(&mut self, path: P) -> Result<()> { + // Unload an existing core. + if self.get_frontend().core_loaded() { + let _ = self.get_frontend().unload_core(); + } + + self.get_frontend().load_core(path)?; + Ok(()) + } + + pub fn load_game>(&mut self, path: P) -> Result<()> { + self.get_frontend().load_game(path)?; + Ok(()) + } + + /// Initalizes the headless EGL context used for OpenGL rendering. + fn hw_gl_egl_init(&mut self) { + self.egl_context = Some(DeviceContext::new(0)); + } + + /// Destroys OpenGL resources and the EGL context. + fn hw_gl_destroy(&mut self) { + if self.egl_context.is_some() { + self.gl_framebuffer.destroy(); + self.egl_context.take().unwrap().destroy() + } + } + + pub fn tick(&mut self) { + let av_info = self.get_frontend().get_av_info().expect("???"); + let step_ms = (1.0 / av_info.timing.fps) * 1000.; + let step_duration = Duration::from_millis(step_ms as u64); + + self.get_frontend().run_frame(); + let _ = self.event_tx.blocking_send(RetroEvent::Frame); + std::thread::sleep(step_duration); + } + + /// Bleh, I don't like this is an associated fn, but whatever + fn update_impl(framebuffer: Arc>, slice: &[u32], pitch: u32, from_opengl: bool) { + let mut framebuffer_locked = framebuffer.lock().expect("could not lock framebuffer"); + let size = framebuffer_locked.size.clone(); + + let has_disconnected_pitch = pitch != size.width as u32; + + // If this frame came from OpenGL we need to flip the image around + // so it is right side up (from our perspective). + if from_opengl { + let mut scanlines: Vec<&[u32]> = Vec::with_capacity(size.height as usize); + + // Push scanline slices in reverse order (which will actually flip them to the right orientation) + for y in (0..size.height).rev() { + let src_line_off = (y as u32 * pitch) as usize; + let src_slice = &slice[src_line_off..src_line_off + size.width as usize]; + scanlines.push(src_slice); + } + + // Draw them + for y in 0..size.height { + let src_line_off = (y as u32 * pitch) as usize; + let mut dest_line_off = src_line_off; + + // copy only + if has_disconnected_pitch { + dest_line_off = (y * size.width.min(pitch)) as usize; + } + + let dest_slice = &mut framebuffer_locked.get_buffer() + [dest_line_off..dest_line_off + size.width as usize]; + + dest_slice.copy_from_slice(scanlines[y as usize]); + + // swap the scanline pixels to BGRA order to make minifb happy + // not the fastest code but this should do for an example + //for pix in dest_slice { + // let a = (*pix & 0xff000000) >> 24; + // let b = (*pix & 0x00ff0000) >> 16; + // let g = (*pix & 0x0000ff00) >> 8; + // let r = *pix & 0x000000ff; + // *pix = a << 24 | r << 16 | g << 8 | b; + //} + } + } else { + for y in 0..size.height { + let src_line_off = (y as u32 * pitch) as usize; + let mut dest_line_off = src_line_off; + + // copy only + if has_disconnected_pitch { + dest_line_off = (y * size.width) as usize; + } + + // Create slices repressenting each part + let src_slice = &slice[src_line_off..src_line_off + size.width as usize]; + let dest_slice = &mut framebuffer_locked.get_buffer() + [dest_line_off..dest_line_off + size.width as usize]; + + dest_slice.copy_from_slice(src_slice); + } + } + } +} + +impl FrontendInterface for App { + fn video_resize(&mut self, width: u32, height: u32) { + tracing::info!("Resized to {width}x{height}"); + + if self.egl_context.is_some() { + self.gl_framebuffer.resize(width, height); + let raw = self.gl_framebuffer.as_raw(); + + // Notify the frontend layer about the new FBO ID + self.get_frontend().set_gl_fbo(raw); + + // Resize the readback buffer + self.readback_buffer.resize(Size { width, height }); + } + + self.framebuffer + .lock() + .expect("its over?") + .resize(Size { width, height }); + + let _ = self.event_tx.blocking_send(RetroEvent::Resize { + size: Size { width, height }, + }); + } + + fn video_update(&mut self, slice: &[u32], pitch: u32) { + Self::update_impl(self.framebuffer.clone(), slice, pitch, false); + } + + fn video_update_gl(&mut self) { + let dimensions = self.get_frontend().get_size(); + + // Read back the framebuffer + let slice = { + //self.gl_framebuffer.read_pixels( + // &mut self.readback_buffer.get_buffer()[..], + // dimensions.0, + // dimensions.1, + //); + + unsafe { + let scope = self.gl_framebuffer.bind(); + gl::ReadPixels( + 0, + 0, + dimensions.0 as i32, + dimensions.1 as i32, + gl::BGRA, + gl::UNSIGNED_BYTE, + (&mut self.readback_buffer.get_buffer()).as_mut_ptr() as *mut std::ffi::c_void, + ); + } + + self.readback_buffer.get_buffer() + }; + + Self::update_impl(self.framebuffer.clone(), slice, dimensions.0, true); + } + + fn audio_sample(&mut self, _slice: &[i16], _size: usize) {} + + fn input_poll(&mut self) { + self.pad.reset(); + + let (tx, rx) = oneshot::channel(); + let _ = self.event_tx.blocking_send(RetroEvent::WantInputs { tx }); + + let inputs = rx.blocking_recv().expect("what the FUCK are you doing"); + + for key in &inputs { + use xkeysym::key as Key; + + match *key { + Key::backslash => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_SELECT, None); + } + Key::Return => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_START, None); + } + + Key::Up => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_UP, None); + } + Key::Down => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_DOWN, None); + } + Key::Left => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_LEFT, None); + } + Key::Right => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_RIGHT, None); + } + + Key::s => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_B, None); + } + + Key::a => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_A, None); + } + + Key::q => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_X, None); + } + + Key::w => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_Y, None); + } + + Key::Control_L => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_L, None); + } + + Key::Shift_L => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_L2, None); + } + + Key::Alt_L => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_R, None); + } + + Key::z => { + self.pad + .press_button(libretro_sys_new::DEVICE_ID_JOYPAD_R2, None); + } + + _ => {} + } + } + } + + fn hw_gl_init(&mut self) -> Option { + // Only create a new EGL/OpenGL context if we have to. + if self.egl_context.is_none() { + // Initalize EGL + self.hw_gl_egl_init(); + + let context = self.egl_context.as_ref().unwrap(); + let extensions = gpu::egl_helpers::get_extensions(context.get_display()); + + tracing::debug!("Supported EGL extensions: {:?}", extensions); + + // Check for EGL_KHR_get_all_proc_addresses, so we can use eglGetProcAddress() to load OpenGL functions + if !extensions.contains(&"EGL_KHR_get_all_proc_addresses".into()) { + tracing::error!("Your graphics driver doesn't support the EGL_KHR_get_all_proc_addresses extension."); + tracing::error!("Retrodemo currently needs this to load OpenGL functions. HW rendering will be disabled."); + return None; + } + + unsafe { + // Load OpenGL functions using the EGL loader. + gl::load_with(|s| { + let str = std::ffi::CString::new(s).expect("gl::load_with fail"); + std::mem::transmute(gpu::egl::GetProcAddress(str.as_ptr())) + }); + + // set OpenGL debug message callback + gl::Enable(gl::DEBUG_OUTPUT); + gl::DebugMessageCallback(Some(opengl_message_callback), std::ptr::null()); + } + } + + // Create the initial FBO for the core to render to + let dimensions = self.get_frontend().get_size(); + self.gl_framebuffer.resize(dimensions.0, dimensions.1); + + return Some(HwGlInitData { + get_proc_address: gpu::egl::GetProcAddress as *mut std::ffi::c_void, + }); + } +} + +impl Drop for App { + fn drop(&mut self) { + // Terminate EGL and GL resources if need be + self.hw_gl_destroy(); + } +} + +pub enum RetroEvent { + Frame, + Resize { size: Size }, + WantInputs { tx: oneshot::Sender> }, +} + +pub enum RetroInEvent { + Start, +} + +fn retro_thread_main( + surface: Arc>, + event_tx: mpsc::Sender, + mut event_rx: mpsc::Receiver, +) { + let mut app = App::new(surface, event_tx); + + app.load_core("cores/swanstation_libretro.so") + .expect("failed to load core"); + app.load_game("roms/merged/nmv1/us/nmv1_us.cue") //merged/nmv1/us/nmv1_us.cue + .expect("failed to load game"); + + // sync + loop { + match event_rx.blocking_recv() { + None => return (), + Some(msg) => match msg { + RetroInEvent::Start => break, + }, + } + } + + app.init(); + + loop { + app.tick(); + } +} + +pub fn spawn_retro_thread( + surface: Arc>, +) -> (mpsc::Receiver, mpsc::Sender) { + let (event_tx, event_rx) = mpsc::channel(8); + let (event_in_tx, event_in_rx) = mpsc::channel(8); + let fb_clone = surface.clone(); + + std::thread::spawn(move || { + retro_thread_main(fb_clone, event_tx, event_in_rx); + }); + + (event_rx, event_in_tx) +}