diff --git a/Cargo.lock b/Cargo.lock index ca66112..73d26e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -275,6 +275,14 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "letsplay_gpu" +version = "0.1.0" +dependencies = [ + "gl", + "gl_generator", +] + [[package]] name = "libc" version = "0.2.155" @@ -445,7 +453,6 @@ dependencies = [ "libc", "libloading", "libretro-sys", - "rgb565", "serde", "thiserror", "toml", @@ -460,19 +467,13 @@ dependencies = [ "cc", "clap", "gl", - "gl_generator", + "letsplay_gpu", "libvnc-sys", "retro_frontend", "tracing", "tracing-subscriber", ] -[[package]] -name = "rgb565" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43e85498d0bb728f77a88b4313eaf4ed21673f3f8a05c36e835cf6c9c0d066" - [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8ca25ba..eaaa1ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,8 @@ resolver = "2" members = [ "crates/*" ] + +[workspace.dependencies] +gl = "0.14.0" +gl_generator = "0.14.0" +thiserror = "1.0.61" diff --git a/crates/letsplay_gpu/Cargo.toml b/crates/letsplay_gpu/Cargo.toml new file mode 100644 index 0000000..6e9e533 --- /dev/null +++ b/crates/letsplay_gpu/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "letsplay_gpu" +description = "GPU helpers and bindings for Let's Play" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +gl.workspace = true + +[build-dependencies] +gl_generator.workspace = true diff --git a/crates/letsplay_gpu/README.md b/crates/letsplay_gpu/README.md new file mode 100644 index 0000000..d9b203c --- /dev/null +++ b/crates/letsplay_gpu/README.md @@ -0,0 +1,7 @@ +# `letsplay_gpu` + +Shared helpers for Let's Play GPU stuff. + +- EGL bindings +- GL FBO helpers +- ... diff --git a/crates/letsplay_gpu/build.rs b/crates/letsplay_gpu/build.rs new file mode 100644 index 0000000..a4ca03b --- /dev/null +++ b/crates/letsplay_gpu/build.rs @@ -0,0 +1,28 @@ +use gl_generator::{Api, Fallbacks, Profile, Registry, StaticGenerator}; +use std::env; +use std::fs::File; +use std::path::Path; + +fn main() { + // EGL + + let dest = env::var("OUT_DIR").unwrap(); + let mut file = File::create(&Path::new(&dest).join("egl_bindings.rs")).unwrap(); + + Registry::new( + Api::Egl, + (1, 5), + Profile::Core, + Fallbacks::All, + [ + "EGL_EXT_platform_base", + "EGL_EXT_device_base", + // This allows getting OpenGL APIs via eglGetProcAddress() + "EGL_KHR_get_all_proc_addresses", + "EGL_KHR_client_get_all_proc_addresses", + "EGL_EXT_platform_device", + ], + ) + .write_bindings(StaticGenerator, &mut file) + .unwrap(); +} diff --git a/crates/letsplay_gpu/src/gl_framebuffer.rs b/crates/letsplay_gpu/src/gl_framebuffer.rs new file mode 100644 index 0000000..ef9653a --- /dev/null +++ b/crates/letsplay_gpu/src/gl_framebuffer.rs @@ -0,0 +1,161 @@ +use std::ffi; +use std::ptr::{addr_of_mut, null}; + +/// Helper wrapper over a OpenGL Frame Buffer Object (FBO) that creates and completes it via the other required +/// OpenGL objects. Useful for render-to-texture or other scenarios. +pub struct GlFramebuffer { + // OpenGL object IDs + texture_id: gl::types::GLuint, + renderbuffer_id: gl::types::GLuint, + fbo_id: gl::types::GLuint, +} + +pub struct BindGuard {} + +impl BindGuard { + fn new(fbo_id: gl::types::GLuint) -> Self { + unsafe { + gl::BindFramebuffer(gl::FRAMEBUFFER, fbo_id); + } + Self {} + } +} + +impl Drop for BindGuard { + fn drop(&mut self) { + unsafe { + gl::BindFramebuffer(gl::FRAMEBUFFER, 0); + } + } +} + +impl GlFramebuffer { + pub fn new() -> Self { + Self { + fbo_id: 0, + texture_id: 0, + renderbuffer_id: 0, + } + } + + /// Destroys this framebuffer. + /// + /// All OpenGL FBO resources (the FBO itself, the render texture, and the renderbuffer used for depth) are deleted by this call. + pub fn destroy(&mut self) { + unsafe { + gl::DeleteFramebuffers(1, addr_of_mut!(self.fbo_id)); + self.fbo_id = 0; + + gl::DeleteTextures(1, addr_of_mut!(self.texture_id)); + self.texture_id = 0; + + gl::DeleteRenderbuffers(1, addr_of_mut!(self.renderbuffer_id)); + self.renderbuffer_id = 0; + } + } + + /// Creates the OpenGL FBO. + pub fn resize(&mut self, width: u32, height: u32) { + unsafe { + if self.fbo_id != 0 { + self.destroy(); + } + + gl::GenTextures(1, 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, + null(), + ); + + gl::BindTexture(gl::TEXTURE_2D, 0); + + gl::GenRenderbuffers(1, 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::BindRenderbuffer(gl::RENDERBUFFER, 0); + + gl::GenFramebuffers(1, addr_of_mut!(self.fbo_id)); + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + + gl::FramebufferTexture2D( + gl::FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gl::TEXTURE_2D, + self.texture_id, + 0, + ); + + gl::FramebufferRenderbuffer( + gl::FRAMEBUFFER, + gl::DEPTH_ATTACHMENT, + gl::RENDERBUFFER, + self.renderbuffer_id, + ); + + gl::Viewport(0, 0, width as i32, height as i32); + + gl::BindFramebuffer(gl::FRAMEBUFFER, 0); + } + } + + pub fn as_raw(&self) -> gl::types::GLuint { + self.fbo_id + } + + /// Obtains the texture ID. This will change on resize, + /// and is managed by this GlFramebuffer, so don't poke around + /// too much with it. (readback or immutable operations in general are fine, + /// as well as a subset of mutable operations.) + pub fn texture_id(&self) -> gl::types::GLuint { + self.texture_id + } + + // TODO: accessors for the render texture + + /// Binds this framebuffer in the current scope. + pub fn bind(&self) -> BindGuard { + BindGuard::new(self.fbo_id) + } + + /// Reads pixels to a CPU-side buffer. Uses glReadPixels so it probably sucks. + pub fn read_pixels(&self, buffer: &mut [u32], width: u32, height: u32) { + let _guard = self.bind(); + + assert_eq!( + buffer.len(), + (width * height) as usize, + "Provided buffer cannot hold the framebuffer" + ); + + // SAFETY: The above assertion prevents the following code from + // violating memory safety by appropiately asserting the invariant + // that we must have width * heigth pixels of space to write to. + unsafe { + gl::ReadPixels( + 0, + 0, + width as i32, + height as i32, + gl::RGBA, + gl::UNSIGNED_BYTE, + buffer.as_mut_ptr() as *mut ffi::c_void, + ); + } + } +} diff --git a/crates/letsplay_gpu/src/lib.rs b/crates/letsplay_gpu/src/lib.rs new file mode 100644 index 0000000..164e626 --- /dev/null +++ b/crates/letsplay_gpu/src/lib.rs @@ -0,0 +1,216 @@ +//! EGL bindings and helpers. + +#[allow(non_camel_case_types)] +#[allow(unused_imports)] +pub mod egl { + pub type khronos_utime_nanoseconds_t = khronos_uint64_t; + pub type khronos_uint64_t = u64; + pub type khronos_ssize_t = std::ffi::c_long; + pub type EGLint = i32; + pub type EGLNativeDisplayType = *const std::ffi::c_void; + pub type EGLNativePixmapType = *const std::ffi::c_void; + pub type EGLNativeWindowType = *const std::ffi::c_void; + + pub type NativeDisplayType = EGLNativeDisplayType; + pub type NativePixmapType = EGLNativePixmapType; + pub type NativeWindowType = EGLNativeWindowType; + + include!(concat!(env!("OUT_DIR"), "/egl_bindings.rs")); + + // link EGL as a library dependency + #[link(name = "EGL")] + extern "C" {} +} + +/// Helper code for making EGL easier to use. +pub mod egl_helpers { + use super::egl; + use egl::*; + + // TODO: Move these helpers to a new "helpers" module. + + pub type GetPlatformDisplayExt = unsafe extern "C" fn( + platform: types::EGLenum, + native_display: *const std::ffi::c_void, + attrib_list: *const types::EGLint, + ) -> types::EGLDisplay; + + pub type QueryDevicesExt = unsafe extern "C" fn( + max_devices: self::types::EGLint, + devices: *mut self::types::EGLDeviceEXT, + devices_present: *mut EGLint, + ) -> types::EGLBoolean; + + /// Queries all available extensions on a display. + pub fn get_extensions(display: types::EGLDisplay) -> Vec { + // SAFETY: eglQueryString() should never return a null pointer. + // If it does your video drivers are more than likely broken beyond repair. + unsafe { + let extensions_ptr = QueryString(display, EXTENSIONS as i32); + assert!(!extensions_ptr.is_null()); + + let extensions_str = std::ffi::CStr::from_ptr(extensions_ptr) + .to_str() + .expect("Invalid EGL_EXTENSIONS"); + + extensions_str + .split(' ') + .map(|str| str.to_string()) + .collect() + } + } + + /// A helper to get a display on the EGL "Device" platform, + /// which allows headless rendering without any window system interface. + pub fn get_device_platform_display(index: usize) -> types::EGLDisplay { + const NR_DEVICES_MAX: usize = 16; + let mut devices: [types::EGLDeviceEXT; NR_DEVICES_MAX] = [std::ptr::null(); NR_DEVICES_MAX]; + + // This is how many devices are actually present, + let mut devices_present: EGLint = 0; + + assert!( + index <= NR_DEVICES_MAX, + "More than {NR_DEVICES_MAX} devices are not supported right now" + ); + + unsafe { + // TODO: These should probbaly be using CStr like above. + let query_devices_ext: QueryDevicesExt = + std::mem::transmute(GetProcAddress(b"eglQueryDevicesEXT\0".as_ptr() as *const i8)); + let get_platform_display_ext: GetPlatformDisplayExt = std::mem::transmute( + GetProcAddress(b"eglGetPlatformDisplayEXT\0".as_ptr() as *const i8), + ); + + (query_devices_ext)( + NR_DEVICES_MAX as i32, + devices.as_mut_ptr(), + std::ptr::addr_of_mut!(devices_present), + ); + + (get_platform_display_ext)(PLATFORM_DEVICE_EXT, devices[index], std::ptr::null()) + } + } + + // FIXME: Make this send and provide a (optional) wrapper which + // allows the context to be temporairly acquired on *one* other OS thread. + // + // This will be needed for zero-copy (or, well.. one-copy. It's still far less round trips >_<) encoding support. + + /// A wrapper over a EGL Device context. Provides easy initialization and + /// cleanup functions. + pub struct DeviceContext { + display: types::EGLDisplay, + context: types::EGLContext, + } + + impl DeviceContext { + pub fn new(index: usize) -> DeviceContext { + // FIXME: We should PROBABLY do slightly better error handling + let display = self::get_device_platform_display(index); + + let context = unsafe { + const EGL_CONFIG_ATTRIBUTES: [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( + display, + std::ptr::addr_of_mut!(egl_major), + std::ptr::addr_of_mut!(egl_minor), + ); + + egl::ChooseConfig( + 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(display, config, egl::NO_CONTEXT, std::ptr::null()); + + context + }; + + Self { display, context } + } + + /// Makes this context current on the currently executing OS thread. + /// # Safety + /// This should only be called on one OS thread at a time. If contexts + /// are to be shared, currently, that needs to be done manually. + pub fn make_current(&self) { + unsafe { + // Make the context current on the display so OpenGL routines "just work" + egl::MakeCurrent(self.display, egl::NO_SURFACE, egl::NO_SURFACE, self.context); + } + } + + /// Releases this context. + pub fn release(&self) { + unsafe { + egl::MakeCurrent( + self.display, + egl::NO_SURFACE, + egl::NO_SURFACE, + egl::NO_CONTEXT, + ); + } + } + + pub fn get_display(&self) -> types::EGLDisplay { + self.display + } + + pub fn destroy(&mut self) { + if self.display.is_null() && self.context.is_null() { + return; + } + + // Destroy and terminate EGL resources. + unsafe { + egl::DestroyContext(self.display, self.context); + egl::Terminate(self.display); + + self.display = std::ptr::null(); + self.context = std::ptr::null(); + } + } + } + + // "safe" + unsafe impl Send for DeviceContext {} + + // TODO: impl Drop? + // This could be problematic because OpenGL resources need to be destroyed + // somehow *before* we are. This could be solved in a number of ways but + // honestly I think the best one (that I can think of) + // is to provide an explicit drop point where OpenGL resources are destroyed + // before the EGL device context is (and then tie that to an `impl Drop for T`.). +} + +pub mod gl_framebuffer; + +pub use gl_framebuffer::*; diff --git a/crates/retro_frontend/Cargo.toml b/crates/retro_frontend/Cargo.toml index 8a1a6d3..cb39edd 100644 --- a/crates/retro_frontend/Cargo.toml +++ b/crates/retro_frontend/Cargo.toml @@ -9,9 +9,8 @@ edition = "2021" libc = "0.2.155" libloading = "0.8.3" libretro-sys = "0.1.1" -rgb565 = "0.1.3" serde = { version = "1.0.204", features = ["derive"] } -thiserror = "1.0.61" +thiserror.workspace = true toml = "0.8.19" tracing = "0.1.40" diff --git a/crates/retro_frontend/src/frontend.rs b/crates/retro_frontend/src/frontend.rs index 2e526f6..b2faa5c 100644 --- a/crates/retro_frontend/src/frontend.rs +++ b/crates/retro_frontend/src/frontend.rs @@ -21,12 +21,10 @@ use tracing::{error, info}; /// Only one instance of Frontend can be active in an application. pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut(); -/// A "Generalized" type for OpenGL's "getProcAddress" idiom -pub type GlGetProcAddress = *mut unsafe extern "C" fn(name: *const i8) -> unsafe extern "C" fn(); - /// Initalization data for HW OpenGL cores. pub struct HwGlInitData { - pub get_proc_address: GlGetProcAddress, + /// A pointer to a function to allow cores to request OpenGL extension functions. + pub get_proc_address: *mut ffi::c_void, } /// Interface for the frontend to call to user code. @@ -48,7 +46,9 @@ pub trait FrontendInterface { fn input_poll(&mut self); /// Initalize hardware accelerated rendering using OpenGL. - fn hw_gl_init(&mut self) -> HwGlInitData; + /// If this returns [Option::None], then it is assumed that + /// OpenGL initalization has failed. + fn hw_gl_init(&mut self) -> Option; } /// Per-core settings @@ -75,7 +75,7 @@ pub struct Frontend { pub(crate) pixel_format: PixelFormat, /// Converted pixel buffer. We store it here so we don't keep allocating over and over. - pub(crate) converted_pixel_buffer: Vec, + pub(crate) converted_pixel_buffer: Option>, // Framebuffer attributes. TODO: This really should be another struct or something // with members to make dealing with it less annoying. @@ -118,7 +118,7 @@ impl Frontend { sys_info: None, pixel_format: PixelFormat::RGB565, - converted_pixel_buffer: Vec::new(), + converted_pixel_buffer: None, fb_width: 0, fb_height: 0, @@ -184,16 +184,20 @@ impl Frontend { } } - fn get_config_file_path(&self) -> String { + fn get_config_file_path(&mut self) -> Result { + let system_info = self.get_system_info()?; + + // SAFETY: libretro declares that the pointers inside of the SystemInfo structure + // must always point to valid constant data. If it doesn't then other frontends + // would probably blow up too. let path = unsafe { - let core_api = self.core_api.as_ref().unwrap(); + #[cfg(debug_assertions)] + assert!( + !system_info.library_name.is_null(), + "Core library name is somehow null" + ); - let mut system_info: MaybeUninit = MaybeUninit::uninit(); - (core_api.retro_get_system_info)(system_info.as_mut_ptr()); - - let info = system_info.assume_init(); - - let c_name = ffi::CStr::from_ptr(info.library_name); + let c_name = ffi::CStr::from_ptr(system_info.library_name); format!( "{}/{}.toml", @@ -202,13 +206,15 @@ impl Frontend { ) }; - path + Ok(path) } // TODO: make this a bit less janky (and use Results) pub fn load_settings(&mut self) { - let path_string = self.get_config_file_path(); + let path_string = self + .get_config_file_path() + .expect("Could not get config file path"); let path: &Path = path_string.as_ref(); match path.try_exists() { @@ -230,7 +236,9 @@ impl Frontend { } pub fn save_settings(&mut self) { - let path = self.get_config_file_path(); + let path = self + .get_config_file_path() + .expect("Could not get config file path"); let settings = CoreSettingsFile { variables: self.variables.clone(), @@ -327,6 +335,7 @@ impl Frontend { (core_api_ref.retro_set_audio_sample_batch)( libretro_callbacks::audio_sample_batch_callback, ); + (core_api_ref.retro_set_audio_sample)(libretro_callbacks::audio_sample_callback); info!("Core {} loaded", path.as_ref().display()); } @@ -364,6 +373,7 @@ impl Frontend { self.fb_width = 0; self.fb_height = 0; self.fb_pitch = 0; + self.converted_pixel_buffer = None; // disconnect all currently connected joypads self.input_devices.clear(); @@ -508,13 +518,15 @@ impl Frontend { impl Drop for Frontend { fn drop(&mut self) { - if self.core_loaded() { - let _ = self.unload_core(); - } - + // Null out the global frontend pointer first, + // so any attempted UAF will instead result in a segfault unsafe { assert!(!FRONTEND.is_null()); FRONTEND = std::ptr::null_mut(); } + + if self.core_loaded() { + let _ = self.unload_core(); + } } } diff --git a/crates/retro_frontend/src/input_devices/analog_retropad.rs b/crates/retro_frontend/src/input_devices/analog_retropad.rs new file mode 100644 index 0000000..583b1f6 --- /dev/null +++ b/crates/retro_frontend/src/input_devices/analog_retropad.rs @@ -0,0 +1,115 @@ +use crate::libretro_sys_new; + +use super::{InputDevice, RetroPad}; + +// private helper type for packaging up the stick data +struct Stick { + pub x: i16, + pub y: i16, +} + +impl Stick { + fn new() -> Self { + Self { x: 0, y: 0 } + } + + fn clear(&mut self) { + self.x = 0; + self.y = 0; + } +} + +/// Implementation of the [InputDevice] trait for the +/// Analog RetroPad. Currently, this is mostly a stub which calls +/// into the RetroPad implementation w/out actually implementing +/// any of the analog axes or addl. features. +pub struct AnalogRetroPad { + pad: RetroPad, + left_stick: Stick, + right_stick: Stick, +} + +impl AnalogRetroPad { + pub fn new() -> Self { + Self { + pad: RetroPad::new(), + left_stick: Stick::new(), + right_stick: Stick::new(), + } + } +} + +// Sidenote: I really don't like the fact I have to manually thunk, +// but thankfully there's only like one, maybe 2 levels of subclassing +// in the Libretro input APIs, so it won't grow too awfully.. + +impl InputDevice for AnalogRetroPad { + fn device_type(&self) -> u32 { + libretro_sys_new::DEVICE_ANALOG + } + + fn device_type_compatible(&self, id: u32) -> bool { + if self.pad.device_type_compatible(id) { + // If the RetroPad likes it, then so do we. + true + } else { + // Check for the analog type + id == libretro_sys_new::DEVICE_ANALOG + } + } + + fn reset(&mut self) { + self.pad.reset(); + self.left_stick.clear(); + self.right_stick.clear(); + } + + fn get_index(&self, index: u32, id: u32) -> i16 { + // Nasty but you can blame libretro. + let fallback = self.pad.get_index(0, id); + return match index { + libretro_sys_new::DEVICE_INDEX_ANALOG_LEFT => match id { + libretro_sys_new::DEVICE_ID_ANALOG_X => self.left_stick.x, + libretro_sys_new::DEVICE_ID_ANALOG_Y => self.left_stick.y, + _ => fallback, + }, + libretro_sys_new::DEVICE_INDEX_ANALOG_RIGHT => match id { + libretro_sys_new::DEVICE_ID_ANALOG_X => self.right_stick.x, + libretro_sys_new::DEVICE_ID_ANALOG_Y => self.right_stick.y, + _ => fallback, + }, + + _ => 0i16, + }; + } + + fn press_button(&mut self, id: u32, pressure: Option) { + // FIXME: "press" axes. + self.pad.press_button_friend(id, pressure); + } + + fn press_analog_axis(&mut self, index: u32, id: u32, pressure: Option) { + let pressure = if let Some(pressure_value) = pressure { + pressure_value + } else { + //0x4000 // 0.5 in Libretro's mapping + 0x7fff // 1.0 in Libretro's mapping + }; + + match index { + libretro_sys_new::DEVICE_INDEX_ANALOG_LEFT => match id { + libretro_sys_new::DEVICE_ID_ANALOG_X => self.left_stick.x = pressure, + libretro_sys_new::DEVICE_ID_ANALOG_Y => self.left_stick.y = pressure, + _ => {} + }, + + libretro_sys_new::DEVICE_INDEX_ANALOG_RIGHT => match id { + libretro_sys_new::DEVICE_ID_ANALOG_X => self.right_stick.x = pressure, + libretro_sys_new::DEVICE_ID_ANALOG_Y => self.right_stick.y = pressure, + _ => {} + }, + + _ => {} + } + } +} diff --git a/crates/retro_frontend/src/input_devices/mod.rs b/crates/retro_frontend/src/input_devices/mod.rs index f39b187..b9885fe 100644 --- a/crates/retro_frontend/src/input_devices/mod.rs +++ b/crates/retro_frontend/src/input_devices/mod.rs @@ -5,18 +5,43 @@ pub use retropad::*; pub mod mouse; pub use mouse::*; -/// Trait for implementing Libretro input devices +pub mod analog_retropad; +pub use analog_retropad::*; + +/// Trait/abstraction for implementing Libretro input devices. pub trait InputDevice { /// Gets the device type. This should never EVER change, and simply return a constant. fn device_type(&self) -> u32; + /// Returns true if the input device is compatible with + /// the given libretro device ID. + /// + /// This is needed because Libretro will often look up a device with + /// either its "base" type (i.e: RetroPad) and then ask for a subclass + /// by changing the ID to what it wants to look for (say, Analog RetroPad). + /// + /// Therefore, a simple "id matches exactly" comparision doesn't work. + fn device_type_compatible(&self, id: u32) -> bool; + /// Gets the state of one button/axis. - /// is_pressed(id) can simply be expressed as `(get_button(id) != 0)`. - fn get_button(&self, id: u32) -> i16; + /// is_pressed(index, id) can simply be expressed in a digital way as `(get_index(index, id) != 0)`. + /// analog could be `(get_index(index_id) as f32 / 32768.)` + fn get_index(&self, _index: u32, _id: u32) -> i16; + + /// Like get_index, but prescales to a float in the inclusive range [-1.0 .. 1.0]. + /// (This is how I want all the API's to be at some point) + fn get_axis(&self, index: u32, id: u32) -> f32 { + self.get_index(index, id) as f32 / 32768. + } /// Clears the state of all buttons/axes. fn reset(&mut self); - /// Presses a button/axis. + /// Presses a button. [pressure] is permitted to be ignored. fn press_button(&mut self, id: u32, pressure: Option); + + /// Presses a joystick axis. + /// FIXME: It may make more sense to pass a f32 here and do the scale internally. + /// should do that for buttons too. Also do we really have to use _sys constants? + fn press_analog_axis(&mut self, _index: u32, _id: u32, _pressure: Option) {} } diff --git a/crates/retro_frontend/src/input_devices/mouse.rs b/crates/retro_frontend/src/input_devices/mouse.rs index e219553..4250fff 100644 --- a/crates/retro_frontend/src/input_devices/mouse.rs +++ b/crates/retro_frontend/src/input_devices/mouse.rs @@ -5,7 +5,6 @@ use crate::libretro_sys_new; /// Implementation of the [InputDevice] trait for the Libretro mouse. pub struct Mouse { buttons: [i16; 8], - // TODO: hold the last x/y so we can calculate relative position } @@ -25,7 +24,11 @@ impl InputDevice for Mouse { libretro_sys_new::DEVICE_MOUSE } - fn get_button(&self, id: u32) -> i16 { + fn device_type_compatible(&self, id: u32) -> bool { + id == self.device_type() + } + + fn get_index(&self, _index: u32, id: u32) -> i16 { if id > 8 { return 0; } diff --git a/crates/retro_frontend/src/input_devices/retropad.rs b/crates/retro_frontend/src/input_devices/retropad.rs index 59ee7cc..afe32eb 100644 --- a/crates/retro_frontend/src/input_devices/retropad.rs +++ b/crates/retro_frontend/src/input_devices/retropad.rs @@ -13,28 +13,20 @@ impl RetroPad { pub fn new() -> Self { Self { buttons: [0; 16] } } -} -impl InputDevice for RetroPad { - fn device_type(&self) -> u32 { - libretro_sys_new::DEVICE_JOYPAD - } + pub(crate) fn button_mask(&self) -> i16 { + let mut mask = 0u16; - fn get_button(&self, id: u32) -> i16 { - if id > 16 { - return 0; + for i in 0..self.buttons.len() { + if self.buttons[i] != 0 { + mask |= 1 << i; + } } - self.buttons[id as usize] + mask as i16 } - fn reset(&mut self) { - for button in &mut self.buttons { - *button = 0i16; - } - } - - fn press_button(&mut self, id: u32, pressure: Option) { + pub(crate) fn press_button_friend(&mut self, id: u32, pressure: Option) { if id > 16 { return; } @@ -50,3 +42,41 @@ impl InputDevice for RetroPad { } } } + +impl InputDevice for RetroPad { + fn device_type(&self) -> u32 { + libretro_sys_new::DEVICE_JOYPAD + } + + fn device_type_compatible(&self, id: u32) -> bool { + id == self.device_type() + } + + fn get_index(&self, index: u32, id: u32) -> i16 { + return match index { + 0 => { + if id == libretro_sys_new::DEVICE_ID_JOYPAD_MASK { + return self.button_mask(); + } + + if id > 16 { + 0i16 + } else { + self.buttons[id as usize] + } + } + _ => 0i16, + }; + } + + fn reset(&mut self) { + for button in &mut self.buttons { + *button = 0i16; + } + } + + fn press_button(&mut self, id: u32, _pressure: Option) { + // Pressure is ignored and treated as binary. + self.press_button_friend(id, Some(1)); + } +} diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs index d39aa27..1c8e140 100644 --- a/crates/retro_frontend/src/libretro_callbacks.rs +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -1,9 +1,8 @@ //! Callbacks for libretro +use crate::input_devices::InputDevice; use crate::{frontend::*, libretro_log, util}; use crate::{libretro_core_variable, libretro_sys_new::*}; -use rgb565::Rgb565; - use std::ffi; use tracing::{debug, error}; @@ -110,26 +109,40 @@ 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 - { - error!( - "Core is trying to request an context type we don't support ({:?}), failing", - hw_render_context_type - ); - return false; + match hw_render_context_type { + HwContextType::OpenGL | HwContextType::OpenGLCore => { + let init_data = (*(*FRONTEND).interface).hw_gl_init(); + + if init_data.is_none() { + return false; + } + + let init_data_unwrapped = init_data.unwrap(); + + hw_render.get_current_framebuffer = hw_gl_get_framebuffer; + hw_render.get_proc_address = + std::mem::transmute(init_data_unwrapped.get_proc_address); + + // Reset the context now that we have given the core the correct information + (hw_render.context_reset)(); + + tracing::info!( + "{:?} HWContext initalized successfully", + hw_render_context_type + ); + } + + _ => { + error!( + "Core is trying to request an context type we don't support ({:?}), failing", + hw_render_context_type + ); + return false; + } } - let init_data = (*(*FRONTEND).interface).hw_gl_init(); - - hw_render.get_current_framebuffer = hw_gl_get_framebuffer; - hw_render.get_proc_address = std::mem::transmute(init_data.get_proc_address); - - // reset context - (hw_render.context_reset)(); - - // Once we have initalized HW rendering any data here doesn't matter and isn't needed. - (*FRONTEND).converted_pixel_buffer.clear(); + // Once we have initalized HW rendering, we do not need a conversion buffer. + (*FRONTEND).converted_pixel_buffer = None; return true; } @@ -206,22 +219,22 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( height: ffi::c_uint, pitch: usize, ) { - // I guess this must be how duplicated frames are signaled. - // one word: Bleh + // This is how frame duplication is signaled, even w/ HW rendering (or some cores are just badly written. + // I mean I've already had to baby cores enough!). + // One word: Bleh. if pixels.is_null() { return; } - //info!("Video refresh called, {width}, {height}, {pitch}"); - if (*FRONTEND).fb_width != width || (*FRONTEND).fb_height != height { + // bleh + (*FRONTEND).fb_width = width; + (*FRONTEND).fb_height = height; + (*(*FRONTEND).interface).video_resize(width, height); } - // bleh - (*FRONTEND).fb_width = width; - (*FRONTEND).fb_height = height; - + // This means that hardware context was used to render and we need to get it via that. if pixels == (-1i64 as *const ffi::c_void) { (*(*FRONTEND).interface).video_update_gl(); return; @@ -232,43 +245,103 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( let pitch = (*FRONTEND).fb_pitch as usize; + // Resize or allocate the conversion buffer if we need to + if (*FRONTEND).converted_pixel_buffer.is_none() { + (*FRONTEND).converted_pixel_buffer = Some(util::alloc_boxed_slice(pitch * height as usize)); + } else { + let buffer = (*FRONTEND).converted_pixel_buffer.as_ref().unwrap(); + if (pitch * height as usize) as usize != buffer.len() { + (*FRONTEND).converted_pixel_buffer = + Some(util::alloc_boxed_slice(pitch * height as usize)); + } + } + + let buffer = (*FRONTEND).converted_pixel_buffer.as_mut().unwrap(); + + // Depending on the pixel format, do the appropiate conversion to XRGB8888. + // + // We use XRGB8888 as a standard format since it's more convinent to work with + // and also directly works with NVENC. (Other encoders will require GPU-side kernels to + // conv. to YUV or NV12, but that's a battle for later.) match (*FRONTEND).pixel_format { + PixelFormat::ARGB1555 => { + let pixel_data_slice = std::slice::from_raw_parts( + pixels as *const u16, + (pitch * height as usize) as usize, + ); + + for x in 0..pitch as usize { + for y in 0..height as usize { + let pixel = pixel_data_slice[y * pitch as usize + x]; + + // We currently ignore the alpha bit + let comp = ( + (pixel & 0x7c00) as u8, + ((pixel & 0x3e0) >> 8) as u8, + (pixel & 0x1f) as u8, + ); + + // Finally save the pixel data in the result array as an XRGB8888 value + buffer[y * pitch as usize + x] = (255u32 << 24) + | ((comp.2 as u32) << 16) + | ((comp.1 as u32) << 8) | (comp.0 as u32); + } + } + } + PixelFormat::RGB565 => { let pixel_data_slice = std::slice::from_raw_parts( pixels as *const u16, (pitch * height as usize) as usize, ); - // Resize the conversion buffer if we need to - if (pitch * height as usize) as usize != (*FRONTEND).converted_pixel_buffer.len() { - (*FRONTEND) - .converted_pixel_buffer - .resize((pitch * height as usize) as usize, 0); - } - for x in 0..pitch as usize { for y in 0..height as usize { - let rgb = Rgb565::from_rgb565(pixel_data_slice[y * pitch as usize + x]); - let comp = rgb.to_rgb888_components(); + let pixel = pixel_data_slice[y * pitch as usize + x]; + let comp: (u8, u8, u8) = ( + ((pixel >> 11 & 0x1f) * 255 / 0x1f) as u8, + ((pixel >> 5 & 0x3f) * 255 / 0x3f) as u8, + ((pixel & 0x1f) * 255 / 0x1f) as u8, + ); - // Finally save the pixel data in the result array as an XRGB8888 value - (*FRONTEND).converted_pixel_buffer[y * pitch as usize + x] = - ((comp[0] as u32) << 16) | ((comp[1] as u32) << 8) | (comp[2] as u32); + buffer[y * pitch as usize + x] = (255u32 << 24) + | ((comp.2 as u32) << 16) + | ((comp.1 as u32) << 8) | (comp.0 as u32); } } - - (*(*FRONTEND).interface) - .video_update(&(*FRONTEND).converted_pixel_buffer[..], pitch as u32); } - _ => { + + PixelFormat::ARGB8888 => { let pixel_data_slice = std::slice::from_raw_parts( pixels as *const u32, (pitch * height as usize) as usize, ); - (*(*FRONTEND).interface).video_update(&pixel_data_slice, pitch as u32); + // FIXME: could be simd-ified to do this across 4 or 8 pixels at once per line + // practically speaking however, it's *probably* not worth doing so because + // cores that might take advantage of such a optimized cpu kernel + // will probably have hardware rendering support. + // (therefore, we don't need to copy or change the format of anything) + for x in 0..pitch as usize { + for y in 0..height as usize { + let pixel = pixel_data_slice[y * pitch as usize + x]; + + let comp = ( + ((pixel & 0xff_00_00_00) >> 24) as u8, + ((pixel & 0x00_ff_00_00) >> 16) as u8, + ((pixel & 0x00_00_ff_00) >> 8) as u8, + (pixel & 0x00_00_00_ff) as u8, + ); + + buffer[y * pitch as usize + x] = (255u32 << 24) + | ((comp.3 as u32) << 16) + | ((comp.2 as u32) << 8) | (comp.1 as u32); + } + } } } + + (*(*FRONTEND).interface).video_update(&buffer[..], pitch as u32); } pub(crate) unsafe extern "C" fn input_poll_callback() { @@ -278,23 +351,27 @@ pub(crate) unsafe extern "C" fn input_poll_callback() { pub(crate) unsafe extern "C" fn input_state_callback( port: ffi::c_uint, device: ffi::c_uint, - _index: ffi::c_uint, // not used? + index: ffi::c_uint, // not used? button_id: ffi::c_uint, ) -> ffi::c_short { if (*FRONTEND).input_devices.contains_key(&port) { - let joypad = *(*FRONTEND) - .input_devices - .get(&port) - .expect("How do we get here when contains_key() returns true but the key doen't exist"); + let joypad: &dyn InputDevice = &*(*(*FRONTEND).input_devices.get(&port).expect( + "How do we get here when contains_key() returns true but the key doen't exist", + )); - if device == (*joypad).device_type() { - return (*joypad).get_button(button_id); + if joypad.device_type_compatible(device) { + return (*joypad).get_index(index, button_id); } } 0 } +pub(crate) unsafe extern "C" fn audio_sample_callback(_left: i16, _right: i16) { + // FIXME: we should batch these internally and then call the sample callback + // (wouldn't be too hard..) +} + pub(crate) unsafe extern "C" fn audio_sample_batch_callback( // Is actually a [[l, r]] interleaved pair. samples: *const i16, diff --git a/crates/retro_frontend/src/libretro_core_variable.rs b/crates/retro_frontend/src/libretro_core_variable.rs index f527a89..45d048f 100644 --- a/crates/retro_frontend/src/libretro_core_variable.rs +++ b/crates/retro_frontend/src/libretro_core_variable.rs @@ -20,7 +20,6 @@ pub struct CoreVariable { } impl CoreVariable { - /// Parses this core variable. pub fn parse(str: &str) -> Self { let string = str.to_string(); @@ -29,6 +28,7 @@ impl CoreVariable { Some(index) => { let name = &string[0..index]; + // FIXME: Instead of panicing, we should return a Result or the like if string.chars().nth(index + 1).unwrap() != ' ' { panic!("Improperly formatted core variable"); } diff --git a/crates/retro_frontend/src/libretro_log.rs b/crates/retro_frontend/src/libretro_log.rs index 356a5cd..b1fd00e 100644 --- a/crates/retro_frontend/src/libretro_log.rs +++ b/crates/retro_frontend/src/libretro_log.rs @@ -6,9 +6,12 @@ use tracing::*; #[no_mangle] /// This recieves log messages from our C++ helper code, and pulls them out into Tracing messages. pub extern "C" fn libretro_log_recieve(level: LogLevel, buf: *const ffi::c_char) { - // Safety: This pointer is never null, and always comes from the stack; + // SAFETY: This pointer should never be null since it comes from the address of a C++ stack variable. // we really only should get UTF-8 errors here in the case a core spits out something invalid. unsafe { + #[cfg(debug_assertions)] + assert!(!buf.is_null(), "This pointer should NEVER be null"); + match ffi::CStr::from_ptr(buf).to_str() { Ok(message) => match level { LogLevel::Debug => { diff --git a/crates/retro_frontend/src/libretro_sys_new.rs b/crates/retro_frontend/src/libretro_sys_new.rs index 71137d4..9efc499 100644 --- a/crates/retro_frontend/src/libretro_sys_new.rs +++ b/crates/retro_frontend/src/libretro_sys_new.rs @@ -3,6 +3,10 @@ pub use libretro_sys::*; use std::ffi; +/// Represents a bitmask that describes the state of all [DEVICE_ID_JOYPAD] button constants, +/// rather than the state of a single button. +pub const DEVICE_ID_JOYPAD_MASK: libc::c_uint = 256; + /// Defines overrides which modify frontend handling of specific content file types. /// An array of [SystemContentInfoOverride] is passed to [RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE] #[repr(C)] diff --git a/crates/retro_frontend/src/util.rs b/crates/retro_frontend/src/util.rs index 7e51009..3e63d06 100644 --- a/crates/retro_frontend/src/util.rs +++ b/crates/retro_frontend/src/util.rs @@ -1,4 +1,5 @@ use crate::libretro_sys_new::*; +use std::alloc; pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 { match pf { @@ -7,10 +8,9 @@ pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 { } } - /// Boilerplate code for dealing with NULL/otherwise terminated arrays, /// which converts them into a Rust slice. -/// +/// /// We rely on a user-provided callback currently to determine when iteration is complete. /// This *could* be replaced with a object-safe trait (and a constraint to allow us to use said trait) to codify /// the expected "end conditions" of a terminated array of a given type, but for now, the callback works. @@ -18,7 +18,10 @@ pub fn terminated_array<'a, T>(ptr: *const T, end_fn: impl Fn(&T) -> bool) -> &' // Make sure the array pointer itself isn't null. Strictly speaking, this check should be done // *before* this is called by the user, but to avoid anything going haywire // we additionally check here. - assert!(!ptr.is_null(), "pointer to array given to terminated_array! cannot be null"); + assert!( + !ptr.is_null(), + "pointer to array given to terminated_array! cannot be null" + ); unsafe { let mut iter = ptr.clone(); @@ -39,7 +42,21 @@ pub fn terminated_array<'a, T>(ptr: *const T, end_fn: impl Fn(&T) -> bool) -> &' } } -/* +/// Allocates a boxed slice. +/// Unlike a [Vec<_>], this can't grow, +/// but is just as safe to use, and slightly more predictable. +pub fn alloc_boxed_slice(len: usize) -> Box<[T]> { + assert_ne!(len, 0, "length cannot be 0"); + let layout = alloc::Layout::array::(len).expect("?"); + + let ptr = unsafe { alloc::alloc_zeroed(layout) as *mut T }; + + let slice = core::ptr::slice_from_raw_parts_mut(ptr, len); + + unsafe { Box::from_raw(slice) } +} + +/* #[doc(hidden)] #[macro_export] macro_rules! __terminated_array { diff --git a/crates/retrovnc/Cargo.toml b/crates/retrovnc/Cargo.toml index 637aa15..1db3cdf 100644 --- a/crates/retrovnc/Cargo.toml +++ b/crates/retrovnc/Cargo.toml @@ -7,12 +7,12 @@ publish = false [dependencies] anyhow = "1.0.86" clap = { version = "4.5.6", features = ["cargo"] } -gl = "0.14.0" +gl.workspace = true libvnc-sys = "0.1.4" retro_frontend = { path = "../retro_frontend" } +letsplay_gpu = { path = "../letsplay_gpu" } tracing = "0.1.40" tracing-subscriber = "0.3.18" [build-dependencies] cc = "1.0.99" -gl_generator = "0.14.0" diff --git a/crates/retrovnc/build.rs b/crates/retrovnc/build.rs index 33b66ee..9d0c700 100644 --- a/crates/retrovnc/build.rs +++ b/crates/retrovnc/build.rs @@ -1,31 +1,6 @@ use cc; -use gl_generator::{Api, Fallbacks, Profile, Registry, StaticGenerator}; -use std::env; -use std::fs::File; -use std::path::Path; - - fn main() { - // EGL - - let dest = env::var("OUT_DIR").unwrap(); - let mut file = File::create(&Path::new(&dest).join("egl_bindings.rs")).unwrap(); - - Registry::new(Api::Egl, (1, 5), Profile::Core, Fallbacks::All, [ - "EGL_EXT_platform_base", - "EGL_EXT_device_base", - - // This allows getting OpenGL APIs via eglGetProcAddress() - "EGL_KHR_get_all_proc_addresses", - "EGL_KHR_client_get_all_proc_addresses", - - "EGL_EXT_platform_device" - - ]) - .write_bindings(StaticGenerator, &mut file) - .unwrap(); - // C++ let mut build = cc::Build::new(); build diff --git a/crates/retrovnc/src/app.rs b/crates/retrovnc/src/app.rs index 09972f1..8ae042a 100644 --- a/crates/retrovnc/src/app.rs +++ b/crates/retrovnc/src/app.rs @@ -1,15 +1,14 @@ -use crate::egl; use crate::rfb::*; use std::{path::Path, time::Duration}; -use std::ptr::{addr_of_mut, null}; - use retro_frontend::{ frontend::{Frontend, FrontendInterface, HwGlInitData}, input_devices::{InputDevice, RetroPad}, }; +use letsplay_gpu as gpu; + use anyhow::Result; /// Called by OpenGL. We use this to dump errors. @@ -45,17 +44,13 @@ pub struct App { pad: RetroPad, + // EGL state + egl_context: Option, + /// True if HW rendering is active. using_hardware_rendering: 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, + gl_framebuffer: gpu::GlFramebuffer, /// Cached readback buffer. readback_buffer: Vec, @@ -67,12 +62,9 @@ impl App { frontend: None, rfb_server: RfbServer::new(rfb_config)?, pad: RetroPad::new(), + egl_context: None, using_hardware_rendering: false, - egl_display: null(), - egl_context: null(), - texture_id: 0, - renderbuffer_id: 0, - fbo_id: 0, + gl_framebuffer: gpu::GlFramebuffer::new(), readback_buffer: Vec::new(), }); @@ -137,172 +129,36 @@ impl App { Ok(()) } - /// Initalizes a headless EGL context for OpenGL rendering. + /// Initalizes the headless EGL context used for OpenGL rendering. fn hw_gl_egl_init(&mut self) { - // Currently we assume the first device on the Device platform. - // In most cases (at least on NVIDIA), this is usually a real GPU. - self.egl_display = egl::get_device_platform_display(0); - - self.egl_context = unsafe { - 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 = null(); - - egl::Initialize( - self.egl_display, - addr_of_mut!(egl_major), - addr_of_mut!(egl_minor), - ); - - egl::ChooseConfig( - self.egl_display, - EGL_CONFIG_ATTRIBUTES.as_ptr() as *const egl::EGLint, - addr_of_mut!(config), - 1, - addr_of_mut!(egl_config_count), - ); - - egl::BindAPI(egl::OPENGL_API); - - let context = egl::CreateContext(self.egl_display, config, egl::NO_CONTEXT, 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 - }; + // expected + self.egl_context = Some(gpu::egl_helpers::DeviceContext::new(0)); + self.egl_context.as_mut().unwrap().make_current(); } - /// Destroys EGL resources. - fn hw_gl_egl_exit(&mut self) { - if self.using_hardware_rendering { - // Delete FBO - self.hw_gl_delete_fbo(); + /// Destroys OpenGL resources and the EGL context. + fn hw_gl_destroy(&mut self) { + self.gl_framebuffer.destroy(); - // Release the EGL context we created before destroying it - unsafe { - 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 = null(); - self.egl_context = null(); + { + let m = self.egl_context.as_mut().unwrap(); + m.release(); + m.destroy(); } - } - /// 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, addr_of_mut!(self.fbo_id)); - self.fbo_id = 0; - - gl::DeleteTextures(1, addr_of_mut!(self.texture_id)); - self.texture_id = 0; - - gl::DeleteRenderbuffers(1, 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, addr_of_mut!(self.fbo_id)); - gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); - - gl::GenTextures(1, 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, - null(), - ); - - gl::GenRenderbuffers(1, 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); - } + self.egl_context = None; } /// 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 { + let av_info = 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); + frontend.run_frame(); std::thread::sleep(step_duration); } @@ -315,7 +171,14 @@ impl FrontendInterface for App { // Recreate the OpenGL FBO on resize. if self.using_hardware_rendering { - self.hw_gl_create_fbo(width, height); + self.gl_framebuffer.resize(width, height); + + let raw = self.gl_framebuffer.as_raw(); + self.get_frontend().set_gl_fbo(raw); + + self.readback_buffer.resize((width * height) as usize, 0); + + tracing::info!("GPU FBO Resized to {width}x{height}"); } self.rfb_server.resize(width as u16, height as u16); @@ -333,7 +196,7 @@ impl FrontendInterface for App { // I know it sucks but it works for this case. // SAFETY: self.readback_buffer will always be allocated to the proper size before reaching here unsafe { - gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + gl::BindFramebuffer(gl::FRAMEBUFFER, self.gl_framebuffer.as_raw()); gl::ReadPixels( 0, @@ -366,48 +229,52 @@ impl FrontendInterface for App { } } - fn hw_gl_init(&mut self) -> HwGlInitData { - if self.using_hardware_rendering { - panic!("Cannot initalize HW rendering while already initalized"); - } + fn hw_gl_init(&mut self) -> Option { + // test SW-only even if a core wants to upgrade + //return None; - // Initalize EGL - self.hw_gl_egl_init(); + if self.egl_context.is_none() { + self.hw_gl_egl_init(); - let extensions = egl::get_extensions(self.egl_display); + // Only create a new EGL/OpenGL context if we have to. + let extensions = + gpu::egl_helpers::get_extensions(self.egl_context.as_mut().unwrap().get_display()); - tracing::debug!("Supported EGL extensions: {:?}", extensions); + tracing::debug!("Supported EGL extensions: {:?}", extensions); - // Check for EGL_KHR_get_all_proc_addresses, so we can use eglGetProcAddress() to load OpenGL functions - // TODO: instead of panicing, we should probably make this return a Option<_>, and treat None on the frontend side - // as a failure. - 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. Failing"); - panic!("Cannot initalize OpenGL rendering"); - } + // 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!("retrovnc 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("Uhh huh."); - std::mem::transmute(egl::GetProcAddress(str.as_ptr())) - }); + unsafe { + 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), null()); + // set OpenGL debug message callback + gl::Enable(gl::DEBUG_OUTPUT); + gl::DebugMessageCallback(Some(opengl_message_callback), std::ptr::null()); + } + + // If we get here, we can be certain that we're no longer + // going to use software rendering. + self.using_hardware_rendering = true; + + tracing::info!("Rendering with OpenGL via EGL"); } + // 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.gl_framebuffer.resize(dimensions.0, dimensions.1); - self.using_hardware_rendering = true; - - return unsafe { - HwGlInitData { - get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void), - } - }; + return Some(HwGlInitData { + get_proc_address: gpu::egl::GetProcAddress as *mut std::ffi::c_void, + }); } } diff --git a/crates/retrovnc/src/egl.rs b/crates/retrovnc/src/egl.rs deleted file mode 100644 index 5387180..0000000 --- a/crates/retrovnc/src/egl.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! EGL bindings and helpers. - -#[allow(non_camel_case_types)] -#[allow(unused_imports)] -mod egl_impl { - pub type khronos_utime_nanoseconds_t = khronos_uint64_t; - pub type khronos_uint64_t = u64; - pub type khronos_ssize_t = std::ffi::c_long; - pub type EGLint = i32; - pub type EGLNativeDisplayType = *const std::ffi::c_void; - pub type EGLNativePixmapType = *const std::ffi::c_void; - pub type EGLNativeWindowType = *const std::ffi::c_void; - - pub type NativeDisplayType = EGLNativeDisplayType; - pub type NativePixmapType = EGLNativePixmapType; - pub type NativeWindowType = EGLNativeWindowType; - - include!(concat!(env!("OUT_DIR"), "/egl_bindings.rs")); - - // TODO: Move these helpers to a new "helpers" module. - - pub type GetPlatformDisplayExt = unsafe extern "C" fn( - platform: types::EGLenum, - native_display: *const std::ffi::c_void, - attrib_list: *const types::EGLint, - ) -> types::EGLDisplay; - - pub type QueryDevicesExt = unsafe extern "C" fn( - max_devices: self::types::EGLint, - devices: *mut self::types::EGLDeviceEXT, - devices_present: *mut EGLint, - ) -> types::EGLBoolean; - - pub fn get_extensions(display: types::EGLDisplay) -> Vec { - // SAFETY: eglQueryString() should never return a null pointer. - // If it does your video drivers are more than likely broken beyond repair. - unsafe { - let extensions_ptr = QueryString(display, EXTENSIONS as i32); - assert!(!extensions_ptr.is_null()); - - let extensions_str = std::ffi::CStr::from_ptr(extensions_ptr) - .to_str() - .expect("Invalid EGL_EXTENSIONS"); - - extensions_str - .split(' ') - .map(|str| str.to_string()) - .collect() - } - } - - /// A helper to get a display on the EGL "Device" platform, which allows headless rendering, - /// without any window system interface. - pub fn get_device_platform_display(index: usize) -> types::EGLDisplay { - const NR_DEVICES_MAX: usize = 16; - let mut devices: [types::EGLDeviceEXT; NR_DEVICES_MAX] = [std::ptr::null(); NR_DEVICES_MAX]; - - // This is how many devices are actually present, - let mut devices_present: EGLint = 0; - - assert!(index <= NR_DEVICES_MAX, "More than {NR_DEVICES_MAX} devices are not supported right now"); - - unsafe { - // TODO: These should probbaly be using CStr like above. - let query_devices_ext: QueryDevicesExt = - std::mem::transmute(GetProcAddress(b"eglQueryDevicesEXT\0".as_ptr() as *const i8)); - let get_platform_display_ext: GetPlatformDisplayExt = std::mem::transmute( - GetProcAddress(b"eglGetPlatformDisplayEXT\0".as_ptr() as *const i8), - ); - - (query_devices_ext)( - NR_DEVICES_MAX as i32, - devices.as_mut_ptr(), - std::ptr::addr_of_mut!(devices_present), - ); - - (get_platform_display_ext)(PLATFORM_DEVICE_EXT, devices[index], std::ptr::null()) - } - } - - // link EGL as a library dependency - #[link(name = "EGL")] - extern "C" {} -} - -pub use egl_impl::*; diff --git a/crates/retrovnc/src/main.rs b/crates/retrovnc/src/main.rs index ce41a30..51dd27c 100644 --- a/crates/retrovnc/src/main.rs +++ b/crates/retrovnc/src/main.rs @@ -8,7 +8,6 @@ use clap::{arg, command, value_parser}; mod app; -mod egl; mod rfb; use app::*;