From b972478ee9ff3176b6219e6f909908fce66c1b47 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Mon, 5 Aug 2024 08:49:40 -0400 Subject: [PATCH] Initial OpenGL core support mmmmmm, this really is NOT as clean as I want it to be, but here it is! --- Cargo.lock | 34 +++ crates/retro_frontend/src/frontend.rs | 25 +- .../retro_frontend/src/libretro_callbacks.rs | 48 +++- crates/retrovnc/Cargo.toml | 2 + crates/retrovnc/build.rs | 29 +- crates/retrovnc/src/egl.rs | 34 +++ crates/retrovnc/src/main.rs | 260 +++++++++++++++++- crates/retrovnc/src/rfb.rs | 56 +++- 8 files changed, 457 insertions(+), 31 deletions(-) create mode 100644 crates/retrovnc/src/egl.rs diff --git a/Cargo.lock b/Cargo.lock index cacdcf5..ca66112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.1" @@ -237,6 +257,12 @@ dependencies = [ "either", ] +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "lazy_static" version = "1.5.0" @@ -433,6 +459,8 @@ dependencies = [ "anyhow", "cc", "clap", + "gl", + "gl_generator", "libvnc-sys", "retro_frontend", "tracing", @@ -785,3 +813,9 @@ checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] + +[[package]] +name = "xml-rs" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" diff --git a/crates/retro_frontend/src/frontend.rs b/crates/retro_frontend/src/frontend.rs index ae7147b..55f7782 100644 --- a/crates/retro_frontend/src/frontend.rs +++ b/crates/retro_frontend/src/frontend.rs @@ -21,11 +21,22 @@ 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, +} + /// Interface for the frontend to call to user code. pub trait FrontendInterface { /// Called when video is updated. fn video_update(&mut self, slice: &[u32], pitch: u32); + /// Called when video is updated and the core is using HW OpenGL rendering. + fn video_update_gl(&mut self); + /// Called when resize occurs. fn video_resize(&mut self, width: u32, height: u32); @@ -36,10 +47,8 @@ pub trait FrontendInterface { /// Called to poll input fn input_poll(&mut self); - // TODO(lily): Provide APIs for gathering a GL context/fbo. - // This should be abstracted so if a frontend written on top of retro_frontend - // wants to give a FBO from a GLX/WGL/windowed whatever context, it can, - // but a headless frontend can itself provide a headless EGL context just the same. + /// Initalize hardware accelerated rendering using OpenGL. + fn hw_gl_init(&mut self) -> HwGlInitData; } /// Per-core settings @@ -73,6 +82,9 @@ pub struct Frontend { pub(crate) fb_height: u32, pub(crate) fb_pitch: u32, + // HW OpenGL FBO id. + pub(crate) gl_fbo_id: u32, + /// The "system" directory. Used for BIOS roms. pub(crate) system_directory: CString, @@ -109,6 +121,7 @@ impl Frontend { fb_width: 0, fb_height: 0, fb_pitch: 0, + gl_fbo_id: 0, // TODO: We should let callers set these, probably. // For now, this is probably fine. @@ -431,6 +444,10 @@ impl Frontend { (self.fb_width, self.fb_height) } + pub fn set_gl_fbo(&mut self, id: u32) { + self.gl_fbo_id = id; + } + pub fn reset(&mut self) { let core_api = self.core_api.as_ref().unwrap(); diff --git a/crates/retro_frontend/src/libretro_callbacks.rs b/crates/retro_frontend/src/libretro_callbacks.rs index c4fb4b9..4459f3b 100644 --- a/crates/retro_frontend/src/libretro_callbacks.rs +++ b/crates/retro_frontend/src/libretro_callbacks.rs @@ -8,6 +8,11 @@ use std::ffi; use tracing::{debug, error, info}; +/// This function is used with HW OpenGL cores to transfer the current FBO's ID. +unsafe extern "C" fn hw_gl_get_framebuffer() -> usize { + (*FRONTEND).gl_fbo_id as usize +} + pub(crate) unsafe extern "C" fn environment_callback( environment_command: u32, data: *mut ffi::c_void, @@ -99,6 +104,33 @@ pub(crate) unsafe extern "C" fn environment_callback( return true; } + ENVIRONMENT_SET_HW_RENDER => { + let hw_render = (data as *mut HwRenderCallback).as_mut().unwrap(); + + let hw_render_context_type = + HwContextType::from_uint(hw_render.context_type).expect("FUCK"); + + 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 ({:?})", + hw_render_context_type + ); + return false; + } + + info!("Core requesting context type {:?}", hw_render_context_type); + + 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)(); + + return true; + } + ENVIRONMENT_GET_VARIABLE => { // Make sure the core actually is giving us a pointer to a *Variable // so we can fill it in. @@ -179,9 +211,19 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( //info!("Video refresh called, {width}, {height}, {pitch}"); + if (*FRONTEND).fb_width != width || (*FRONTEND).fb_height != height { + (*(*FRONTEND).interface).video_resize(width, height); + } + // bleh (*FRONTEND).fb_width = width; (*FRONTEND).fb_height = height; + + if pixels == (-1i64 as *const ffi::c_void) { + (*(*FRONTEND).interface).video_update_gl(); + return; + } + (*FRONTEND).fb_pitch = pitch as u32 / util::bytes_per_pixel_from_libretro((*FRONTEND).pixel_format); @@ -200,9 +242,6 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( (*FRONTEND) .converted_pixel_buffer .resize((pitch * height as usize) as usize, 0); - - // some cores are stupid - (*(*FRONTEND).interface).video_resize(width, height); } // TODO: Make this convert from weird pitches to native resolution where possible. @@ -217,7 +256,8 @@ pub(crate) unsafe extern "C" fn video_refresh_callback( } } - (*(*FRONTEND).interface).video_update(&(*FRONTEND).converted_pixel_buffer[..], pitch as u32); + (*(*FRONTEND).interface) + .video_update(&(*FRONTEND).converted_pixel_buffer[..], pitch as u32); } _ => { let pixel_data_slice = std::slice::from_raw_parts( diff --git a/crates/retrovnc/Cargo.toml b/crates/retrovnc/Cargo.toml index b9f899a..637aa15 100644 --- a/crates/retrovnc/Cargo.toml +++ b/crates/retrovnc/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] anyhow = "1.0.86" clap = { version = "4.5.6", features = ["cargo"] } +gl = "0.14.0" libvnc-sys = "0.1.4" retro_frontend = { path = "../retro_frontend" } tracing = "0.1.40" @@ -14,3 +15,4 @@ 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 38a2e31..33b66ee 100644 --- a/crates/retrovnc/build.rs +++ b/crates/retrovnc/build.rs @@ -1,8 +1,33 @@ use cc; -fn main() { - let mut build = cc::Build::new(); +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 .emit_rerun_if_env_changed(true) .cpp(true) diff --git a/crates/retrovnc/src/egl.rs b/crates/retrovnc/src/egl.rs new file mode 100644 index 0000000..15b8147 --- /dev/null +++ b/crates/retrovnc/src/egl.rs @@ -0,0 +1,34 @@ +#[allow(non_camel_case_types)] +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")); + + 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: *mut EGLint, + ) -> types::EGLBoolean; + + // 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 dea6a08..cdfb405 100644 --- a/crates/retrovnc/src/main.rs +++ b/crates/retrovnc/src/main.rs @@ -1,9 +1,9 @@ -use std::{net::Ipv4Addr, path::Path}; +use std::{net::Ipv4Addr, path::Path, ptr::addr_of_mut}; use anyhow::Result; use retro_frontend::{ - frontend::{Frontend, FrontendInterface}, + frontend::{Frontend, FrontendInterface, HwGlInitData}, input_devices::{InputDevice, RetroPad}, }; use tracing::Level; @@ -11,6 +11,32 @@ use tracing_subscriber::FmtSubscriber; use clap::{arg, command, value_parser}; + + +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 egl; mod rfb; use rfb::*; @@ -18,6 +44,15 @@ struct App { frontend: Option>, rfb_server: Box, pad: RetroPad, + + hw_render: bool, + + // opengl state shits + texture_id: gl::types::GLuint, + renderbuffer_id: gl::types::GLuint, + fbo_id: gl::types::GLuint, + + readback_buffer: Vec, } impl App { @@ -26,6 +61,11 @@ impl App { frontend: None, rfb_server: RfbServer::new(rfb_config)?, pad: RetroPad::new(), + hw_render: false, + texture_id: 0, + renderbuffer_id: 0, + fbo_id: 0, + readback_buffer: Vec::new(), }); // Very very nasty, but honestly it works. @@ -80,6 +120,81 @@ impl App { Ok(()) } + fn hw_gl_resize(&mut self, width: u32, height: u32) { + // TODO: if requested, add depth renderbuffer. + unsafe { + if self.fbo_id == 0 { + gl::GenFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + } else { + gl::DeleteFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); + + gl::GenFramebuffers(1, std::ptr::addr_of_mut!(self.fbo_id)); + gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); + } + + if self.texture_id == 0 { + gl::GenTextures(1, std::ptr::addr_of_mut!(self.texture_id)); + gl::BindTexture(gl::TEXTURE_2D, self.texture_id); + } else { + gl::DeleteTextures(1, std::ptr::addr_of_mut!(self.texture_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(), + ); + + if self.renderbuffer_id != 0 { + 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); + + let id = self.fbo_id; + self.get_frontend().set_gl_fbo(id); + + self.readback_buffer.resize((width * height) as usize, 0); + } + } + // Main loop fn main_loop(&mut self) -> ! { let frontend = self.get_frontend(); @@ -101,13 +216,41 @@ impl FrontendInterface for App { //let height = height * 2; tracing::info!("Resized to {width}x{height}"); + + // Resize OpenGL + if self.hw_render { + self.hw_gl_resize(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); + 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 hey + 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) {} @@ -123,6 +266,112 @@ impl FrontendInterface for App { } } } + + fn hw_gl_init(&mut self) -> HwGlInitData { + if self.hw_render { + panic!("Nope!"); + } + + let display = unsafe { + const NR_DEVICES_MAX: usize = 16; + let mut devices: [egl::types::EGLDeviceEXT; NR_DEVICES_MAX] = + [std::ptr::null(); NR_DEVICES_MAX]; + + let mut nr_devices_real: egl::EGLint = 0; + + let query_devices_ext: egl::QueryDevicesExt = std::mem::transmute(egl::GetProcAddress( + b"eglQueryDevicesEXT\0".as_ptr() as *const i8, + )); + let get_platform_display_ext: egl::GetPlatformDisplayExt = std::mem::transmute( + egl::GetProcAddress(b"eglGetPlatformDisplayEXT\0".as_ptr() as *const i8), + ); + + // 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("you MOTHERFUCKER"); + std::mem::transmute(egl::GetProcAddress(str.as_ptr())) + }); + + tracing::info!("before query devices"); + + (query_devices_ext)( + NR_DEVICES_MAX as i32, + devices.as_mut_ptr(), + std::ptr::addr_of_mut!(nr_devices_real), + ); + + tracing::info!("after query devices"); + (get_platform_display_ext)(egl::PLATFORM_DEVICE_EXT, devices[0], std::ptr::null()) + }; + + tracing::info!("made EGL display"); + + let 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 = 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); + + egl::CreateContext(display, config, egl::NO_CONTEXT, std::ptr::null()) + }; + + unsafe { + egl::MakeCurrent(display, egl::NO_SURFACE, egl::NO_SURFACE, context); + + gl::Enable(gl::DEBUG_OUTPUT); + gl::DebugMessageCallback(Some(opengl_message_callback), std::ptr::null()); + + let dimensions = self.get_frontend().get_size(); + + self.hw_gl_resize(dimensions.0, dimensions.1); + + gl::BindFramebuffer(gl::FRAMEBUFFER, 0); + } + + self.hw_render = true; + + return unsafe { + HwGlInitData { + get_proc_address: std::mem::transmute::< + _, + retro_frontend::frontend::GlGetProcAddress, + >(egl::GetProcAddress as usize), + } + }; + } } fn main() -> Result<()> { @@ -170,11 +419,10 @@ fn main() -> Result<()> { app.load_core(core_path)?; - if let Some(rom_path) = matches.get_one::("rom") { app.load_game(rom_path)? } - + // Initalize app app.init(); diff --git a/crates/retrovnc/src/rfb.rs b/crates/retrovnc/src/rfb.rs index 4d4d26b..38e6f3b 100644 --- a/crates/retrovnc/src/rfb.rs +++ b/crates/retrovnc/src/rfb.rs @@ -20,7 +20,7 @@ pub struct RfbServer { buttons: [bool; 32], width: u16, - height: u16 + height: u16, } impl RfbServer { @@ -57,7 +57,7 @@ impl RfbServer { framebuffer: Vec::new(), buttons: [false; 32], width: config.width, - height: config.height + height: config.height, }); // Disable the normal libvnc cursor (it's annoying) @@ -99,8 +99,7 @@ impl RfbServer { } } - pub fn update_buffer(&mut self, slice: &[u32], pitch: u32) { - + pub fn update_buffer(&mut self, slice: &[u32], pitch: u32, flipped: bool) { //self.framebuffer.copy_from_slice(&slice); let has_disconnected_pitch = pitch != self.width as u32; @@ -110,20 +109,47 @@ impl RfbServer { //} // copy a line at a time (test) - for y in 0..self.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 * self.width) as usize; + if flipped { + let mut scanlines: Vec<&[u32]> = Vec::with_capacity(self.height as usize); + + for y in (0..self.height).rev() { + let src_line_off = (y as u32 * pitch) as usize; + let src_slice = &slice[src_line_off..src_line_off + self.width as usize]; + scanlines.push(src_slice); } - // Create slices repressenting each part - let src_slice = &slice[src_line_off..src_line_off + self.width as usize]; - let dest_slice = &mut self.framebuffer[dest_line_off..dest_line_off + self.width as usize]; + for y in (0..self.height) { + let src_line_off = (y as u32 * pitch) as usize; + let mut dest_line_off = src_line_off; - dest_slice.copy_from_slice(src_slice); + // copy only + if has_disconnected_pitch { + dest_line_off = (y * self.width) as usize; + } + + let dest_slice = + &mut self.framebuffer[dest_line_off..dest_line_off + self.width as usize]; + + dest_slice.copy_from_slice(scanlines[y as usize]); + } + } else { + for y in 0..self.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 * self.width) as usize; + } + + // Create slices repressenting each part + let src_slice = &slice[src_line_off..src_line_off + self.width as usize]; + let dest_slice = + &mut self.framebuffer[dest_line_off..dest_line_off + self.width as usize]; + + dest_slice.copy_from_slice(src_slice); + } } unsafe { @@ -155,7 +181,7 @@ impl RfbServer { } } - // TODO: Pull this out of here, and instead do it in retrovnc app proper. + // TODO: Pull this out of here, and instead do it in retrovnc app proper. // This will also make the rfb code more generic which is always a good thing pub fn get_buttons(&self) -> [bool; 32] {