diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs index 7edc863..d39aa27 100644 --- a/crates/retro_frontend/src/libretro_callbacks.rs +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -6,7 +6,7 @@ use rgb565::Rgb565; use std::ffi; -use tracing::{debug, error, info}; +use tracing::{debug, error}; /// This function is used with HW OpenGL cores to transfer the current FBO's ID. unsafe extern "C" fn hw_gl_get_framebuffer() -> usize { @@ -110,7 +110,9 @@ pub(crate) unsafe extern "C" fn environment_callback( let hw_render_context_type = HwContextType::from_uint(hw_render.context_type).expect("Uh oh!"); - if hw_render_context_type != HwContextType::OpenGL && hw_render_context_type != HwContextType::OpenGLCore { + if hw_render_context_type != HwContextType::OpenGL + && hw_render_context_type != HwContextType::OpenGLCore + { error!( "Core is trying to request an context type we don't support ({:?}), failing", hw_render_context_type diff --git a/crates/retrovnc/src/app.rs b/crates/retrovnc/src/app.rs new file mode 100644 index 0000000..1dd7e5b --- /dev/null +++ b/crates/retrovnc/src/app.rs @@ -0,0 +1,394 @@ +use crate::egl; +use crate::rfb::*; + +use std::{path::Path, time::Duration}; + +use retro_frontend::{ + frontend::{Frontend, FrontendInterface, HwGlInitData}, + input_devices::{InputDevice, RetroPad}, +}; + +use anyhow::Result; + +/// 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 { + /// The frontend. + frontend: Option>, + + /// VNC server + rfb_server: Box, + + pad: RetroPad, + + /// True if HW rendering is active. + hw_render: bool, + + // EGL state + egl_display: egl::types::EGLDisplay, + egl_context: egl::types::EGLContext, + + // OpenGL object IDs + texture_id: gl::types::GLuint, + renderbuffer_id: gl::types::GLuint, + fbo_id: gl::types::GLuint, + + /// Cached readback buffer. + readback_buffer: Vec, +} + +impl App { + pub fn new(rfb_config: RfbServerConfig) -> Result> { + let mut boxed = Box::new(Self { + frontend: None, + rfb_server: RfbServer::new(rfb_config)?, + pad: RetroPad::new(), + hw_render: false, + egl_display: std::ptr::null(), + egl_context: std::ptr::null(), + texture_id: 0, + renderbuffer_id: 0, + fbo_id: 0, + readback_buffer: Vec::new(), + }); + + // SAFETY: The boxed allocation will never drop since the main loop always loops forever. + // Even if it did, 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". + // + // 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 as *mut dyn FrontendInterface)); + + Ok(boxed) + } + + fn get_frontend(&mut self) -> &mut Frontend { + self.frontend.as_mut().unwrap() + } + + pub fn init(&mut self) { + // Currently retrovnc just hardcodes the assumption of a single RetroPad. + + // SAFETY: This too won't ever be Use-After-Free'd because the only oppoturnity 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); + + // Initalize the display + self.init_display(); + } + + fn init_display(&mut self) { + let av_info = self.get_frontend().get_av_info().expect("No AV info"); + + // Start VNC server. + { + let server = &mut self.rfb_server; + tracing::info!("Starting VNC server"); + server.start(); + server.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 a headless EGL context for OpenGL rendering. + unsafe fn hw_gl_egl_init(&mut self) { + self.egl_display = egl::get_device_platform_display(); + + self.egl_context = { + const EGL_CONFIG_ATTRIBUTES: [egl::types::EGLenum; 13] = [ + egl::SURFACE_TYPE, + egl::PBUFFER_BIT, + egl::BLUE_SIZE, + 8, + egl::RED_SIZE, + 8, + egl::BLUE_SIZE, + 8, + egl::DEPTH_SIZE, + 8, + egl::RENDERABLE_TYPE, + egl::OPENGL_BIT, + egl::NONE, + ]; + let mut egl_major: egl::EGLint = 0; + let mut egl_minor: egl::EGLint = 0; + + let mut egl_config_count: egl::EGLint = 0; + + let mut config: egl::types::EGLConfig = std::ptr::null(); + + egl::Initialize( + self.egl_display, + std::ptr::addr_of_mut!(egl_major), + std::ptr::addr_of_mut!(egl_minor), + ); + + egl::ChooseConfig( + self.egl_display, + EGL_CONFIG_ATTRIBUTES.as_ptr() as *const egl::EGLint, + std::ptr::addr_of_mut!(config), + 1, + std::ptr::addr_of_mut!(egl_config_count), + ); + + egl::BindAPI(egl::OPENGL_API); + + let context = + egl::CreateContext(self.egl_display, config, egl::NO_CONTEXT, std::ptr::null()); + + // Make the context current on the display so OpenGL routines "just work" + egl::MakeCurrent(self.egl_display, egl::NO_SURFACE, egl::NO_SURFACE, context); + + context + }; + } + + /// Destroys EGL resources. + unsafe fn hw_gl_egl_exit(&mut self) { + if self.hw_render { + // Delete FBO + self.hw_gl_delete_fbo(); + + // Release the EGL context we created before destroying it + egl::MakeCurrent( + self.egl_display, + egl::NO_SURFACE, + egl::NO_SURFACE, + egl::NO_CONTEXT, + ); + egl::DestroyContext(self.egl_display, self.egl_context); + egl::Terminate(self.egl_display); + self.egl_display = std::ptr::null(); + self.egl_context = std::ptr::null(); + } + } + + /// Deletes all OpenGL FBO resources (the FBO itself, the render texture, and the renderbuffer used for depth) + fn hw_gl_delete_fbo(&mut self) { + unsafe { + gl::DeleteFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); + self.fbo_id = 0; + + gl::DeleteTextures(1, std::ptr::addr_of_mut!(self.texture_id)); + self.texture_id = 0; + + gl::DeleteRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id)); + self.renderbuffer_id = 0; + } + } + + fn hw_gl_create_fbo(&mut self, width: u32, height: u32) { + unsafe { + if self.fbo_id != 0 { + self.hw_gl_delete_fbo(); + } + + gl::GenFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + + gl::GenTextures(1, std::ptr::addr_of_mut!(self.texture_id)); + gl::BindTexture(gl::TEXTURE_2D, self.texture_id); + + gl::TexImage2D( + gl::TEXTURE_2D, + 0, + gl::RGBA8 as i32, + width as i32, + height as i32, + 0, + gl::RGBA, + gl::UNSIGNED_BYTE, + std::ptr::null(), + ); + + gl::GenRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id)); + gl::BindRenderbuffer(gl::RENDERBUFFER, self.renderbuffer_id); + + gl::RenderbufferStorage( + gl::RENDERBUFFER, + gl::DEPTH_COMPONENT, + width as i32, + height as i32, + ); + + gl::FramebufferTexture2D( + gl::FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gl::TEXTURE_2D, + self.texture_id, + 0, + ); + + gl::BindTexture(gl::TEXTURE_2D, 0); + + gl::FramebufferRenderbuffer( + gl::FRAMEBUFFER, + gl::DEPTH_ATTACHMENT, + gl::RENDERBUFFER, + self.renderbuffer_id, + ); + + gl::BindRenderbuffer(gl::RENDERBUFFER, 0); + + gl::Viewport(0, 0, width as i32, height as i32); + + gl::BindFramebuffer(gl::FRAMEBUFFER, 0); + + // Notify the frontend layer about the new FBO + let id = self.fbo_id; + self.get_frontend().set_gl_fbo(id); + + // Resize the readback buffer + self.readback_buffer.resize((width * height) as usize, 0); + } + } + + /// The main loop. Should probably be abstracted a bit better. + pub fn main_loop(&mut self) -> ! { + let frontend = self.get_frontend(); + + let av_info = frontend.get_av_info().expect("???"); + let step_ms = (1.0 / av_info.timing.fps) * 1000.; + let step_duration = Duration::from_micros((step_ms * 1000.) as u64); + + // Do the main loop + loop { + frontend.run_frame(); + std::thread::sleep(step_duration); + } + } +} + +impl FrontendInterface for App { + fn video_resize(&mut self, width: u32, height: u32) { + tracing::info!("Resized to {width}x{height}"); + + // Resize OpenGL resources if we need to. + if self.hw_render { + self.hw_gl_create_fbo(width, height); + } + + self.rfb_server.resize(width as u16, height as u16); + } + + fn video_update(&mut self, slice: &[u32], pitch: u32) { + //let framebuffer_size = self.get_frontend().get_size(); + self.rfb_server.update_buffer(&slice, pitch, false); + } + + fn video_update_gl(&mut self) { + let dimensions = self.get_frontend().get_size(); + + unsafe { + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + + // I know this sucks but it works for this case. + gl::ReadPixels( + 0, + 0, + dimensions.0 as i32, + dimensions.1 as i32, + gl::RGBA, + gl::UNSIGNED_BYTE, + self.readback_buffer.as_mut_ptr() as *mut std::ffi::c_void, + ); + + self.rfb_server + .update_buffer(&self.readback_buffer[..], dimensions.0, true); + + gl::BindFramebuffer(gl::FRAMEBUFFER, 0); + } + } + + fn audio_sample(&mut self, _slice: &[i16], _size: usize) {} + + fn input_poll(&mut self) { + self.pad.reset(); + + // Press all buttons the VNC server marked as pressed + let buttons = self.rfb_server.get_buttons(); + for i in 0..buttons.len() { + if buttons[i] { + self.pad.press_button(i as u32, None); + } + } + } + + fn hw_gl_init(&mut self) -> HwGlInitData { + if self.hw_render { + panic!("Cannot initalize HW rendering more than once"); + } + + unsafe { + // Initalize EGL + self.hw_gl_egl_init(); + + // load OpenGL functions (using EGL loader. We should probably check the one extension exists) + gl::load_with(|s| { + let str = std::ffi::CString::new(s).expect("Uhh huh."); + std::mem::transmute(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.hw_gl_create_fbo(dimensions.0, dimensions.1); + } + + self.hw_render = true; + + return unsafe { + HwGlInitData { + get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void), + } + }; + } +} diff --git a/crates/retrovnc/src/main.rs b/crates/retrovnc/src/main.rs index 9784f4d..ce41a30 100644 --- a/crates/retrovnc/src/main.rs +++ b/crates/retrovnc/src/main.rs @@ -1,392 +1,18 @@ -use std::{net::Ipv4Addr, path::Path, time::Duration}; - use anyhow::Result; +use std::net::Ipv4Addr; -use retro_frontend::{ - frontend::{Frontend, FrontendInterface, HwGlInitData}, - input_devices::{InputDevice, RetroPad}, -}; use tracing::Level; use tracing_subscriber::FmtSubscriber; use clap::{arg, command, value_parser}; -/// 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 - ); - } - } -} +mod app; mod egl; mod rfb; -use rfb::*; -struct App { - frontend: Option>, - rfb_server: Box, - pad: RetroPad, - - hw_render: bool, - - // EGL state - egl_display: egl::types::EGLDisplay, - egl_context: egl::types::EGLContext, - - // OpenGL object IDs - texture_id: gl::types::GLuint, - renderbuffer_id: gl::types::GLuint, - fbo_id: gl::types::GLuint, - - /// Cached readback buffer. - readback_buffer: Vec, -} - -impl App { - fn new(rfb_config: RfbServerConfig) -> Result> { - let mut boxed = Box::new(Self { - frontend: None, - rfb_server: RfbServer::new(rfb_config)?, - pad: RetroPad::new(), - hw_render: false, - egl_display: std::ptr::null(), - egl_context: std::ptr::null(), - texture_id: 0, - renderbuffer_id: 0, - fbo_id: 0, - readback_buffer: Vec::new(), - }); - - // SAFETY: The boxed allocation will never drop since the main loop always loops forever. - // Even if it did, 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". - // - // 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 as *mut dyn FrontendInterface)); - - Ok(boxed) - } - - fn get_frontend(&mut self) -> &mut Frontend { - self.frontend.as_mut().unwrap() - } - - fn init(&mut self) { - // Currently retrovnc just hardcodes the assumption of a single RetroPad. - - // SAFETY: This too won't ever be Use-After-Free'd because the only oppoturnity 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); - - // Initalize the display - self.init_display(); - } - - fn init_display(&mut self) { - let av_info = self.get_frontend().get_av_info().expect("No AV info"); - - // Start VNC server. - { - let server = &mut self.rfb_server; - tracing::info!("Starting VNC server"); - server.start(); - server.resize( - av_info.geometry.base_width as u16, - av_info.geometry.base_height as u16, - ); - } - } - - 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(()) - } - - fn load_game>(&mut self, path: P) -> Result<()> { - self.get_frontend().load_game(path)?; - Ok(()) - } - - /// Initalizes a headless EGL context for OpenGL rendering. - unsafe fn hw_gl_egl_init(&mut self) { - self.egl_display = egl::get_device_platform_display(); - - self.egl_context = { - const EGL_CONFIG_ATTRIBUTES: [egl::types::EGLenum; 13] = [ - egl::SURFACE_TYPE, - egl::PBUFFER_BIT, - egl::BLUE_SIZE, - 8, - egl::RED_SIZE, - 8, - egl::BLUE_SIZE, - 8, - egl::DEPTH_SIZE, - 8, - egl::RENDERABLE_TYPE, - egl::OPENGL_BIT, - egl::NONE, - ]; - let mut egl_major: egl::EGLint = 0; - let mut egl_minor: egl::EGLint = 0; - - let mut egl_config_count: egl::EGLint = 0; - - let mut config: egl::types::EGLConfig = std::ptr::null(); - - egl::Initialize( - self.egl_display, - std::ptr::addr_of_mut!(egl_major), - std::ptr::addr_of_mut!(egl_minor), - ); - - egl::ChooseConfig( - self.egl_display, - EGL_CONFIG_ATTRIBUTES.as_ptr() as *const egl::EGLint, - std::ptr::addr_of_mut!(config), - 1, - std::ptr::addr_of_mut!(egl_config_count), - ); - - egl::BindAPI(egl::OPENGL_API); - - let context = - egl::CreateContext(self.egl_display, config, egl::NO_CONTEXT, std::ptr::null()); - - // Make the context current on the display so OpenGL routines "just work" - egl::MakeCurrent(self.egl_display, egl::NO_SURFACE, egl::NO_SURFACE, context); - - context - }; - } - - /// Destroys EGL resources. - unsafe fn hw_gl_egl_exit(&mut self) { - // Release the EGL context we created before destroying it - egl::MakeCurrent( - self.egl_display, - egl::NO_SURFACE, - egl::NO_SURFACE, - egl::NO_CONTEXT, - ); - egl::DestroyContext(self.egl_display, self.egl_context); - egl::Terminate(self.egl_display); - self.egl_display = std::ptr::null(); - self.egl_context = std::ptr::null(); - } - - /// Deletes all OpenGL FBO resources (the FBO itself, the render texture, and the renderbuffer used for depth) - fn hw_gl_delete_fbo(&mut self) { - unsafe { - gl::DeleteFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); - self.fbo_id = 0; - - gl::DeleteTextures(1, std::ptr::addr_of_mut!(self.texture_id)); - self.texture_id = 0; - - gl::DeleteRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id)); - self.renderbuffer_id = 0; - } - } - - fn hw_gl_create_fbo(&mut self, width: u32, height: u32) { - unsafe { - if self.fbo_id != 0 { - self.hw_gl_delete_fbo(); - } - - gl::GenFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); - gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); - - gl::GenTextures(1, std::ptr::addr_of_mut!(self.texture_id)); - gl::BindTexture(gl::TEXTURE_2D, self.texture_id); - - gl::TexImage2D( - gl::TEXTURE_2D, - 0, - gl::RGBA8 as i32, - width as i32, - height as i32, - 0, - gl::RGBA, - gl::UNSIGNED_BYTE, - std::ptr::null(), - ); - - gl::GenRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id)); - gl::BindRenderbuffer(gl::RENDERBUFFER, self.renderbuffer_id); - - gl::RenderbufferStorage( - gl::RENDERBUFFER, - gl::DEPTH_COMPONENT, - width as i32, - height as i32, - ); - - gl::FramebufferTexture2D( - gl::FRAMEBUFFER, - gl::COLOR_ATTACHMENT0, - gl::TEXTURE_2D, - self.texture_id, - 0, - ); - - gl::BindTexture(gl::TEXTURE_2D, 0); - - gl::FramebufferRenderbuffer( - gl::FRAMEBUFFER, - gl::DEPTH_ATTACHMENT, - gl::RENDERBUFFER, - self.renderbuffer_id, - ); - - gl::BindRenderbuffer(gl::RENDERBUFFER, 0); - - gl::Viewport(0, 0, width as i32, height as i32); - - gl::BindFramebuffer(gl::FRAMEBUFFER, 0); - - // Notify the frontend layer about the new FBO - let id = self.fbo_id; - self.get_frontend().set_gl_fbo(id); - - // Resize the readback buffer - self.readback_buffer.resize((width * height) as usize, 0); - } - } - - // Main loop - fn main_loop(&mut self) -> ! { - let frontend = self.get_frontend(); - - let av_info = frontend.get_av_info().expect("???"); - let step_ms = (1.0 / av_info.timing.fps) * 1000.; - let step_duration = Duration::from_micros((step_ms * 1000.) as u64); - - // Do the main loop - loop { - frontend.run_frame(); - std::thread::sleep(step_duration); - } - } -} - -impl FrontendInterface for App { - fn video_resize(&mut self, width: u32, height: u32) { - tracing::info!("Resized to {width}x{height}"); - - // Resize OpenGL resources if we need to. - if self.hw_render { - self.hw_gl_create_fbo(width, height); - } - - self.rfb_server.resize(width as u16, height as u16); - } - - fn video_update(&mut self, slice: &[u32], pitch: u32) { - //let framebuffer_size = self.get_frontend().get_size(); - self.rfb_server.update_buffer(&slice, pitch, false); - } - - fn video_update_gl(&mut self) { - let dimensions = self.get_frontend().get_size(); - - unsafe { - gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); - - // I know this sucks but it works for this case. - gl::ReadPixels( - 0, - 0, - dimensions.0 as i32, - dimensions.1 as i32, - gl::RGBA, - gl::UNSIGNED_BYTE, - self.readback_buffer.as_mut_ptr() as *mut std::ffi::c_void, - ); - - self.rfb_server - .update_buffer(&self.readback_buffer[..], dimensions.0, true); - - gl::BindFramebuffer(gl::FRAMEBUFFER, 0); - } - } - - fn audio_sample(&mut self, _slice: &[i16], _size: usize) {} - - fn input_poll(&mut self) { - self.pad.reset(); - - // Press all buttons the VNC server marked as pressed - let buttons = self.rfb_server.get_buttons(); - for i in 0..buttons.len() { - if buttons[i] { - self.pad.press_button(i as u32, None); - } - } - } - - fn hw_gl_init(&mut self) -> HwGlInitData { - if self.hw_render { - panic!("Cannot initalize HW rendering more than once"); - } - - unsafe { - // Initalize EGL - self.hw_gl_egl_init(); - - // load OpenGL functions (using EGL loader. We should probably check the one extension exists) - gl::load_with(|s| { - let str = std::ffi::CString::new(s).expect("Uhh huh."); - std::mem::transmute(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.hw_gl_create_fbo(dimensions.0, dimensions.1); - } - - self.hw_render = true; - - return unsafe { - HwGlInitData { - get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void), - } - }; - } -} +use app::*; +use rfb::RfbServerConfig; fn main() -> Result<()> { // Setup a tracing subscriber